Skip to content

Commit d0fc977

Browse files
authored
cast: Improve debugger when tracing on-chain transactions/calls (foundry-rs#10596)
* 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. * cast: Strip placeholder from bytecode-object for source-map and matching If a contract contains some libraries, and thus has an "unlinked" bytecode object, it will never be matched against a deployed instance, and the source map will never be set. This fixes this issue by striping from the unlinked bytecode all placeholders, replacing them with the `0x00..00` address. It doesn't change anything regarding source-maps, but could change the matching of the runtime bytecode. The changes are usually minimal in this case, though.
1 parent a3b6b33 commit d0fc977

File tree

10 files changed

+134
-16
lines changed

10 files changed

+134
-16
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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/common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ itertools.workspace = true
5757
jiff.workspace = true
5858
num-format.workspace = true
5959
path-slash.workspace = true
60+
regex.workspace = true
6061
reqwest.workspace = true
6162
semver.workspace = true
6263
serde = { workspace = true, features = ["derive"] }

crates/common/src/contracts.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Commonly used contract types and functions.
22
3-
use crate::compile::PathOrContractInfo;
3+
use crate::{compile::PathOrContractInfo, strip_bytecode_placeholders};
44
use alloy_dyn_abi::JsonAbiExt;
55
use alloy_json_abi::{Event, Function, JsonAbi};
66
use alloy_primitives::{hex, Address, Bytes, Selector, B256};
@@ -87,6 +87,16 @@ impl ContractData {
8787
pub fn deployed_bytecode(&self) -> Option<&Bytes> {
8888
self.deployed_bytecode.as_ref()?.bytes().filter(|b| !b.is_empty())
8989
}
90+
91+
/// Returns the bytecode without placeholders, if present.
92+
pub fn bytecode_without_placeholders(&self) -> Option<Bytes> {
93+
strip_bytecode_placeholders(self.bytecode.as_ref()?.object.as_ref()?)
94+
}
95+
96+
/// Returns the deployed bytecode without placeholders, if present.
97+
pub fn deployed_bytecode_without_placeholders(&self) -> Option<Bytes> {
98+
strip_bytecode_placeholders(self.deployed_bytecode.as_ref()?.object.as_ref()?)
99+
}
90100
}
91101

92102
type ArtifactWithContractRef<'a> = (&'a ArtifactId, &'a ContractData);

crates/common/src/utils.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
//! Uncategorised utilities.
22
3-
use alloy_primitives::{keccak256, B256, U256};
3+
use alloy_primitives::{hex, keccak256, Bytes, B256, U256};
4+
use foundry_compilers::artifacts::BytecodeObject;
5+
use regex::Regex;
6+
use std::sync::LazyLock;
7+
8+
static BYTECODE_PLACEHOLDER_RE: LazyLock<Regex> =
9+
LazyLock::new(|| Regex::new(r"__\$.{34}\$__").expect("invalid regex"));
10+
411
/// Block on a future using the current tokio runtime on the current thread.
512
pub fn block_on<F: std::future::Future>(future: F) -> F::Output {
613
block_on_handle(&tokio::runtime::Handle::current(), future)
@@ -54,3 +61,19 @@ pub fn ignore_metadata_hash(bytecode: &[u8]) -> &[u8] {
5461
bytecode
5562
}
5663
}
64+
65+
/// Strips all __$xxx$__ placeholders from the bytecode if it's an unlinked bytecode.
66+
/// by replacing them with 20 zero bytes.
67+
/// This is useful for matching bytecodes to a contract source, and for the source map,
68+
/// in which the actual address of the placeholder isn't important.
69+
pub fn strip_bytecode_placeholders(bytecode: &BytecodeObject) -> Option<Bytes> {
70+
match &bytecode {
71+
BytecodeObject::Bytecode(bytes) => Some(bytes.clone()),
72+
BytecodeObject::Unlinked(s) => {
73+
// Replace all __$xxx$__ placeholders with 32 zero bytes
74+
let s = (*BYTECODE_PLACEHOLDER_RE).replace_all(s, "00".repeat(40));
75+
let bytes = hex::decode(s.as_bytes());
76+
Some(bytes.ok()?.into())
77+
}
78+
}
79+
}

crates/evm/traces/src/debug/sources.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use eyre::{Context, Result};
2-
use foundry_common::compact_to_contract;
2+
use foundry_common::{compact_to_contract, strip_bytecode_placeholders};
33
use foundry_compilers::{
44
artifacts::{
55
sourcemap::{SourceElement, SourceMap},
@@ -94,9 +94,9 @@ impl ArtifactData {
9494
})
9595
};
9696

97-
// Only parse bytecode if it's not empty.
98-
let pc_ic_map = if let Some(bytes) = b.bytes() {
99-
(!bytes.is_empty()).then(|| PcIcMap::new(bytes))
97+
// Only parse bytecode if it's not empty, stripping placeholders if necessary.
98+
let pc_ic_map = if let Some(bytes) = strip_bytecode_placeholders(&b.object) {
99+
(!bytes.is_empty()).then(|| PcIcMap::new(bytes.as_ref()))
100100
} else {
101101
None
102102
};

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

Lines changed: 24 additions & 5 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.
@@ -48,9 +56,9 @@ impl<'a> LocalTraceIdentifier<'a> {
4856
let contract = self.known_contracts.get(id)?;
4957
// Select bytecodes to compare based on `is_creation` flag.
5058
let (contract_bytecode, current_bytecode) = if is_creation {
51-
(contract.bytecode(), creation_code)
59+
(contract.bytecode_without_placeholders(), creation_code)
5260
} else {
53-
(contract.deployed_bytecode(), runtime_code)
61+
(contract.deployed_bytecode_without_placeholders(), runtime_code)
5462
};
5563

5664
if let Some(bytecode) = contract_bytecode {
@@ -67,7 +75,7 @@ impl<'a> LocalTraceIdentifier<'a> {
6775
}
6876
}
6977

70-
let score = bytecode_diff_score(bytecode, current_bytecode);
78+
let score = bytecode_diff_score(&bytecode, current_bytecode);
7179
if score == 0.0 {
7280
trace!(target: "evm::traces::local", "found exact match");
7381
return Some((id, &contract.abi));
@@ -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: 12 additions & 1 deletion
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};
@@ -79,6 +79,17 @@ impl<'a> TraceIdentifiers<'a> {
7979
self
8080
}
8181

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

0 commit comments

Comments
 (0)