From 7ceaa0677151e35274e46c63cf3edc1f98928f41 Mon Sep 17 00:00:00 2001 From: mo Date: Mon, 22 Jan 2024 14:01:25 +0100 Subject: [PATCH 01/12] proxy: get source contract address --- boa/explorer.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/boa/explorer.py b/boa/explorer.py index 44779b46..b00a4533 100644 --- a/boa/explorer.py +++ b/boa/explorer.py @@ -12,6 +12,25 @@ def fetch_abi_from_etherscan( ): address = Address(address) + params = dict(module="contract", action="getsourcecode", address=address) + if api_key is not None: + params["apikey"] = api_key + + res = SESSION.get(uri, params=params) + res.raise_for_status() + + data = res.json() + + if int(data["status"]) != 1: + raise ValueError(f"Failed to retrieve data from API: {data}") + + data = data["result"][0] + + if "Proxy" in data and int(data["Proxy"]) == 1: + address = data.get("Implementation") + else: + address = address + params = dict(module="contract", action="getabi", address=address) if api_key is not None: params["apikey"] = api_key @@ -25,3 +44,4 @@ def fetch_abi_from_etherscan( raise ValueError(f"Failed to retrieve data from API: {data}") return json.loads(data["result"].strip()) + \ No newline at end of file From 7013595379889ad27bcdf6cc09364f878c0fe150 Mon Sep 17 00:00:00 2001 From: mo Date: Mon, 22 Jan 2024 14:01:40 +0100 Subject: [PATCH 02/12] from_etherscan_abi --- boa/__init__.py | 1 + boa/interpret.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/boa/__init__.py b/boa/__init__.py index b7301d58..7ffe59ed 100644 --- a/boa/__init__.py +++ b/boa/__init__.py @@ -8,6 +8,7 @@ from boa.interpret import ( BoaError, from_etherscan, + from_etherscan_abi, load, load_abi, load_partial, diff --git a/boa/interpret.py b/boa/interpret.py index 2c076ff9..5949a9f9 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -126,4 +126,11 @@ def from_etherscan( return ABIContractFactory.from_abi_dict(abi, name=name).at(addr) +def from_etherscan_abi( + address: Any, name=None, uri="https://api.etherscan.io/api", api_key=None +): + addr = Address(address) + return fetch_abi_from_etherscan(addr, uri, api_key) + + __all__ = ["BoaError"] From c0b651e0f64ee76b993194370cac772b0fe0ac24 Mon Sep 17 00:00:00 2001 From: mo Date: Mon, 22 Jan 2024 15:16:03 +0100 Subject: [PATCH 03/12] add test --- tests/unitary/test_from_etherscan.py | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/unitary/test_from_etherscan.py diff --git a/tests/unitary/test_from_etherscan.py b/tests/unitary/test_from_etherscan.py new file mode 100644 index 00000000..2b623a13 --- /dev/null +++ b/tests/unitary/test_from_etherscan.py @@ -0,0 +1,36 @@ +import boa, os + +crvusd = '0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E' +voting_agent = "0xE478de485ad2fe566d49342Cbd03E49ed7DB3356" + + +def test_from_etherscan_abi(): + + boa.env.fork(os.environ["ALCHEMY_MAINNET_ENDPOINT"]) + abi = boa.from_etherscan_abi(crvusd, api_key=os.environ["ETHERSCAN_API_KEY"]) + + return abi + + +def test_from_etherscan(): + + boa.env.fork(os.environ["ALCHEMY_MAINNET_ENDPOINT"]) + crvusd = boa.from_etherscan(crvusd, api_key=os.environ["ETHERSCAN_API_KEY"]) + + return crvusd.name() + + +def test_proxy_contract(): + + boa.env.fork(os.environ["ALCHEMY_MAINNET_ENDPOINT"]) + contract = boa.from_etherscan_abi(voting_agent, api_key=os.environ["ETHERSCAN_API_KEY"]) + + return contract.getVote(100) + + +def test_proxy_contract_abi(): + + boa.env.fork(os.environ["ALCHEMY_MAINNET_ENDPOINT"]) + contract = boa.from_etherscan_abi(voting_agent, api_key=os.environ["ETHERSCAN_API_KEY"]) + + return contract From 0bd8ee8dd75e58b95bee4b527e2f4f7ff495e5f8 Mon Sep 17 00:00:00 2001 From: mo Date: Mon, 22 Jan 2024 15:17:36 +0100 Subject: [PATCH 04/12] cleanup --- boa/explorer.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/boa/explorer.py b/boa/explorer.py index b00a4533..d71a5308 100644 --- a/boa/explorer.py +++ b/boa/explorer.py @@ -18,30 +18,27 @@ def fetch_abi_from_etherscan( res = SESSION.get(uri, params=params) res.raise_for_status() + source_data = res.json() - data = res.json() - - if int(data["status"]) != 1: + if int(source_data["status"]) != 1: raise ValueError(f"Failed to retrieve data from API: {data}") - data = data["result"][0] + source_data = source_data["result"][0] - if "Proxy" in data and int(data["Proxy"]) == 1: - address = data.get("Implementation") - else: - address = address + # check if the contract is a proxy + if int(source_data["Proxy"]) == 1: + address = source_data.get("Implementation") + # fetch ABI of `address` params = dict(module="contract", action="getabi", address=address) if api_key is not None: params["apikey"] = api_key res = SESSION.get(uri, params=params) res.raise_for_status() - data = res.json() if int(data["status"]) != 1: raise ValueError(f"Failed to retrieve data from API: {data}") return json.loads(data["result"].strip()) - \ No newline at end of file From b478df8c9e81ff07ccf7e4f271ab0df1162ca811 Mon Sep 17 00:00:00 2001 From: mo Date: Mon, 22 Jan 2024 22:34:22 +0100 Subject: [PATCH 05/12] seperate function to get implementation address --- boa/explorer.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/boa/explorer.py b/boa/explorer.py index d71a5308..62442328 100644 --- a/boa/explorer.py +++ b/boa/explorer.py @@ -10,35 +10,42 @@ def fetch_abi_from_etherscan( address: str, uri: str = "https://api.etherscan.io/api", api_key: str = None ): - address = Address(address) + # check if `address` is a proxy contract + address = Address(_get_implementation_address(address, uri, api_key)) - params = dict(module="contract", action="getsourcecode", address=address) + # fetch ABI of `address` + params = dict(module="contract", action="getabi", address=address) if api_key is not None: params["apikey"] = api_key res = SESSION.get(uri, params=params) res.raise_for_status() - source_data = res.json() + data = res.json() - if int(source_data["status"]) != 1: + if int(data["status"]) != 1: raise ValueError(f"Failed to retrieve data from API: {data}") - source_data = source_data["result"][0] + return json.loads(data["result"].strip()) - # check if the contract is a proxy - if int(source_data["Proxy"]) == 1: - address = source_data.get("Implementation") - # fetch ABI of `address` - params = dict(module="contract", action="getabi", address=address) +def _get_implementation_address( + address: str, uri: str = "https://api.etherscan.io/api", api_key: str = None +): + params = dict(module="contract", action="getsourcecode", address=address) if api_key is not None: params["apikey"] = api_key res = SESSION.get(uri, params=params) res.raise_for_status() - data = res.json() + source_data = res.json() - if int(data["status"]) != 1: - raise ValueError(f"Failed to retrieve data from API: {data}") + if int(source_data["status"]) != 1: + raise ValueError(f"Failed to retrieve source from Etherscan: {source_data}") - return json.loads(data["result"].strip()) + source_data = source_data["result"][0] + + # check if the contract is a proxy + if int(source_data["Proxy"]) == 1: + return source_data.get("Implementation") + else: + return address From 0ab44b6ce655f84ee97313f5b7ef0254d81d4f07 Mon Sep 17 00:00:00 2001 From: mo Date: Mon, 22 Jan 2024 22:38:57 +0100 Subject: [PATCH 06/12] add asserts to tests --- tests/unitary/test_from_etherscan.py | 35 ++++++++++------------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/tests/unitary/test_from_etherscan.py b/tests/unitary/test_from_etherscan.py index 2b623a13..3898091b 100644 --- a/tests/unitary/test_from_etherscan.py +++ b/tests/unitary/test_from_etherscan.py @@ -1,36 +1,25 @@ -import boa, os +import os -crvusd = '0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E' -voting_agent = "0xE478de485ad2fe566d49342Cbd03E49ed7DB3356" - - -def test_from_etherscan_abi(): +import boa - boa.env.fork(os.environ["ALCHEMY_MAINNET_ENDPOINT"]) - abi = boa.from_etherscan_abi(crvusd, api_key=os.environ["ETHERSCAN_API_KEY"]) - - return abi +crvusd = "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" +voting_agent = "0xE478de485ad2fe566d49342Cbd03E49ed7DB3356" def test_from_etherscan(): - boa.env.fork(os.environ["ALCHEMY_MAINNET_ENDPOINT"]) - crvusd = boa.from_etherscan(crvusd, api_key=os.environ["ETHERSCAN_API_KEY"]) - - return crvusd.name() + contract = boa.from_etherscan(crvusd, api_key=os.environ["ETHERSCAN_API_KEY"]) + assert contract.totalSupply() > 0 + assert contract.symbol() == "crvUSD" -def test_proxy_contract(): +def test_proxy_contract(): boa.env.fork(os.environ["ALCHEMY_MAINNET_ENDPOINT"]) - contract = boa.from_etherscan_abi(voting_agent, api_key=os.environ["ETHERSCAN_API_KEY"]) + contract = boa.from_etherscan(voting_agent, api_key=os.environ["ETHERSCAN_API_KEY"]) - return contract.getVote(100) + assert contract.minTime == 43200 -def test_proxy_contract_abi(): - - boa.env.fork(os.environ["ALCHEMY_MAINNET_ENDPOINT"]) - contract = boa.from_etherscan_abi(voting_agent, api_key=os.environ["ETHERSCAN_API_KEY"]) - - return contract +test_from_etherscan() +test_proxy_contract() From ce4bb9bccef942629490fb4b5050a6bef5ab7665 Mon Sep 17 00:00:00 2001 From: mo Date: Tue, 23 Jan 2024 14:45:37 +0100 Subject: [PATCH 07/12] fix test --- tests/unitary/test_from_etherscan.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/unitary/test_from_etherscan.py b/tests/unitary/test_from_etherscan.py index 3898091b..56a15ab6 100644 --- a/tests/unitary/test_from_etherscan.py +++ b/tests/unitary/test_from_etherscan.py @@ -8,7 +8,7 @@ def test_from_etherscan(): boa.env.fork(os.environ["ALCHEMY_MAINNET_ENDPOINT"]) - contract = boa.from_etherscan(crvusd, api_key=os.environ["ETHERSCAN_API_KEY"]) + contract = boa.from_etherscan(crvusd, name="crvUSD", api_key=os.environ["ETHERSCAN_API_KEY"]) assert contract.totalSupply() > 0 assert contract.symbol() == "crvUSD" @@ -16,10 +16,7 @@ def test_from_etherscan(): def test_proxy_contract(): boa.env.fork(os.environ["ALCHEMY_MAINNET_ENDPOINT"]) - contract = boa.from_etherscan(voting_agent, api_key=os.environ["ETHERSCAN_API_KEY"]) + contract = boa.from_etherscan(voting_agent, name="VotingAgent", api_key=os.environ["ETHERSCAN_API_KEY"]) - assert contract.minTime == 43200 + assert contract.minTime() == 43200 - -test_from_etherscan() -test_proxy_contract() From 15a5ebe054d5e9f27cbb2d4d4abd636cea353d2e Mon Sep 17 00:00:00 2001 From: mo Date: Mon, 29 Jan 2024 15:55:35 +0100 Subject: [PATCH 08/12] move tests --- tests/integration/fork/test_from_etherscan.py | 32 +++++++++++++++++++ tests/unitary/test_from_etherscan.py | 22 ------------- 2 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 tests/integration/fork/test_from_etherscan.py delete mode 100644 tests/unitary/test_from_etherscan.py diff --git a/tests/integration/fork/test_from_etherscan.py b/tests/integration/fork/test_from_etherscan.py new file mode 100644 index 00000000..dd18b371 --- /dev/null +++ b/tests/integration/fork/test_from_etherscan.py @@ -0,0 +1,32 @@ +import os +import pytest + +import boa + +crvusd = "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" +voting_agent = "0xE478de485ad2fe566d49342Cbd03E49ed7DB3356" + + +@pytest.fixture(scope="module") +def crvusd_contract(): + contract = boa.from_etherscan(crvusd, name="crvUSD", api_key=os.environ["ETHERSCAN_API_KEY"]) + + return contract + + +@pytest.fixture(scope="module") +def proxy_contract(): + contract = boa.from_etherscan(voting_agent, name="VotingAgent", api_key=os.environ["ETHERSCAN_API_KEY"]) + + return contract + + +def test_crvusd_contract(crvusd_contract): + assert crvusd_contract.totalSupply() > 0 + assert crvusd_contract.symbol() == "crvUSD" + + +def test_proxy_contract(proxy_contract): + assert proxy_contract.minTime() == 43200 + assert proxy_contract.voteTime() == 604800 + assert proxy_contract.minBalance() == 2500000000000000000000 diff --git a/tests/unitary/test_from_etherscan.py b/tests/unitary/test_from_etherscan.py deleted file mode 100644 index 56a15ab6..00000000 --- a/tests/unitary/test_from_etherscan.py +++ /dev/null @@ -1,22 +0,0 @@ -import os - -import boa - -crvusd = "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" -voting_agent = "0xE478de485ad2fe566d49342Cbd03E49ed7DB3356" - - -def test_from_etherscan(): - boa.env.fork(os.environ["ALCHEMY_MAINNET_ENDPOINT"]) - contract = boa.from_etherscan(crvusd, name="crvUSD", api_key=os.environ["ETHERSCAN_API_KEY"]) - - assert contract.totalSupply() > 0 - assert contract.symbol() == "crvUSD" - - -def test_proxy_contract(): - boa.env.fork(os.environ["ALCHEMY_MAINNET_ENDPOINT"]) - contract = boa.from_etherscan(voting_agent, name="VotingAgent", api_key=os.environ["ETHERSCAN_API_KEY"]) - - assert contract.minTime() == 43200 - From 82f6f7d1a87f737f04ea1be1d4ac8c0342fa0c11 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 31 Jan 2024 15:07:32 -0500 Subject: [PATCH 09/12] remove user-facing from_etherscan_abi --- boa/interpret.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/boa/interpret.py b/boa/interpret.py index 5949a9f9..98ba3cc9 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -126,11 +126,4 @@ def from_etherscan( return ABIContractFactory.from_abi_dict(abi, name=name).at(addr) -def from_etherscan_abi( - address: Any, name=None, uri="https://api.etherscan.io/api", api_key=None -): - addr = Address(address) - return fetch_abi_from_etherscan(addr, uri, api_key) - - -__all__ = ["BoaError"] +__all__ = [] From f52bb3e1f97a47e79103930ba701c55f9a5e3f62 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 31 Jan 2024 15:07:48 -0500 Subject: [PATCH 10/12] refactor etherscan fetching code --- boa/explorer.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/boa/explorer.py b/boa/explorer.py index 62442328..2d29349e 100644 --- a/boa/explorer.py +++ b/boa/explorer.py @@ -6,15 +6,7 @@ SESSION = requests.Session() - -def fetch_abi_from_etherscan( - address: str, uri: str = "https://api.etherscan.io/api", api_key: str = None -): - # check if `address` is a proxy contract - address = Address(_get_implementation_address(address, uri, api_key)) - - # fetch ABI of `address` - params = dict(module="contract", action="getabi", address=address) +def _fetch_etherscan(uri: str, api_key: str = None, **params) -> Any: if api_key is not None: params["apikey"] = api_key @@ -25,27 +17,31 @@ def fetch_abi_from_etherscan( if int(data["status"]) != 1: raise ValueError(f"Failed to retrieve data from API: {data}") - return json.loads(data["result"].strip()) + return data -def _get_implementation_address( +def fetch_abi_from_etherscan( address: str, uri: str = "https://api.etherscan.io/api", api_key: str = None ): - params = dict(module="contract", action="getsourcecode", address=address) - if api_key is not None: - params["apikey"] = api_key + # resolve implementation address if `address` is a proxy contract + address = _resolve_implementation_address(address, uri, api_key) - res = SESSION.get(uri, params=params) - res.raise_for_status() - source_data = res.json() + # fetch ABI of `address` + params = dict(module="contract", action="getabi", address=address) + data = _fetch_etherscan(uri, api_key, **params) + + return json.loads(data["result"].strip()) - if int(source_data["status"]) != 1: - raise ValueError(f"Failed to retrieve source from Etherscan: {source_data}") - source_data = source_data["result"][0] +# fetch the address of a contract; resolves at most one layer of indirection +# if the address is a proxy contract. +def _resolve_implementation_address(address: str, uri: str, api_key: str): + params = dict(module="contract", action="getsourcecode", address=address) + data = _fetch_etherscan(uri, api_key, **params) + source_data = data["result"][0] # check if the contract is a proxy if int(source_data["Proxy"]) == 1: - return source_data.get("Implementation") + return source_data["Implementation"] else: return address From db64f6e5959798e9f7ed8a2fcb6bf20e48da32bf Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 31 Jan 2024 15:13:51 -0500 Subject: [PATCH 11/12] small fixes --- boa/__init__.py | 1 - boa/explorer.py | 2 +- tests/integration/fork/test_from_etherscan.py | 10 +++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/boa/__init__.py b/boa/__init__.py index 7ffe59ed..b7301d58 100644 --- a/boa/__init__.py +++ b/boa/__init__.py @@ -8,7 +8,6 @@ from boa.interpret import ( BoaError, from_etherscan, - from_etherscan_abi, load, load_abi, load_partial, diff --git a/boa/explorer.py b/boa/explorer.py index 2d29349e..770084fd 100644 --- a/boa/explorer.py +++ b/boa/explorer.py @@ -6,7 +6,7 @@ SESSION = requests.Session() -def _fetch_etherscan(uri: str, api_key: str = None, **params) -> Any: +def _fetch_etherscan(uri: str, api_key: str = None, **params) -> dict: if api_key is not None: params["apikey"] = api_key diff --git a/tests/integration/fork/test_from_etherscan.py b/tests/integration/fork/test_from_etherscan.py index dd18b371..5798901d 100644 --- a/tests/integration/fork/test_from_etherscan.py +++ b/tests/integration/fork/test_from_etherscan.py @@ -6,17 +6,21 @@ crvusd = "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" voting_agent = "0xE478de485ad2fe566d49342Cbd03E49ed7DB3356" +@pytest.fixture(scope="module") +def api_key(): + return os.environ.get("ETHERSCAN_API_KEY") + @pytest.fixture(scope="module") -def crvusd_contract(): - contract = boa.from_etherscan(crvusd, name="crvUSD", api_key=os.environ["ETHERSCAN_API_KEY"]) +def crvusd_contract(api_key): + contract = boa.from_etherscan(crvusd, name="crvUSD", api_key=api_key) return contract @pytest.fixture(scope="module") def proxy_contract(): - contract = boa.from_etherscan(voting_agent, name="VotingAgent", api_key=os.environ["ETHERSCAN_API_KEY"]) + contract = boa.from_etherscan(voting_agent, name="VotingAgent", api_key=api_key) return contract From 7420f727a31bc378bfa655a6972f81293295bf17 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 31 Jan 2024 15:23:52 -0500 Subject: [PATCH 12/12] fix a fixture --- tests/integration/fork/test_from_etherscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/fork/test_from_etherscan.py b/tests/integration/fork/test_from_etherscan.py index 5798901d..36538809 100644 --- a/tests/integration/fork/test_from_etherscan.py +++ b/tests/integration/fork/test_from_etherscan.py @@ -19,7 +19,7 @@ def crvusd_contract(api_key): @pytest.fixture(scope="module") -def proxy_contract(): +def proxy_contract(api_key): contract = boa.from_etherscan(voting_agent, name="VotingAgent", api_key=api_key) return contract