From b15f717b0a380b2e4dd539451b09f35f13b6e530 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 9 Apr 2025 12:23:22 +0200 Subject: [PATCH 01/13] refactor: signatures identifier --- crates/cast/src/args.rs | 30 +- crates/cast/src/lib.rs | 29 +- crates/cast/src/opts.rs | 6 +- crates/cast/src/tx.rs | 25 +- crates/chisel/src/dispatcher.rs | 9 +- crates/cli/src/utils/cmd.rs | 53 ++- .../common/src/provider/runtime_transport.rs | 6 +- crates/common/src/selectors.rs | 232 ++++++------ crates/evm/traces/src/decoder/mod.rs | 58 +-- crates/evm/traces/src/decoder/precompiles.rs | 21 +- crates/evm/traces/src/identifier/etherscan.rs | 14 +- crates/evm/traces/src/identifier/local.rs | 22 +- crates/evm/traces/src/identifier/mod.rs | 24 +- .../evm/traces/src/identifier/signatures.rs | 336 +++++++++++------- crates/evm/traces/src/lib.rs | 9 +- crates/forge/src/cmd/selectors.rs | 2 +- crates/forge/src/cmd/test/mod.rs | 10 +- crates/forge/tests/it/config.rs | 4 +- crates/script/src/execute.rs | 9 +- crates/script/src/simulate.rs | 2 +- 20 files changed, 490 insertions(+), 411 deletions(-) diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index a01d87b76d373..78ca1d3076a63 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -20,11 +20,10 @@ use foundry_common::{ selectors::{ decode_calldata, decode_event_topic, decode_function_selector, decode_selectors, import_selectors, parse_signatures, pretty_calldata, ParsedSignatures, SelectorImportData, - SelectorType, + SelectorKind, }, shell, stdin, }; -use foundry_config::Config; use std::time::Instant; /// Run the `cast` command-line interface. @@ -209,11 +208,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { let data = data.strip_prefix("0x").unwrap_or(data.as_str()); let selector = data.get(..64).unwrap_or_default(); let identified_event = - SignaturesIdentifier::new(Config::foundry_cache_dir(), false)? - .write() - .await - .identify_event(&hex::decode(selector)?) - .await; + SignaturesIdentifier::new(false)?.identify_event(selector.parse()?).await; if let Some(event) = identified_event { let _ = sh_println!("{}", event.signature()); let data = data.get(64..).unwrap_or_default(); @@ -235,11 +230,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { let data = data.strip_prefix("0x").unwrap_or(data.as_str()); let selector = data.get(..8).unwrap_or_default(); let identified_error = - SignaturesIdentifier::new(Config::foundry_cache_dir(), false)? - .write() - .await - .identify_error(&hex::decode(selector)?) - .await; + SignaturesIdentifier::new(false)?.identify_error(selector.parse()?).await; if let Some(error) = identified_error { let _ = sh_println!("{}", error.signature()); error @@ -385,9 +376,12 @@ pub async fn run_command(args: CastArgs) -> Result<()> { let max_mutability_len = functions.iter().map(|r| r.2.len()).max().unwrap_or(0); let resolve_results = if resolve { - let selectors_it = functions.iter().map(|r| &r.0); - let ds = decode_selectors(SelectorType::Function, selectors_it).await?; - ds.into_iter().map(|v| v.unwrap_or_default().join("|")).collect() + let selectors = functions + .iter() + .map(|&(selector, ..)| SelectorKind::Function(selector)) + .collect::>(); + let ds = decode_selectors(&selectors).await?; + ds.into_iter().map(|v| v.join("|")).collect() } else { vec![] }; @@ -501,7 +495,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { // 4Byte CastSubcommand::FourByte { selector } => { let selector = stdin::unwrap_line(selector)?; - let sigs = decode_function_selector(&selector).await?; + let sigs = decode_function_selector(selector).await?; if sigs.is_empty() { eyre::bail!("No matching function signatures found for selector `{selector}`"); } @@ -514,7 +508,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { let calldata = stdin::unwrap_line(calldata)?; if calldata.len() == 10 { - let sigs = decode_function_selector(&calldata).await?; + let sigs = decode_function_selector(calldata.parse()?).await?; if sigs.is_empty() { eyre::bail!("No matching function signatures found for calldata `{calldata}`"); } @@ -544,7 +538,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { CastSubcommand::FourByteEvent { topic } => { let topic = stdin::unwrap_line(topic)?; - let sigs = decode_event_topic(&topic).await?; + let sigs = decode_event_topic(topic).await?; if sigs.is_empty() { eyre::bail!("No matching event signatures found for topic `{topic}`"); } diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index cb8bd28986aaa..f369c5de03f46 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -9,7 +9,7 @@ use alloy_network::AnyNetwork; use alloy_primitives::{ hex, utils::{keccak256, ParseUnits, Unit}, - Address, Keccak256, TxHash, TxKind, B256, I256, U256, + Address, Keccak256, Selector, TxHash, TxKind, B256, I256, U256, }; use alloy_provider::{ network::eip2718::{Decodable2718, Encodable2718}, @@ -367,22 +367,16 @@ impl> Cast

{ } async fn block_field_as_num>(&self, block: B, field: String) -> Result { - let block = block.into(); - let block_field = Self::block( + Self::block( self, - block, + block.into(), false, // Select only select field Some(field), ) - .await?; - - let ret = if block_field.starts_with("0x") { - U256::from_str_radix(strip_0x(&block_field), 16).expect("Unable to convert hex to U256") - } else { - U256::from_str_radix(&block_field, 10).expect("Unable to convert decimal to U256") - }; - Ok(ret) + .await? + .parse() + .map_err(Into::into) } pub async fn base_fee>(&self, block: B) -> Result { @@ -2132,15 +2126,16 @@ impl SimpleCast { /// # Example /// /// ``` + /// use alloy_primitives::fixed_bytes; /// use cast::SimpleCast as Cast; /// /// let bytecode = "6080604052348015600e575f80fd5b50600436106026575f3560e01c80632125b65b14602a575b5f80fd5b603a6035366004603c565b505050565b005b5f805f60608486031215604d575f80fd5b833563ffffffff81168114605f575f80fd5b925060208401356001600160a01b03811681146079575f80fd5b915060408401356001600160e01b03811681146093575f80fd5b80915050925092509256"; /// let functions = Cast::extract_functions(bytecode)?; - /// assert_eq!(functions, vec![("0x2125b65b".to_string(), "uint32,address,uint224".to_string(), "pure")]); + /// assert_eq!(functions, vec![(fixed_bytes!("0x2125b65b"), "uint32,address,uint224".to_string(), "pure")]); /// # Ok::<(), eyre::Report>(()) /// ``` - pub fn extract_functions(bytecode: &str) -> Result> { - let code = hex::decode(strip_0x(bytecode))?; + pub fn extract_functions(bytecode: &str) -> Result> { + let code = hex::decode(bytecode)?; let info = evmole::contract_info( evmole::ContractInfoArgs::new(&code) .with_selectors() @@ -2153,7 +2148,7 @@ impl SimpleCast { .into_iter() .map(|f| { ( - hex::encode_prefixed(f.selector), + f.selector.into(), f.arguments .expect("arguments extraction was requested") .into_iter() @@ -2180,7 +2175,7 @@ impl SimpleCast { /// let tx_envelope = Cast::decode_raw_transaction(&tx)?; /// # Ok::<(), eyre::Report>(()) pub fn decode_raw_transaction(tx: &str) -> Result { - let tx_hex = hex::decode(strip_0x(tx))?; + let tx_hex = hex::decode(tx)?; let tx = TxEnvelope::decode_2718(&mut tx_hex.as_slice())?; Ok(tx) } diff --git a/crates/cast/src/opts.rs b/crates/cast/src/opts.rs index 9bf4ef27801ed..8ffb6326c025e 100644 --- a/crates/cast/src/opts.rs +++ b/crates/cast/src/opts.rs @@ -5,7 +5,7 @@ use crate::cmd::{ mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs, txpool::TxPoolSubcommands, wallet::WalletSubcommands, }; -use alloy_primitives::{Address, B256, U256}; +use alloy_primitives::{Address, Selector, B256, U256}; use alloy_rpc_types::BlockId; use clap::{Parser, Subcommand, ValueHint}; use eyre::Result; @@ -646,7 +646,7 @@ pub enum CastSubcommand { #[command(name = "4byte", visible_aliases = &["4", "4b"])] FourByte { /// The function selector. - selector: Option, + selector: Option, }, /// Decode ABI-encoded calldata using . @@ -661,7 +661,7 @@ pub enum CastSubcommand { FourByteEvent { /// Topic 0 #[arg(value_name = "TOPIC_0")] - topic: Option, + topic: Option, }, /// Upload the given signatures to . diff --git a/crates/cast/src/tx.rs b/crates/cast/src/tx.rs index 46f7c811bced3..1097da794e15f 100644 --- a/crates/cast/src/tx.rs +++ b/crates/cast/src/tx.rs @@ -459,23 +459,18 @@ where /// Helper function that tries to decode custom error name and inputs from error payload data. async fn decode_execution_revert(data: &RawValue) -> Result> { - if let Some(err_data) = serde_json::from_str::(data.get())?.strip_prefix("0x") { - let Some(selector) = err_data.get(..8) else { return Ok(None) }; - - if let Some(known_error) = SignaturesIdentifier::new(Config::foundry_cache_dir(), false)? - .write() - .await - .identify_error(&hex::decode(selector)?) - .await - { - let mut decoded_error = known_error.name.clone(); - if !known_error.inputs.is_empty() { - if let Ok(error) = known_error.decode_error(&hex::decode(err_data)?) { - write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?; - } + let err_data = serde_json::from_str::(data.get())?; + let Some(selector) = err_data.get(..4) else { return Ok(None) }; + if let Some(known_error) = + SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await + { + let mut decoded_error = known_error.name.clone(); + if !known_error.inputs.is_empty() { + if let Ok(error) = known_error.decode_error(&err_data) { + write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?; } - return Ok(Some(decoded_error)) } + return Ok(Some(decoded_error)) } Ok(None) } diff --git a/crates/chisel/src/dispatcher.rs b/crates/chisel/src/dispatcher.rs index 97b78369ec565..5dd6c882ffed6 100644 --- a/crates/chisel/src/dispatcher.rs +++ b/crates/chisel/src/dispatcher.rs @@ -13,7 +13,7 @@ use crate::{ use alloy_json_abi::{InternalType, JsonAbi}; use alloy_primitives::{hex, Address}; use forge_fmt::FormatterConfig; -use foundry_config::{Config, RpcEndpointUrl}; +use foundry_config::RpcEndpointUrl; use foundry_evm::{ decode::decode_console_logs, traces::{ @@ -925,9 +925,8 @@ impl ChiselDispatcher { ) -> eyre::Result { let mut decoder = CallTraceDecoderBuilder::new() .with_labels(result.labeled_addresses.clone()) - .with_signature_identifier(SignaturesIdentifier::new( - Config::foundry_cache_dir(), - session_config.foundry_config.offline, + .with_signature_identifier(SignaturesIdentifier::from_config( + &session_config.foundry_config, )?) .build(); @@ -965,7 +964,7 @@ impl ChiselDispatcher { for (kind, trace) in &mut result.traces { // Display all Setup + Execution traces. if matches!(kind, TraceKind::Setup | TraceKind::Execution) { - decode_trace_arena(trace, decoder).await?; + decode_trace_arena(trace, decoder).await; sh_println!("{}", render_trace_arena(trace))?; } } diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 3dcec54ec218d..1c10f8a1f3e36 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -1,7 +1,10 @@ use alloy_json_abi::JsonAbi; use alloy_primitives::Address; use eyre::{Result, WrapErr}; -use foundry_common::{compile::ProjectCompiler, fs, shell, ContractsByArtifact, TestFunctionExt}; +use foundry_common::{ + compile::ProjectCompiler, fs, selectors::SelectorKind, shell, ContractsByArtifact, + TestFunctionExt, +}; use foundry_compilers::{ artifacts::{CompactBytecode, Settings}, cache::{CacheEntry, CompilerCache}, @@ -16,7 +19,7 @@ use foundry_evm::{ traces::{ debug::{ContractSources, DebugTraceIdentifier}, decode_trace_arena, - identifier::{CachedSignatures, SignaturesIdentifier, TraceIdentifiers}, + identifier::{SignaturesCache, SignaturesIdentifier, TraceIdentifiers}, render_trace_arena_inner, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces, }, }; @@ -364,10 +367,7 @@ pub async fn handle_traces( let mut builder = CallTraceDecoderBuilder::new() .with_labels(labels.chain(config_labels)) - .with_signature_identifier(SignaturesIdentifier::new( - Config::foundry_cache_dir(), - config.offline, - )?); + .with_signature_identifier(SignaturesIdentifier::from_config(config)?); let mut identifier = TraceIdentifiers::new().with_etherscan(config, chain)?; if let Some(contracts) = &known_contracts { builder = builder.with_known_contracts(contracts); @@ -416,7 +416,7 @@ pub async fn print_traces( } for (_, arena) in traces { - decode_trace_arena(arena, decoder).await?; + decode_trace_arena(arena, decoder).await; sh_println!("{}", render_trace_arena_inner(arena, verbose, state_changes))?; } @@ -437,34 +437,21 @@ pub async fn print_traces( /// Traverse the artifacts in the project to generate local signatures and merge them into the cache /// file. -pub fn cache_local_signatures(output: &ProjectCompileOutput, cache_path: PathBuf) -> Result<()> { - let path = cache_path.join("signatures"); - let mut cached_signatures = CachedSignatures::load(cache_path); - output.artifacts().for_each(|(_, artifact)| { +pub fn cache_local_signatures(output: &ProjectCompileOutput, cache_dir: &Path) -> Result<()> { + let path = cache_dir.join("signatures"); + let mut signatures = SignaturesCache::load(&path); + for (_, artifact) in output.artifacts() { if let Some(abi) = &artifact.abi { - for func in abi.functions() { - cached_signatures.functions.insert(func.selector().to_string(), func.signature()); - } - for event in abi.events() { - cached_signatures - .events - .insert(event.selector().to_string(), event.full_signature()); - } - for error in abi.errors() { - cached_signatures.errors.insert(error.selector().to_string(), error.signature()); - } - // External libraries doesn't have functions included in abi, but `methodIdentifiers`. - if let Some(method_identifiers) = &artifact.method_identifiers { - method_identifiers.iter().for_each(|(signature, selector)| { - cached_signatures - .functions - .entry(format!("0x{selector}")) - .or_insert(signature.to_string()); - }); - } + signatures.extend_from_abi(abi); } - }); - fs::write_json_file(&path, &cached_signatures)?; + // External libraries don't have functions included in abi, but `methodIdentifiers`. + if let Some(method_identifiers) = &artifact.method_identifiers { + signatures.extend(method_identifiers.iter().filter_map(|(selector, signature)| { + Some((SelectorKind::Function(selector.parse().ok()?), signature.clone())) + })); + } + } + signatures.save(&path); Ok(()) } diff --git a/crates/common/src/provider/runtime_transport.rs b/crates/common/src/provider/runtime_transport.rs index 3796e9123e308..9a59f6ed20423 100644 --- a/crates/common/src/provider/runtime_transport.rs +++ b/crates/common/src/provider/runtime_transport.rs @@ -138,7 +138,7 @@ impl RuntimeTransport { /// Connects the underlying transport, depending on the URL scheme. pub async fn connect(&self) -> Result { match self.url.scheme() { - "http" | "https" => self.connect_http().await, + "http" | "https" => self.connect_http(), "ws" | "wss" => self.connect_ws().await, "file" => self.connect_ipc().await, _ => Err(RuntimeTransportError::BadScheme(self.url.scheme().to_string())), @@ -190,7 +190,7 @@ impl RuntimeTransport { } /// Connects to an HTTP [alloy_transport_http::Http] transport. - async fn connect_http(&self) -> Result { + fn connect_http(&self) -> Result { let client = self.reqwest_client()?; Ok(InnerTransport::Http(Http::with_client(client, self.url.clone()))) } @@ -351,7 +351,7 @@ mod tests { let transport = RuntimeTransportBuilder::new(url.clone()) .with_headers(vec!["User-Agent: test-agent".to_string()]) .build(); - let inner = transport.connect_http().await.unwrap(); + let inner = transport.connect_http().unwrap(); match inner { InnerTransport::Http(http) => { diff --git a/crates/common/src/selectors.rs b/crates/common/src/selectors.rs index c360a353c35c3..35ddf2f0044d9 100644 --- a/crates/common/src/selectors.rs +++ b/crates/common/src/selectors.rs @@ -4,8 +4,9 @@ use crate::{abi::abi_decode_calldata, provider::runtime_transport::RuntimeTransportBuilder}; use alloy_json_abi::JsonAbi; -use alloy_primitives::map::HashMap; +use alloy_primitives::{map::HashMap, Selector, B256}; use eyre::Context; +use itertools::Itertools; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ fmt, @@ -26,6 +27,9 @@ const REQ_TIMEOUT: Duration = Duration::from_secs(15); /// How many request can time out before we decide this is a spurious connection. const MAX_TIMEDOUT_REQ: usize = 4usize; +/// List of signatures for a given [`SelectorKind`]. +pub type OpenChainSignatures = Vec; + /// A client that can request API data from OpenChain. #[derive(Clone, Debug)] pub struct OpenChainClient { @@ -54,7 +58,7 @@ impl OpenChainClient { }) } - async fn get_text(&self, url: &str) -> reqwest::Result { + async fn get_text(&self, url: impl reqwest::IntoUrl + fmt::Display) -> reqwest::Result { trace!(%url, "GET"); self.inner .get(url) @@ -128,29 +132,16 @@ impl OpenChainClient { /// Decodes the given function or event selector using OpenChain pub async fn decode_selector( &self, - selector: &str, - selector_type: SelectorType, - ) -> eyre::Result> { - self.decode_selectors(selector_type, std::iter::once(selector)) - .await? - .pop() // Not returning on the previous line ensures a vector with exactly 1 element - .unwrap() - .ok_or_else(|| eyre::eyre!("No signature found")) + selector: SelectorKind, + ) -> eyre::Result { + Ok(self.decode_selectors(&[selector]).await?.pop().unwrap()) } /// Decodes the given function, error or event selectors using OpenChain. pub async fn decode_selectors( &self, - selector_type: SelectorType, - selectors: impl IntoIterator>, - ) -> eyre::Result>>> { - let selectors: Vec = selectors - .into_iter() - .map(Into::into) - .map(|s| s.to_lowercase()) - .map(|s| if s.starts_with("0x") { s } else { format!("0x{s}") }) - .collect(); - + selectors: &[SelectorKind], + ) -> eyre::Result> { if selectors.is_empty() { return Ok(vec![]); } @@ -158,78 +149,62 @@ impl OpenChainClient { debug!(len = selectors.len(), "decoding selectors"); trace!(?selectors, "decoding selectors"); - // exit early if spurious connection + // Exit early if spurious connection. self.ensure_not_spurious()?; - let expected_len = match selector_type { - SelectorType::Function | SelectorType::Error => 10, // 0x + hex(4bytes) - SelectorType::Event => 66, // 0x + hex(32bytes) - }; - if let Some(s) = selectors.iter().find(|s| s.len() != expected_len) { - eyre::bail!( - "Invalid selector {s}: expected {expected_len} characters (including 0x prefix)." - ) - } - - #[derive(Deserialize)] - struct Decoded { - name: String, - } - - #[derive(Deserialize)] - struct ApiResult { - event: HashMap>>, - function: HashMap>>, - } - - #[derive(Deserialize)] - struct ApiResponse { - ok: bool, - result: ApiResult, - } - - let url = format!( - "{SELECTOR_LOOKUP_URL}?{ltype}={selectors_str}", - ltype = match selector_type { - SelectorType::Function | SelectorType::Error => "function", - SelectorType::Event => "event", - }, - selectors_str = selectors.join(",") - ); - - let res = self.get_text(&url).await?; - let api_response = match serde_json::from_str::(&res) { - Ok(inner) => inner, - Err(err) => { - eyre::bail!("Could not decode response:\n {res}.\nError: {err}") + // Build the URL with the query string. + let mut url: url::Url = SELECTOR_LOOKUP_URL.parse().unwrap(); + { + let mut query = url.query_pairs_mut(); + let functions = selectors.iter().filter_map(SelectorKind::as_function); + if functions.clone().next().is_some() { + query.append_pair("function", &functions.format(",").to_string()); } - }; - - if !api_response.ok { - eyre::bail!("Failed to decode:\n {res}") + let events = selectors.iter().filter_map(SelectorKind::as_event); + if events.clone().next().is_some() { + query.append_pair("event", &events.format(",").to_string()); + } + let _ = query.finish(); } - let decoded = match selector_type { - SelectorType::Function | SelectorType::Error => api_response.result.function, - SelectorType::Event => api_response.result.event, + let text = self.get_text(url).await?; + let SignatureResponse { ok, mut result } = match serde_json::from_str(&text) { + Ok(response) => response, + Err(err) => eyre::bail!("could not decode response: {err}: {text}"), }; + if !ok { + eyre::bail!("OpenChain returned an error: {text}"); + } Ok(selectors - .into_iter() - .map(|selector| match decoded.get(&selector) { - Some(Some(r)) => Some(r.iter().map(|d| d.name.clone()).collect()), - _ => None, + .iter() + .map(|selector| { + let signatures = match selector { + SelectorKind::Function(selector) | SelectorKind::Error(selector) => { + result.function.remove(selector) + } + SelectorKind::Event(hash) => result.event.remove(hash), + }; + signatures + .unwrap_or_default() + .unwrap_or_default() + .into_iter() + .map(|sig| sig.name) + .collect() }) .collect()) } /// Fetches a function signature given the selector using OpenChain - pub async fn decode_function_selector(&self, selector: &str) -> eyre::Result> { - self.decode_selector(selector, SelectorType::Function).await + pub async fn decode_function_selector( + &self, + selector: Selector, + ) -> eyre::Result { + self.decode_selector(SelectorKind::Function(selector)).await } /// Fetches all possible signatures and attempts to abi decode the calldata - pub async fn decode_calldata(&self, calldata: &str) -> eyre::Result> { + pub async fn decode_calldata(&self, calldata: &str) -> eyre::Result { let calldata = calldata.strip_prefix("0x").unwrap_or(calldata); if calldata.len() < 8 { eyre::bail!( @@ -238,19 +213,15 @@ impl OpenChainClient { ) } - let sigs = self.decode_function_selector(&calldata[..8]).await?; - - // filter for signatures that can be decoded - Ok(sigs - .iter() - .filter(|sig| abi_decode_calldata(sig, calldata, true, true).is_ok()) - .cloned() - .collect::>()) + let mut sigs = self.decode_function_selector(calldata[..8].parse().unwrap()).await?; + // Retain only signatures that can be decoded. + sigs.retain(|sig| abi_decode_calldata(sig, calldata, true, true).is_ok()); + Ok(sigs) } - /// Fetches an event signature given the 32 byte topic using OpenChain - pub async fn decode_event_topic(&self, topic: &str) -> eyre::Result> { - self.decode_selector(topic, SelectorType::Event).await + /// Fetches an event signature given the 32 byte topic using OpenChain. + pub async fn decode_event_topic(&self, topic: B256) -> eyre::Result { + self.decode_selector(SelectorKind::Event(topic)).await } /// Pretty print calldata and if available, fetch possible function signatures @@ -284,6 +255,7 @@ impl OpenChainClient { let sigs = if offline { vec![] } else { + let selector = selector.parse()?; self.decode_function_selector(selector).await.unwrap_or_default().into_iter().collect() }; let (_, data) = calldata.split_at(8); @@ -314,7 +286,7 @@ impl OpenChainClient { let request = match data { SelectorImportData::Abi(abis) => { - let functions_and_errors: Vec = abis + let functions_and_errors: OpenChainSignatures = abis .iter() .flat_map(|abi| { abi.functions() @@ -344,12 +316,12 @@ impl OpenChainClient { pub enum SelectorOrSig { Selector(String), - Sig(Vec), + Sig(OpenChainSignatures), } pub struct PossibleSigs { method: SelectorOrSig, - data: Vec, + data: OpenChainSignatures, } impl PossibleSigs { @@ -382,45 +354,59 @@ impl fmt::Display for PossibleSigs { } } -/// The type of selector fetched from OpenChain. -#[derive(Clone, Copy)] -pub enum SelectorType { +/// The kind of selector to fetch from OpenChain. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum SelectorKind { /// A function selector. - Function, + Function(Selector), + /// A custom error selector. Behaves the same as a function selector. + Error(Selector), /// An event selector. - Event, - /// An custom error selector. - Error, + Event(B256), +} + +impl SelectorKind { + /// Returns the function selector if it is a function OR custom error. + pub fn as_function(&self) -> Option { + match *self { + Self::Function(selector) | Self::Error(selector) => Some(selector), + _ => None, + } + } + + /// Returns the event selector if it is an event. + pub fn as_event(&self) -> Option { + match *self { + Self::Event(hash) => Some(hash), + _ => None, + } + } } /// Decodes the given function or event selector using OpenChain. -pub async fn decode_selector( - selector_type: SelectorType, - selector: &str, -) -> eyre::Result> { - OpenChainClient::new()?.decode_selector(selector, selector_type).await +pub async fn decode_selector(selector: SelectorKind) -> eyre::Result { + OpenChainClient::new()?.decode_selector(selector).await } /// Decodes the given function or event selectors using OpenChain. pub async fn decode_selectors( - selector_type: SelectorType, - selectors: impl IntoIterator>, -) -> eyre::Result>>> { - OpenChainClient::new()?.decode_selectors(selector_type, selectors).await + selectors: &[SelectorKind], +) -> eyre::Result> { + OpenChainClient::new()?.decode_selectors(selectors).await } /// Fetches a function signature given the selector using OpenChain. -pub async fn decode_function_selector(selector: &str) -> eyre::Result> { +pub async fn decode_function_selector(selector: Selector) -> eyre::Result { OpenChainClient::new()?.decode_function_selector(selector).await } /// Fetches all possible signatures and attempts to abi decode the calldata using OpenChain. -pub async fn decode_calldata(calldata: &str) -> eyre::Result> { +pub async fn decode_calldata(calldata: &str) -> eyre::Result { OpenChainClient::new()?.decode_calldata(calldata).await } /// Fetches an event signature given the 32 byte topic using OpenChain. -pub async fn decode_event_topic(topic: &str) -> eyre::Result> { +pub async fn decode_event_topic(topic: B256) -> eyre::Result { OpenChainClient::new()?.decode_event_topic(topic).await } @@ -448,9 +434,9 @@ pub async fn pretty_calldata( #[derive(Debug, Default, PartialEq, Eq, Serialize)] pub struct RawSelectorImportData { - pub function: Vec, - pub event: Vec, - pub error: Vec, + pub function: OpenChainSignatures, + pub event: OpenChainSignatures, + pub error: OpenChainSignatures, } impl RawSelectorImportData { @@ -468,8 +454,8 @@ pub enum SelectorImportData { #[derive(Debug, Default, Serialize)] struct SelectorImportRequest { - function: Vec, - event: Vec, + function: OpenChainSignatures, + event: OpenChainSignatures, } #[derive(Debug, Deserialize)] @@ -574,6 +560,24 @@ pub fn parse_signatures(tokens: Vec) -> ParsedSignatures { ParsedSignatures { signatures, abis } } +/// [`SELECTOR_LOOKUP_URL`] response. +#[derive(Deserialize)] +struct SignatureResponse { + ok: bool, + result: SignatureResult, +} + +#[derive(Deserialize)] +struct SignatureResult { + event: HashMap>>, + function: HashMap>>, +} + +#[derive(Deserialize)] +struct Signature { + name: String, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index e023285eb054f..afd2d042807dd 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -1,8 +1,6 @@ use crate::{ debug::DebugTraceIdentifier, - identifier::{ - AddressIdentity, LocalTraceIdentifier, SingleSignaturesIdentifier, TraceIdentifier, - }, + identifier::{AddressIdentity, LocalTraceIdentifier, SignaturesIdentifier, TraceIdentifier}, CallTrace, CallTraceArena, CallTraceNode, DecodedCallData, }; use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt, FunctionExt, JsonAbiExt}; @@ -12,7 +10,8 @@ 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::SelectorKind, + ContractsByArtifact, SELECTOR_LEN, }; use foundry_evm_core::{ abi::{console, Vm}, @@ -85,7 +84,7 @@ impl CallTraceDecoderBuilder { /// Sets the signature identifier for events and functions. #[inline] - pub fn with_signature_identifier(mut self, identifier: SingleSignaturesIdentifier) -> Self { + pub fn with_signature_identifier(mut self, identifier: SignaturesIdentifier) -> Self { self.decoder.signature_identifier = Some(identifier); self } @@ -132,7 +131,7 @@ pub struct CallTraceDecoder { pub revert_decoder: RevertDecoder, /// A signature identifier for events and functions. - pub signature_identifier: Option, + pub signature_identifier: Option, /// Verbosity level pub verbosity: u8, @@ -212,7 +211,7 @@ impl CallTraceDecoder { /// /// Unknown contracts are contracts that either lack a label or an ABI. pub fn identify(&mut self, trace: &CallTraceArena, identifier: &mut impl TraceIdentifier) { - self.collect_identities(identifier.identify_addresses(self.trace_addresses(trace))); + self.collect_identities(identifier.identify_addresses(&self.trace_addresses(trace))); } /// Adds a single event to the decoder. @@ -246,7 +245,7 @@ impl CallTraceDecoder { pub fn trace_addresses<'a>( &'a self, arena: &'a CallTraceArena, - ) -> impl Iterator, Option<&'a [u8]>)> + Clone + 'a { + ) -> Vec<(&'a Address, Option<&'a [u8]>, Option<&'a [u8]>)> { arena .nodes() .iter() @@ -260,6 +259,7 @@ impl CallTraceDecoder { .filter(|&(address, _, _)| { !self.labels.contains_key(address) || !self.contracts.contains_key(address) }) + .collect() } fn collect_identities(&mut self, identities: Vec>) { @@ -348,15 +348,13 @@ impl CallTraceDecoder { } if cdata.len() >= SELECTOR_LEN { - let selector = &cdata[..SELECTOR_LEN]; + let selector = Selector::try_from(&cdata[..SELECTOR_LEN]).unwrap(); let mut functions = Vec::new(); - let functions = match self.functions.get(selector) { + let functions = match self.functions.get(&selector) { Some(fs) => fs, None => { if let Some(identifier) = &self.signature_identifier { - if let Some(function) = - identifier.write().await.identify_function(selector).await - { + if let Some(function) = identifier.identify_function(selector).await { functions.push(function); } } @@ -612,7 +610,7 @@ impl CallTraceDecoder { Some(es) => es, None => { if let Some(identifier) = &self.signature_identifier { - if let Some(event) = identifier.write().await.identify_event(&t0[..]).await { + if let Some(event) = identifier.identify_event(t0).await { events.push(get_indexed_event(event, log)); } } @@ -645,24 +643,28 @@ impl CallTraceDecoder { /// Prefetches function and event signatures into the identifier cache pub async fn prefetch_signatures(&self, nodes: &[CallTraceNode]) { let Some(identifier) = &self.signature_identifier else { return }; - - let events_it = nodes + let events = nodes .iter() .flat_map(|node| node.logs.iter().filter_map(|log| log.raw_log.topics().first())) - .unique(); - identifier.write().await.identify_events(events_it).await; - - const DEFAULT_CREATE2_DEPLOYER_BYTES: [u8; 20] = DEFAULT_CREATE2_DEPLOYER.0 .0; - let funcs_it = nodes + .copied(); + let functions = nodes .iter() - .filter_map(|n| match n.trace.address.0 .0 { - DEFAULT_CREATE2_DEPLOYER_BYTES => None, - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01..=0x0a] => None, - _ => n.trace.data.get(..SELECTOR_LEN), + .filter_map(|n| { + if n.trace.address == DEFAULT_CREATE2_DEPLOYER || + precompiles::is_known_precompile(n.trace.address, 1) + { + return None; + } + n.trace.data.get(..SELECTOR_LEN) }) - .filter(|v| !self.functions.contains_key(*v)) - .unique(); - identifier.write().await.identify_functions(funcs_it).await; + .map(|bytes| Selector::try_from(bytes).unwrap()) + .filter(|selector| !self.functions.contains_key(selector)); + let selectors = events + .map(SelectorKind::Event) + .chain(functions.map(SelectorKind::Function)) + .unique() + .collect::>(); + identifier.identify(&selectors).await; } /// Pretty-prints a value. diff --git a/crates/evm/traces/src/decoder/precompiles.rs b/crates/evm/traces/src/decoder/precompiles.rs index 0719f29b77aef..508bf1e1c4320 100644 --- a/crates/evm/traces/src/decoder/precompiles.rs +++ b/crates/evm/traces/src/decoder/precompiles.rs @@ -1,5 +1,5 @@ use crate::{CallTrace, DecodedCallData}; -use alloy_primitives::{hex, B256, U256}; +use alloy_primitives::{hex, Address, B256, U256}; use alloy_sol_types::{abi, sol, SolCall}; use foundry_evm_core::precompiles::{ BLAKE_2F, EC_ADD, EC_MUL, EC_PAIRING, EC_RECOVER, IDENTITY, MOD_EXP, POINT_EVALUATION, @@ -46,9 +46,26 @@ macro_rules! tri { }; } +pub(super) fn is_known_precompile(address: Address, _chain_id: u64) -> bool { + address[..19].iter().all(|&x| x == 0) && + matches!( + address, + EC_RECOVER | + SHA_256 | + RIPEMD_160 | + IDENTITY | + MOD_EXP | + EC_ADD | + EC_MUL | + EC_PAIRING | + BLAKE_2F | + POINT_EVALUATION + ) +} + /// Tries to decode a precompile call. Returns `Some` if successful. pub(super) fn decode(trace: &CallTrace, _chain_id: u64) -> Option { - if !trace.address[..19].iter().all(|&x| x == 0) { + if !is_known_precompile(trace.address, _chain_id) { return None; } diff --git a/crates/evm/traces/src/identifier/etherscan.rs b/crates/evm/traces/src/identifier/etherscan.rs index 96e4b69667b32..b560d7e6521de 100644 --- a/crates/evm/traces/src/identifier/etherscan.rs +++ b/crates/evm/traces/src/identifier/etherscan.rs @@ -95,17 +95,17 @@ impl EtherscanIdentifier { } impl TraceIdentifier for EtherscanIdentifier { - fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec> - where - A: Iterator, Option<&'a [u8]>)>, - { - trace!(target: "evm::traces", "identify {:?} addresses", addresses.size_hint().1); - + fn identify_addresses( + &mut self, + addresses: &[(&Address, Option<&[u8]>, Option<&[u8]>)], + ) -> Vec> { if self.invalid_api_key.load(Ordering::Relaxed) { // api key was marked as invalid return Vec::new() } + trace!(target: "evm::traces::etherscan", "identify {} addresses", addresses.len()); + let mut identities = Vec::new(); let mut fetcher = EtherscanFetcher::new( self.client.clone(), @@ -114,7 +114,7 @@ impl TraceIdentifier for EtherscanIdentifier { Arc::clone(&self.invalid_api_key), ); - for (addr, _, _) in addresses { + for &(addr, _, _) in addresses { if let Some(metadata) = self.contracts.get(addr) { let label = metadata.contract_name.clone(); let abi = metadata.abi().ok().map(Cow::Owned); diff --git a/crates/evm/traces/src/identifier/local.rs b/crates/evm/traces/src/identifier/local.rs index 718f7162fb920..5a1beeb1701da 100644 --- a/crates/evm/traces/src/identifier/local.rs +++ b/crates/evm/traces/src/identifier/local.rs @@ -69,7 +69,7 @@ impl<'a> LocalTraceIdentifier<'a> { let score = bytecode_diff_score(bytecode, current_bytecode); if score == 0.0 { - trace!(target: "evm::traces", "found exact match"); + trace!(target: "evm::traces::local", "found exact match"); return Some((id, &contract.abi)); } if score < *min_score { @@ -114,7 +114,7 @@ impl<'a> LocalTraceIdentifier<'a> { } } - trace!(target: "evm::traces", %min_score, "no exact match found"); + trace!(target: "evm::traces::local", %min_score, "no exact match found"); // Note: the diff score can be inaccurate for small contracts so we're using a relatively // high threshold here to avoid filtering out too many contracts. @@ -141,19 +141,21 @@ impl<'a> LocalTraceIdentifier<'a> { } impl TraceIdentifier for LocalTraceIdentifier<'_> { - fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec> - where - A: Iterator, Option<&'a [u8]>)>, - { - trace!(target: "evm::traces", "identify {:?} addresses", addresses.size_hint().1); + fn identify_addresses( + &mut self, + addresses: &[(&Address, Option<&[u8]>, Option<&[u8]>)], + ) -> Vec> { + trace!(target: "evm::traces::local", "identify {} addresses", addresses.len()); addresses + .iter() + .copied() .filter_map(|(address, runtime_code, creation_code)| { - let _span = trace_span!(target: "evm::traces", "identify", %address).entered(); + let _span = + trace_span!(target: "evm::traces::local", "identify", %address).entered(); - trace!(target: "evm::traces", "identifying"); let (id, abi) = self.identify_code(runtime_code?, creation_code?)?; - trace!(target: "evm::traces", id=%id.identifier(), "identified"); + trace!(target: "evm::traces::local", id=%id.identifier(), "identified"); Some(AddressIdentity { address: *address, diff --git a/crates/evm/traces/src/identifier/mod.rs b/crates/evm/traces/src/identifier/mod.rs index 51f949832659f..1a64c33fac9ad 100644 --- a/crates/evm/traces/src/identifier/mod.rs +++ b/crates/evm/traces/src/identifier/mod.rs @@ -12,7 +12,7 @@ mod etherscan; pub use etherscan::EtherscanIdentifier; mod signatures; -pub use signatures::{CachedSignatures, SignaturesIdentifier, SingleSignaturesIdentifier}; +pub use signatures::{SignaturesCache, SignaturesIdentifier}; /// An address identity pub struct AddressIdentity<'a> { @@ -33,9 +33,10 @@ pub struct AddressIdentity<'a> { /// Trace identifiers figure out what ABIs and labels belong to all the addresses of the trace. pub trait TraceIdentifier { /// Attempts to identify an address in one or more call traces. - fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec> - where - A: Iterator, Option<&'a [u8]>)> + Clone; + fn identify_addresses( + &mut self, + addresses: &[(&Address, Option<&[u8]>, Option<&[u8]>)], + ) -> Vec>; } /// A collection of trace identifiers. @@ -53,13 +54,16 @@ impl Default for TraceIdentifiers<'_> { } impl TraceIdentifier for TraceIdentifiers<'_> { - fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec> - where - A: Iterator, Option<&'a [u8]>)> + Clone, - { - let mut identities = Vec::new(); + fn identify_addresses( + &mut self, + addresses: &[(&Address, Option<&[u8]>, Option<&[u8]>)], + ) -> Vec> { + let mut identities = Vec::with_capacity(addresses.len()); if let Some(local) = &mut self.local { - identities.extend(local.identify_addresses(addresses.clone())); + identities.extend(local.identify_addresses(addresses)); + if identities.len() >= addresses.len() { + return identities; + } } if let Some(etherscan) = &mut self.etherscan { identities.extend(etherscan.identify_addresses(addresses)); diff --git a/crates/evm/traces/src/identifier/signatures.rs b/crates/evm/traces/src/identifier/signatures.rs index d1c6f61aa3571..d13aff4b29150 100644 --- a/crates/evm/traces/src/identifier/signatures.rs +++ b/crates/evm/traces/src/identifier/signatures.rs @@ -1,176 +1,266 @@ -use alloy_json_abi::{Error, Event, Function}; -use alloy_primitives::{hex, map::HashSet}; +use alloy_json_abi::{Error, Event, Function, JsonAbi}; +use alloy_primitives::{map::HashMap, Selector, B256}; +use eyre::Result; use foundry_common::{ abi::{get_error, get_event, get_func}, fs, - selectors::{OpenChainClient, SelectorType}, + selectors::{OpenChainClient, SelectorKind}, }; +use foundry_config::Config; use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + sync::Arc, +}; use tokio::sync::RwLock; -pub type SingleSignaturesIdentifier = Arc>; +/// Cache for function, event and error signatures. Used by [`SignaturesIdentifier`]. +#[derive(Debug, Default, Deserialize)] +#[serde(try_from = "SignaturesDiskCache")] +pub struct SignaturesCache { + signatures: HashMap>, +} -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct CachedSignatures { - pub errors: BTreeMap, - pub events: BTreeMap, - pub functions: BTreeMap, +/// Disk representation of the signatures cache. +#[derive(Serialize, Deserialize)] +struct SignaturesDiskCache { + functions: BTreeMap, + errors: BTreeMap, + events: BTreeMap, } -impl CachedSignatures { +impl From for SignaturesCache { + fn from(value: SignaturesDiskCache) -> Self { + let functions = value + .functions + .into_iter() + .map(|(selector, signature)| (SelectorKind::Function(selector), signature)); + let errors = value + .errors + .into_iter() + .map(|(selector, signature)| (SelectorKind::Error(selector), signature)); + let events = value + .events + .into_iter() + .map(|(selector, signature)| (SelectorKind::Event(selector), signature)); + Self { + signatures: functions + .chain(errors) + .chain(events) + .map(|(sel, sig)| (sel, (!sig.is_empty()).then_some(sig))) + .collect(), + } + } +} + +impl From<&SignaturesCache> for SignaturesDiskCache { + fn from(value: &SignaturesCache) -> Self { + let (functions, errors, events) = value.signatures.iter().fold( + (BTreeMap::new(), BTreeMap::new(), BTreeMap::new()), + |mut acc, (kind, signature)| { + let value = signature.clone().unwrap_or_default(); + match *kind { + SelectorKind::Function(selector) => _ = acc.0.insert(selector, value), + SelectorKind::Error(selector) => _ = acc.1.insert(selector, value), + SelectorKind::Event(selector) => _ = acc.2.insert(selector, value), + } + acc + }, + ); + Self { functions, errors, events } + } +} + +impl Serialize for SignaturesCache { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + SignaturesDiskCache::from(self).serialize(serializer) + } +} + +impl SignaturesCache { + /// Loads the cache from a file. #[instrument(target = "evm::traces")] - pub fn load(cache_path: PathBuf) -> Self { - let path = cache_path.join("signatures"); - if path.is_file() { - fs::read_json_file(&path) - .map_err( - |err| warn!(target: "evm::traces", ?path, ?err, "failed to read cache file"), - ) - .unwrap_or_default() - } else { - if let Err(err) = std::fs::create_dir_all(cache_path) { - warn!(target: "evm::traces", "could not create signatures cache dir: {:?}", err); + pub fn load(path: &Path) -> Self { + trace!(target: "evm::traces", ?path, "reading signature cache"); + fs::read_json_file(path) + .inspect_err( + |err| warn!(target: "evm::traces", ?path, ?err, "failed to read cache file"), + ) + .unwrap_or_default() + } + + /// Saves the cache to a file. + #[instrument(target = "evm::traces", skip(self))] + pub fn save(&self, path: &Path) { + if let Some(parent) = path.parent() { + if let Err(err) = std::fs::create_dir_all(parent) { + warn!(target: "evm::traces", ?parent, %err, "failed to create cache"); } - Self::default() } + if let Err(err) = fs::write_json_file(path, self) { + warn!(target: "evm::traces", %err, "failed to flush signature cache"); + } else { + trace!(target: "evm::traces", "flushed signature cache") + } + } + + /// Updates the cache from an ABI. + pub fn extend_from_abi(&mut self, abi: &JsonAbi) { + self.extend(abi.items().filter_map(|item| match item { + alloy_json_abi::AbiItem::Function(f) => { + Some((SelectorKind::Function(f.selector()), f.signature())) + } + alloy_json_abi::AbiItem::Error(e) => { + Some((SelectorKind::Error(e.selector()), e.signature())) + } + alloy_json_abi::AbiItem::Event(e) => { + Some((SelectorKind::Event(e.selector()), e.full_signature())) + } + _ => None, + })); + } + + /// Inserts a single signature into the cache. + pub fn insert(&mut self, key: SelectorKind, value: String) { + self.extend(std::iter::once((key, value))); + } + + /// Extends the cache with multiple signatures. + pub fn extend(&mut self, signatures: impl IntoIterator) { + self.signatures.extend(signatures.into_iter().map(|(k, v)| (k, Some(v)))); + } + + /// Gets a signature from the cache. + pub fn get(&self, key: &SelectorKind) -> Option> { + self.signatures.get(key).cloned() + } + + /// Returns true if the cache contains a signature. + pub fn contains_key(&self, key: &SelectorKind) -> bool { + self.signatures.contains_key(key) } } + /// An identifier that tries to identify functions and events using signatures found at /// `https://openchain.xyz` or a local cache. -#[derive(Debug)] +#[derive(Clone, Debug)] +#[allow(clippy::new_without_default)] pub struct SignaturesIdentifier { /// Cached selectors for functions, events and custom errors. - cached: CachedSignatures, - /// Location where to save `CachedSignatures`. - cached_path: Option, - /// Selectors that were unavailable during the session. - unavailable: HashSet, - /// The OpenChain client to fetch signatures from. + cache: Arc>, + /// Location where to save the signature cache. + cache_path: Option, + /// The OpenChain client to fetch signatures from. `None` if disabled on construction. client: Option, } impl SignaturesIdentifier { - #[instrument(target = "evm::traces")] - pub fn new( - cache_path: Option, - offline: bool, - ) -> eyre::Result { - let client = if !offline { Some(OpenChainClient::new()?) } else { None }; + /// Creates a new `SignaturesIdentifier` with the default cache directory. + pub fn new(offline: bool) -> Result { + Self::new_with(Config::foundry_cache_dir().as_deref(), offline) + } - let identifier = if let Some(cache_path) = cache_path { - let path = cache_path.join("signatures"); - trace!(target: "evm::traces", ?path, "reading signature cache"); - let cached = CachedSignatures::load(cache_path); - Self { cached, cached_path: Some(path), unavailable: HashSet::default(), client } + /// Creates a new `SignaturesIdentifier` from the global configuration. + pub fn from_config(config: &Config) -> Result { + Self::new(config.offline) + } + + /// Creates a new `SignaturesIdentifier`. + /// + /// - `cache_dir` is the cache directory to store the signatures. + /// - `offline` disables the OpenChain client. + pub fn new_with(cache_dir: Option<&Path>, offline: bool) -> Result { + let client = if !offline { Some(OpenChainClient::new()?) } else { None }; + let (cache, cache_path) = if let Some(cache_dir) = cache_dir { + let path = cache_dir.join("signatures"); + let cache = SignaturesCache::load(&path); + (cache, Some(path)) } else { - Self { - cached: Default::default(), - cached_path: None, - unavailable: HashSet::default(), - client, - } + Default::default() }; - - Ok(Arc::new(RwLock::new(identifier))) + Ok(Self { cache: Arc::new(RwLock::new(cache)), cache_path, client }) } - #[instrument(target = "evm::traces", skip(self))] + /// Saves the cache to the file system. pub fn save(&self) { - if let Some(cached_path) = &self.cached_path { - if let Some(parent) = cached_path.parent() { - if let Err(err) = std::fs::create_dir_all(parent) { - warn!(target: "evm::traces", ?parent, ?err, "failed to create cache"); - } - } - if let Err(err) = fs::write_json_file(cached_path, &self.cached) { - warn!(target: "evm::traces", ?cached_path, ?err, "failed to flush signature cache"); - } else { - trace!(target: "evm::traces", ?cached_path, "flushed signature cache") - } + if let Some(path) = &self.cache_path { + self.cache.blocking_read().save(path); } } } impl SignaturesIdentifier { - async fn identify( - &mut self, - selector_type: SelectorType, - identifiers: impl IntoIterator>, - get_type: impl Fn(&str) -> eyre::Result, - ) -> Vec> { - let cache = match selector_type { - SelectorType::Function => &mut self.cached.functions, - SelectorType::Event => &mut self.cached.events, - SelectorType::Error => &mut self.cached.errors, - }; - - let hex_identifiers: Vec = - identifiers.into_iter().map(hex::encode_prefixed).collect(); - - if let Some(client) = &self.client { - let query: Vec<_> = hex_identifiers - .iter() - .filter(|v| !cache.contains_key(v.as_str())) - .filter(|v| !self.unavailable.contains(v.as_str())) - .collect(); - - if let Ok(res) = client.decode_selectors(selector_type, query.clone()).await { - for (hex_id, selector_result) in query.into_iter().zip(res.into_iter()) { - let mut found = false; - if let Some(decoded_results) = selector_result { - if let Some(decoded_result) = decoded_results.into_iter().next() { - cache.insert(hex_id.clone(), decoded_result); - found = true; - } - } - if !found { - self.unavailable.insert(hex_id.clone()); - } - } - } - } - - hex_identifiers.iter().map(|v| cache.get(v).and_then(|v| get_type(v).ok())).collect() - } - - /// Identifies `Function`s from its cache or `https://api.openchain.xyz` + /// Identifies `Function`s. pub async fn identify_functions( - &mut self, - identifiers: impl IntoIterator>, + &self, + identifiers: impl IntoIterator, ) -> Vec> { - self.identify(SelectorType::Function, identifiers, get_func).await + self.identify_map(identifiers.into_iter().map(SelectorKind::Function), get_func).await } - /// Identifies `Function` from its cache or `https://api.openchain.xyz` - pub async fn identify_function(&mut self, identifier: &[u8]) -> Option { - self.identify_functions(&[identifier]).await.pop().unwrap() + /// Identifies a `Function`. + pub async fn identify_function(&self, identifier: Selector) -> Option { + self.identify_functions([identifier]).await.pop().unwrap() } - /// Identifies `Event`s from its cache or `https://api.openchain.xyz` + /// Identifies `Event`s. pub async fn identify_events( - &mut self, - identifiers: impl IntoIterator>, + &self, + identifiers: impl IntoIterator, ) -> Vec> { - self.identify(SelectorType::Event, identifiers, get_event).await + self.identify_map(identifiers.into_iter().map(SelectorKind::Event), get_event).await } - /// Identifies `Event` from its cache or `https://api.openchain.xyz` - pub async fn identify_event(&mut self, identifier: &[u8]) -> Option { - self.identify_events(&[identifier]).await.pop().unwrap() + /// Identifies an `Event`. + pub async fn identify_event(&self, identifier: B256) -> Option { + self.identify_events([identifier]).await.pop().unwrap() } - /// Identifies `Error`s from its cache or `https://api.openchain.xyz`. + /// Identifies `Error`s. pub async fn identify_errors( - &mut self, - identifiers: impl IntoIterator>, + &self, + identifiers: impl IntoIterator, ) -> Vec> { - self.identify(SelectorType::Error, identifiers, get_error).await + self.identify_map(identifiers.into_iter().map(SelectorKind::Error), get_error).await + } + + /// Identifies an `Error`. + pub async fn identify_error(&self, identifier: Selector) -> Option { + self.identify_errors([identifier]).await.pop().unwrap() } - /// Identifies `Error` from its cache or `https://api.openchain.xyz`. - pub async fn identify_error(&mut self, identifier: &[u8]) -> Option { - self.identify_errors(&[identifier]).await.pop().unwrap() + /// Identifies a list of selectors. + pub async fn identify(&self, selectors: &[SelectorKind]) -> Vec> { + let mut cache_r = self.cache.read().await; + if let Some(client) = &self.client { + let query = + selectors.iter().copied().filter(|v| !cache_r.contains_key(v)).collect::>(); + if !query.is_empty() { + drop(cache_r); + let mut cache_w = self.cache.write().await; + if let Ok(res) = client.decode_selectors(&query).await { + for (selector, signatures) in std::iter::zip(query, res) { + cache_w.signatures.insert(selector, signatures.into_iter().next()); + } + } + drop(cache_w); + cache_r = self.cache.read().await; + } + } + selectors.iter().map(|selector| cache_r.get(selector).unwrap_or_default()).collect() + } + + async fn identify_map( + &self, + selectors: impl IntoIterator, + get_type: impl Fn(&str) -> Result, + ) -> Vec> { + let results = self.identify(&Vec::from_iter(selectors)).await; + results.into_iter().map(|r| r.and_then(|r| get_type(&r).ok())).collect() } } diff --git a/crates/evm/traces/src/lib.rs b/crates/evm/traces/src/lib.rs index 0d22352ca9e3b..273739ac717f4 100644 --- a/crates/evm/traces/src/lib.rs +++ b/crates/evm/traces/src/lib.rs @@ -174,14 +174,9 @@ impl DerefMut for SparsedTraceArena { /// Decode a collection of call traces. /// /// The traces will be decoded using the given decoder, if possible. -pub async fn decode_trace_arena( - arena: &mut CallTraceArena, - decoder: &CallTraceDecoder, -) -> Result<(), std::fmt::Error> { +pub async fn decode_trace_arena(arena: &mut CallTraceArena, decoder: &CallTraceDecoder) { decoder.prefetch_signatures(arena.nodes()).await; decoder.populate_traces(arena.nodes_mut()).await; - - Ok(()) } /// Render a collection of call traces to a string. @@ -260,7 +255,7 @@ pub fn load_contracts<'a>( let decoder = CallTraceDecoder::new(); let mut contracts = ContractsByAddress::new(); for trace in traces { - for address in local_identifier.identify_addresses(decoder.trace_addresses(trace)) { + for address in local_identifier.identify_addresses(&decoder.trace_addresses(trace)) { if let (Some(contract), Some(abi)) = (address.contract, address.abi) { contracts.insert(address.address, (contract, abi.into_owned())); } diff --git a/crates/forge/src/cmd/selectors.rs b/crates/forge/src/cmd/selectors.rs index c3d0b67eda930..5e858c9da9c58 100644 --- a/crates/forge/src/cmd/selectors.rs +++ b/crates/forge/src/cmd/selectors.rs @@ -95,7 +95,7 @@ impl SelectorsSubcommands { // compile the project to get the artifacts/abis let project = build_args.project()?; let outcome = ProjectCompiler::new().quiet(true).compile(&project)?; - cache_local_signatures(&outcome, Config::foundry_cache_dir().unwrap())? + cache_local_signatures(&outcome, &Config::foundry_cache_dir().unwrap())? } Self::Upload { contract, all, project_paths } => { let build_args = BuildOpts { diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index bf45088dc1000..a1f3e64bb574f 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -368,7 +368,7 @@ impl TestArgs { // Decode traces. let decoder = outcome.last_run_decoder.as_ref().unwrap(); - decode_trace_arena(arena, decoder).await?; + decode_trace_arena(arena, decoder).await; let mut fst = folded_stack_trace::build(arena); let label = if self.flamegraph { "flamegraph" } else { "flamechart" }; @@ -524,10 +524,8 @@ impl TestArgs { .with_verbosity(verbosity); // Signatures are of no value for gas reports. if !self.gas_report { - builder = builder.with_signature_identifier(SignaturesIdentifier::new( - Config::foundry_cache_dir(), - config.offline, - )?); + builder = + builder.with_signature_identifier(SignaturesIdentifier::from_config(&config)?); } if self.decode_internal { @@ -637,7 +635,7 @@ impl TestArgs { }; if should_include { - decode_trace_arena(arena, &decoder).await?; + decode_trace_arena(arena, &decoder).await; decoded_traces.push(render_trace_arena_inner(arena, false, verbosity > 4)); } } diff --git a/crates/forge/tests/it/config.rs b/crates/forge/tests/it/config.rs index 390735b3245ec..022303d51380f 100644 --- a/crates/forge/tests/it/config.rs +++ b/crates/forge/tests/it/config.rs @@ -77,9 +77,7 @@ impl TestConfig { let decoded_traces = join_all(result.traces.iter_mut().map(|(_, arena)| { let decoder = &call_trace_decoder; async move { - decode_trace_arena(arena, decoder) - .await - .expect("Failed to decode traces"); + decode_trace_arena(arena, decoder).await; render_trace_arena(arena) } })) diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index 1073f7cc6bd95..48d37cfca4329 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -20,7 +20,7 @@ use foundry_common::{ provider::get_http_provider, ContractsByArtifact, }; -use foundry_config::{Config, NamedChain}; +use foundry_config::NamedChain; use foundry_debugger::Debugger; use foundry_evm::{ decode::decode_console_logs, @@ -328,9 +328,8 @@ impl ExecutedState { .with_labels(self.execution_result.labeled_addresses.clone()) .with_verbosity(self.script_config.evm_opts.verbosity) .with_known_contracts(known_contracts) - .with_signature_identifier(SignaturesIdentifier::new( - Config::foundry_cache_dir(), - self.script_config.config.offline, + .with_signature_identifier(SignaturesIdentifier::from_config( + &self.script_config.config, )?) .build(); @@ -428,7 +427,7 @@ impl PreSimulationState { if should_include { let mut trace = trace.clone(); - decode_trace_arena(&mut trace, decoder).await?; + decode_trace_arena(&mut trace, decoder).await; sh_println!("{}", render_trace_arena(&trace))?; } } diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index a58b1058ebc2e..7cd23ee74bace 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -168,7 +168,7 @@ impl PreSimulationState { // Transaction will be `None`, if execution didn't pass. if tx.is_none() || self.script_config.evm_opts.verbosity > 3 { for (_, trace) in &mut traces { - decode_trace_arena(trace, &self.execution_artifacts.decoder).await?; + decode_trace_arena(trace, &self.execution_artifacts.decoder).await; sh_println!("{}", render_trace_arena(trace))?; } } From fa44176778f043d8eb51edad9bf836011bad1efc Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 9 Apr 2025 12:42:13 +0200 Subject: [PATCH 02/13] nit --- crates/evm/traces/src/decoder/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index afd2d042807dd..c8de1381b6db4 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -655,9 +655,8 @@ impl CallTraceDecoder { { return None; } - n.trace.data.get(..SELECTOR_LEN) + n.trace.data.first_chunk().map(Selector::from) }) - .map(|bytes| Selector::try_from(bytes).unwrap()) .filter(|selector| !self.functions.contains_key(selector)); let selectors = events .map(SelectorKind::Event) From 4b9b39f8fa2ba72b8a9afdcd0d0721bc60757ecc Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:12:18 +0200 Subject: [PATCH 03/13] cleanup --- crates/common/src/selectors.rs | 18 +++++++++++------- crates/evm/traces/src/decoder/mod.rs | 13 +++++++++---- crates/evm/traces/src/identifier/etherscan.rs | 3 +-- crates/evm/traces/src/identifier/local.rs | 4 ++++ crates/evm/traces/src/identifier/signatures.rs | 9 +++++++-- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/crates/common/src/selectors.rs b/crates/common/src/selectors.rs index 35ddf2f0044d9..9dc20044f2ce5 100644 --- a/crates/common/src/selectors.rs +++ b/crates/common/src/selectors.rs @@ -146,8 +146,11 @@ impl OpenChainClient { return Ok(vec![]); } - debug!(len = selectors.len(), "decoding selectors"); - trace!(?selectors, "decoding selectors"); + if enabled!(tracing::Level::TRACE) { + trace!(?selectors, "decoding selectors"); + } else { + debug!(len = selectors.len(), "decoding selectors"); + } // Exit early if spurious connection. self.ensure_not_spurious()?; @@ -168,7 +171,7 @@ impl OpenChainClient { } let text = self.get_text(url).await?; - let SignatureResponse { ok, mut result } = match serde_json::from_str(&text) { + let SignatureResponse { ok, result } = match serde_json::from_str(&text) { Ok(response) => response, Err(err) => eyre::bail!("could not decode response: {err}: {text}"), }; @@ -181,15 +184,16 @@ impl OpenChainClient { .map(|selector| { let signatures = match selector { SelectorKind::Function(selector) | SelectorKind::Error(selector) => { - result.function.remove(selector) + result.function.get(selector) } - SelectorKind::Event(hash) => result.event.remove(hash), + SelectorKind::Event(hash) => result.event.get(hash), }; signatures + .map(Option::as_deref) .unwrap_or_default() .unwrap_or_default() - .into_iter() - .map(|sig| sig.name) + .iter() + .map(|sig| sig.name.clone()) .collect() }) .collect()) diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index c8de1381b6db4..191825c7fd67f 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -256,14 +256,15 @@ impl CallTraceDecoder { node.trace.kind.is_any_create().then_some(&node.trace.data[..]), ) }) - .filter(|&(address, _, _)| { + .filter(|&(address, ..)| { !self.labels.contains_key(address) || !self.contracts.contains_key(address) }) .collect() } - fn collect_identities(&mut self, identities: Vec>) { - // Skip logging if there are no identities. + fn collect_identities(&mut self, mut identities: Vec>) { + identities.sort_by_key(|identity| identity.address); + identities.dedup_by_key(|identity| identity.address); if identities.is_empty() { return; } @@ -287,7 +288,11 @@ impl CallTraceDecoder { } fn collect_abi(&mut self, abi: &JsonAbi, address: Option<&Address>) { - trace!(target: "evm::traces", len=abi.len(), ?address, "collecting ABI"); + let len = abi.len(); + if len == 0 { + return; + } + trace!(target: "evm::traces", len, ?address, "collecting ABI"); for function in abi.functions() { self.push_function(function.clone()); } diff --git a/crates/evm/traces/src/identifier/etherscan.rs b/crates/evm/traces/src/identifier/etherscan.rs index b560d7e6521de..6d16866cf0f69 100644 --- a/crates/evm/traces/src/identifier/etherscan.rs +++ b/crates/evm/traces/src/identifier/etherscan.rs @@ -99,8 +99,7 @@ impl TraceIdentifier for EtherscanIdentifier { &mut self, addresses: &[(&Address, Option<&[u8]>, Option<&[u8]>)], ) -> Vec> { - if self.invalid_api_key.load(Ordering::Relaxed) { - // api key was marked as invalid + if self.invalid_api_key.load(Ordering::Relaxed) || addresses.is_empty() { return Vec::new() } diff --git a/crates/evm/traces/src/identifier/local.rs b/crates/evm/traces/src/identifier/local.rs index 5a1beeb1701da..b0640eabd7f95 100644 --- a/crates/evm/traces/src/identifier/local.rs +++ b/crates/evm/traces/src/identifier/local.rs @@ -145,6 +145,10 @@ impl TraceIdentifier for LocalTraceIdentifier<'_> { &mut self, addresses: &[(&Address, Option<&[u8]>, Option<&[u8]>)], ) -> Vec> { + if addresses.is_empty() { + return Vec::new(); + } + trace!(target: "evm::traces::local", "identify {} addresses", addresses.len()); addresses diff --git a/crates/evm/traces/src/identifier/signatures.rs b/crates/evm/traces/src/identifier/signatures.rs index d13aff4b29150..5f304c4268764 100644 --- a/crates/evm/traces/src/identifier/signatures.rs +++ b/crates/evm/traces/src/identifier/signatures.rs @@ -131,7 +131,8 @@ impl SignaturesCache { /// Extends the cache with multiple signatures. pub fn extend(&mut self, signatures: impl IntoIterator) { - self.signatures.extend(signatures.into_iter().map(|(k, v)| (k, Some(v)))); + self.signatures + .extend(signatures.into_iter().map(|(k, v)| (k, (!v.is_empty()).then_some(v)))); } /// Gets a signature from the cache. @@ -148,7 +149,6 @@ impl SignaturesCache { /// An identifier that tries to identify functions and events using signatures found at /// `https://openchain.xyz` or a local cache. #[derive(Clone, Debug)] -#[allow(clippy::new_without_default)] pub struct SignaturesIdentifier { /// Cached selectors for functions, events and custom errors. cache: Arc>, @@ -235,6 +235,11 @@ impl SignaturesIdentifier { /// Identifies a list of selectors. pub async fn identify(&self, selectors: &[SelectorKind]) -> Vec> { + if selectors.is_empty() { + return vec![]; + } + trace!(target: "evm::traces", ?selectors, "identifying selectors"); + let mut cache_r = self.cache.read().await; if let Some(client) = &self.client { let query = From ffba128d32843b9884c353be5961d77c6325fe88 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:32:28 +0200 Subject: [PATCH 04/13] dedup fallback decoding --- crates/evm/traces/src/decoder/mod.rs | 54 +++++++++++++++------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index 191825c7fd67f..968b83552e5cc 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -6,7 +6,7 @@ use crate::{ use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt, FunctionExt, JsonAbiExt}; use alloy_json_abi::{Error, Event, Function, JsonAbi}; use alloy_primitives::{ - map::{hash_map::Entry, HashMap}, + map::{hash_map::Entry, HashMap, HashSet}, Address, LogData, Selector, B256, }; use foundry_common::{ @@ -119,9 +119,10 @@ pub struct CallTraceDecoder { /// Address labels. pub labels: HashMap, /// Contract addresses that have a receive function. - pub receive_contracts: Vec

, - /// Contract addresses that have fallback functions, mapped to function sigs. - pub fallback_contracts: HashMap>, + pub receive_contracts: HashSet
, + /// Contract addresses that have fallback functions, mapped to function selectors of that + /// contract. + pub fallback_contracts: HashMap>, /// All known functions. pub functions: HashMap>, @@ -304,15 +305,12 @@ impl CallTraceDecoder { } if let Some(address) = address { if abi.receive.is_some() { - self.receive_contracts.push(*address); + self.receive_contracts.insert(*address); } if abi.fallback.is_some() { - let mut functions_sig = vec![]; - for function in abi.functions() { - functions_sig.push(function.signature()); - } - self.fallback_contracts.insert(*address, functions_sig); + self.fallback_contracts + .insert(*address, abi.functions().map(|f| f.selector()).collect()); } } } @@ -369,12 +367,7 @@ impl CallTraceDecoder { let [func, ..] = &functions[..] else { return DecodedCallTrace { label, - call_data: self.fallback_contracts.get(&trace.address).map(|_| { - DecodedCallData { - signature: "fallback()".to_string(), - args: vec![cdata.to_string()], - } - }), + call_data: Some(self.fallback_calldata(trace)), return_data: self.default_return_data(trace), }; }; @@ -383,8 +376,8 @@ impl CallTraceDecoder { // If not, then replace call data signature with `fallback`. let mut call_data = self.decode_function_input(trace, func); if let Some(fallback_functions) = self.fallback_contracts.get(&trace.address) { - if !fallback_functions.contains(&func.signature()) { - call_data.signature = "fallback()".to_string(); + if !fallback_functions.contains(&selector) { + call_data = self.fallback_calldata(trace); } } @@ -394,14 +387,9 @@ impl CallTraceDecoder { return_data: self.decode_function_output(trace, functions), } } else { - let has_receive = self.receive_contracts.contains(&trace.address); - let signature = - if cdata.is_empty() && has_receive { "receive()" } else { "fallback()" } - .to_string(); - let args = if cdata.is_empty() { Vec::new() } else { vec![cdata.to_string()] }; DecodedCallTrace { label, - call_data: Some(DecodedCallData { signature, args }), + call_data: Some(self.fallback_calldata(trace)), return_data: self.default_return_data(trace), } } @@ -601,6 +589,18 @@ impl CallTraceDecoder { .map(Into::into) } + fn fallback_calldata(&self, trace: &CallTrace) -> DecodedCallData { + let cdata = &trace.data; + let signature = if cdata.is_empty() && self.receive_contracts.contains(&trace.address) { + "receive()" + } else { + "fallback()" + } + .to_string(); + let args = if cdata.is_empty() { Vec::new() } else { vec![cdata.to_string()] }; + DecodedCallData { signature, args } + } + /// 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))) @@ -655,11 +655,17 @@ impl CallTraceDecoder { let functions = nodes .iter() .filter_map(|n| { + // Ignore CREATE2 and precompiles. if n.trace.address == DEFAULT_CREATE2_DEPLOYER || + n.is_precompile() || precompiles::is_known_precompile(n.trace.address, 1) { return None; } + // Ignore non-ABI calldata. + if n.trace.data.len().saturating_sub(4) % 32 != 0 { + return None; + } n.trace.data.first_chunk().map(Selector::from) }) .filter(|selector| !self.functions.contains_key(selector)); From 36d69933c22226050787bd443aa9126d19749990 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:34:19 +0200 Subject: [PATCH 05/13] fix --- crates/evm/traces/src/identifier/signatures.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/evm/traces/src/identifier/signatures.rs b/crates/evm/traces/src/identifier/signatures.rs index 5f304c4268764..41f50ee02f0ba 100644 --- a/crates/evm/traces/src/identifier/signatures.rs +++ b/crates/evm/traces/src/identifier/signatures.rs @@ -188,7 +188,7 @@ impl SignaturesIdentifier { /// Saves the cache to the file system. pub fn save(&self) { if let Some(path) = &self.cache_path { - self.cache.blocking_read().save(path); + foundry_compilers::utils::RuntimeOrHandle::new().block_on(self.cache.read()).save(path); } } } From 40f1786b98fb5f5e656ee7765472115c2343c3c1 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:37:56 +0200 Subject: [PATCH 06/13] chore: clippy --- crates/evm/traces/src/decoder/mod.rs | 3 ++- crates/evm/traces/src/identifier/mod.rs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index 968b83552e5cc..18a50c5cd3f0e 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -242,7 +242,8 @@ impl CallTraceDecoder { self.revert_decoder.push_error(error); } - /// Returns an iterator over the trace addresses. + /// Returns a list over all the addresses in the given trace. + #[allow(clippy::type_complexity)] pub fn trace_addresses<'a>( &'a self, arena: &'a CallTraceArena, diff --git a/crates/evm/traces/src/identifier/mod.rs b/crates/evm/traces/src/identifier/mod.rs index 1a64c33fac9ad..766c6e7ed09c1 100644 --- a/crates/evm/traces/src/identifier/mod.rs +++ b/crates/evm/traces/src/identifier/mod.rs @@ -33,6 +33,7 @@ pub struct AddressIdentity<'a> { /// Trace identifiers figure out what ABIs and labels belong to all the addresses of the trace. pub trait TraceIdentifier { /// Attempts to identify an address in one or more call traces. + #[allow(clippy::type_complexity)] fn identify_addresses( &mut self, addresses: &[(&Address, Option<&[u8]>, Option<&[u8]>)], From cde4346d2563da31638515a4695f566e62c830c6 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:15:13 +0200 Subject: [PATCH 07/13] feat: ignore non ABI calldata --- crates/evm/traces/src/decoder/mod.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index 18a50c5cd3f0e..098064c46b578 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -351,7 +351,7 @@ impl CallTraceDecoder { }; } - if cdata.len() >= SELECTOR_LEN { + if is_abi_calldata(cdata) { let selector = Selector::try_from(&cdata[..SELECTOR_LEN]).unwrap(); let mut functions = Vec::new(); let functions = match self.functions.get(&selector) { @@ -664,7 +664,7 @@ impl CallTraceDecoder { return None; } // Ignore non-ABI calldata. - if n.trace.data.len().saturating_sub(4) % 32 != 0 { + if !is_abi_calldata(&n.trace.data) { return None; } n.trace.data.first_chunk().map(Selector::from) @@ -689,6 +689,29 @@ impl CallTraceDecoder { } } +/// Returns `true` if the given function calldata (including function selector) is ABI-encoded. +/// +/// This is a simple heuristic to avoid fetching non ABI-encoded selectors. +fn is_abi_calldata(data: &[u8]) -> bool { + match data.len().cmp(&SELECTOR_LEN) { + std::cmp::Ordering::Less => false, + std::cmp::Ordering::Equal => true, + std::cmp::Ordering::Greater => is_abi_data(&data[SELECTOR_LEN..]), + } +} + +/// Returns `true` if the given data is ABI-encoded. +/// +/// See [`is_abi_calldata`] for more details. +fn is_abi_data(data: &[u8]) -> bool { + let rem = data.len() % 32; + if rem == 0 || data.len() == 0 { + return true; + } + // If the length is not a multiple of 32, also accept when the last remainder bytes are all 0. + data[data.len() - rem..].iter().all(|byte| *byte == 0) +} + /// Restore the order of the params of a decoded event, /// as Alloy returns the indexed and unindexed params separately. fn reconstruct_params(event: &Event, decoded: &DecodedEvent) -> Vec { From a7d6030b3b2de58051b43ea8c34ef36c68694165 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:15:30 +0200 Subject: [PATCH 08/13] feat: skip decoding create traces --- crates/evm/traces/src/decoder/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index 098064c46b578..5fcdcaa0902f6 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -321,7 +321,9 @@ impl CallTraceDecoder { /// [CallTraceDecoder::decode_event] for more details. pub async fn populate_traces(&self, traces: &mut Vec) { for node in traces { - node.trace.decoded = self.decode_function(&node.trace).await; + if !node.trace.kind.is_any_create() { + node.trace.decoded = self.decode_function(&node.trace).await; + } for log in &mut node.logs { log.decoded = self.decode_event(&log.raw_log).await; } @@ -664,7 +666,7 @@ impl CallTraceDecoder { return None; } // Ignore non-ABI calldata. - if !is_abi_calldata(&n.trace.data) { + if n.trace.kind.is_any_create() || !is_abi_calldata(&n.trace.data) { return None; } n.trace.data.first_chunk().map(Selector::from) From b8456496848953ebea162ff55536b13e742f05a4 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:57:48 +0200 Subject: [PATCH 09/13] fixes --- crates/cast/tests/cli/main.rs | 2 - crates/cli/src/utils/cmd.rs | 4 +- crates/evm/traces/src/decoder/mod.rs | 37 +++++++++++-------- .../evm/traces/src/identifier/signatures.rs | 2 - crates/test-utils/src/util.rs | 2 +- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 6e5ad4aa66b6e..5e511256f88cd 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -2179,11 +2179,9 @@ contract CounterInExternalLibScript is Script { .tx_hash(); // Cache project selectors. - cmd.forge_fuse().set_current_dir(prj.root()); cmd.forge_fuse().args(["selectors", "cache"]).assert_success(); // Assert cast with local artifacts can decode external lib signature. - cmd.cast_fuse().set_current_dir(prj.root()); cmd.cast_fuse() .args(["run", format!("{tx_hash}").as_str(), "--rpc-url", &handle.http_endpoint()]) .assert_success() diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 1c10f8a1f3e36..a28b0780e450b 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -445,9 +445,9 @@ pub fn cache_local_signatures(output: &ProjectCompileOutput, cache_dir: &Path) - signatures.extend_from_abi(abi); } - // External libraries don't have functions included in abi, but `methodIdentifiers`. + // External libraries don't have functions included in the ABI, but `methodIdentifiers`. if let Some(method_identifiers) = &artifact.method_identifiers { - signatures.extend(method_identifiers.iter().filter_map(|(selector, signature)| { + signatures.extend(method_identifiers.iter().filter_map(|(signature, selector)| { Some((SelectorKind::Function(selector.parse().ok()?), signature.clone())) })); } diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index 5fcdcaa0902f6..33047ef36da13 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -321,9 +321,7 @@ impl CallTraceDecoder { /// [CallTraceDecoder::decode_event] for more details. pub async fn populate_traces(&self, traces: &mut Vec) { for node in traces { - if !node.trace.kind.is_any_create() { - node.trace.decoded = self.decode_function(&node.trace).await; - } + node.trace.decoded = self.decode_function(&node.trace).await; for log in &mut node.logs { log.decoded = self.decode_event(&log.raw_log).await; } @@ -338,12 +336,16 @@ impl CallTraceDecoder { /// Decodes a call trace. pub async fn decode_function(&self, trace: &CallTrace) -> DecodedCallTrace { + let label = self.labels.get(&trace.address).cloned(); + + if trace.kind.is_any_create() { + return DecodedCallTrace { label, ..Default::default() }; + } + if let Some(trace) = precompiles::decode(trace, 1) { return trace; } - let label = self.labels.get(&trace.address).cloned(); - let cdata = &trace.data; if trace.address == DEFAULT_CREATE2_DEPLOYER { return DecodedCallTrace { @@ -353,7 +355,7 @@ impl CallTraceDecoder { }; } - if is_abi_calldata(cdata) { + if is_abi_call_data(cdata) { let selector = Selector::try_from(&cdata[..SELECTOR_LEN]).unwrap(); let mut functions = Vec::new(); let functions = match self.functions.get(&selector) { @@ -370,7 +372,7 @@ impl CallTraceDecoder { let [func, ..] = &functions[..] else { return DecodedCallTrace { label, - call_data: Some(self.fallback_calldata(trace)), + call_data: self.fallback_call_data(trace), return_data: self.default_return_data(trace), }; }; @@ -380,7 +382,9 @@ impl CallTraceDecoder { let mut call_data = self.decode_function_input(trace, func); if let Some(fallback_functions) = self.fallback_contracts.get(&trace.address) { if !fallback_functions.contains(&selector) { - call_data = self.fallback_calldata(trace); + if let Some(cd) = self.fallback_call_data(trace) { + call_data = cd; + } } } @@ -392,7 +396,7 @@ impl CallTraceDecoder { } else { DecodedCallTrace { label, - call_data: Some(self.fallback_calldata(trace)), + call_data: self.fallback_call_data(trace), return_data: self.default_return_data(trace), } } @@ -592,16 +596,19 @@ impl CallTraceDecoder { .map(Into::into) } - fn fallback_calldata(&self, trace: &CallTrace) -> DecodedCallData { + #[track_caller] + fn fallback_call_data(&self, trace: &CallTrace) -> Option { let cdata = &trace.data; let signature = if cdata.is_empty() && self.receive_contracts.contains(&trace.address) { "receive()" - } else { + } else if self.fallback_contracts.contains_key(&trace.address) { "fallback()" + } else { + return None; } .to_string(); let args = if cdata.is_empty() { Vec::new() } else { vec![cdata.to_string()] }; - DecodedCallData { signature, args } + Some(DecodedCallData { signature, args }) } /// The default decoded return data for a trace. @@ -666,7 +673,7 @@ impl CallTraceDecoder { return None; } // Ignore non-ABI calldata. - if n.trace.kind.is_any_create() || !is_abi_calldata(&n.trace.data) { + if n.trace.kind.is_any_create() || !is_abi_call_data(&n.trace.data) { return None; } n.trace.data.first_chunk().map(Selector::from) @@ -677,7 +684,7 @@ impl CallTraceDecoder { .chain(functions.map(SelectorKind::Function)) .unique() .collect::>(); - identifier.identify(&selectors).await; + let _ = identifier.identify(&selectors).await; } /// Pretty-prints a value. @@ -694,7 +701,7 @@ impl CallTraceDecoder { /// Returns `true` if the given function calldata (including function selector) is ABI-encoded. /// /// This is a simple heuristic to avoid fetching non ABI-encoded selectors. -fn is_abi_calldata(data: &[u8]) -> bool { +fn is_abi_call_data(data: &[u8]) -> bool { match data.len().cmp(&SELECTOR_LEN) { std::cmp::Ordering::Less => false, std::cmp::Ordering::Equal => true, diff --git a/crates/evm/traces/src/identifier/signatures.rs b/crates/evm/traces/src/identifier/signatures.rs index 41f50ee02f0ba..549430cf67df4 100644 --- a/crates/evm/traces/src/identifier/signatures.rs +++ b/crates/evm/traces/src/identifier/signatures.rs @@ -191,9 +191,7 @@ impl SignaturesIdentifier { foundry_compilers::utils::RuntimeOrHandle::new().block_on(self.cache.read()).save(path); } } -} -impl SignaturesIdentifier { /// Identifies `Function`s. pub async fn identify_functions( &self, diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index 17d6036ac5bcd..42d7e4f6a855e 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -774,7 +774,7 @@ pub struct TestCommand { /// The actual command we use to control the process. cmd: Command, // initial: Command, - current_dir_lock: Option>, + current_dir_lock: Option>, stdin_fun: Option>, /// If true, command output is redacted. redact_output: bool, From 26f32cc88a34178a5446a35295dd20f08b6cac7e Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 9 Apr 2025 16:36:59 +0200 Subject: [PATCH 10/13] fixes --- crates/cast/tests/cli/selectors.rs | 4 +++- crates/evm/traces/src/decoder/mod.rs | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/cast/tests/cli/selectors.rs b/crates/cast/tests/cli/selectors.rs index d72f0e85f3a1c..d3a7e84b88079 100644 --- a/crates/cast/tests/cli/selectors.rs +++ b/crates/cast/tests/cli/selectors.rs @@ -20,7 +20,9 @@ transfer(address,uint256) casttest!(fourbyte_invalid, |_prj, cmd| { cmd.args(["4byte", "0xa9059c"]).assert_failure().stderr_eq(str![[r#" -Error: Invalid selector 0xa9059c: expected 10 characters (including 0x prefix). +error: invalid value '0xa9059c' for '[SELECTOR]': invalid string length + +For more information, try '--help'. "#]]); }); diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index 33047ef36da13..85fc597ce5412 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -383,7 +383,7 @@ impl CallTraceDecoder { if let Some(fallback_functions) = self.fallback_contracts.get(&trace.address) { if !fallback_functions.contains(&selector) { if let Some(cd) = self.fallback_call_data(trace) { - call_data = cd; + call_data.signature = cd.signature; } } } @@ -711,10 +711,10 @@ fn is_abi_call_data(data: &[u8]) -> bool { /// Returns `true` if the given data is ABI-encoded. /// -/// See [`is_abi_calldata`] for more details. +/// See [`is_abi_call_data`] for more details. fn is_abi_data(data: &[u8]) -> bool { let rem = data.len() % 32; - if rem == 0 || data.len() == 0 { + if rem == 0 || data.is_empty() { return true; } // If the length is not a multiple of 32, also accept when the last remainder bytes are all 0. From d507b06a860b505e4b9f1c3b6c79d2357b2ca2d3 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:54:32 +0200 Subject: [PATCH 11/13] chore: use CallTraceNode directly --- crates/evm/traces/src/decoder/mod.rs | 63 +++++++++---------- crates/evm/traces/src/identifier/etherscan.rs | 27 ++++---- crates/evm/traces/src/identifier/local.rs | 27 ++++---- crates/evm/traces/src/identifier/mod.rs | 32 ++++------ crates/evm/traces/src/lib.rs | 4 +- 5 files changed, 71 insertions(+), 82 deletions(-) diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index 85fc597ce5412..a2362eb1312e8 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -1,6 +1,6 @@ use crate::{ debug::DebugTraceIdentifier, - identifier::{AddressIdentity, LocalTraceIdentifier, SignaturesIdentifier, TraceIdentifier}, + identifier::{IdentifiedAddress, LocalTraceIdentifier, SignaturesIdentifier, TraceIdentifier}, CallTrace, CallTraceArena, CallTraceNode, DecodedCallData, }; use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt, FunctionExt, JsonAbiExt}; @@ -211,8 +211,23 @@ impl CallTraceDecoder { /// Identify unknown addresses in the specified call trace using the specified identifier. /// /// Unknown contracts are contracts that either lack a label or an ABI. - pub fn identify(&mut self, trace: &CallTraceArena, identifier: &mut impl TraceIdentifier) { - self.collect_identities(identifier.identify_addresses(&self.trace_addresses(trace))); + pub fn identify(&mut self, arena: &CallTraceArena, identifier: &mut impl TraceIdentifier) { + self.collect_identified_addresses(self.identify_addresses(arena, identifier)); + } + + /// Identify unknown addresses in the specified call trace using the specified identifier. + /// + /// Unknown contracts are contracts that either lack a label or an ABI. + pub fn identify_addresses<'a>( + &self, + arena: &CallTraceArena, + identifier: &'a mut impl TraceIdentifier, + ) -> Vec> { + let nodes = arena.nodes().iter().filter(|node| { + let address = &node.trace.address; + !self.labels.contains_key(address) || !self.contracts.contains_key(address) + }); + identifier.identify_addresses(&nodes.collect::>()) } /// Adds a single event to the decoder. @@ -242,37 +257,15 @@ impl CallTraceDecoder { self.revert_decoder.push_error(error); } - /// Returns a list over all the addresses in the given trace. - #[allow(clippy::type_complexity)] - pub fn trace_addresses<'a>( - &'a self, - arena: &'a CallTraceArena, - ) -> Vec<(&'a Address, Option<&'a [u8]>, Option<&'a [u8]>)> { - arena - .nodes() - .iter() - .map(|node| { - ( - &node.trace.address, - node.trace.kind.is_any_create().then_some(&node.trace.output[..]), - node.trace.kind.is_any_create().then_some(&node.trace.data[..]), - ) - }) - .filter(|&(address, ..)| { - !self.labels.contains_key(address) || !self.contracts.contains_key(address) - }) - .collect() - } - - fn collect_identities(&mut self, mut identities: Vec>) { - identities.sort_by_key(|identity| identity.address); - identities.dedup_by_key(|identity| identity.address); - if identities.is_empty() { + fn collect_identified_addresses(&mut self, mut addrs: Vec>) { + addrs.sort_by_key(|identity| identity.address); + addrs.dedup_by_key(|identity| identity.address); + if addrs.is_empty() { return; } - trace!(target: "evm::traces", len=identities.len(), "collecting address identities"); - for AddressIdentity { address, label, contract, abi, artifact_id: _ } in identities { + trace!(target: "evm::traces", len=addrs.len(), "collecting address identities"); + for IdentifiedAddress { address, label, contract, abi, artifact_id: _ } in addrs { let _span = trace_span!(target: "evm::traces", "identity", ?contract, ?label).entered(); if let Some(contract) = contract { @@ -284,12 +277,12 @@ impl CallTraceDecoder { } if let Some(abi) = abi { - self.collect_abi(&abi, Some(&address)); + self.collect_abi(&abi, Some(address)); } } } - fn collect_abi(&mut self, abi: &JsonAbi, address: Option<&Address>) { + fn collect_abi(&mut self, abi: &JsonAbi, address: Option
) { let len = abi.len(); if len == 0 { return; @@ -306,12 +299,12 @@ impl CallTraceDecoder { } if let Some(address) = address { if abi.receive.is_some() { - self.receive_contracts.insert(*address); + self.receive_contracts.insert(address); } if abi.fallback.is_some() { self.fallback_contracts - .insert(*address, abi.functions().map(|f| f.selector()).collect()); + .insert(address, abi.functions().map(|f| f.selector()).collect()); } } } diff --git a/crates/evm/traces/src/identifier/etherscan.rs b/crates/evm/traces/src/identifier/etherscan.rs index 6d16866cf0f69..f40104656bfec 100644 --- a/crates/evm/traces/src/identifier/etherscan.rs +++ b/crates/evm/traces/src/identifier/etherscan.rs @@ -1,4 +1,4 @@ -use super::{AddressIdentity, TraceIdentifier}; +use super::{IdentifiedAddress, TraceIdentifier}; use crate::debug::ContractSources; use alloy_primitives::Address; use foundry_block_explorers::{ @@ -12,6 +12,7 @@ use futures::{ stream::{FuturesUnordered, Stream, StreamExt}, task::{Context, Poll}, }; +use revm_inspectors::tracing::types::CallTraceNode; use std::{ borrow::Cow, collections::BTreeMap, @@ -95,15 +96,12 @@ impl EtherscanIdentifier { } impl TraceIdentifier for EtherscanIdentifier { - fn identify_addresses( - &mut self, - addresses: &[(&Address, Option<&[u8]>, Option<&[u8]>)], - ) -> Vec> { - if self.invalid_api_key.load(Ordering::Relaxed) || addresses.is_empty() { + fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec> { + if self.invalid_api_key.load(Ordering::Relaxed) || nodes.is_empty() { return Vec::new() } - trace!(target: "evm::traces::etherscan", "identify {} addresses", addresses.len()); + trace!(target: "evm::traces::etherscan", "identify {} addresses", nodes.len()); let mut identities = Vec::new(); let mut fetcher = EtherscanFetcher::new( @@ -113,20 +111,21 @@ impl TraceIdentifier for EtherscanIdentifier { Arc::clone(&self.invalid_api_key), ); - for &(addr, _, _) in addresses { - if let Some(metadata) = self.contracts.get(addr) { + for node in nodes { + let address = node.trace.address; + if let Some(metadata) = self.contracts.get(&address) { let label = metadata.contract_name.clone(); let abi = metadata.abi().ok().map(Cow::Owned); - identities.push(AddressIdentity { - address: *addr, + identities.push(IdentifiedAddress { + address, label: Some(label.clone()), contract: Some(label), abi, artifact_id: None, }); } else { - fetcher.push(*addr); + fetcher.push(address); } } @@ -137,7 +136,7 @@ impl TraceIdentifier for EtherscanIdentifier { let abi = metadata.abi().ok().map(Cow::Owned); self.contracts.insert(address, metadata); - AddressIdentity { + IdentifiedAddress { address, label: Some(label.clone()), contract: Some(label), @@ -145,7 +144,7 @@ impl TraceIdentifier for EtherscanIdentifier { artifact_id: None, } }) - .collect::>>(), + .collect::>>(), ); identities.extend(fetched_identities); diff --git a/crates/evm/traces/src/identifier/local.rs b/crates/evm/traces/src/identifier/local.rs index b0640eabd7f95..54a61b65e4cb7 100644 --- a/crates/evm/traces/src/identifier/local.rs +++ b/crates/evm/traces/src/identifier/local.rs @@ -1,9 +1,9 @@ -use super::{AddressIdentity, TraceIdentifier}; +use super::{IdentifiedAddress, TraceIdentifier}; use alloy_dyn_abi::JsonAbiExt; use alloy_json_abi::JsonAbi; -use alloy_primitives::Address; use foundry_common::contracts::{bytecode_diff_score, ContractsByArtifact}; use foundry_compilers::ArtifactId; +use revm_inspectors::tracing::types::CallTraceNode; use std::borrow::Cow; /// A trace identifier that tries to identify addresses using local contracts. @@ -141,19 +141,22 @@ impl<'a> LocalTraceIdentifier<'a> { } impl TraceIdentifier for LocalTraceIdentifier<'_> { - fn identify_addresses( - &mut self, - addresses: &[(&Address, Option<&[u8]>, Option<&[u8]>)], - ) -> Vec> { - if addresses.is_empty() { + fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec> { + if nodes.is_empty() { return Vec::new(); } - trace!(target: "evm::traces::local", "identify {} addresses", addresses.len()); + trace!(target: "evm::traces::local", "identify {} addresses", nodes.len()); - addresses + nodes .iter() - .copied() + .map(|node| { + ( + node.trace.address, + node.trace.kind.is_any_create().then_some(&node.trace.output[..]), + node.trace.kind.is_any_create().then_some(&node.trace.data[..]), + ) + }) .filter_map(|(address, runtime_code, creation_code)| { let _span = trace_span!(target: "evm::traces::local", "identify", %address).entered(); @@ -161,8 +164,8 @@ impl TraceIdentifier for LocalTraceIdentifier<'_> { let (id, abi) = self.identify_code(runtime_code?, creation_code?)?; trace!(target: "evm::traces::local", id=%id.identifier(), "identified"); - Some(AddressIdentity { - address: *address, + Some(IdentifiedAddress { + address, contract: Some(id.identifier()), label: Some(id.name.clone()), abi: Some(Cow::Borrowed(abi)), diff --git a/crates/evm/traces/src/identifier/mod.rs b/crates/evm/traces/src/identifier/mod.rs index 766c6e7ed09c1..0654b5087764f 100644 --- a/crates/evm/traces/src/identifier/mod.rs +++ b/crates/evm/traces/src/identifier/mod.rs @@ -3,6 +3,7 @@ use alloy_primitives::Address; use foundry_common::ContractsByArtifact; use foundry_compilers::ArtifactId; use foundry_config::{Chain, Config}; +use revm_inspectors::tracing::types::CallTraceNode; use std::borrow::Cow; mod local; @@ -14,17 +15,17 @@ pub use etherscan::EtherscanIdentifier; mod signatures; pub use signatures::{SignaturesCache, SignaturesIdentifier}; -/// An address identity -pub struct AddressIdentity<'a> { - /// The address this identity belongs to +/// An address identified by a [`TraceIdentifier`]. +pub struct IdentifiedAddress<'a> { + /// The address. pub address: Address, - /// The label for the address + /// The label for the address. pub label: Option, - /// The contract this address represents + /// The contract this address represents. /// /// Note: This may be in the format `":"`. pub contract: Option, - /// The ABI of the contract at this address + /// The ABI of the contract at this address. pub abi: Option>, /// The artifact ID of the contract, if any. pub artifact_id: Option, @@ -33,11 +34,7 @@ pub struct AddressIdentity<'a> { /// Trace identifiers figure out what ABIs and labels belong to all the addresses of the trace. pub trait TraceIdentifier { /// Attempts to identify an address in one or more call traces. - #[allow(clippy::type_complexity)] - fn identify_addresses( - &mut self, - addresses: &[(&Address, Option<&[u8]>, Option<&[u8]>)], - ) -> Vec>; + fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec>; } /// A collection of trace identifiers. @@ -55,19 +52,16 @@ impl Default for TraceIdentifiers<'_> { } impl TraceIdentifier for TraceIdentifiers<'_> { - fn identify_addresses( - &mut self, - addresses: &[(&Address, Option<&[u8]>, Option<&[u8]>)], - ) -> Vec> { - let mut identities = Vec::with_capacity(addresses.len()); + fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec> { + let mut identities = Vec::with_capacity(nodes.len()); if let Some(local) = &mut self.local { - identities.extend(local.identify_addresses(addresses)); - if identities.len() >= addresses.len() { + identities.extend(local.identify_addresses(nodes)); + if identities.len() >= nodes.len() { return identities; } } if let Some(etherscan) = &mut self.etherscan { - identities.extend(etherscan.identify_addresses(addresses)); + identities.extend(etherscan.identify_addresses(nodes)); } identities } diff --git a/crates/evm/traces/src/lib.rs b/crates/evm/traces/src/lib.rs index 273739ac717f4..2107644793463 100644 --- a/crates/evm/traces/src/lib.rs +++ b/crates/evm/traces/src/lib.rs @@ -42,7 +42,7 @@ pub use revm_inspectors::tracing::{ /// /// Identifiers figure out what ABIs and labels belong to all the addresses of the trace. pub mod identifier; -use identifier::{LocalTraceIdentifier, TraceIdentifier}; +use identifier::LocalTraceIdentifier; mod decoder; pub use decoder::{CallTraceDecoder, CallTraceDecoderBuilder}; @@ -255,7 +255,7 @@ pub fn load_contracts<'a>( let decoder = CallTraceDecoder::new(); let mut contracts = ContractsByAddress::new(); for trace in traces { - for address in local_identifier.identify_addresses(&decoder.trace_addresses(trace)) { + for address in decoder.identify_addresses(trace, &mut local_identifier) { if let (Some(contract), Some(abi)) = (address.contract, address.abi) { contracts.insert(address.address, (contract, abi.into_owned())); } From ff3244c7ee7fca90c422fa153efffeca5f624329 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:57:51 +0200 Subject: [PATCH 12/13] chore: etherscan code dedup --- crates/evm/traces/src/identifier/etherscan.rs | 41 +++++++++---------- crates/evm/traces/src/identifier/local.rs | 2 +- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/crates/evm/traces/src/identifier/etherscan.rs b/crates/evm/traces/src/identifier/etherscan.rs index f40104656bfec..64b34e2363912 100644 --- a/crates/evm/traces/src/identifier/etherscan.rs +++ b/crates/evm/traces/src/identifier/etherscan.rs @@ -93,6 +93,22 @@ impl EtherscanIdentifier { Ok(sources) } + + fn identify_from_metadata( + &self, + address: Address, + metadata: &Metadata, + ) -> IdentifiedAddress<'static> { + let label = metadata.contract_name.clone(); + let abi = metadata.abi().ok().map(Cow::Owned); + IdentifiedAddress { + address, + label: Some(label.clone()), + contract: Some(label), + abi, + artifact_id: None, + } + } } impl TraceIdentifier for EtherscanIdentifier { @@ -111,19 +127,10 @@ impl TraceIdentifier for EtherscanIdentifier { Arc::clone(&self.invalid_api_key), ); - for node in nodes { + for &node in nodes { let address = node.trace.address; if let Some(metadata) = self.contracts.get(&address) { - let label = metadata.contract_name.clone(); - let abi = metadata.abi().ok().map(Cow::Owned); - - identities.push(IdentifiedAddress { - address, - label: Some(label.clone()), - contract: Some(label), - abi, - artifact_id: None, - }); + identities.push(self.identify_from_metadata(address, metadata)); } else { fetcher.push(address); } @@ -132,17 +139,9 @@ impl TraceIdentifier for EtherscanIdentifier { let fetched_identities = foundry_common::block_on( fetcher .map(|(address, metadata)| { - let label = metadata.contract_name.clone(); - let abi = metadata.abi().ok().map(Cow::Owned); + let addr = self.identify_from_metadata(address, &metadata); self.contracts.insert(address, metadata); - - IdentifiedAddress { - address, - label: Some(label.clone()), - contract: Some(label), - abi, - artifact_id: None, - } + addr }) .collect::>>(), ); diff --git a/crates/evm/traces/src/identifier/local.rs b/crates/evm/traces/src/identifier/local.rs index 54a61b65e4cb7..dcd4c31bcaf9f 100644 --- a/crates/evm/traces/src/identifier/local.rs +++ b/crates/evm/traces/src/identifier/local.rs @@ -150,7 +150,7 @@ impl TraceIdentifier for LocalTraceIdentifier<'_> { nodes .iter() - .map(|node| { + .map(|&node| { ( node.trace.address, node.trace.kind.is_any_create().then_some(&node.trace.output[..]), From dd6e4141ca5460bbbac7df57b5d62247b4d2bf79 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 10 Apr 2025 13:09:55 +0200 Subject: [PATCH 13/13] chore: more filtering --- crates/evm/traces/src/decoder/mod.rs | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index a2362eb1312e8..2e22ca7cdc358 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -127,6 +127,8 @@ pub struct CallTraceDecoder { /// All known functions. pub functions: HashMap>, /// All known events. + /// + /// Key is: `(topics[0], topics.len() - 1)`. pub events: BTreeMap<(B256, usize), Vec>, /// Revert decoder. Contains all known custom errors. pub revert_decoder: RevertDecoder, @@ -653,24 +655,38 @@ impl CallTraceDecoder { let Some(identifier) = &self.signature_identifier else { return }; let events = nodes .iter() - .flat_map(|node| node.logs.iter().filter_map(|log| log.raw_log.topics().first())) + .flat_map(|node| { + node.logs + .iter() + .map(|log| log.raw_log.topics()) + .filter(|&topics| { + if let Some(&first) = topics.first() { + if self.events.contains_key(&(first, topics.len() - 1)) { + return false; + } + } + true + }) + .filter_map(|topics| topics.first()) + }) .copied(); let functions = nodes .iter() - .filter_map(|n| { - // Ignore CREATE2 and precompiles. + .filter(|&n| { + // Ignore known addresses. if n.trace.address == DEFAULT_CREATE2_DEPLOYER || n.is_precompile() || precompiles::is_known_precompile(n.trace.address, 1) { - return None; + return false; } // Ignore non-ABI calldata. if n.trace.kind.is_any_create() || !is_abi_call_data(&n.trace.data) { - return None; + return false; } - n.trace.data.first_chunk().map(Selector::from) + true }) + .filter_map(|n| n.trace.data.first_chunk().map(Selector::from)) .filter(|selector| !self.functions.contains_key(selector)); let selectors = events .map(SelectorKind::Event)