Etherscan – How to Get Implementation Contract Address from Proxy Contract Address

abibscetherscanproxy-contractsweb3.py

I'm using web3.py to interact with multiple unknown contracts, where I don't know beforehand if a contract is a proxy contract or the actual contract. I'm getting the ABIs for the contracts on-the-fly from BSCScan's API. Assume I am only interested in contracts whose ABIs are available on BSCscan.

When I retrieve the ABI of a contract from BSCScan, sometimes the ABI has functions like
['admin', 'changeAdmin', 'implementation', 'upgradeTo', 'upgradeToAndCall'] which is how I understand it's a proxy contract address.

Now I'd like to get the address of the implementation contract from the proxy contract. The final goal is to get the ABI of the implementation contract via BSCScan, for which I need the address of the implementation contract.

How do I get the ABI (or the address) of the implementation contract?

(While I am using BSCScan and BSC, for the purposes of this answer I assume the answer for Ethereum and Etherscan is the same)

from bscscan import BscScan
from web3 import Web3

contract_address = '0x88f1A5ae2A3BF98AEAF342D26B30a79438c9142e'  # Test case


def get_abi(contract_address):
    with BscScan(api_key, asynchronous=False) as client:
        abi_string = client.get_contract_abi(contract_address)
    return abi_string


web3 = Web3(Web3.HTTPProvider(bsc))
abi_string = get_abi(address)
contract = web3.eth.contract(address=address, abi=abi_string)
if 'implementation' in [foo.function_identifier for foo in contract.all_functions()]:
    # it's a proxy contract as its ABI contained implementation()
    implementation_address = contract.functions.implementation().call()  # <-- Error
    implementation_abi_string = get_abi(implementation_address)
    contract = web3.eth.contract(address=address, abi=implementation_abi_string)


desired_result = contract.functions.somefunction().call()

This results in

Traceback (most recent call last):
  ...
  File "helpers.py", line 86, in create_contract_object
    implementation_address = contract.functions.implementation().call()
  File "/venv-mac-py/lib/python3.9/site-packages/web3/contract.py", line 957, in call
    return call_contract_function(
  File "/venv-mac-py/lib/python3.9/site-packages/web3/contract.py", line 1501, in call_contract_function
    return_data = web3.eth.call(
  File "/venv-mac-py/lib/python3.9/site-packages/web3/module.py", line 57, in caller
    result = w3.manager.request_blocking(method_str, params, error_formatters)
  File "/venv-mac-py/lib/python3.9/site-packages/web3/manager.py", line 159, in request_blocking
    apply_error_formatters(error_formatters, response)
  File "/venv-mac-py/lib/python3.9/site-packages/web3/manager.py", line 63, in apply_error_formatters
    formatted_response = pipe(response, error_formatters)
  File "cytoolz/functoolz.pyx", line 667, in cytoolz.functoolz.pipe
  File "cytoolz/functoolz.pyx", line 642, in cytoolz.functoolz.c_pipe
  File "/venv-mac-py/lib/python3.9/site-packages/web3/_utils/method_formatters.py", line 544, in raise_solidity_error_on_revert
    raise ContractLogicError('execution reverted')
web3.exceptions.ContractLogicError: execution reverted

Best Answer

Most proxy contracts typically have a public variable defined as a:

address public implementation;

Which defines the address of the implementation contract. You could then call it as a view function in python, with something like:

proxy_contract = web3.eth.contract(address=address, abi=abi_string)
implementation_contract_address = proxy_contract.functions.implementation.call()

However, this contract, in particular, doesn't have an implementation function and has some private functions.

Here is the code for their proxy contract where they call the delegatecall to the implementation:

bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    /**
     * @dev Returns the current implementation address.
     */
    function _implementation() internal override view returns (address impl) {
        bytes32 slot = _IMPLEMENTATION_SLOT;
        // solhint-disable-next-line no-inline-assembly
        assembly {
            impl := sload(slot)
        }
    }

So you'd have to read directly off the storage of the blockchain to understand what _IMPLEMENTATION_SLOT is. We also need to know some yul/assembly to understand what's going on.

sload(p) loads the value that is stored in storage at location p. In this contract, this means the implementation contract address is stored at location 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc in memory.

We can confirm this, by looking at the _setImplementation method, where it stores the new implementation address with sstore (more Yul code, that sets a value at a location in storage).

function _setImplementation(address newImplementation) private {
        require(Address.isContract(newImplementation), "UpgradeableProxy: new implementation is not a contract");

        bytes32 slot = _IMPLEMENTATION_SLOT;

        // solhint-disable-next-line no-inline-assembly
        assembly {
            sstore(slot, newImplementation)
        }
    }

So, all we have to do to read this, is read the address that is located in the storage of this contract at the location defined in _IMPLEMENTATION_SLOT.

We can find this with:

def main():
    web3 = Web3(Web3.HTTPProvider("https://bsc-dataseed2.defibit.io/"))
    impl_contract = Web3.toHex(
        web3.eth.get_storage_at(
            contract_address,
            "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
        )
    )
    print(impl_contract)

To which we get: 0x000000000000000000000000ba5fe23f8a3a24bed3236f05f2fcf35fd0bf0b5c

Which we then just undo the 0 padding, to get the address of: 0xba5fe23f8a3a24bed3236f05f2fcf35fd0bf0b5c. Which is indeed the implementation contract.

Edit

It should be noted, that a lot of contracts will use this IMPLEMENTATION_SLOT, as it follows the guidelines of the ERC1967