diff --git a/Cargo.lock b/Cargo.lock index fb9157a4d6025..22b51194222c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4816,6 +4816,7 @@ dependencies = [ "alloy-json-abi", "alloy-primitives", "alloy-sol-types", + "async-trait", "eyre", "foundry-block-explorers", "foundry-common", @@ -4827,6 +4828,7 @@ dependencies = [ "itertools 0.14.0", "memchr", "rayon", + "reqwest", "revm", "revm-inspectors", "serde", diff --git a/crates/cast/src/debug.rs b/crates/cast/src/debug.rs index fa8fa7a1be636..e8d3fbf2da042 100644 --- a/crates/cast/src/debug.rs +++ b/crates/cast/src/debug.rs @@ -56,7 +56,7 @@ pub(crate) async fn handle_traces( .with_labels(labels.chain(config_labels)) .with_signature_identifier(SignaturesIdentifier::from_config(config)?) .with_label_disabled(disable_label); - let mut identifier = TraceIdentifiers::new().with_etherscan(config, chain)?; + let mut identifier = TraceIdentifiers::new().with_external(config, chain)?; if let Some(contracts) = &known_contracts { builder = builder.with_known_contracts(contracts); identifier = identifier.with_local_and_bytecodes(contracts, contracts_bytecode); @@ -69,7 +69,7 @@ pub(crate) async fn handle_traces( } if decode_internal || debug { - if let Some(ref etherscan_identifier) = identifier.etherscan { + if let Some(ref etherscan_identifier) = identifier.external { sources.merge(etherscan_identifier.get_compiled_contracts().await?); } diff --git a/crates/chisel/src/dispatcher.rs b/crates/chisel/src/dispatcher.rs index a7fbcd335e9fd..0a99825c0b03a 100644 --- a/crates/chisel/src/dispatcher.rs +++ b/crates/chisel/src/dispatcher.rs @@ -181,7 +181,7 @@ impl ChiselDispatcher { )?) .build(); - let mut identifier = TraceIdentifiers::new().with_etherscan( + let mut identifier = TraceIdentifiers::new().with_external( &session_config.foundry_config, session_config.evm_opts.get_remote_chain_id().await, )?; diff --git a/crates/evm/traces/Cargo.toml b/crates/evm/traces/Cargo.toml index 527ea422bbd07..4eed2fcf4f10b 100644 --- a/crates/evm/traces/Cargo.toml +++ b/crates/evm/traces/Cargo.toml @@ -32,19 +32,21 @@ alloy-primitives = { workspace = true, features = [ alloy-sol-types.workspace = true revm-inspectors.workspace = true +async-trait.workspace = true eyre.workspace = true futures.workspace = true itertools.workspace = true +memchr.workspace = true +rayon.workspace = true +reqwest.workspace = true +revm.workspace = true +serde_json = { workspace = true, features = ["raw_value"] } serde.workspace = true -serde_json.workspace = true +solar.workspace = true +tempfile.workspace = true tokio = { workspace = true, features = ["time", "macros"] } tracing.workspace = true -tempfile.workspace = true -rayon.workspace = true -solar.workspace = true -revm.workspace = true yansi.workspace = true -memchr.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/crates/evm/traces/src/identifier/etherscan.rs b/crates/evm/traces/src/identifier/etherscan.rs deleted file mode 100644 index fa0f8c03dcf77..0000000000000 --- a/crates/evm/traces/src/identifier/etherscan.rs +++ /dev/null @@ -1,278 +0,0 @@ -use super::{IdentifiedAddress, TraceIdentifier}; -use crate::debug::ContractSources; -use alloy_primitives::Address; -use foundry_block_explorers::{ - contract::{ContractMetadata, Metadata}, - errors::EtherscanError, -}; -use foundry_common::compile::etherscan_project; -use foundry_config::{Chain, Config}; -use futures::{ - future::join_all, - stream::{FuturesUnordered, Stream, StreamExt}, - task::{Context, Poll}, -}; -use revm_inspectors::tracing::types::CallTraceNode; -use std::{ - borrow::Cow, - collections::BTreeMap, - pin::Pin, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, -}; -use tokio::time::{Duration, Interval}; - -/// A trace identifier that tries to identify addresses using Etherscan. -pub struct EtherscanIdentifier { - /// The Etherscan client - client: Arc, - /// Tracks whether the API key provides was marked as invalid - /// - /// After the first [EtherscanError::InvalidApiKey] this will get set to true, so we can - /// prevent any further attempts - invalid_api_key: Arc, - pub contracts: BTreeMap, - pub sources: BTreeMap, -} - -impl EtherscanIdentifier { - /// Creates a new Etherscan identifier with the given client - pub fn new(config: &Config, chain: Option) -> eyre::Result> { - // In offline mode, don't use Etherscan. - if config.offline { - return Ok(None); - } - - let config = match config.get_etherscan_config_with_chain(chain) { - Ok(Some(config)) => config, - Ok(None) => { - warn!(target: "traces::etherscan", "etherscan config not found"); - return Ok(None); - } - Err(err) => { - warn!(?err, "failed to get etherscan config"); - return Ok(None); - } - }; - - trace!(target: "traces::etherscan", chain=?config.chain, url=?config.api_url, "using etherscan identifier"); - Ok(Some(Self { - client: Arc::new(config.into_client()?), - invalid_api_key: Arc::new(AtomicBool::new(false)), - contracts: BTreeMap::new(), - sources: BTreeMap::new(), - })) - } - - /// Goes over the list of contracts we have pulled from the traces, clones their source from - /// Etherscan and compiles them locally, for usage in the debugger. - pub async fn get_compiled_contracts(&self) -> eyre::Result { - let outputs_fut = self - .contracts - .iter() - // filter out vyper files - .filter(|(_, metadata)| !metadata.is_vyper()) - .map(|(address, metadata)| async move { - sh_println!("Compiling: {} {address}", metadata.contract_name)?; - let root = tempfile::tempdir()?; - let root_path = root.path(); - let project = etherscan_project(metadata, root_path)?; - let output = project.compile()?; - - if output.has_compiler_errors() { - eyre::bail!("{output}") - } - - Ok((project, output, root)) - }) - .collect::>(); - - // poll all the futures concurrently - let outputs = join_all(outputs_fut).await; - - let mut sources: ContractSources = Default::default(); - - // construct the map - for res in outputs { - let (project, output, _root) = res?; - sources.insert(&output, project.root(), None)?; - } - - 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 { - 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", nodes.len()); - - let mut identities = Vec::new(); - let mut fetcher = EtherscanFetcher::new( - self.client.clone(), - Duration::from_secs(1), - 5, - Arc::clone(&self.invalid_api_key), - ); - - for &node in nodes { - let address = node.trace.address; - if let Some(metadata) = self.contracts.get(&address) { - identities.push(self.identify_from_metadata(address, metadata)); - } else { - fetcher.push(address); - } - } - - let fetched_identities = foundry_common::block_on( - fetcher - .map(|(address, metadata)| { - let addr = self.identify_from_metadata(address, &metadata); - self.contracts.insert(address, metadata); - addr - }) - .collect::>>(), - ); - - identities.extend(fetched_identities); - identities - } -} - -type EtherscanFuture = - Pin)>>>; - -/// A rate limit aware Etherscan client. -/// -/// Fetches information about multiple addresses concurrently, while respecting rate limits. -struct EtherscanFetcher { - /// The Etherscan client - client: Arc, - /// The time we wait if we hit the rate limit - timeout: Duration, - /// The interval we are currently waiting for before making a new request - backoff: Option, - /// The maximum amount of requests to send concurrently - concurrency: usize, - /// The addresses we have yet to make requests for - queue: Vec
, - /// The in progress requests - in_progress: FuturesUnordered, - /// tracks whether the API key provides was marked as invalid - invalid_api_key: Arc, -} - -impl EtherscanFetcher { - fn new( - client: Arc, - timeout: Duration, - concurrency: usize, - invalid_api_key: Arc, - ) -> Self { - Self { - client, - timeout, - backoff: None, - concurrency, - queue: Vec::new(), - in_progress: FuturesUnordered::new(), - invalid_api_key, - } - } - - fn push(&mut self, address: Address) { - self.queue.push(address); - } - - fn queue_next_reqs(&mut self) { - while self.in_progress.len() < self.concurrency { - let Some(addr) = self.queue.pop() else { break }; - let client = Arc::clone(&self.client); - self.in_progress.push(Box::pin(async move { - trace!(target: "traces::etherscan", ?addr, "fetching info"); - let res = client.contract_source_code(addr).await; - (addr, res) - })); - } - } -} - -impl Stream for EtherscanFetcher { - type Item = (Address, Metadata); - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let pin = self.get_mut(); - - loop { - if let Some(mut backoff) = pin.backoff.take() - && backoff.poll_tick(cx).is_pending() - { - pin.backoff = Some(backoff); - return Poll::Pending; - } - - pin.queue_next_reqs(); - - let mut made_progress_this_iter = false; - match pin.in_progress.poll_next_unpin(cx) { - Poll::Pending => {} - Poll::Ready(None) => return Poll::Ready(None), - Poll::Ready(Some((addr, res))) => { - made_progress_this_iter = true; - match res { - Ok(mut metadata) => { - if let Some(item) = metadata.items.pop() { - return Poll::Ready(Some((addr, item))); - } - } - Err(EtherscanError::RateLimitExceeded) => { - warn!(target: "traces::etherscan", "rate limit exceeded on attempt"); - pin.backoff = Some(tokio::time::interval(pin.timeout)); - pin.queue.push(addr); - } - Err(EtherscanError::InvalidApiKey) => { - warn!(target: "traces::etherscan", "invalid api key"); - // mark key as invalid - pin.invalid_api_key.store(true, Ordering::Relaxed); - return Poll::Ready(None); - } - Err(EtherscanError::BlockedByCloudflare) => { - warn!(target: "traces::etherscan", "blocked by cloudflare"); - // mark key as invalid - pin.invalid_api_key.store(true, Ordering::Relaxed); - return Poll::Ready(None); - } - Err(err) => { - warn!(target: "traces::etherscan", "could not get etherscan info: {:?}", err); - } - } - } - } - - if !made_progress_this_iter { - return Poll::Pending; - } - } - } -} diff --git a/crates/evm/traces/src/identifier/external.rs b/crates/evm/traces/src/identifier/external.rs new file mode 100644 index 0000000000000..3ebb8d1929810 --- /dev/null +++ b/crates/evm/traces/src/identifier/external.rs @@ -0,0 +1,472 @@ +use super::{IdentifiedAddress, TraceIdentifier}; +use crate::debug::ContractSources; +use alloy_primitives::{ + Address, + map::{Entry, HashMap}, +}; +use foundry_block_explorers::{contract::Metadata, errors::EtherscanError}; +use foundry_common::compile::etherscan_project; +use foundry_config::{Chain, Config}; +use futures::{ + future::join_all, + stream::{FuturesUnordered, Stream, StreamExt}, + task::{Context, Poll}, +}; +use revm_inspectors::tracing::types::CallTraceNode; +use serde::Deserialize; +use std::{ + borrow::Cow, + pin::Pin, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; +use tokio::time::{Duration, Interval}; + +/// A trace identifier that tries to identify addresses using Etherscan. +pub struct ExternalIdentifier { + fetchers: Vec>, + /// Cached contracts. + contracts: HashMap)>, +} + +impl ExternalIdentifier { + /// Creates a new external identifier with the given client + pub fn new(config: &Config, mut chain: Option) -> eyre::Result> { + if config.offline { + return Ok(None); + } + + let config = match config.get_etherscan_config_with_chain(chain) { + Ok(Some(config)) => { + chain = config.chain; + Some(config) + } + Ok(None) => { + warn!(target: "evm::traces::external", "etherscan config not found"); + None + } + Err(err) => { + warn!(target: "evm::traces::external", ?err, "failed to get etherscan config"); + None + } + }; + + let mut fetchers = Vec::>::new(); + if let Some(chain) = chain { + debug!(target: "evm::traces::external", ?chain, "using sourcify identifier"); + fetchers.push(Arc::new(SourcifyFetcher::new(chain))); + } + if let Some(config) = config { + debug!(target: "evm::traces::external", chain=?config.chain, url=?config.api_url, "using etherscan identifier"); + fetchers.push(Arc::new(EtherscanFetcher::new(config.into_client()?))); + } + if fetchers.is_empty() { + debug!(target: "evm::traces::external", "no fetchers enabled"); + return Ok(None); + } + + Ok(Some(Self { fetchers, contracts: Default::default() })) + } + + /// Goes over the list of contracts we have pulled from the traces, clones their source from + /// Etherscan and compiles them locally, for usage in the debugger. + pub async fn get_compiled_contracts(&self) -> eyre::Result { + let outputs_fut = self + .contracts + .iter() + // filter out vyper files + .filter(|(_, (_, metadata))| { + metadata.as_ref().is_some_and(|metadata| !metadata.is_vyper()) + }) + .map(|(address, (_, metadata))| async move { + let metadata = metadata.as_ref().unwrap(); + sh_println!("Compiling: {} {address}", metadata.contract_name)?; + let root = tempfile::tempdir()?; + let root_path = root.path(); + let project = etherscan_project(metadata, root_path)?; + let output = project.compile()?; + + if output.has_compiler_errors() { + eyre::bail!("{output}") + } + + Ok((project, output, root)) + }) + .collect::>(); + + // poll all the futures concurrently + let outputs = join_all(outputs_fut).await; + + let mut sources: ContractSources = Default::default(); + + // construct the map + for res in outputs { + let (project, output, _root) = res?; + sources.insert(&output, project.root(), None)?; + } + + 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 ExternalIdentifier { + fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec> { + if nodes.is_empty() { + return Vec::new(); + } + + trace!(target: "evm::traces::external", "identify {} addresses", nodes.len()); + + let mut identities = Vec::new(); + let mut to_fetch = Vec::new(); + + // Check cache first. + for &node in nodes { + let address = node.trace.address; + if let Some((_, metadata)) = self.contracts.get(&address) { + if let Some(metadata) = metadata { + identities.push(self.identify_from_metadata(address, metadata)); + } else { + // Do nothing. We know that this contract was not verified. + } + } else { + to_fetch.push(address); + } + } + + if to_fetch.is_empty() { + return identities; + } + trace!(target: "evm::traces::external", "fetching {} addresses", to_fetch.len()); + + let fetchers = + self.fetchers.iter().map(|fetcher| ExternalFetcher::new(fetcher.clone(), &to_fetch)); + let fetched_identities = foundry_common::block_on( + futures::stream::select_all(fetchers) + .filter_map(|(address, value)| { + let addr = value + .1 + .as_ref() + .map(|metadata| self.identify_from_metadata(address, metadata)); + match self.contracts.entry(address) { + Entry::Occupied(mut occupied_entry) => { + // Override if: + // - new is from Etherscan and old is not + // - new is Some and old is None, meaning verified only in one source + if !matches!(occupied_entry.get().0, FetcherKind::Etherscan) + || value.1.is_none() + { + occupied_entry.insert(value); + } + } + Entry::Vacant(vacant_entry) => { + vacant_entry.insert(value); + } + } + async move { addr } + }) + .collect::>>(), + ); + trace!(target: "evm::traces::external", "fetched {} addresses: {fetched_identities:#?}", fetched_identities.len()); + + identities.extend(fetched_identities); + identities + } +} + +type FetchFuture = + Pin, EtherscanError>)>>>; + +/// A rate limit aware fetcher. +/// +/// Fetches information about multiple addresses concurrently, while respecting rate limits. +struct ExternalFetcher { + /// The fetcher + fetcher: Arc, + /// The time we wait if we hit the rate limit + timeout: Duration, + /// The interval we are currently waiting for before making a new request + backoff: Option, + /// The maximum amount of requests to send concurrently + concurrency: usize, + /// The addresses we have yet to make requests for + queue: Vec
, + /// The in progress requests + in_progress: FuturesUnordered, +} + +impl ExternalFetcher { + fn new(fetcher: Arc, to_fetch: &[Address]) -> Self { + Self { + timeout: fetcher.timeout(), + backoff: None, + concurrency: fetcher.concurrency(), + fetcher, + queue: to_fetch.to_vec(), + in_progress: FuturesUnordered::new(), + } + } + + fn queue_next_reqs(&mut self) { + while self.in_progress.len() < self.concurrency { + let Some(addr) = self.queue.pop() else { break }; + let fetcher = Arc::clone(&self.fetcher); + self.in_progress.push(Box::pin(async move { + trace!(target: "evm::traces::external", ?addr, "fetching info"); + let res = fetcher.fetch(addr).await; + (addr, res) + })); + } + } +} + +impl Stream for ExternalFetcher { + type Item = (Address, (FetcherKind, Option)); + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let pin = self.get_mut(); + + let _guard = + info_span!("evm::traces::external", kind=?pin.fetcher.kind(), "ExternalFetcher") + .entered(); + + if pin.fetcher.invalid_api_key().load(Ordering::Relaxed) { + return Poll::Ready(None); + } + + loop { + if let Some(mut backoff) = pin.backoff.take() + && backoff.poll_tick(cx).is_pending() + { + pin.backoff = Some(backoff); + return Poll::Pending; + } + + pin.queue_next_reqs(); + + let mut made_progress_this_iter = false; + match pin.in_progress.poll_next_unpin(cx) { + Poll::Pending => {} + Poll::Ready(None) => return Poll::Ready(None), + Poll::Ready(Some((addr, res))) => { + made_progress_this_iter = true; + match res { + Ok(metadata) => { + return Poll::Ready(Some((addr, (pin.fetcher.kind(), metadata)))); + } + Err(EtherscanError::ContractCodeNotVerified(_)) => { + return Poll::Ready(Some((addr, (pin.fetcher.kind(), None)))); + } + Err(EtherscanError::RateLimitExceeded) => { + warn!(target: "evm::traces::external", "rate limit exceeded on attempt"); + pin.backoff = Some(tokio::time::interval(pin.timeout)); + pin.queue.push(addr); + } + Err(EtherscanError::InvalidApiKey) => { + warn!(target: "evm::traces::external", "invalid api key"); + // mark key as invalid + pin.fetcher.invalid_api_key().store(true, Ordering::Relaxed); + return Poll::Ready(None); + } + Err(EtherscanError::BlockedByCloudflare) => { + warn!(target: "evm::traces::external", "blocked by cloudflare"); + // mark key as invalid + pin.fetcher.invalid_api_key().store(true, Ordering::Relaxed); + return Poll::Ready(None); + } + Err(err) => { + warn!(target: "evm::traces::external", ?err, "could not get info"); + } + } + } + } + + if !made_progress_this_iter { + return Poll::Pending; + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FetcherKind { + Etherscan, + Sourcify, +} + +#[async_trait::async_trait] +trait ExternalFetcherT: Send + Sync { + fn kind(&self) -> FetcherKind; + fn timeout(&self) -> Duration; + fn concurrency(&self) -> usize; + fn invalid_api_key(&self) -> &AtomicBool; + async fn fetch(&self, address: Address) -> Result, EtherscanError>; +} + +struct EtherscanFetcher { + client: foundry_block_explorers::Client, + invalid_api_key: AtomicBool, +} + +impl EtherscanFetcher { + fn new(client: foundry_block_explorers::Client) -> Self { + Self { client, invalid_api_key: AtomicBool::new(false) } + } +} + +#[async_trait::async_trait] +impl ExternalFetcherT for EtherscanFetcher { + fn kind(&self) -> FetcherKind { + FetcherKind::Etherscan + } + + fn timeout(&self) -> Duration { + Duration::from_secs(1) + } + + fn concurrency(&self) -> usize { + 5 + } + + fn invalid_api_key(&self) -> &AtomicBool { + &self.invalid_api_key + } + + async fn fetch(&self, address: Address) -> Result, EtherscanError> { + self.client.contract_source_code(address).await.map(|mut metadata| metadata.items.pop()) + } +} + +struct SourcifyFetcher { + client: reqwest::Client, + url: String, + invalid_api_key: AtomicBool, +} + +impl SourcifyFetcher { + fn new(chain: Chain) -> Self { + Self { + client: reqwest::Client::new(), + url: format!("https://sourcify.dev/server/v2/contract/{}", chain.id()), + invalid_api_key: AtomicBool::new(false), + } + } +} + +#[async_trait::async_trait] +impl ExternalFetcherT for SourcifyFetcher { + fn kind(&self) -> FetcherKind { + FetcherKind::Sourcify + } + + fn timeout(&self) -> Duration { + Duration::from_secs(1) + } + + fn concurrency(&self) -> usize { + 5 + } + + fn invalid_api_key(&self) -> &AtomicBool { + &self.invalid_api_key + } + + async fn fetch(&self, address: Address) -> Result, EtherscanError> { + let url = format!("{url}/{address}?fields=abi,compilation", url = self.url); + let response = self.client.get(url).send().await?; + let code = response.status(); + let response: SourcifyResponse = response.json().await?; + trace!(target: "evm::traces::external", "Sourcify response for {address}: {response:#?}"); + match code.as_u16() { + // Not verified. + 404 => return Err(EtherscanError::ContractCodeNotVerified(address)), + // Too many requests. + 429 => return Err(EtherscanError::RateLimitExceeded), + _ => {} + } + match response { + SourcifyResponse::Success(metadata) => Ok(Some(metadata.into())), + SourcifyResponse::Error(error) => Err(EtherscanError::Unknown(format!("{error:#?}"))), + } + } +} + +/// Sourcify API response for `/v2/contract/{chainId}/{address}`. +#[derive(Debug, Clone, Deserialize)] +#[serde(untagged)] +enum SourcifyResponse { + Success(SourcifyMetadata), + Error(SourcifyError), +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +#[expect(dead_code)] // Used in Debug. +struct SourcifyError { + custom_code: String, + message: String, + error_id: String, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SourcifyMetadata { + #[serde(default)] + abi: Option>, + #[serde(default)] + compilation: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Compilation { + #[serde(default)] + compiler_version: String, + #[serde(default)] + name: String, +} + +impl From for Metadata { + fn from(metadata: SourcifyMetadata) -> Self { + let SourcifyMetadata { abi, compilation } = metadata; + // Defaulted fields may be fetched from sourcify but we don't make use of them. + Metadata { + source_code: foundry_block_explorers::contract::SourceCodeMetadata::Sources( + Default::default(), + ), + abi: Box::::from(abi.unwrap_or_default()).into(), + contract_name: compilation.as_ref().map(|c| c.name.clone()).unwrap_or_default(), + compiler_version: compilation + .as_ref() + .map(|c| c.compiler_version.clone()) + .unwrap_or_default(), + optimization_used: 0, + runs: 0, + constructor_arguments: Default::default(), + evm_version: String::new(), + library: String::new(), + license_type: String::new(), + proxy: 0, + implementation: None, + swarm_source: String::new(), + } + } +} diff --git a/crates/evm/traces/src/identifier/mod.rs b/crates/evm/traces/src/identifier/mod.rs index 8ad929b7317a0..521c42010f25b 100644 --- a/crates/evm/traces/src/identifier/mod.rs +++ b/crates/evm/traces/src/identifier/mod.rs @@ -9,13 +9,14 @@ use std::borrow::Cow; mod local; pub use local::LocalTraceIdentifier; -mod etherscan; -pub use etherscan::EtherscanIdentifier; +mod external; +pub use external::ExternalIdentifier; mod signatures; pub use signatures::{SignaturesCache, SignaturesIdentifier}; /// An address identified by a [`TraceIdentifier`]. +#[derive(Debug)] pub struct IdentifiedAddress<'a> { /// The address. pub address: Address, @@ -41,8 +42,8 @@ pub trait TraceIdentifier { pub struct TraceIdentifiers<'a> { /// The local trace identifier. pub local: Option>, - /// The optional Etherscan trace identifier. - pub etherscan: Option, + /// The optional external trace identifier. + pub external: Option, } impl Default for TraceIdentifiers<'_> { @@ -53,6 +54,10 @@ impl Default for TraceIdentifiers<'_> { impl TraceIdentifier for TraceIdentifiers<'_> { fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec> { + if nodes.is_empty() { + return Vec::new(); + } + let mut identities = Vec::with_capacity(nodes.len()); if let Some(local) = &mut self.local { identities.extend(local.identify_addresses(nodes)); @@ -60,8 +65,8 @@ impl TraceIdentifier for TraceIdentifiers<'_> { return identities; } } - if let Some(etherscan) = &mut self.etherscan { - identities.extend(etherscan.identify_addresses(nodes)); + if let Some(external) = &mut self.external { + identities.extend(external.identify_addresses(nodes)); } identities } @@ -70,7 +75,7 @@ impl TraceIdentifier for TraceIdentifiers<'_> { impl<'a> TraceIdentifiers<'a> { /// Creates a new, empty instance. pub const fn new() -> Self { - Self { local: None, etherscan: None } + Self { local: None, external: None } } /// Sets the local identifier. @@ -90,14 +95,14 @@ impl<'a> TraceIdentifiers<'a> { self } - /// Sets the etherscan identifier. - pub fn with_etherscan(mut self, config: &Config, chain: Option) -> eyre::Result { - self.etherscan = EtherscanIdentifier::new(config, chain)?; + /// Sets the external identifier. + pub fn with_external(mut self, config: &Config, chain: Option) -> eyre::Result { + self.external = ExternalIdentifier::new(config, chain)?; Ok(self) } /// Returns `true` if there are no set identifiers. pub fn is_empty(&self) -> bool { - self.local.is_none() && self.etherscan.is_none() + self.local.is_none() && self.external.is_none() } } diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index 7c8a47da1bad9..1065a516fcb37 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -525,10 +525,10 @@ impl TestArgs { // Set up trace identifiers. let mut identifier = TraceIdentifiers::new().with_local(&known_contracts); - // Avoid using etherscan for gas report as we decode more traces and this will be + // Avoid using external identifiers for gas report as we decode more traces and this will be // expensive. if !self.gas_report { - identifier = identifier.with_etherscan(&config, remote_chain_id)?; + identifier = identifier.with_external(&config, remote_chain_id)?; } // Build the trace decoder. diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index 06a665b86823a..e9bcb8e813700 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -335,7 +335,7 @@ impl ExecutedState { .with_label_disabled(self.args.disable_labels) .build(); - let mut identifier = TraceIdentifiers::new().with_local(known_contracts).with_etherscan( + let mut identifier = TraceIdentifiers::new().with_local(known_contracts).with_external( &self.script_config.config, self.script_config.evm_opts.get_remote_chain_id().await, )?;