Skip to content

Commit c65688c

Browse files
committed
chain/ethereum: Move eth_call helper functions to separate module
1 parent 896267b commit c65688c

File tree

3 files changed

+139
-130
lines changed

3 files changed

+139
-130
lines changed

chain/ethereum/src/call_helper.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use crate::{ContractCallError, ENV_VARS};
2+
use graph::{
3+
abi,
4+
data::store::ethereum::call,
5+
prelude::{web3, Logger},
6+
slog::info,
7+
};
8+
9+
// ------------------------------------------------------------------
10+
// Constants and helper utilities used across eth_call handling
11+
// ------------------------------------------------------------------
12+
13+
// Try to check if the call was reverted. The JSON-RPC response for reverts is
14+
// not standardized, so we have ad-hoc checks for each Ethereum client.
15+
16+
// 0xfe is the "designated bad instruction" of the EVM, and Solidity uses it for
17+
// asserts.
18+
const PARITY_BAD_INSTRUCTION_FE: &str = "Bad instruction fe";
19+
20+
// 0xfd is REVERT, but on some contracts, and only on older blocks,
21+
// this happens. Makes sense to consider it a revert as well.
22+
const PARITY_BAD_INSTRUCTION_FD: &str = "Bad instruction fd";
23+
24+
const PARITY_BAD_JUMP_PREFIX: &str = "Bad jump";
25+
const PARITY_STACK_LIMIT_PREFIX: &str = "Out of stack";
26+
27+
// See f0af4ab0-6b7c-4b68-9141-5b79346a5f61.
28+
const PARITY_OUT_OF_GAS: &str = "Out of gas";
29+
30+
// Also covers Nethermind reverts
31+
const PARITY_VM_EXECUTION_ERROR: i64 = -32015;
32+
const PARITY_REVERT_PREFIX: &str = "revert";
33+
34+
const XDAI_REVERT: &str = "revert";
35+
36+
// Deterministic Geth execution errors. We might need to expand this as
37+
// subgraphs come across other errors. See
38+
// https://github.com/ethereum/go-ethereum/blob/cd57d5cd38ef692de8fbedaa56598b4e9fbfbabc/core/vm/errors.go
39+
const GETH_EXECUTION_ERRORS: &[&str] = &[
40+
// The "revert" substring covers a few known error messages, including:
41+
// Hardhat: "error: transaction reverted",
42+
// Ganache and Moonbeam: "vm exception while processing transaction: revert",
43+
// Geth: "execution reverted"
44+
// And others.
45+
"revert",
46+
"invalid jump destination",
47+
"invalid opcode",
48+
// Ethereum says 1024 is the stack sizes limit, so this is deterministic.
49+
"stack limit reached 1024",
50+
// See f0af4ab0-6b7c-4b68-9141-5b79346a5f61 for why the gas limit is considered deterministic.
51+
"out of gas",
52+
"stack underflow",
53+
];
54+
55+
/// Helper that checks if a geth style RPC error message corresponds to a revert.
56+
fn is_geth_revert_message(message: &str) -> bool {
57+
let env_geth_call_errors = ENV_VARS.geth_eth_call_errors.iter();
58+
let mut execution_errors = GETH_EXECUTION_ERRORS
59+
.iter()
60+
.copied()
61+
.chain(env_geth_call_errors.map(|s| s.as_str()));
62+
execution_errors.any(|e| message.to_lowercase().contains(e))
63+
}
64+
65+
/// Decode a Solidity revert(reason) payload, returning the reason string when possible.
66+
fn as_solidity_revert_reason(bytes: &[u8]) -> Option<String> {
67+
let selector = &tiny_keccak::keccak256(b"Error(string)")[..4];
68+
if bytes.len() >= 4 && &bytes[..4] == selector {
69+
abi::DynSolType::String
70+
.abi_decode(&bytes[4..])
71+
.ok()
72+
.and_then(|val| val.clone().as_str().map(ToOwned::to_owned))
73+
} else {
74+
None
75+
}
76+
}
77+
78+
/// Interpret the error returned by `eth_call`, distinguishing genuine failures from
79+
/// EVM reverts. Returns `Ok(Null)` for reverts or a proper error otherwise.
80+
pub fn interpret_eth_call_error(
81+
logger: &Logger,
82+
err: web3::Error,
83+
) -> Result<call::Retval, ContractCallError> {
84+
fn reverted(logger: &Logger, reason: &str) -> Result<call::Retval, ContractCallError> {
85+
info!(logger, "Contract call reverted"; "reason" => reason);
86+
Ok(call::Retval::Null)
87+
}
88+
89+
if let web3::Error::Rpc(rpc_error) = &err {
90+
if is_geth_revert_message(&rpc_error.message) {
91+
return reverted(logger, &rpc_error.message);
92+
}
93+
}
94+
95+
if let web3::Error::Rpc(rpc_error) = &err {
96+
let code = rpc_error.code.code();
97+
let data = rpc_error.data.as_ref().and_then(|d| d.as_str());
98+
99+
if code == PARITY_VM_EXECUTION_ERROR {
100+
if let Some(data) = data {
101+
if is_parity_revert(data) {
102+
return reverted(logger, &parity_revert_reason(data));
103+
}
104+
}
105+
}
106+
}
107+
108+
Err(ContractCallError::Web3Error(err))
109+
}
110+
111+
fn is_parity_revert(data: &str) -> bool {
112+
data.to_lowercase().starts_with(PARITY_REVERT_PREFIX)
113+
|| data.starts_with(PARITY_BAD_JUMP_PREFIX)
114+
|| data.starts_with(PARITY_STACK_LIMIT_PREFIX)
115+
|| data == PARITY_BAD_INSTRUCTION_FE
116+
|| data == PARITY_BAD_INSTRUCTION_FD
117+
|| data == PARITY_OUT_OF_GAS
118+
|| data == XDAI_REVERT
119+
}
120+
121+
/// Checks if the given `web3::Error` corresponds to a Parity / Nethermind style EVM
122+
/// revert and, if so, tries to extract a human-readable revert reason. Returns `Some`
123+
/// with the reason when the error is identified as a revert, otherwise `None`.
124+
fn parity_revert_reason(data: &str) -> String {
125+
if data == PARITY_BAD_INSTRUCTION_FE {
126+
return PARITY_BAD_INSTRUCTION_FE.to_owned();
127+
}
128+
129+
// Otherwise try to decode a Solidity revert reason payload.
130+
let payload = data.trim_start_matches(PARITY_REVERT_PREFIX);
131+
hex::decode(payload)
132+
.ok()
133+
.and_then(|decoded| as_solidity_revert_reason(&decoded))
134+
.unwrap_or_else(|| "no reason".to_owned())
135+
}

chain/ethereum/src/ethereum_adapter.rs

Lines changed: 3 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use graph::{
2828
blockchain::{block_stream::BlockWithTriggers, BlockPtr, IngestorError},
2929
prelude::{
3030
anyhow::{self, anyhow, bail, ensure, Context},
31-
async_trait, debug, error, hex, info, retry, serde_json as json, tiny_keccak, trace, warn,
31+
async_trait, debug, error, hex, info, retry, serde_json as json, trace, warn,
3232
web3::{
3333
self,
3434
types::{
@@ -56,6 +56,7 @@ use std::time::Instant;
5656

5757
use crate::adapter::EthereumRpcError;
5858
use crate::adapter::ProviderStatus;
59+
use crate::call_helper::interpret_eth_call_error;
5960
use crate::chain::BlockFinality;
6061
use crate::trigger::LogRef;
6162
use crate::Chain;
@@ -115,134 +116,6 @@ impl CheapClone for EthereumAdapter {
115116
}
116117

117118
impl EthereumAdapter {
118-
// ------------------------------------------------------------------
119-
// Constants and helper utilities used across eth_call handling
120-
// ------------------------------------------------------------------
121-
122-
// Try to check if the call was reverted. The JSON-RPC response for reverts is
123-
// not standardized, so we have ad-hoc checks for each Ethereum client.
124-
125-
// 0xfe is the "designated bad instruction" of the EVM, and Solidity uses it for
126-
// asserts.
127-
const PARITY_BAD_INSTRUCTION_FE: &str = "Bad instruction fe";
128-
129-
// 0xfd is REVERT, but on some contracts, and only on older blocks,
130-
// this happens. Makes sense to consider it a revert as well.
131-
const PARITY_BAD_INSTRUCTION_FD: &str = "Bad instruction fd";
132-
133-
const PARITY_BAD_JUMP_PREFIX: &str = "Bad jump";
134-
const PARITY_STACK_LIMIT_PREFIX: &str = "Out of stack";
135-
136-
// See f0af4ab0-6b7c-4b68-9141-5b79346a5f61.
137-
const PARITY_OUT_OF_GAS: &str = "Out of gas";
138-
139-
// Also covers Nethermind reverts
140-
const PARITY_VM_EXECUTION_ERROR: i64 = -32015;
141-
const PARITY_REVERT_PREFIX: &str = "revert";
142-
143-
const XDAI_REVERT: &str = "revert";
144-
145-
// Deterministic Geth execution errors. We might need to expand this as
146-
// subgraphs come across other errors. See
147-
// https://github.com/ethereum/go-ethereum/blob/cd57d5cd38ef692de8fbedaa56598b4e9fbfbabc/core/vm/errors.go
148-
const GETH_EXECUTION_ERRORS: &[&str] = &[
149-
// The "revert" substring covers a few known error messages, including:
150-
// Hardhat: "error: transaction reverted",
151-
// Ganache and Moonbeam: "vm exception while processing transaction: revert",
152-
// Geth: "execution reverted"
153-
// And others.
154-
"revert",
155-
"invalid jump destination",
156-
"invalid opcode",
157-
// Ethereum says 1024 is the stack sizes limit, so this is deterministic.
158-
"stack limit reached 1024",
159-
// See f0af4ab0-6b7c-4b68-9141-5b79346a5f61 for why the gas limit is considered deterministic.
160-
"out of gas",
161-
"stack underflow",
162-
];
163-
164-
/// Helper that checks if a geth style RPC error message corresponds to a revert.
165-
fn is_geth_revert_message(message: &str) -> bool {
166-
let env_geth_call_errors = ENV_VARS.geth_eth_call_errors.iter();
167-
let mut execution_errors = Self::GETH_EXECUTION_ERRORS
168-
.iter()
169-
.copied()
170-
.chain(env_geth_call_errors.map(|s| s.as_str()));
171-
execution_errors.any(|e| message.to_lowercase().contains(e))
172-
}
173-
174-
/// Decode a Solidity revert(reason) payload, returning the reason string when possible.
175-
fn as_solidity_revert_reason(bytes: &[u8]) -> Option<String> {
176-
let selector = &tiny_keccak::keccak256(b"Error(string)")[..4];
177-
if bytes.len() >= 4 && &bytes[..4] == selector {
178-
abi::DynSolType::String
179-
.abi_decode(&bytes[4..])
180-
.ok()
181-
.and_then(|val| val.clone().as_str().map(ToOwned::to_owned))
182-
} else {
183-
None
184-
}
185-
}
186-
187-
/// Interpret the error returned by `eth_call`, distinguishing genuine failures from
188-
/// EVM reverts. Returns `Ok(Null)` for reverts or a proper error otherwise.
189-
fn interpret_eth_call_error(
190-
logger: &Logger,
191-
err: web3::Error,
192-
) -> Result<call::Retval, ContractCallError> {
193-
fn reverted(logger: &Logger, reason: &str) -> Result<call::Retval, ContractCallError> {
194-
info!(logger, "Contract call reverted"; "reason" => reason);
195-
Ok(call::Retval::Null)
196-
}
197-
198-
if let web3::Error::Rpc(rpc_error) = &err {
199-
if Self::is_geth_revert_message(&rpc_error.message) {
200-
return reverted(logger, &rpc_error.message);
201-
}
202-
}
203-
204-
if let web3::Error::Rpc(rpc_error) = &err {
205-
let code = rpc_error.code.code();
206-
let data = rpc_error.data.as_ref().and_then(|d| d.as_str());
207-
208-
if code == Self::PARITY_VM_EXECUTION_ERROR {
209-
if let Some(data) = data {
210-
if Self::is_parity_revert(data) {
211-
return reverted(logger, &Self::parity_revert_reason(data));
212-
}
213-
}
214-
}
215-
}
216-
217-
Err(ContractCallError::Web3Error(err))
218-
}
219-
220-
fn is_parity_revert(data: &str) -> bool {
221-
data.to_lowercase().starts_with(Self::PARITY_REVERT_PREFIX)
222-
|| data.starts_with(Self::PARITY_BAD_JUMP_PREFIX)
223-
|| data.starts_with(Self::PARITY_STACK_LIMIT_PREFIX)
224-
|| data == Self::PARITY_BAD_INSTRUCTION_FE
225-
|| data == Self::PARITY_BAD_INSTRUCTION_FD
226-
|| data == Self::PARITY_OUT_OF_GAS
227-
|| data == Self::XDAI_REVERT
228-
}
229-
230-
/// Checks if the given `web3::Error` corresponds to a Parity / Nethermind style EVM
231-
/// revert and, if so, tries to extract a human-readable revert reason. Returns `Some`
232-
/// with the reason when the error is identified as a revert, otherwise `None`.
233-
fn parity_revert_reason(data: &str) -> String {
234-
if data == Self::PARITY_BAD_INSTRUCTION_FE {
235-
return Self::PARITY_BAD_INSTRUCTION_FE.to_owned();
236-
}
237-
238-
// Otherwise try to decode a Solidity revert reason payload.
239-
let payload = data.trim_start_matches(Self::PARITY_REVERT_PREFIX);
240-
hex::decode(payload)
241-
.ok()
242-
.and_then(|decoded| Self::as_solidity_revert_reason(&decoded))
243-
.unwrap_or_else(|| "no reason".to_owned())
244-
}
245-
246119
pub fn is_call_only(&self) -> bool {
247120
self.call_only
248121
}
@@ -767,7 +640,7 @@ impl EthereumAdapter {
767640

768641
match result {
769642
Ok(bytes) => Ok(call::Retval::Value(scalar::Bytes::from(bytes))),
770-
Err(err) => Self::interpret_eth_call_error(&logger, err),
643+
Err(err) => interpret_eth_call_error(&logger, err),
771644
}
772645
}
773646
})

chain/ethereum/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod adapter;
22
mod buffered_call_cache;
3+
mod call_helper;
34
mod capabilities;
45
pub mod codec;
56
mod data_source;

0 commit comments

Comments
 (0)