Skip to content
Merged
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.

4 changes: 4 additions & 0 deletions crates/cast/src/cmd/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ use regex::Regex;
use revm::context::TransactionType;
use std::{str::FromStr, sync::LazyLock};

use super::run::fetch_contracts_bytecode_from_trace;

// matches override pattern <address>:<slot>:<value>
// e.g. 0x123:0x1:0x1234
static OVERRIDE_PATTERN: LazyLock<Regex> =
Expand Down Expand Up @@ -301,10 +303,12 @@ impl CallArgs {
),
};

let contracts_bytecode = fetch_contracts_bytecode_from_trace(&provider, &trace).await?;
handle_traces(
trace,
&config,
chain,
&contracts_bytecode,
labels,
with_local_artifacts,
debug,
Expand Down
51 changes: 49 additions & 2 deletions crates/cast/src/cmd/run.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use alloy_consensus::Transaction;
use alloy_network::{AnyNetwork, TransactionResponse};
use alloy_provider::Provider;
use alloy_primitives::{
map::{HashMap, HashSet},
Address, Bytes,
};
use alloy_provider::{Provider, RootProvider};
use alloy_rpc_types::BlockTransactions;
use clap::Parser;
use eyre::{Result, WrapErr};
Expand All @@ -21,7 +25,7 @@ use foundry_config::{
use foundry_evm::{
executors::{EvmError, TracingExecutor},
opts::EvmOpts,
traces::{InternalTraceMode, TraceMode},
traces::{InternalTraceMode, TraceMode, Traces},
utils::configure_tx_env,
Env,
};
Expand Down Expand Up @@ -272,10 +276,12 @@ impl RunArgs {
}
};

let contracts_bytecode = fetch_contracts_bytecode_from_trace(&provider, &result).await?;
handle_traces(
result,
&config,
chain,
&contracts_bytecode,
self.label,
self.with_local_artifacts,
self.debug,
Expand All @@ -287,6 +293,47 @@ impl RunArgs {
}
}

pub async fn fetch_contracts_bytecode_from_trace(
provider: &RootProvider<AnyNetwork>,
result: &TraceResult,
) -> Result<HashMap<Address, Bytes>> {
let mut contracts_bytecode = HashMap::default();
if let Some(ref traces) = result.traces {
let addresses = gather_trace_addresses(traces);
let results = futures::future::join_all(addresses.into_iter().map(async |a| {
(
a,
provider.get_code_at(a).await.unwrap_or_else(|e| {
sh_warn!("Failed to fetch code for {a:?}: {e:?}").ok();
Bytes::new()
}),
)
}))
.await;
for (address, code) in results {
if !code.is_empty() {
contracts_bytecode.insert(address, code);
}
}
}
Ok(contracts_bytecode)
}

fn gather_trace_addresses(traces: &Traces) -> HashSet<Address> {
let mut addresses = HashSet::default();
for (_, trace) in traces {
for node in trace.arena.nodes() {
if !node.trace.address.is_zero() {
addresses.insert(node.trace.address);
}
if !node.trace.caller.is_zero() {
addresses.insert(node.trace.caller);
}
}
}
addresses
}

impl figment::Provider for RunArgs {
fn metadata(&self) -> Metadata {
Metadata::named("RunArgs")
Expand Down
6 changes: 4 additions & 2 deletions crates/cli/src/utils/cmd.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use alloy_json_abi::JsonAbi;
use alloy_primitives::Address;
use alloy_primitives::{map::HashMap, Address, Bytes};
use eyre::{Result, WrapErr};
use foundry_common::{
compile::ProjectCompiler, fs, selectors::SelectorKind, shell, ContractsByArtifact,
Expand Down Expand Up @@ -330,10 +330,12 @@ impl TryFrom<Result<RawCallResult>> for TraceResult {
}

/// labels the traces, conditionally prints them or opens the debugger
#[expect(clippy::too_many_arguments)]
pub async fn handle_traces(
mut result: TraceResult,
config: &Config,
chain: Option<Chain>,
contracts_bytecode: &HashMap<Address, Bytes>,
labels: Vec<String>,
with_local_artifacts: bool,
debug: bool,
Expand Down Expand Up @@ -372,7 +374,7 @@ pub async fn handle_traces(
let mut identifier = TraceIdentifiers::new().with_etherscan(config, chain)?;
if let Some(contracts) = &known_contracts {
builder = builder.with_known_contracts(contracts);
identifier = identifier.with_local(contracts);
identifier = identifier.with_local_and_bytecodes(contracts, contracts_bytecode);
}

let mut decoder = builder.build();
Expand Down
1 change: 1 addition & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ itertools.workspace = true
jiff.workspace = true
num-format.workspace = true
path-slash.workspace = true
regex.workspace = true
reqwest.workspace = true
semver.workspace = true
serde = { workspace = true, features = ["derive"] }
Expand Down
12 changes: 11 additions & 1 deletion crates/common/src/contracts.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Commonly used contract types and functions.

use crate::compile::PathOrContractInfo;
use crate::{compile::PathOrContractInfo, strip_bytecode_placeholders};
use alloy_dyn_abi::JsonAbiExt;
use alloy_json_abi::{Event, Function, JsonAbi};
use alloy_primitives::{hex, Address, Bytes, Selector, B256};
Expand Down Expand Up @@ -87,6 +87,16 @@ impl ContractData {
pub fn deployed_bytecode(&self) -> Option<&Bytes> {
self.deployed_bytecode.as_ref()?.bytes().filter(|b| !b.is_empty())
}

/// Returns the bytecode without placeholders, if present.
pub fn bytecode_without_placeholders(&self) -> Option<Bytes> {
strip_bytecode_placeholders(self.bytecode.as_ref()?.object.as_ref()?)
}

/// Returns the deployed bytecode without placeholders, if present.
pub fn deployed_bytecode_without_placeholders(&self) -> Option<Bytes> {
strip_bytecode_placeholders(self.deployed_bytecode.as_ref()?.object.as_ref()?)
}
}

type ArtifactWithContractRef<'a> = (&'a ArtifactId, &'a ContractData);
Expand Down
25 changes: 24 additions & 1 deletion crates/common/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
//! Uncategorised utilities.

use alloy_primitives::{keccak256, B256, U256};
use alloy_primitives::{hex, keccak256, Bytes, B256, U256};
use foundry_compilers::artifacts::BytecodeObject;
use regex::Regex;
use std::sync::LazyLock;

static BYTECODE_PLACEHOLDER_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"__\$.{34}\$__").expect("invalid regex"));

/// Block on a future using the current tokio runtime on the current thread.
pub fn block_on<F: std::future::Future>(future: F) -> F::Output {
block_on_handle(&tokio::runtime::Handle::current(), future)
Expand Down Expand Up @@ -54,3 +61,19 @@ pub fn ignore_metadata_hash(bytecode: &[u8]) -> &[u8] {
bytecode
}
}

/// Strips all __$xxx$__ placeholders from the bytecode if it's an unlinked bytecode.
/// by replacing them with 20 zero bytes.
/// This is useful for matching bytecodes to a contract source, and for the source map,
/// in which the actual address of the placeholder isn't important.
pub fn strip_bytecode_placeholders(bytecode: &BytecodeObject) -> Option<Bytes> {
match &bytecode {
BytecodeObject::Bytecode(bytes) => Some(bytes.clone()),
BytecodeObject::Unlinked(s) => {
// Replace all __$xxx$__ placeholders with 32 zero bytes
let s = (*BYTECODE_PLACEHOLDER_RE).replace_all(s, "00".repeat(40));
let bytes = hex::decode(s.as_bytes());
Some(bytes.ok()?.into())
}
}
}
8 changes: 4 additions & 4 deletions crates/evm/traces/src/debug/sources.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use eyre::{Context, Result};
use foundry_common::compact_to_contract;
use foundry_common::{compact_to_contract, strip_bytecode_placeholders};
use foundry_compilers::{
artifacts::{
sourcemap::{SourceElement, SourceMap},
Expand Down Expand Up @@ -94,9 +94,9 @@ impl ArtifactData {
})
};

// Only parse bytecode if it's not empty.
let pc_ic_map = if let Some(bytes) = b.bytes() {
(!bytes.is_empty()).then(|| PcIcMap::new(bytes))
// Only parse bytecode if it's not empty, stripping placeholders if necessary.
let pc_ic_map = if let Some(bytes) = strip_bytecode_placeholders(&b.object) {
(!bytes.is_empty()).then(|| PcIcMap::new(bytes.as_ref()))
} else {
None
};
Expand Down
29 changes: 24 additions & 5 deletions crates/evm/traces/src/identifier/local.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::{IdentifiedAddress, TraceIdentifier};
use alloy_dyn_abi::JsonAbiExt;
use alloy_json_abi::JsonAbi;
use alloy_primitives::{map::HashMap, Address, Bytes};
use foundry_common::contracts::{bytecode_diff_score, ContractsByArtifact};
use foundry_compilers::ArtifactId;
use revm_inspectors::tracing::types::CallTraceNode;
Expand All @@ -12,6 +13,8 @@ pub struct LocalTraceIdentifier<'a> {
known_contracts: &'a ContractsByArtifact,
/// Vector of pairs of artifact ID and the runtime code length of the given artifact.
ordered_ids: Vec<(&'a ArtifactId, usize)>,
/// The contracts bytecode.
contracts_bytecode: Option<&'a HashMap<Address, Bytes>>,
}

impl<'a> LocalTraceIdentifier<'a> {
Expand All @@ -24,7 +27,12 @@ impl<'a> LocalTraceIdentifier<'a> {
.map(|(id, bytecode)| (id, bytecode.len()))
.collect::<Vec<_>>();
ordered_ids.sort_by_key(|(_, len)| *len);
Self { known_contracts, ordered_ids }
Self { known_contracts, ordered_ids, contracts_bytecode: None }
}

pub fn with_bytecodes(mut self, contracts_bytecode: &'a HashMap<Address, Bytes>) -> Self {
self.contracts_bytecode = Some(contracts_bytecode);
self
}

/// Returns the known contracts.
Expand All @@ -48,9 +56,9 @@ impl<'a> LocalTraceIdentifier<'a> {
let contract = self.known_contracts.get(id)?;
// Select bytecodes to compare based on `is_creation` flag.
let (contract_bytecode, current_bytecode) = if is_creation {
(contract.bytecode(), creation_code)
(contract.bytecode_without_placeholders(), creation_code)
} else {
(contract.deployed_bytecode(), runtime_code)
(contract.deployed_bytecode_without_placeholders(), runtime_code)
};

if let Some(bytecode) = contract_bytecode {
Expand All @@ -67,7 +75,7 @@ impl<'a> LocalTraceIdentifier<'a> {
}
}

let score = bytecode_diff_score(bytecode, current_bytecode);
let score = bytecode_diff_score(&bytecode, current_bytecode);
if score == 0.0 {
trace!(target: "evm::traces::local", "found exact match");
return Some((id, &contract.abi));
Expand Down Expand Up @@ -161,7 +169,18 @@ impl TraceIdentifier for LocalTraceIdentifier<'_> {
let _span =
trace_span!(target: "evm::traces::local", "identify", %address).entered();

let (id, abi) = self.identify_code(runtime_code?, creation_code?)?;
// In order to identify the addresses, we need at least the runtime code. It can be
// obtained from the trace itself (if it's a CREATE* call), or from the fetched
// bytecodes.
let (runtime_code, creation_code) = match (runtime_code, creation_code) {
(Some(runtime_code), Some(creation_code)) => (runtime_code, creation_code),
(Some(runtime_code), _) => (runtime_code, &[] as &[u8]),
_ => {
let code = self.contracts_bytecode?.get(&address)?;
(code.as_ref(), &[] as &[u8])
}
};
let (id, abi) = self.identify_code(runtime_code, creation_code)?;
trace!(target: "evm::traces::local", id=%id.identifier(), "identified");

Some(IdentifiedAddress {
Expand Down
13 changes: 12 additions & 1 deletion crates/evm/traces/src/identifier/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use alloy_json_abi::JsonAbi;
use alloy_primitives::Address;
use alloy_primitives::{map::HashMap, Address, Bytes};
use foundry_common::ContractsByArtifact;
use foundry_compilers::ArtifactId;
use foundry_config::{Chain, Config};
Expand Down Expand Up @@ -79,6 +79,17 @@ impl<'a> TraceIdentifiers<'a> {
self
}

/// Sets the local identifier.
pub fn with_local_and_bytecodes(
mut self,
known_contracts: &'a ContractsByArtifact,
contracts_bytecode: &'a HashMap<Address, Bytes>,
) -> Self {
self.local =
Some(LocalTraceIdentifier::new(known_contracts).with_bytecodes(contracts_bytecode));
self
}

/// Sets the etherscan identifier.
pub fn with_etherscan(mut self, config: &Config, chain: Option<Chain>) -> eyre::Result<Self> {
self.etherscan = EtherscanIdentifier::new(config, chain)?;
Expand Down
Loading