6

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
3
  • Does BSCScan's Proxy Contract Verification help? Jul 7, 2021 at 14:48
  • That URL works in a slightly unexpected way; it does provide an implementation address but then asks visitors to validate its output(!) I take it there is no standard way to link a proxy to its current implementation?
    – salian.eth
    Jul 7, 2021 at 15:22
  • I also want to know the answer for this problem. Anyone helps?
    – user40780
    Jul 7, 2021 at 17:55

2 Answers 2

13

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

11
  • 2
    That's an amazing answer with many pointers for further learning. Thanks Patrick!
    – salian.eth
    Jul 8, 2021 at 2:15
  • Thanks so much! Do you know if we can do the same the resolve proxy contract for Ethereum mainnet and polygon?
    – user40780
    Jul 8, 2021 at 12:38
  • You should be able to, but it depends on the proxy pattern the contracts use rather than the blockchain Jul 8, 2021 at 12:42
  • Thanks again. for other chains, would this number be the same "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" , or do we need to adjust this number for different chains?
    – user40780
    Jul 8, 2021 at 12:52
  • 1
    it should be noted, that a lot of contracts will probably use this location though, as this follows the EIP-1967 proxy standard: eips.ethereum.org/EIPS/eip-1967#logic-contract-address Jul 9, 2021 at 18:38
0

When using Hardhat to deploy an upgradable contract, we could use erc1967 to get the implementation and admint contract.

const UpgradeableFactory = await ethers.getContractFactory(
    "Upgradeable",
    accounts[0]
  );

  const UpgradeableInstance = await upgrades.deployProxy(
    UpgradeableFactory
  );

  await UpgradeableInstance.deployed();

  const proxyAddress = UpgradeableInstance.address;
  const implementationAddress = await upgrades.erc1967.getImplementationAddress(
    proxyAddress
  );
  const adminAddress = await upgrades.erc1967.getAdminAddress(proxyAddress);

  console.log("Proxy: ", proxyAddress);
  console.log("Implementation: ", implementationAddress);
  console.log("Admin: ", adminAddress);
1
  • OP question refers to 3rd party proxy and implementation contracts already deployed to mainnet, not contracts deployed by themselves through hardhat.
    – Bigbrotha
    Aug 2 at 10:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.