Web3.py – Accessing External View Data Through Proxy Implementation Contract

proxy-contractssolidityusdcweb3.pyweb3js

I want to use web3.py to access all sorts of view-data from smart contracts. In some cases, it's pretty simple. Take USDt:

You have the USDt contract on mainnet, 0xdAC17F958D2ee523a2206206994597C13D831ec7, you get its ABI, initialize the contract and then you can easily call any external view function through your endpoint. The code looks something like this:

def query_contract(address, function):
    args = request.args.getlist("args") or []
    args = [web3.toChecksumAddress(arg) if web3.isAddress(arg) else arg for arg in args]
    abi = retrieve_contract_abi(address, api_key)
    if abi is None:
        return (
            jsonify(error="Failed to retrieve ABI for the specified contract address."),
            400,
        )

    try:
        contract = web3.eth.contract(address=Web3.toChecksumAddress(address), abi=abi)
        result = contract.functions[function](*args).call()
        return jsonify(result)
    except Exception as e:
        print(e)
        return jsonify(error="An error occurred while processing your request."), 500

When I wanted to do the same thing with USDC, 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48, I came to realize that this is not going to work with proxy contracts. The proxy contract itself only has two view functions that aren't even external (implementation and admin). I dug into the contract until I found the piece of code where calls to the proxy are being delegated to its implementation contract:

function _delegate(address implementation) internal {
    assembly {
      // Copy msg.data. We take full control of memory in this inline assembly
      // block because it will not return to Solidity code. We overwrite the
      // Solidity scratch pad at memory position 0.
      calldatacopy(0, 0, calldatasize)

      // Call the implementation.
      // out and outsize are 0 because we don't know the size yet.
      let result := delegatecall(gas, implementation, 0, calldatasize, 0, 0)

      // Copy the returned data.
      returndatacopy(0, 0, returndatasize)

      switch result
      // delegatecall returns 0 on error.
      case 0 { revert(0, returndatasize) }
      default { return(0, returndatasize) }
    }
  }

Since there's no direct function "allowance" to call for example, there needs to be a fallback function that somehow triggers the delegate call (as far as I understood it). So digging deeper, the only place where _delegate is called is inside

function _fallback() internal {
    _willFallback();
    _delegate(_implementation());
  }

Since it's internal, this doesn't help me at all. The only time where _fallback() is called, is inside

function () payable external {
    _fallback();
  }

Okay, understood. This is the fallback-function (no name, payable, external, etc.). The only issue is, that… well it's payable. This means if I want to call a view-function of the implementation contract by having the proxy delegate to it, I would need to pay at least 1 wei if my understanding is correct. But this does not make sense, right? For example, if I go on etherscan, there is a "Read as Proxy" button in the Contract tab. This means it's definitely possible to call a proxy's implementation contract's view functions for free. But how?

I was thinking that instead, I'd just extract the address of the implementation contract from the proxy contract, get the ABI of the implementation contract and then instantiate the implementation contract. Then I can just call its view-functions directly. But it turned out that calling them directly does not work since the implementation contract does not store the values. It's just the logic. If I understood it correctly the proxy itself stores the values instead. That's why calling the functions directly on the implementation contract returned wrong values (try it on etherscan, if you call "totalSupply" directly on the implementation contract, it returns 0 which is obviously not the case)…

So… long story short, how does etherscan call a proxy contract's implementation contract's functions, in the context of the proxy contract, and thus getting the correct values from the proxy contract, especially without paying any wei for the delegation of the function calls?

Best Answer

For transparent proxies like Centre's FiatToken (USDC)

  • You do not need to care about an implementation contract at all. You do not need to know implementation's address, or that it event exists.
  • Use the given ABI file that is for the implementation. You can obtain one from Centre's Github repository.
  • Initialize web3.contract.Contract instance at the proxy address
usdc = Contract(usdc_proxy_address, abi=usdc_abi)

Then you can call

print(usdc.functions.symbol().call())

You can also check some of the existing USDC example code here, likely not relevant for your problem.

This means if I want to call a view-function of the implementation contract by having the proxy delegate to it, I would need to pay at least 1 wei if my understanding is correct. But this does not make sense, right?

This does not make sense.

Related Topic