Resolving proxy contracts#
Note
If you know the abi of the contract, always prefer creating Contract directly from constructor.
Contract.from_address
must perform some calls to Starknet to get an abi of the contract.
Resolving proxies is a powerful feature of starknet.py. If your contract is a proxy to some implementation, you can use
high-level Contract.from_address
method to get a contract instance.
Contract.from_address
works with contracts which are not proxies, so it is the most universal method of getting
a contract not knowing the abi.
from starknet_py.contract import Contract
# Getting the direct contract from address
contract = await Contract.from_address(address=address, provider=account)
# To use contract behind a proxy as a regular contract, set proxy_config to True
# It will check if your proxy is OpenZeppelin proxy / ArgentX proxy
contract = await Contract.from_address(
address=address, provider=account, proxy_config=True
)
# After that contract can be used as usual
ProxyChecks#
Since the Proxy contracts on Starknet can have different implementations, as every user can define their custom implementation, there is no single way of checking if some contract is a Proxy contract.
- There are two main ways of proxying a contract on Starknet:
forward the calls using
library_call
andclass_hash
of proxied contractforward the calls using
delegate_call
andaddress
of proxied contract
Contract.from_address
uses proxy_checks
to fetch the implementation
(address or class hash) of the proxied contract.
ProxyCheck checks whether the contract is a Proxy contract.
It does that by trying to get the address
or class_hash
of the implementation.
- By default,
proxy_config
uses a configuration with two ProxyChecks: ArgentProxyCheck - resolves Argent Proxy.
OpenZeppelinProxyCheck - resolves OpenZeppelin Proxy.
Warning
StarknetEthProxyCheck
has been removed, because the StarkGate ETH Token was upgraded to Cairo 2, meaning it isn’t a Proxy anymore. Currently, all StarkGate’s Token contracts use interface of ERC20 to interact.
It’s possible to define own ProxyCheck implementation and later pass it to Contract.from_address
, so it knows how to resolve the Proxy.
The ProxyCheck base class implements the following interface:
class ProxyCheck(ABC):
@abstractmethod
async def implementation_address(
self, address: Address, client: Client
) -> Optional[int]:
"""
:return: Implementation address of contract being proxied by proxy contract at `address`
given as an argument or None if implementation does not exist.
"""
@abstractmethod
async def implementation_hash(
self, address: Address, client: Client
) -> Optional[int]:
"""
:return: Implementation class hash of contract being proxied by proxy contract at `address`
given as an argument or None if implementation does not exist.
"""
- It has two methods:
implementation_address - returns the address of the proxied contract (implement this if your Proxy contract uses the address of another contract as implementation)
implementation_hash - returns the class_hash of the proxied contract (implement this if your Proxy contract uses the class_hash of another contract as implementation)
Here is the complete example:
# To resolve proxy contract other than OpenZeppelin / ArgentX, a custom ProxyCheck is needed
# The ProxyCheck below resolves proxy contracts which have implementation
# stored in impl() function as class hash
class CustomProxyCheck(ProxyCheck):
async def implementation_address(
self, address: Address, client: Client
) -> Optional[int]:
# Note that None is returned, since our custom Proxy uses
# the class hash of another contract as implementation and not the address
return None
async def implementation_hash(
self, address: Address, client: Client
) -> Optional[int]:
call = Call(
to_addr=address,
selector=get_selector_from_name("impl"),
calldata=[],
)
(implementation,) = await client.call_contract(call=call)
return implementation
# Create ProxyConfig with the CustomProxyCheck
proxy_config = ProxyConfig(proxy_checks=[CustomProxyCheck()])
# More ProxyCheck instances can be passed to proxy_checks for it to be flexible
proxy_config = ProxyConfig(proxy_checks=[CustomProxyCheck(), ArgentProxyCheck()])
contract = await Contract.from_address(
address=address, provider=account, proxy_config=proxy_config
)