diff --git a/brownie/exceptions.py b/brownie/exceptions.py index 5fdfd2009..66d7c532a 100644 --- a/brownie/exceptions.py +++ b/brownie/exceptions.py @@ -19,6 +19,21 @@ ERROR_SIG = "0x08c379a0" +# error codes used in Solidity >=0.8.0 +# docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require +SOLIDITY_ERROR_CODES = { + 1: "Failed assertion", + 17: "Integer overflow", + 18: "Division or modulo by zero", + 33: "Conversion to enum out of bounds", + 24: "Access to storage byte array that is incorrectly encoded", + 49: "Pop from empty array", + 50: "Index out of range", + 65: "Attempted to allocate too much memory", + 81: "Call to zero-initialized variable of internal function type", +} + + class UnknownAccount(Exception): pass @@ -103,7 +118,7 @@ def __init__(self, exc: ValueError) -> None: if isinstance(exc["data"], str) and exc["data"].startswith("0x"): self.revert_type = "revert" - self.revert_msg = self._decode_custom_error(exc["data"]) + self.revert_msg = decode_typed_error(exc["data"]) return try: @@ -267,6 +282,10 @@ def parse_errors_from_abi(abi: List): def decode_typed_error(data: str) -> str: selector = data[:10] + if selector == "0x4e487b71": + # special case, solidity compiler panics + error_code = int(data[4:].hex(), 16) + return SOLIDITY_ERROR_CODES.get(error_code, f"Unknown compiler Panic: {error_code}") if selector in _errors: types_list = get_type_strings(_errors[selector]["inputs"]) result = eth_abi.decode(types_list, HexBytes(data)[4:]) diff --git a/brownie/network/contract.py b/brownie/network/contract.py index 1041bca6d..4def75ecf 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -58,9 +58,10 @@ UndeployedLibrary, VirtualMachineError, parse_errors_from_abi, + decode_typed_error, ) from brownie.project import compiler, ethpm -from brownie.project.compiler.solidity import SOLIDITY_ERROR_CODES + from brownie.project.flattener import Flattener from brownie.typing import AccountsType, TransactionReceiptType from brownie.utils import color @@ -1704,21 +1705,12 @@ def call( except ValueError as e: raise VirtualMachineError(e) from None - selector = HexBytes(data)[:4].hex() - - if selector == "0x08c379a0": - revert_str = eth_abi.decode(["string"], HexBytes(data)[4:])[0] - raise ValueError(f"Call reverted: {revert_str}") - elif selector == "0x4e487b71": - error_code = int(HexBytes(data)[4:].hex(), 16) - if error_code in SOLIDITY_ERROR_CODES: - revert_str = SOLIDITY_ERROR_CODES[error_code] - else: - revert_str = f"Panic (error code: {error_code})" - raise ValueError(f"Call reverted: {revert_str}") if self.abi["outputs"] and not data: raise ValueError("No data was returned - the call likely reverted") - return self.decode_output(data) + try: + return self.decode_output(data) + except: + raise ValueError(f"Call reverted: {decode_typed_error(data)}") from None def transact(self, silent: bool = False, *args: Tuple) -> TransactionReceiptType: """ diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index 73ac8e6c8..f7e3d8c61 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -20,10 +20,9 @@ from brownie._config import CONFIG from brownie.convert import EthAddress, Wei -from brownie.exceptions import ContractNotFound, RPCRequestError +from brownie.exceptions import ContractNotFound, RPCRequestError, decode_typed_error from brownie.project import build from brownie.project import main as project_main -from brownie.project.compiler.solidity import SOLIDITY_ERROR_CODES from brownie.project.sources import highlight_source from brownie.test import coverage from brownie.utils import color @@ -632,8 +631,8 @@ def _get_trace(self) -> None: try: trace = web3.provider.make_request( # type: ignore # Set enableMemory to all RPC as anvil return the memory key - "debug_traceTransaction", (self.txid, { - "disableStorage": CONFIG.mode != "console", "enableMemory": True}) + "debug_traceTransaction", + (self.txid, {"disableStorage": CONFIG.mode != "console", "enableMemory": True}), ) except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: msg = f"Encountered a {type(e).__name__} while requesting " @@ -679,7 +678,8 @@ def _get_trace(self) -> None: # Check if gasCost is hex before converting. if isinstance(step["gasCost"], str): step["gasCost"] = int.from_bytes( - HexBytes(step["gasCost"]), "big", signed=True) + HexBytes(step["gasCost"]), "big", signed=True + ) if isinstance(step["pc"], str): # Check if pc is hex before converting. step["pc"] = int(step["pc"], 16) @@ -718,20 +718,7 @@ def _reverted_trace(self, trace: Sequence) -> None: if step["op"] == "REVERT" and int(step["stack"][-2], 16): # get returned error string from stack data = _get_memory(step, -1) - - selector = data[:4].hex() - - if selector == "0x4e487b71": # keccak of Panic(uint256) - error_code = int(data[4:].hex(), 16) - if error_code in SOLIDITY_ERROR_CODES: - self._revert_msg = SOLIDITY_ERROR_CODES[error_code] - else: - self._revert_msg = f"Panic (error code: {error_code})" - elif selector == "0x08c379a0": # keccak of Error(string) - self._revert_msg = decode(["string"], data[4:])[0] - else: - # TODO: actually parse the data - self._revert_msg = f"typed error: {data.hex()}" + self._revert_msg = decode_typed_error(data.hex()) elif self.contract_address: self._revert_msg = "invalid opcode" if step["op"] == "INVALID" else "" diff --git a/brownie/project/compiler/solidity.py b/brownie/project/compiler/solidity.py index 85a55b062..0e497a52f 100644 --- a/brownie/project/compiler/solidity.py +++ b/brownie/project/compiler/solidity.py @@ -12,7 +12,7 @@ from solcast.nodes import NodeBase, is_inside_offset from brownie._config import EVM_EQUIVALENTS -from brownie.exceptions import CompilerError, IncompatibleSolcVersion +from brownie.exceptions import CompilerError, IncompatibleSolcVersion, SOLIDITY_ERROR_CODES # noqa from brownie.project.compiler.utils import _get_alias, expand_source_map from . import sources @@ -32,20 +32,6 @@ ("byzantium", Version("0.4.0")), ] -# error codes used in Solidity >=0.8.0 -# docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require -SOLIDITY_ERROR_CODES = { - 1: "Failed assertion", - 17: "Integer overflow", - 18: "Division or modulo by zero", - 33: "Conversion to enum out of bounds", - 24: "Access to storage byte array that is incorrectly encoded", - 49: "Pop from empty array", - 50: "Index out of range", - 65: "Attempted to allocate too much memory", - 81: "Call to zero-initialized variable of internal function type", -} - def get_version() -> Version: return solcx.get_solc_version(with_commit_hash=True) @@ -54,7 +40,6 @@ def get_version() -> Version: def compile_from_input_json( input_json: Dict, silent: bool = True, allow_paths: Optional[str] = None ) -> Dict: - """ Compiles contracts from a standard input json. @@ -131,7 +116,6 @@ def find_solc_versions( install_latest: bool = False, silent: bool = True, ) -> Dict: - """ Analyzes contract pragmas and determines which solc version(s) to use. @@ -199,7 +183,6 @@ def find_best_solc_version( install_latest: bool = False, silent: bool = True, ) -> str: - """ Analyzes contract pragmas and finds the best version compatible with all sources. @@ -217,7 +200,6 @@ def find_best_solc_version( available_versions, installed_versions = _get_solc_version_list() for path, source in contract_sources.items(): - pragma_spec = sources.get_pragma_spec(source, path) installed_versions = [i for i in installed_versions if i in pragma_spec] available_versions = [i for i in available_versions if i in pragma_spec] @@ -528,7 +510,6 @@ def _find_revert_offset( fn_node: NodeBase, fn_name: Optional[str], ) -> None: - # attempt to infer a source offset for reverts that do not have one if source_map: @@ -550,7 +531,6 @@ def _find_revert_offset( # get the offset of the next instruction next_offset = None if source_map and source_map[0][2] != -1: - next_offset = (source_map[0][0], source_map[0][0] + source_map[0][1]) # if the next instruction offset is not equal to the offset of the active function,