Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions crates/chisel/src/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions crates/evm/traces/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
18 changes: 16 additions & 2 deletions crates/evm/traces/src/identifier/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -43,6 +46,8 @@ pub struct TraceIdentifiers<'a> {
pub local: Option<LocalTraceIdentifier<'a>>,
/// The optional Etherscan trace identifier.
pub etherscan: Option<EtherscanIdentifier>,
/// The optional Sourcify trace identifier.
pub sourcify: Option<SourcifyIdentifier>,
}

impl Default for TraceIdentifiers<'_> {
Expand All @@ -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));
}
Expand All @@ -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.
Expand All @@ -96,8 +104,14 @@ impl<'a> TraceIdentifiers<'a> {
Ok(self)
}

/// Sets the sourcify identifier.
pub fn with_sourcify(mut self, chain: Option<Chain>) -> 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()
}
}
120 changes: 120 additions & 0 deletions crates/evm/traces/src/identifier/sourcify.rs
Original file line number Diff line number Diff line change
@@ -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<Chain>) -> 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<IdentifiedAddress<'_>> {
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::<JsonAbi>(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<JsonAbi, _> = serde_json::from_value(abi_value.clone());

assert!(abi.is_ok());
let abi = abi.unwrap();
assert_eq!(abi.len(), 1);
}
}
8 changes: 5 additions & 3 deletions crates/forge/src/cmd/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 5 additions & 4 deletions crates/script/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down