Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for custom / typed errors #1728

Merged
merged 6 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 69 additions & 13 deletions brownie/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
#!/usr/bin/python3

import json
import sys
from typing import Optional, Type
from pathlib import Path
from typing import Dict, List, Optional, Type

import eth_abi
import psutil
import yaml
from hexbytes import HexBytes

import brownie
from brownie._config import _get_data_folder
from brownie.convert.utils import build_function_selector, get_type_strings

# network

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 @@ -97,19 +116,9 @@ def __init__(self, exc: ValueError) -> None:

self.message: str = exc["message"].rstrip(".")

if isinstance(exc["data"], str):
# handle parity exceptions - this logic probably is not perfect
if not exc["data"].startswith(ERROR_SIG):
err_msg = exc["data"]
if err_msg.endswith("0x"):
err_msg = exc["data"][:-2].strip()
raise ValueError(f"{self.message}: {err_msg}") from None

if isinstance(exc["data"], str) and exc["data"].startswith("0x"):
self.revert_type = "revert"
err_msg = exc["data"][len(ERROR_SIG) :]
(err_msg,) = eth_abi.decode(["string"], HexBytes(err_msg))
self.revert_msg = err_msg

self.revert_msg = decode_typed_error(exc["data"])
return

try:
Expand All @@ -125,6 +134,9 @@ def __init__(self, exc: ValueError) -> None:
self.pc -= 1

self.revert_msg = data.get("reason")
if isinstance(data.get("reason"), str) and data["reason"].startswith("0x"):
self.revert_msg = decode_typed_error(data["reason"])

self.dev_revert_msg = brownie.project.build._get_dev_revert(self.pc)
if self.revert_msg is None and self.revert_type in ("revert", "invalid opcode"):
self.revert_msg = self.dev_revert_msg
Expand Down Expand Up @@ -239,3 +251,47 @@ class BrownieTestWarning(Warning):

class BrownieConfigWarning(Warning):
pass


def __get_path() -> Path:
return _get_data_folder().joinpath("errors.json")


def parse_errors_from_abi(abi: List):
updated = False
for item in [i for i in abi if i.get("type", None) == "error"]:
selector = build_function_selector(item)
if selector in _errors:
continue
updated = True
_errors[selector] = item

if updated:
with __get_path().open("w") as fp:
json.dump(_errors, fp, sort_keys=True, indent=2)


_errors: Dict = {ERROR_SIG: {"name": "Error", "inputs": [{"name": "", "type": "string"}]}}

try:
with __get_path().open() as fp:
_errors.update(json.load(fp))
except (FileNotFoundError, json.decoder.JSONDecodeError):
pass


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:])
if selector == ERROR_SIG:
return result[0]
else:
return f"{_errors[selector]['name']}: {', '.join(result)}"
else:
return f"Unknown typed error: {data}"
8 changes: 4 additions & 4 deletions brownie/network/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,9 +480,9 @@ def _check_for_revert(self, tx: Dict) -> None:
skip_keys = {"gasPrice", "maxFeePerGas", "maxPriorityFeePerGas"}
web3.eth.call({k: v for k, v in tx.items() if k not in skip_keys and v})
except ValueError as exc:
msg = exc.args[0]["message"] if isinstance(exc.args[0], dict) else str(exc)
exc = VirtualMachineError(exc)
raise ValueError(
f"Execution reverted during call: '{msg}'. This transaction will likely revert. "
f"Execution reverted during call: '{exc.revert_msg}'. This transaction will likely revert. "
"If you wish to broadcast, include `allow_revert:True` as a transaction parameter.",
) from None

Expand Down Expand Up @@ -617,9 +617,9 @@ def estimate_gas(
if revert_gas_limit:
return revert_gas_limit

msg = exc.args[0]["message"] if isinstance(exc.args[0], dict) else str(exc)
exc = VirtualMachineError(exc)
raise ValueError(
f"Gas estimation failed: '{msg}'. This transaction will likely revert. "
f"Gas estimation failed: '{exc.revert_msg}'. This transaction will likely revert. "
"If you wish to broadcast, you must set the gas limit manually."
)

Expand Down
33 changes: 11 additions & 22 deletions brownie/network/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@
ContractNotFound,
UndeployedLibrary,
VirtualMachineError,
decode_typed_error,
parse_errors_from_abi,
)
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 @@ -86,12 +87,11 @@
"aurorascan": "AURORASCAN_TOKEN",
"moonscan": "MOONSCAN_TOKEN",
"gnosisscan": "GNOSISSCAN_TOKEN",
"base": "BASESCAN_TOKEN"
"base": "BASESCAN_TOKEN",
}


class _ContractBase:

_dir_color = "bright magenta"

def __init__(self, project: Any, build: Dict, sources: Dict) -> None:
Expand All @@ -106,6 +106,7 @@ def __init__(self, project: Any, build: Dict, sources: Dict) -> None:
self.signatures = {
i["name"]: build_function_selector(i) for i in self.abi if i["type"] == "function"
}
parse_errors_from_abi(self.abi)

@property
def abi(self) -> List:
Expand Down Expand Up @@ -508,7 +509,6 @@ def _slice_source(self, source: str, offset: list) -> str:


class ContractConstructor:

_dir_color = "bright magenta"

def __init__(self, parent: "ContractContainer", name: str) -> None:
Expand Down Expand Up @@ -562,7 +562,7 @@ def __call__(
required_confs=tx["required_confs"],
allow_revert=tx.get("allow_revert"),
publish_source=publish_source,
silent=silent
silent=silent,
)

@staticmethod
Expand Down Expand Up @@ -1621,7 +1621,6 @@ def info(self) -> None:


class _ContractMethod:

_dir_color = "bright magenta"

def __init__(
Expand Down Expand Up @@ -1700,23 +1699,14 @@ 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:
def transact(self, silent: bool = False, *args: Tuple) -> TransactionReceiptType:
"""
Broadcast a transaction that calls this contract method.

Expand Down Expand Up @@ -1751,7 +1741,7 @@ def transact(self, silent: bool = False, *args: Tuple) -> TransactionReceiptTyp
required_confs=tx["required_confs"],
data=self.encode_input(*args),
allow_revert=tx["allow_revert"],
silent=silent
silent=silent,
)

def decode_input(self, hexstr: str) -> List:
Expand Down Expand Up @@ -1970,7 +1960,6 @@ def _get_tx(owner: Optional[AccountsType], args: Tuple) -> Tuple:
def _get_method_object(
address: str, abi: Dict, name: str, owner: Optional[AccountsType], natspec: Dict
) -> Union["ContractCall", "ContractTx"]:

if "constant" in abi:
constant = abi["constant"]
else:
Expand Down
2 changes: 1 addition & 1 deletion brownie/network/middlewares/ganache7.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def process_request(self, make_request: Callable, method: str, params: List) ->
# "VM Exception while processing transaction: {reason} {message}"
msg = result["error"]["message"].split(": ", maxsplit=1)[-1]
if msg.startswith("revert"):
data = {"error": "revert", "reason": msg[7:]}
data = {"error": "revert", "reason": result["error"]["data"]}
else:
data = {"error": msg, "reason": None}
result["error"]["data"] = {"0x": data}
Expand Down
3 changes: 3 additions & 0 deletions brownie/network/middlewares/hardhat.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def process_request(self, make_request: Callable, method: str, params: List) ->
data.update({"error": "revert", "reason": message[7:]})
elif "reverted with reason string '" in message:
data.update(error="revert", reason=re.findall(".*?'(.*)'$", message)[0])
elif "reverted with an unrecognized custom error" in message:
message = message[message.index("0x") : -1]
data.update(error="revert", reason=message)
else:
data["error"] = message
return result
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
Loading
Loading