Etherscan

如何從代理合約地址獲取執行合約地址?

  • November 17, 2021

我正在使用 web3.py 與多個未知契約進行互動,我事先不知道契約是代理契約還是實際契約。我正在從 BSCScan 的 API 即時獲取契約的 ABI。假設我只對 BSCscan 上提供 ABI 的合約感興趣。

當我從 BSCScan 檢索契約的 ABI 時,有時 ABI 具有類似 ['admin', 'changeAdmin', 'implementation', 'upgradeTo', 'upgradeToAndCall']我理解它是代理契約地址的功能。

現在我想從代理合約中獲取實現合約的地址。最終目標是通過 BSCScan 獲取實現合約的 ABI,為此我需要實現合約的地址。

如何獲取執行合約的 ABI(或地址)?

(雖然我使用的是 BSCScan 和 BSC,但出於此答案的目的,我假設 Ethereum 和 Etherscan 的答案是相同的)

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()

這導致

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

大多數代理合約通常都有一個公共變數,定義為:

address public implementation;

其中定義了實現合約的地址。然後,您可以在 python 中將其稱為視圖函式,例如:

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

但是,特別是這個合約沒有implementation功能,並且有一些私有功能。

這是他們呼叫delegatecall實現的代理合約的程式碼:

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)
       }
   }

因此,您必須直接從區塊鏈的儲存中讀取以了解其內容_IMPLEMENTATION_SLOT。我們還需要了解一些 yul/assembly 以了解發生了什麼。

sload(p)載入儲存在 location 中的值p。在這個合約中,這意味著實現合約地址儲存0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc在記憶體中的位置。

我們可以通過查看_setImplementation儲存新實現地址的方法來確認這一點sstore(更多的 Yul 程式碼,它在儲存位置設置值)。

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)
       }
   }

因此,我們所要做的就是讀取位於該合約儲存中定義的位置的地址 _IMPLEMENTATION_SLOT

我們可以通過以下方式找到:

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)

我們得到:0x000000000000000000000000ba5fe23f8a3a24bed3236f05f2fcf35fd0bf0b5c

然後我們只需撤消 0 填充,以獲取: 的地址 0xba5fe23f8a3a24bed3236f05f2fcf35fd0bf0b5c。這確實是執行契約。

編輯

需要注意的是,很多合約都會使用這個IMPLEMENTATION_SLOT,因為它遵循ERC1967的指導方針

引用自:https://ethereum.stackexchange.com/questions/103143