|
2 | 2 | import os |
3 | 3 | from collections import defaultdict |
4 | 4 | from functools import lru_cache |
5 | | -from typing import List, Union, Optional, Callable |
| 5 | +from typing import List, Union, Optional, Callable, Any |
6 | 6 |
|
7 | 7 | import eth_abi |
| 8 | +from brownie import Contract |
8 | 9 | from brownie.utils import color |
9 | 10 | from eth_typing.evm import HexAddress |
10 | | -from eth_utils import keccak |
11 | | -from hexbytes import HexBytes |
12 | 11 | from web3 import Web3 |
13 | 12 |
|
| 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). |
14 | 15 | from avotes_parser.core import parse_script, EncodedCall, Call, FuncInput, decode_function_call |
15 | 16 | from avotes_parser.core.ABI import get_cached_combined |
16 | 17 |
|
|
22 | 23 | ) |
23 | 24 |
|
24 | 25 | EMPTY_CALLSCRIPT = "0x00000001" |
25 | | -ETHERSCAN_API_KEY = os.getenv("ETHERSCAN_API_KEY", "TGXU5WGVTVYRDDV2MY71R5JYB7147M13FC") |
| 26 | +ETHERSCAN_TOKEN = os.getenv("ETHERSCAN_TOKEN", "TGXU5WGVTVYRDDV2MY71R5JYB7147M13FC") |
26 | 27 |
|
27 | 28 |
|
28 | 29 | def create_executor_id(id) -> str: |
@@ -78,13 +79,18 @@ def decode_evm_script( |
78 | 79 | logging.basicConfig(level=logging.INFO) |
79 | 80 | return [repr(err)] |
80 | 81 |
|
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) |
82 | 84 |
|
83 | 85 | calls = [] |
84 | 86 | called_contracts = defaultdict(lambda: defaultdict(dict)) |
85 | 87 | for ind, call in enumerate(parsed.calls): |
86 | 88 | 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) |
88 | 94 |
|
89 | 95 | if call_info is not None: |
90 | 96 | 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: |
135 | 141 | """Format printing for Call instance.""" |
136 | 142 | return color.highlight(repr(call)) |
137 | 143 |
|
| 144 | + |
138 | 145 | def encode_error(error: str, values=None) -> str: |
139 | | - encoded_error = error.split('(')[0] + ': ' |
140 | | - args = '' |
| 146 | + encoded_error = error.split("(")[0] + ": " |
| 147 | + args = "" |
141 | 148 | if values is not None: |
142 | | - args = ', '.join(str(x) for x in values) |
| 149 | + args = ", ".join(str(x) for x in values) |
143 | 150 | return f"{encoded_error}{args}" |
144 | 151 | 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