Skip to content

Commit

Permalink
refactor: aggregate logic for decoding typed errors
Browse files Browse the repository at this point in the history
  • Loading branch information
iamdefinitelyahuman committed Jan 31, 2024
1 parent 11f356c commit c68a62e
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 55 deletions.
21 changes: 20 additions & 1 deletion brownie/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:])
Expand Down
20 changes: 6 additions & 14 deletions brownie/network/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
"""
Expand Down
25 changes: 6 additions & 19 deletions brownie/network/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 ""
Expand Down
22 changes: 1 addition & 21 deletions brownie/project/compiler/solidity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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]
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand Down

0 comments on commit c68a62e

Please sign in to comment.