diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 1e4af95dfdf55..01e7ab3cd2e12 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -1,7 +1,7 @@ use alloy_json_abi::JsonAbi; use alloy_primitives::Address; use eyre::{Result, WrapErr}; -use foundry_common::{cli_warn, fs, TestFunctionExt}; +use foundry_common::{cli_warn, fs, selectors::OpenChainClient, TestFunctionExt}; use foundry_compilers::{ artifacts::{CompactBytecode, CompactDeployedBytecode, Settings}, cache::{CacheEntry, CompilerCache}, @@ -379,6 +379,7 @@ pub async fn handle_traces( Config::foundry_cache_dir(), config.offline, )?) + .with_openchain_client(OpenChainClient::new()?) .build(); let mut etherscan_identifier = EtherscanIdentifier::new(config, chain)?; diff --git a/crates/common/src/abi.rs b/crates/common/src/abi.rs index bcf82b1cddd4c..89cd0335751ee 100644 --- a/crates/common/src/abi.rs +++ b/crates/common/src/abi.rs @@ -1,7 +1,7 @@ //! ABI related helper functions. use alloy_dyn_abi::{DynSolType, DynSolValue, FunctionExt, JsonAbiExt}; -use alloy_json_abi::{Event, Function, Param}; +use alloy_json_abi::{Error, Event, Function, Param}; use alloy_primitives::{hex, Address, LogData}; use eyre::{Context, ContextCompat, Result}; use foundry_block_explorers::{contract::ContractMetadata, errors::EtherscanError, Client}; @@ -85,6 +85,10 @@ pub fn get_event(sig: &str) -> Result { Event::parse(sig).wrap_err("could not parse event signature") } +pub fn get_error(sig: &str) -> Result { + Error::parse(sig).wrap_err("could not parse error signature") +} + /// Given an event without indexed parameters and a rawlog, it tries to return the event with the /// proper indexed parameters. Otherwise, it returns the original event. pub fn get_indexed_event(mut event: Event, raw_log: &LogData) -> Event { diff --git a/crates/evm/core/src/decode.rs b/crates/evm/core/src/decode.rs index 29f448bcebd58..699b7dc0c7c7f 100644 --- a/crates/evm/core/src/decode.rs +++ b/crates/evm/core/src/decode.rs @@ -5,11 +5,15 @@ use alloy_dyn_abi::JsonAbiExt; use alloy_json_abi::{Error, JsonAbi}; use alloy_primitives::{hex, Log, Selector}; use alloy_sol_types::{SolCall, SolError, SolEventInterface, SolInterface, SolValue}; -use foundry_common::SELECTOR_LEN; +use foundry_common::{abi::get_error, selectors::OpenChainClient, SELECTOR_LEN}; use itertools::Itertools; use revm::interpreter::InstructionResult; use rustc_hash::FxHashMap; -use std::sync::OnceLock; +use std::{ + sync::{mpsc, OnceLock}, + thread, +}; +use tokio::runtime::Handle; /// Decode a set of logs, only returning logs from DSTest logging events and Hardhat's `console.log` pub fn decode_console_logs(logs: &[Log]) -> Vec { @@ -29,6 +33,7 @@ pub fn decode_console_log(log: &Log) -> Option { pub struct RevertDecoder { /// The custom errors to use for decoding. pub errors: FxHashMap>, + pub open_chain_client: Option, } impl Default for &RevertDecoder { @@ -183,6 +188,44 @@ impl RevertDecoder { std::str::from_utf8(data).map_or_else(|_| trimmed_hex(data), String::from) )) } + + pub fn may_decode_using_open_chain(&self, err: &[u8]) -> Option { + let (selector, data) = err.split_at(SELECTOR_LEN); + // try from https://openchain.xyz + if let Some(client) = self.open_chain_client.clone() { + if let Ok(handle) = Handle::try_current() { + let (tx, rx) = mpsc::channel(); + let encoded_selector = hex::encode(selector); + thread::spawn(move || { + let result = + handle.block_on(client.decode_function_selector(&encoded_selector)); + tx.send(result).unwrap(); + }); + + let result = match rx.recv() { + Ok(Ok(sigs)) => Some(sigs), + Ok(Err(_)) | Err(_) => None, + }; + if let Some(sigs) = result { + for sig in sigs { + if let Ok(error) = get_error(&sig) { + if let Ok(decoded) = error.abi_decode_input(data, true) { + return Some(format!( + "{}({})", + error.name, + decoded + .iter() + .map(foundry_common::fmt::format_token) + .format(", ") + )); + } + } + } + } + } + } + None + } } fn trimmed_hex(s: &[u8]) -> String { diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index c8d1e461abbe8..07e561cd51abd 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -9,7 +9,8 @@ use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt, FunctionExt, JsonAbiExt use alloy_json_abi::{Error, Event, Function, JsonAbi}; use alloy_primitives::{Address, LogData, Selector, B256}; use foundry_common::{ - abi::get_indexed_event, fmt::format_token, get_contract_name, ContractsByArtifact, SELECTOR_LEN, + abi::get_indexed_event, fmt::format_token, get_contract_name, selectors::OpenChainClient, + ContractsByArtifact, SELECTOR_LEN, }; use foundry_evm_core::{ abi::{Console, HardhatConsole, Vm, HARDHAT_CONSOLE_SELECTOR_PATCHES}, @@ -89,6 +90,13 @@ impl CallTraceDecoderBuilder { self } + /// Sets the openchain client. + #[inline] + pub fn with_openchain_client(mut self, client: OpenChainClient) -> Self { + self.decoder.revert_decoder.open_chain_client = Some(client); + self + } + /// Sets the debug identifier for the decoder. #[inline] pub fn with_debug_identifier(mut self, identifier: DebugTraceIdentifier) -> Self { @@ -565,7 +573,15 @@ impl CallTraceDecoder { /// The default decoded return data for a trace. fn default_return_data(&self, trace: &CallTrace) -> Option { - (!trace.success).then(|| self.revert_decoder.decode(&trace.output, Some(trace.status))) + (!trace.success).then(|| { + let err_str = self.revert_decoder.decode(&trace.output, Some(trace.status)); + if err_str.contains("custom error") { + if let Some(err) = self.revert_decoder.may_decode_using_open_chain(&trace.output) { + return err; + } + } + err_str + }) } /// Decodes an event.