Skip to content

Commit e862a2c

Browse files
karlbkcloweswolovim
authored
Parse revert reason (#1585)
* Parse revert reason when `call` fails Solidity allows messages in `require` statements which can help a lot when debugging. This commit parses the revert reasons from failing `call`s and raises them as `SolidityError` exceptions. Closes #941. * Add test for revert interface * Convert TransactionFailed into SolidityError In an effort to be backend-agnostic eth-tester changes errors that are specific to one EVM (like Revert) to a TransactionFailed error. This is useful when using only eth-tester. But as web3.py also works directly with other eth clients, we have to abstract over the different revert behaviors ourselves. * Check revert reason in test * fixup! Add test for revert interface Fix linting errors * Revert contract working in geth fixture * Beginnings of Geth integration test * Minor refactoring, eth-tester tests passing * Add parity fixture with revert contract * address linting issues * fix eth_module test and naming * contain eth_tester exceptions * fix lint * check eth_estimateGas for revert exception * wip: new fixture + latest geth revert messages * update geth fixture * handle eth-tester revert messages * wip: standardize client errors * lints * upgrade parity fixture + handle parity revert * dedupe constants, more cleanup * pr review fixes * more pr review Co-authored-by: Keri <kclowes@users.noreply.github.com> Co-authored-by: Marc Garreau <marcdgarreau@gmail.com>
1 parent 4e0e932 commit e862a2c

File tree

21 files changed

+499
-192
lines changed

21 files changed

+499
-192
lines changed

newsfragments/941.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Raise `SolidityError` exceptions that contain the revert reason when a `call` fails.

tests/core/contracts/conftest.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
CONTRACT_RECEIVE_FUNCTION_CODE,
3737
CONTRACT_RECEIVE_FUNCTION_RUNTIME,
3838
)
39+
from web3._utils.module_testing.revert_contract import (
40+
_REVERT_CONTRACT_ABI,
41+
REVERT_CONTRACT_BYTECODE,
42+
REVERT_CONTRACT_RUNTIME_CODE,
43+
)
3944

4045
CONTRACT_NESTED_TUPLE_SOURCE = """
4146
pragma solidity >=0.4.19 <0.6.0;
@@ -893,6 +898,37 @@ def CallerTesterContract(web3, CALLER_TESTER_CONTRACT):
893898
return web3.eth.contract(**CALLER_TESTER_CONTRACT)
894899

895900

901+
@pytest.fixture()
902+
def REVERT_CONTRACT_CODE():
903+
return REVERT_CONTRACT_BYTECODE
904+
905+
906+
@pytest.fixture()
907+
def REVERT_CONTRACT_RUNTIME():
908+
return REVERT_CONTRACT_RUNTIME_CODE
909+
910+
911+
@pytest.fixture()
912+
def REVERT_CONTRACT_ABI():
913+
return _REVERT_CONTRACT_ABI
914+
915+
916+
@pytest.fixture()
917+
def REVERT_FUNCTION_CONTRACT(REVERT_CONTRACT_CODE,
918+
REVERT_CONTRACT_RUNTIME,
919+
REVERT_CONTRACT_ABI):
920+
return {
921+
'bytecode': REVERT_CONTRACT_CODE,
922+
'bytecode_runtime': REVERT_CONTRACT_RUNTIME,
923+
'abi': REVERT_CONTRACT_ABI,
924+
}
925+
926+
927+
@pytest.fixture()
928+
def RevertContract(web3, REVERT_FUNCTION_CONTRACT):
929+
return web3.eth.contract(**REVERT_FUNCTION_CONTRACT)
930+
931+
896932
class LogFunctions:
897933
LogAnonymous = 0
898934
LogNoArguments = 1

tests/core/contracts/test_contract_call_interface.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
MismatchedABI,
3131
NoABIFound,
3232
NoABIFunctionsFound,
33+
SolidityError,
3334
ValidationError,
3435
)
3536

@@ -120,6 +121,19 @@ def payable_tester_contract(web3, PayableTesterContract, address_conversion_func
120121
return deploy(web3, PayableTesterContract, address_conversion_func)
121122

122123

124+
@pytest.fixture()
125+
def revert_contract(web3, RevertContract, address_conversion_func):
126+
return deploy(web3, RevertContract, address_conversion_func)
127+
128+
129+
@pytest.fixture()
130+
def call_transaction():
131+
return {
132+
'data': '0x61bc221a',
133+
'to': '0xc305c901078781C232A2a521C2aF7980f8385ee9'
134+
}
135+
136+
123137
@pytest.fixture(params=[
124138
'0x0406040604060406040604060406040604060406040604060406040604060406',
125139
'0406040604060406040604060406040604060406040604060406040604060406',
@@ -855,3 +869,11 @@ def test_call_tuple_contract(tuple_contract, method_input, expected):
855869
def test_call_nested_tuple_contract(nested_tuple_contract, method_input, expected):
856870
result = nested_tuple_contract.functions.method(method_input).call()
857871
assert result == expected
872+
873+
874+
def test_call_revert_contract(revert_contract):
875+
with pytest.raises(SolidityError, match="Function has been reverted."):
876+
# eth-tester will do a gas estimation if we don't submit a gas value,
877+
# which does not contain the revert reason. Avoid that by giving a gas
878+
# value.
879+
revert_contract.functions.revertWithMessage().call({'gas': 100000})
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import pytest
2+
3+
from web3._utils.method_formatters import (
4+
get_error_formatters,
5+
raise_solidity_error_on_revert,
6+
)
7+
from web3._utils.rpc_abi import (
8+
RPC,
9+
)
10+
from web3.exceptions import (
11+
SolidityError,
12+
)
13+
from web3.types import (
14+
RPCResponse,
15+
)
16+
17+
REVERT_WITH_MSG = RPCResponse({
18+
'jsonrpc': '2.0',
19+
'error': {
20+
'code': -32015,
21+
'message': 'VM execution error.',
22+
'data': (
23+
'Reverted '
24+
'0x08c379a'
25+
'00000000000000000000000000000000000000000000000000000000000000020'
26+
'0000000000000000000000000000000000000000000000000000000000000016'
27+
'6e6f7420616c6c6f77656420746f206d6f6e69746f7200000000000000000000'
28+
),
29+
},
30+
'id': 2987,
31+
})
32+
33+
REVERT_WITHOUT_MSG = RPCResponse({
34+
'jsonrpc': '2.0',
35+
'error': {
36+
'code': -32015,
37+
'message': 'VM execution error.',
38+
'data': 'Reverted 0x',
39+
},
40+
'id': 2987,
41+
})
42+
43+
OTHER_ERROR = RPCResponse({
44+
"jsonrpc": "2.0",
45+
"error": {
46+
"code": -32601,
47+
"message": "Method not found",
48+
},
49+
"id": 1,
50+
})
51+
52+
53+
@pytest.mark.parametrize(
54+
"response,expected",
55+
(
56+
(REVERT_WITH_MSG, 'execution reverted: not allowed to monitor'),
57+
(REVERT_WITHOUT_MSG, 'execution reverted'),
58+
),
59+
ids=[
60+
'test-get-revert-reason-with-msg',
61+
'test-get-revert-reason-without-msg',
62+
])
63+
def test_get_revert_reason(response, expected) -> None:
64+
with pytest.raises(SolidityError, match=expected):
65+
raise_solidity_error_on_revert(response)
66+
67+
68+
def test_get_revert_reason_other_error() -> None:
69+
assert raise_solidity_error_on_revert(OTHER_ERROR) is OTHER_ERROR
70+
71+
72+
def test_get_error_formatters() -> None:
73+
formatters = get_error_formatters(RPC.eth_call)
74+
with pytest.raises(SolidityError, match='not allowed to monitor'):
75+
formatters(REVERT_WITH_MSG)
76+
with pytest.raises(SolidityError):
77+
formatters(REVERT_WITHOUT_MSG)
78+
assert formatters(OTHER_ERROR) == OTHER_ERROR

tests/integration/conftest.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
MATH_ABI,
1010
MATH_BYTECODE,
1111
)
12+
from web3._utils.module_testing.revert_contract import (
13+
_REVERT_CONTRACT_ABI,
14+
REVERT_CONTRACT_BYTECODE,
15+
)
1216

1317

1418
@pytest.fixture(scope="module")
@@ -19,7 +23,17 @@ def math_contract_factory(web3):
1923

2024
@pytest.fixture(scope="module")
2125
def emitter_contract_factory(web3):
22-
contract_factory = web3.eth.contract(abi=CONTRACT_EMITTER_ABI, bytecode=CONTRACT_EMITTER_CODE)
26+
contract_factory = web3.eth.contract(
27+
abi=CONTRACT_EMITTER_ABI, bytecode=CONTRACT_EMITTER_CODE
28+
)
29+
return contract_factory
30+
31+
32+
@pytest.fixture(scope="module")
33+
def revert_contract_factory(web3):
34+
contract_factory = web3.eth.contract(
35+
abi=_REVERT_CONTRACT_ABI, bytecode=REVERT_CONTRACT_BYTECODE
36+
)
2337
return contract_factory
2438

2539

tests/integration/generate_fixtures/common.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,23 @@
3434
"config": {
3535
"chainId": 131277322940537, # the string 'web3py' as an integer
3636
"homesteadBlock": 0,
37+
"byzantiumBlock": 0,
38+
"constantinopleBlock": 0,
3739
"eip150Block": 0,
3840
"eip155Block": 0,
3941
"eip158Block": 0,
40-
"eip160Block": 0,
4142
},
4243
"nonce": "0x0000000000000042",
4344
"alloc": {
44-
COINBASE: {
45-
"balance": "1000000000000000000000000000"
46-
},
47-
UNLOCKABLE_ACCOUNT: {
48-
"balance": "1000000000000000000000000000"
49-
},
50-
RAW_TXN_ACCOUNT: {
51-
"balance": "1000000000000000000000000000"
52-
}
45+
COINBASE: {"balance": "1000000000000000000000000000"},
46+
UNLOCKABLE_ACCOUNT: {"balance": "1000000000000000000000000000"},
47+
RAW_TXN_ACCOUNT: {"balance": "1000000000000000000000000000"},
48+
"0000000000000000000000000000000000000001": {"balance": "1"},
49+
"0000000000000000000000000000000000000002": {"balance": "1"},
50+
"0000000000000000000000000000000000000003": {"balance": "1"},
51+
"0000000000000000000000000000000000000004": {"balance": "1"},
52+
"0000000000000000000000000000000000000005": {"balance": "1"},
53+
"0000000000000000000000000000000000000006": {"balance": "1"},
5354
},
5455
"timestamp": "0x00",
5556
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",

0 commit comments

Comments
 (0)