Skip to content

Commit 9088fc4

Browse files
committed
cast: Fetch bytecodes in run/call to better match contracts
Without fetching the bytecodes from the current chain, matching the contracts with `--with-local-artifacts` option only works if the matching contracts have been deployed in the trace. This is very limiting when trying to `--debug` an on-chain transaction. By fetching the contracts' bytecodes, we can increase the matching of address to source file, by thus providing the runtime bytecode.
1 parent e44249a commit 9088fc4

File tree

5 files changed

+93
-8
lines changed

5 files changed

+93
-8
lines changed

crates/cast/src/cmd/call.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ use regex::Regex;
3434
use revm::context::TransactionType;
3535
use std::{str::FromStr, sync::LazyLock};
3636

37+
use super::run::fetch_contracts_bytecode_from_trace;
38+
3739
// matches override pattern <address>:<slot>:<value>
3840
// e.g. 0x123:0x1:0x1234
3941
static OVERRIDE_PATTERN: LazyLock<Regex> =
@@ -301,10 +303,12 @@ impl CallArgs {
301303
),
302304
};
303305

306+
let contracts_bytecode = fetch_contracts_bytecode_from_trace(&provider, &trace).await?;
304307
handle_traces(
305308
trace,
306309
&config,
307310
chain,
311+
&contracts_bytecode,
308312
labels,
309313
with_local_artifacts,
310314
debug,

crates/cast/src/cmd/run.rs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use alloy_consensus::Transaction;
22
use alloy_network::{AnyNetwork, TransactionResponse};
3-
use alloy_provider::Provider;
3+
use alloy_primitives::{
4+
map::{HashMap, HashSet},
5+
Address, Bytes,
6+
};
7+
use alloy_provider::{Provider, RootProvider};
48
use alloy_rpc_types::BlockTransactions;
59
use clap::Parser;
610
use eyre::{Result, WrapErr};
@@ -21,7 +25,7 @@ use foundry_config::{
2125
use foundry_evm::{
2226
executors::{EvmError, TracingExecutor},
2327
opts::EvmOpts,
24-
traces::{InternalTraceMode, TraceMode},
28+
traces::{InternalTraceMode, TraceMode, Traces},
2529
utils::configure_tx_env,
2630
Env,
2731
};
@@ -272,10 +276,12 @@ impl RunArgs {
272276
}
273277
};
274278

279+
let contracts_bytecode = fetch_contracts_bytecode_from_trace(&provider, &result).await?;
275280
handle_traces(
276281
result,
277282
&config,
278283
chain,
284+
&contracts_bytecode,
279285
self.label,
280286
self.with_local_artifacts,
281287
self.debug,
@@ -287,6 +293,47 @@ impl RunArgs {
287293
}
288294
}
289295

296+
pub async fn fetch_contracts_bytecode_from_trace(
297+
provider: &RootProvider<AnyNetwork>,
298+
result: &TraceResult,
299+
) -> Result<HashMap<Address, Bytes>> {
300+
let mut contracts_bytecode = HashMap::default();
301+
if let Some(ref traces) = result.traces {
302+
let addresses = gather_trace_addresses(traces);
303+
let results = futures::future::join_all(addresses.into_iter().map(async |a| {
304+
(
305+
a,
306+
provider.get_code_at(a).await.unwrap_or_else(|e| {
307+
sh_warn!("Failed to fetch code for {a:?}: {e:?}").ok();
308+
Bytes::new()
309+
}),
310+
)
311+
}))
312+
.await;
313+
for (address, code) in results {
314+
if !code.is_empty() {
315+
contracts_bytecode.insert(address, code);
316+
}
317+
}
318+
}
319+
Ok(contracts_bytecode)
320+
}
321+
322+
fn gather_trace_addresses(traces: &Traces) -> HashSet<Address> {
323+
let mut addresses = HashSet::default();
324+
for (_, trace) in traces {
325+
for node in trace.arena.nodes() {
326+
if !node.trace.address.is_zero() {
327+
addresses.insert(node.trace.address);
328+
}
329+
if !node.trace.caller.is_zero() {
330+
addresses.insert(node.trace.caller);
331+
}
332+
}
333+
}
334+
addresses
335+
}
336+
290337
impl figment::Provider for RunArgs {
291338
fn metadata(&self) -> Metadata {
292339
Metadata::named("RunArgs")

crates/cli/src/utils/cmd.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use alloy_json_abi::JsonAbi;
2-
use alloy_primitives::Address;
2+
use alloy_primitives::{map::HashMap, Address, Bytes};
33
use eyre::{Result, WrapErr};
44
use foundry_common::{
55
compile::ProjectCompiler, fs, selectors::SelectorKind, shell, ContractsByArtifact,
@@ -330,10 +330,12 @@ impl TryFrom<Result<RawCallResult>> for TraceResult {
330330
}
331331

332332
/// labels the traces, conditionally prints them or opens the debugger
333+
#[expect(clippy::too_many_arguments)]
333334
pub async fn handle_traces(
334335
mut result: TraceResult,
335336
config: &Config,
336337
chain: Option<Chain>,
338+
contracts_bytecode: &HashMap<Address, Bytes>,
337339
labels: Vec<String>,
338340
with_local_artifacts: bool,
339341
debug: bool,
@@ -372,7 +374,7 @@ pub async fn handle_traces(
372374
let mut identifier = TraceIdentifiers::new().with_etherscan(config, chain)?;
373375
if let Some(contracts) = &known_contracts {
374376
builder = builder.with_known_contracts(contracts);
375-
identifier = identifier.with_local(contracts);
377+
identifier = identifier.with_local_and_bytecodes(contracts, contracts_bytecode);
376378
}
377379

378380
let mut decoder = builder.build();

crates/evm/traces/src/identifier/local.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::{IdentifiedAddress, TraceIdentifier};
22
use alloy_dyn_abi::JsonAbiExt;
33
use alloy_json_abi::JsonAbi;
4+
use alloy_primitives::{map::HashMap, Address, Bytes};
45
use foundry_common::contracts::{bytecode_diff_score, ContractsByArtifact};
56
use foundry_compilers::ArtifactId;
67
use revm_inspectors::tracing::types::CallTraceNode;
@@ -12,6 +13,8 @@ pub struct LocalTraceIdentifier<'a> {
1213
known_contracts: &'a ContractsByArtifact,
1314
/// Vector of pairs of artifact ID and the runtime code length of the given artifact.
1415
ordered_ids: Vec<(&'a ArtifactId, usize)>,
16+
/// The contracts bytecode.
17+
contracts_bytecode: Option<&'a HashMap<Address, Bytes>>,
1518
}
1619

1720
impl<'a> LocalTraceIdentifier<'a> {
@@ -24,7 +27,12 @@ impl<'a> LocalTraceIdentifier<'a> {
2427
.map(|(id, bytecode)| (id, bytecode.len()))
2528
.collect::<Vec<_>>();
2629
ordered_ids.sort_by_key(|(_, len)| *len);
27-
Self { known_contracts, ordered_ids }
30+
Self { known_contracts, ordered_ids, contracts_bytecode: None }
31+
}
32+
33+
pub fn with_bytecodes(mut self, contracts_bytecode: &'a HashMap<Address, Bytes>) -> Self {
34+
self.contracts_bytecode = Some(contracts_bytecode);
35+
self
2836
}
2937

3038
/// Returns the known contracts.
@@ -161,7 +169,18 @@ impl TraceIdentifier for LocalTraceIdentifier<'_> {
161169
let _span =
162170
trace_span!(target: "evm::traces::local", "identify", %address).entered();
163171

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

167186
Some(IdentifiedAddress {

crates/evm/traces/src/identifier/mod.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use alloy_json_abi::JsonAbi;
2-
use alloy_primitives::Address;
2+
use alloy_primitives::{map::HashMap, Address, Bytes};
33
use foundry_common::ContractsByArtifact;
44
use foundry_compilers::ArtifactId;
55
use foundry_config::{Chain, Config};
@@ -43,6 +43,8 @@ pub struct TraceIdentifiers<'a> {
4343
pub local: Option<LocalTraceIdentifier<'a>>,
4444
/// The optional Etherscan trace identifier.
4545
pub etherscan: Option<EtherscanIdentifier>,
46+
/// The contracts bytecode.
47+
pub contracts_bytecode: Option<&'a HashMap<Address, Bytes>>,
4648
}
4749

4850
impl Default for TraceIdentifiers<'_> {
@@ -70,7 +72,7 @@ impl TraceIdentifier for TraceIdentifiers<'_> {
7072
impl<'a> TraceIdentifiers<'a> {
7173
/// Creates a new, empty instance.
7274
pub const fn new() -> Self {
73-
Self { local: None, etherscan: None }
75+
Self { local: None, etherscan: None, contracts_bytecode: None }
7476
}
7577

7678
/// Sets the local identifier.
@@ -79,6 +81,17 @@ impl<'a> TraceIdentifiers<'a> {
7981
self
8082
}
8183

84+
/// Sets the local identifier.
85+
pub fn with_local_and_bytecodes(
86+
mut self,
87+
known_contracts: &'a ContractsByArtifact,
88+
contracts_bytecode: &'a HashMap<Address, Bytes>,
89+
) -> Self {
90+
self.local =
91+
Some(LocalTraceIdentifier::new(known_contracts).with_bytecodes(contracts_bytecode));
92+
self
93+
}
94+
8295
/// Sets the etherscan identifier.
8396
pub fn with_etherscan(mut self, config: &Config, chain: Option<Chain>) -> eyre::Result<Self> {
8497
self.etherscan = EtherscanIdentifier::new(config, chain)?;

0 commit comments

Comments
 (0)