Skip to content

Commit c421a68

Browse files
committed
Use brownie's Contract API to decode EVM script calls
1 parent 1c2a769 commit c421a68

File tree

2 files changed

+74
-11
lines changed

2 files changed

+74
-11
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,7 @@ To start a new vote please provide the `DEPLOYER` brownie account name (wallet):
169169
export DEPLOYER=<brownie_wallet_name>
170170
```
171171

172-
To run tests with a contract name resolution guided by the Etherscan you should
173-
provide the etherscan API token:
172+
To run scripts that require decoding of EVM scripts and tests with contract name resolution via Etherscan you should provide the etherscan API token:
174173

175174
```bash
176175
export ETHERSCAN_TOKEN=<etherscan_api_key>

utils/evm_script.py

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
import os
33
from collections import defaultdict
44
from functools import lru_cache
5-
from typing import List, Union, Optional, Callable
5+
from typing import List, Union, Optional, Callable, Any
66

77
import eth_abi
8+
from brownie import Contract
89
from brownie.utils import color
910
from eth_typing.evm import HexAddress
10-
from eth_utils import keccak
11-
from hexbytes import HexBytes
1211
from web3 import Web3
1312

13+
# NOTE: The decode_function_call() method is currently unused; it is retained for fallback to the previous decoder version
14+
# (refer to the NOTEs in the decode_evm_script method).
1415
from avotes_parser.core import parse_script, EncodedCall, Call, FuncInput, decode_function_call
1516
from avotes_parser.core.ABI import get_cached_combined
1617

@@ -22,7 +23,7 @@
2223
)
2324

2425
EMPTY_CALLSCRIPT = "0x00000001"
25-
ETHERSCAN_API_KEY = os.getenv("ETHERSCAN_API_KEY", "TGXU5WGVTVYRDDV2MY71R5JYB7147M13FC")
26+
ETHERSCAN_TOKEN = os.getenv("ETHERSCAN_TOKEN", "TGXU5WGVTVYRDDV2MY71R5JYB7147M13FC")
2627

2728

2829
def create_executor_id(id) -> str:
@@ -78,13 +79,18 @@ def decode_evm_script(
7879
logging.basicConfig(level=logging.INFO)
7980
return [repr(err)]
8081

81-
abi_storage = get_abi_cache(ETHERSCAN_API_KEY, specific_net)
82+
# NOTE: The line below is not used in the current version; it is retained for fallback to the previous decoder version (see NOTE below).
83+
abi_storage = get_abi_cache(ETHERSCAN_TOKEN, specific_net)
8284

8385
calls = []
8486
called_contracts = defaultdict(lambda: defaultdict(dict))
8587
for ind, call in enumerate(parsed.calls):
8688
try:
87-
call_info = decode_function_call(call.address, call.method_id, call.encoded_call_data, abi_storage)
89+
call_info = decode_encoded_call(call)
90+
91+
# NOTE: If the decode_encoded_call(call) method fails, uncomment the line below to fall back to the previous version:
92+
#
93+
# call_info = decode_function_call(call.address, call.method_id, call.encoded_call_data, abi_storage)
8894

8995
if call_info is not None:
9096
for inp in filter(is_encoded_script, call_info.inputs):
@@ -135,10 +141,68 @@ def calls_info_pretty_print(call: Union[str, Call, EncodedCall]) -> str:
135141
"""Format printing for Call instance."""
136142
return color.highlight(repr(call))
137143

144+
138145
def encode_error(error: str, values=None) -> str:
139-
encoded_error = error.split('(')[0] + ': '
140-
args = ''
146+
encoded_error = error.split("(")[0] + ": "
147+
args = ""
141148
if values is not None:
142-
args = ', '.join(str(x) for x in values)
149+
args = ", ".join(str(x) for x in values)
143150
return f"{encoded_error}{args}"
144151
return encoded_error
152+
153+
154+
def decode_encoded_call(encoded_call: EncodedCall) -> Optional[Call]:
155+
"""
156+
Decodes an encoded contract call using Brownie's Contract API.
157+
158+
This function replaces AVotesParser.decode_function_call() and converts the provided
159+
EncodedCall into a Call object or returns None if the decoding wasn't successfull.
160+
Unsuccessfull deconding usually happens when the contract is not verified contract on etherscan
161+
162+
Parameters:
163+
encoded_call (EncodedCall): An object containing the target contract address, method id,
164+
and encoded call data and encoded call data length.
165+
166+
Returns:
167+
Call: A Call object with decoded call details if successful, otherwise None if the method
168+
call cannot be decoded.
169+
"""
170+
contract = Contract(encoded_call.address)
171+
172+
# If the method selector is not found in the locally stored contract, try fetching the full ABI from Etherscan.
173+
if encoded_call.method_id not in contract.selectors:
174+
contract = Contract.from_explorer(encoded_call.address)
175+
176+
# If the method is still not found, the contract may not be verified.
177+
if encoded_call.method_id not in contract.selectors:
178+
return None
179+
180+
method_name = contract.selectors[encoded_call.method_id]
181+
contract_method = getattr(contract, method_name)
182+
183+
method_abi = contract_method.abi
184+
185+
calldata_with_selector = encoded_call.method_id + encoded_call.encoded_call_data[2:]
186+
decoded_calldata = contract_method.decode_input(calldata_with_selector)
187+
188+
inputs = [get_func_input(method_abi["inputs"][idx], arg) for idx, arg in enumerate(decoded_calldata)]
189+
190+
properties = {
191+
"constant": "unknown", # Typically False even for pure methods, but not guaranteed.
192+
"payable": method_abi["stateMutability"] == "payable",
193+
"stateMutability": method_abi["stateMutability"],
194+
"type": "function",
195+
}
196+
197+
return Call(
198+
contract.address,
199+
encoded_call.method_id,
200+
method_name,
201+
inputs,
202+
properties,
203+
method_abi["outputs"],
204+
)
205+
206+
207+
def get_func_input(input_abi: dict, value: Any) -> FuncInput:
208+
return FuncInput(input_abi["name"], input_abi.get("internalType", input_abi.get("type")), value)

0 commit comments

Comments
 (0)