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 and class_hash of proxied contract

  • forward the calls using delegate_call and address 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:

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
)