diff --git a/Cargo.lock b/Cargo.lock index 9237eb196cb9c..5eefbac18d7b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4829,6 +4829,7 @@ dependencies = [ "itertools 0.14.0", "memchr", "rayon", + "reqwest", "revm", "revm-inspectors", "serde", diff --git a/crates/chisel/src/dispatcher.rs b/crates/chisel/src/dispatcher.rs index a7fbcd335e9fd..7fa3bfb884913 100644 --- a/crates/chisel/src/dispatcher.rs +++ b/crates/chisel/src/dispatcher.rs @@ -181,10 +181,10 @@ impl ChiselDispatcher { )?) .build(); - let mut identifier = TraceIdentifiers::new().with_etherscan( - &session_config.foundry_config, - session_config.evm_opts.get_remote_chain_id().await, - )?; + let remote_chain_id = session_config.evm_opts.get_remote_chain_id().await; + let mut identifier = TraceIdentifiers::new() + .with_sourcify(remote_chain_id) + .with_etherscan(&session_config.foundry_config, remote_chain_id)?; if !identifier.is_empty() { for (_, trace) in &mut result.traces { decoder.identify(trace, &mut identifier); diff --git a/crates/evm/traces/Cargo.toml b/crates/evm/traces/Cargo.toml index 527ea422bbd07..d2404bd8027d2 100644 --- a/crates/evm/traces/Cargo.toml +++ b/crates/evm/traces/Cargo.toml @@ -35,6 +35,7 @@ revm-inspectors.workspace = true eyre.workspace = true futures.workspace = true itertools.workspace = true +reqwest.workspace = true serde.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["time", "macros"] } diff --git a/crates/evm/traces/src/identifier/mod.rs b/crates/evm/traces/src/identifier/mod.rs index 8ad929b7317a0..a861d93cfa41a 100644 --- a/crates/evm/traces/src/identifier/mod.rs +++ b/crates/evm/traces/src/identifier/mod.rs @@ -12,6 +12,9 @@ pub use local::LocalTraceIdentifier; mod etherscan; pub use etherscan::EtherscanIdentifier; +mod sourcify; +pub use sourcify::SourcifyIdentifier; + mod signatures; pub use signatures::{SignaturesCache, SignaturesIdentifier}; @@ -43,6 +46,8 @@ pub struct TraceIdentifiers<'a> { pub local: Option>, /// The optional Etherscan trace identifier. pub etherscan: Option, + /// The optional Sourcify trace identifier. + pub sourcify: Option, } impl Default for TraceIdentifiers<'_> { @@ -60,6 +65,9 @@ impl TraceIdentifier for TraceIdentifiers<'_> { return identities; } } + if let Some(sourcify) = &mut self.sourcify { + identities.extend(sourcify.identify_addresses(nodes)); + } if let Some(etherscan) = &mut self.etherscan { identities.extend(etherscan.identify_addresses(nodes)); } @@ -70,7 +78,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, etherscan: None, sourcify: None } } /// Sets the local identifier. @@ -96,8 +104,14 @@ impl<'a> TraceIdentifiers<'a> { Ok(self) } + /// Sets the sourcify identifier. + pub fn with_sourcify(mut self, chain: Option) -> Self { + self.sourcify = Some(SourcifyIdentifier::new(chain)); + 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.etherscan.is_none() && self.sourcify.is_none() } } diff --git a/crates/evm/traces/src/identifier/sourcify.rs b/crates/evm/traces/src/identifier/sourcify.rs new file mode 100644 index 0000000000000..e6516b03698a9 --- /dev/null +++ b/crates/evm/traces/src/identifier/sourcify.rs @@ -0,0 +1,120 @@ +use super::{IdentifiedAddress, TraceIdentifier}; +use alloy_json_abi::JsonAbi; +use foundry_config::Chain; +use revm_inspectors::tracing::types::CallTraceNode; +use std::borrow::Cow; + +/// A trace identifier that uses Sourcify to identify contract ABIs. +pub struct SourcifyIdentifier { + chain_id: u64, +} + +impl SourcifyIdentifier { + /// Creates a new Sourcify identifier for the given chain. + pub fn new(chain: Option) -> Self { + let chain_id = chain.map(|c| c.id()).unwrap_or(1); + Self { chain_id } + } +} + +impl Default for SourcifyIdentifier { + fn default() -> Self { + Self::new(None) + } +} + +impl TraceIdentifier for SourcifyIdentifier { + fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec> { + let mut identities = Vec::new(); + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .expect("Failed to create HTTP client"); + + for &node in nodes { + let address = node.trace.address; + + // Try to get ABI from Sourcify using APIv2 + let abi = foundry_common::block_on(async { + let url = format!( + "https://sourcify.dev/server/v2/contract/{}/{}?fields=abi", + self.chain_id, address + ); + + let response = client.get(&url).send().await.ok()?; + let json: serde_json::Value = response.json().await.ok()?; + let abi_value = json.get("abi")?; + serde_json::from_value::(abi_value.clone()).ok() + }); + + if let Some(abi) = abi { + identities.push(IdentifiedAddress { + address, + label: Some("Sourcify".to_string()), + contract: Some("Sourcify".to_string()), + abi: Some(Cow::Owned(abi)), + artifact_id: None, + }); + } + } + + identities + } +} + +#[cfg(test)] +mod tests { + use super::*; + use foundry_config::NamedChain; + + #[test] + fn test_sourcify_identifier_creation() { + let identifier = SourcifyIdentifier::new(None); + assert_eq!(identifier.chain_id, 1); // Default to mainnet + } + + #[test] + fn test_sourcify_identifier_with_chain() { + let identifier = SourcifyIdentifier::new(Some(NamedChain::Polygon.into())); + assert_eq!(identifier.chain_id, 137); // Polygon chain ID + } + + #[test] + fn test_sourcify_identifier_default() { + let identifier = SourcifyIdentifier::default(); + assert_eq!(identifier.chain_id, 1); // Default to mainnet + } + + #[test] + fn test_empty_nodes() { + let mut identifier = SourcifyIdentifier::default(); + let nodes: Vec<&CallTraceNode> = vec![]; + let result = identifier.identify_addresses(&nodes); + assert!(result.is_empty()); + } + + #[test] + fn test_sourcify_apiv2_response_parsing() { + // Test that we can parse the new APIv2 response format correctly + let response_json = r#"{ + "abi": [ + {"name": "transfer", "type": "function", "inputs": [], "outputs": []} + ], + "matchId": "1532018", + "creationMatch": "match", + "runtimeMatch": "match", + "verifiedAt": "2024-08-08T13:20:07Z", + "match": "match", + "chainId": "1", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }"#; + + let json: serde_json::Value = serde_json::from_str(response_json).unwrap(); + let abi_value = json.get("abi").unwrap(); + let abi: Result = serde_json::from_value(abi_value.clone()); + + assert!(abi.is_ok()); + let abi = abi.unwrap(); + assert_eq!(abi.len(), 1); + } +} diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index 7c8a47da1bad9..5e4700f32ec4c 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -525,10 +525,12 @@ 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 - // expensive. + // Avoid using etherscan and sourcify 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_sourcify(remote_chain_id) + .with_etherscan(&config, remote_chain_id)?; } // Build the trace decoder. diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index 06a665b86823a..7bde465f77e03 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -335,10 +335,11 @@ impl ExecutedState { .with_label_disabled(self.args.disable_labels) .build(); - let mut identifier = TraceIdentifiers::new().with_local(known_contracts).with_etherscan( - &self.script_config.config, - self.script_config.evm_opts.get_remote_chain_id().await, - )?; + let remote_chain_id = self.script_config.evm_opts.get_remote_chain_id().await; + let mut identifier = TraceIdentifiers::new() + .with_local(known_contracts) + .with_sourcify(remote_chain_id) + .with_etherscan(&self.script_config.config, remote_chain_id)?; for (_, trace) in &self.execution_result.traces { decoder.identify(trace, &mut identifier);