Skip to content

Commit a431ff3

Browse files
committed
chain/ethereum: Refactor eth_call in EthereumAdapter
1 parent 1eb13ac commit a431ff3

File tree

1 file changed

+130
-115
lines changed

1 file changed

+130
-115
lines changed

chain/ethereum/src/ethereum_adapter.rs

Lines changed: 130 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,134 @@ impl CheapClone for EthereumAdapter {
9898
}
9999

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

@@ -600,122 +723,14 @@ impl EthereumAdapter {
600723
};
601724
let result = web3.eth().call(req, Some(block_id)).boxed().await;
602725

603-
// Try to check if the call was reverted. The JSON-RPC response for reverts is
604-
// not standardized, so we have ad-hoc checks for each Ethereum client.
605-
606-
// 0xfe is the "designated bad instruction" of the EVM, and Solidity uses it for
607-
// asserts.
608-
const PARITY_BAD_INSTRUCTION_FE: &str = "Bad instruction fe";
609-
610-
// 0xfd is REVERT, but on some contracts, and only on older blocks,
611-
// this happens. Makes sense to consider it a revert as well.
612-
const PARITY_BAD_INSTRUCTION_FD: &str = "Bad instruction fd";
613-
614-
const PARITY_BAD_JUMP_PREFIX: &str = "Bad jump";
615-
const PARITY_STACK_LIMIT_PREFIX: &str = "Out of stack";
616-
617-
// See f0af4ab0-6b7c-4b68-9141-5b79346a5f61.
618-
const PARITY_OUT_OF_GAS: &str = "Out of gas";
619-
620-
// Also covers Nethermind reverts
621-
const PARITY_VM_EXECUTION_ERROR: i64 = -32015;
622-
const PARITY_REVERT_PREFIX: &str = "revert";
623-
624-
const XDAI_REVERT: &str = "revert";
625-
626-
// Deterministic Geth execution errors. We might need to expand this as
627-
// subgraphs come across other errors. See
628-
// https://github.com/ethereum/go-ethereum/blob/cd57d5cd38ef692de8fbedaa56598b4e9fbfbabc/core/vm/errors.go
629-
const GETH_EXECUTION_ERRORS: &[&str] = &[
630-
// The "revert" substring covers a few known error messages, including:
631-
// Hardhat: "error: transaction reverted",
632-
// Ganache and Moonbeam: "vm exception while processing transaction: revert",
633-
// Geth: "execution reverted"
634-
// And others.
635-
"revert",
636-
"invalid jump destination",
637-
"invalid opcode",
638-
// Ethereum says 1024 is the stack sizes limit, so this is deterministic.
639-
"stack limit reached 1024",
640-
// See f0af4ab0-6b7c-4b68-9141-5b79346a5f61 for why the gas limit is considered deterministic.
641-
"out of gas",
642-
"stack underflow",
643-
];
644-
645-
let env_geth_call_errors = ENV_VARS.geth_eth_call_errors.iter();
646-
let mut geth_execution_errors = GETH_EXECUTION_ERRORS
647-
.iter()
648-
.copied()
649-
.chain(env_geth_call_errors.map(|s| s.as_str()));
650-
651-
let as_solidity_revert_with_reason = |bytes: &[u8]| {
652-
let solidity_revert_function_selector =
653-
&tiny_keccak::keccak256(b"Error(string)")[..4];
654-
655-
match bytes.len() >= 4 && &bytes[..4] == solidity_revert_function_selector {
656-
false => None,
657-
true => abi::DynSolType::String
658-
.abi_decode(&bytes[4..])
659-
.ok()
660-
.and_then(|val| val.clone().as_str().map(ToOwned::to_owned)),
661-
}
662-
};
663-
664726
match result {
665-
// A successful response.
666727
Ok(bytes) => Ok(call::Retval::Value(scalar::Bytes::from(bytes))),
667-
668-
// Check for Geth revert.
669-
Err(web3::Error::Rpc(rpc_error))
670-
if geth_execution_errors
671-
.any(|e| rpc_error.message.to_lowercase().contains(e)) =>
672-
{
673-
reverted(&logger, &rpc_error.message)
674-
}
675-
676-
// Check for Parity revert.
677-
Err(web3::Error::Rpc(ref rpc_error))
678-
if rpc_error.code.code() == PARITY_VM_EXECUTION_ERROR =>
679-
{
680-
match rpc_error.data.as_ref().and_then(|d| d.as_str()) {
681-
Some(data)
682-
if data.to_lowercase().starts_with(PARITY_REVERT_PREFIX)
683-
|| data.starts_with(PARITY_BAD_JUMP_PREFIX)
684-
|| data.starts_with(PARITY_STACK_LIMIT_PREFIX)
685-
|| data == PARITY_BAD_INSTRUCTION_FE
686-
|| data == PARITY_BAD_INSTRUCTION_FD
687-
|| data == PARITY_OUT_OF_GAS
688-
|| data == XDAI_REVERT =>
689-
{
690-
let reason = if data == PARITY_BAD_INSTRUCTION_FE {
691-
PARITY_BAD_INSTRUCTION_FE.to_owned()
692-
} else {
693-
let payload = data.trim_start_matches(PARITY_REVERT_PREFIX);
694-
hex::decode(payload)
695-
.ok()
696-
.and_then(|payload| {
697-
as_solidity_revert_with_reason(&payload)
698-
})
699-
.unwrap_or("no reason".to_owned())
700-
};
701-
reverted(&logger, &reason)
702-
}
703-
704-
// The VM execution error was not identified as a revert.
705-
_ => Err(ContractCallError::Web3Error(web3::Error::Rpc(
706-
rpc_error.clone(),
707-
))),
708-
}
709-
}
710-
711-
// The error was not identified as a revert.
712-
Err(err) => Err(ContractCallError::Web3Error(err)),
728+
Err(err) => Self::interpret_eth_call_error(&logger, err),
713729
}
714730
}
715731
})
716-
.map_err(|e| e.into_inner().unwrap_or(ContractCallError::Timeout))
717-
.boxed()
718732
.await
733+
.map_err(|e| e.into_inner().unwrap_or(ContractCallError::Timeout))
719734
}
720735

721736
async fn call_and_cache(

0 commit comments

Comments
 (0)