Skip to content

cast: Improve debugger when tracing on-chain transactions/calls #10596

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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 @@ -292,10 +294,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
44 changes: 42 additions & 2 deletions crates/cast/src/cmd/run.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
use std::hash::RandomState;

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 +27,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 +278,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 +295,38 @@ impl RunArgs {
}
}

pub async fn fetch_contracts_bytecode_from_trace(
provider: &RootProvider<AnyNetwork>,
result: &TraceResult,
) -> Result<HashMap<Address, Bytes, RandomState>> {
let mut contracts_bytecode = HashMap::new();
if let Some(ref traces) = result.traces {
let addresses = gather_trace_addresses(traces);
for address in addresses {
let code = provider.get_code_at(address).await?;
if !code.is_empty() {
contracts_bytecode.insert(address, code);
}
}
}
Ok(contracts_bytecode)
}

fn gather_trace_addresses(traces: &Traces) -> Vec<Address> {
let mut addresses = HashSet::new();
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.into_iter().collect()
}

impl figment::Provider for RunArgs {
fn metadata(&self) -> Metadata {
Metadata::named("RunArgs")
Expand Down
7 changes: 5 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 All @@ -25,6 +25,7 @@ use foundry_evm::{
};
use std::{
fmt::Write,
hash::RandomState,
path::{Path, PathBuf},
str::FromStr,
};
Expand Down Expand Up @@ -332,10 +333,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, RandomState>,
labels: Vec<String>,
with_local_artifacts: bool,
debug: bool,
Expand Down Expand Up @@ -374,7 +377,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
21 changes: 20 additions & 1 deletion crates/common/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Uncategorised utilities.

use alloy_primitives::{keccak256, B256, U256};
use alloy_primitives::{hex, keccak256, Bytes, B256, U256};
use foundry_compilers::artifacts::BytecodeObject;
use regex::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 +56,20 @@ 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 re = Regex::new(r"__\$.{34}\$__").expect("invalid regex");
let s = 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
34 changes: 28 additions & 6 deletions crates/evm/traces/src/identifier/local.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
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;
use std::borrow::Cow;
use std::{borrow::Cow, hash::RandomState};

/// A trace identifier that tries to identify addresses using local contracts.
pub struct LocalTraceIdentifier<'a> {
/// Known contracts to search through.
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, RandomState>>,
}

impl<'a> LocalTraceIdentifier<'a> {
Expand All @@ -24,7 +27,15 @@ 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, RandomState>,
) -> Self {
self.contracts_bytecode = Some(contracts_bytecode);
self
}

/// Returns the known contracts.
Expand All @@ -48,9 +59,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 +78,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 +172,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
19 changes: 16 additions & 3 deletions crates/evm/traces/src/identifier/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
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};
use revm_inspectors::tracing::types::CallTraceNode;
use std::borrow::Cow;
use std::{borrow::Cow, hash::RandomState};

mod local;
pub use local::LocalTraceIdentifier;
Expand Down Expand Up @@ -43,6 +43,8 @@ pub struct TraceIdentifiers<'a> {
pub local: Option<LocalTraceIdentifier<'a>>,
/// The optional Etherscan trace identifier.
pub etherscan: Option<EtherscanIdentifier>,
/// The contracts bytecode.
pub contracts_bytecode: Option<&'a HashMap<Address, Bytes, RandomState>>,
}

impl Default for TraceIdentifiers<'_> {
Expand Down Expand Up @@ -70,7 +72,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, contracts_bytecode: None }
}

/// Sets the local identifier.
Expand All @@ -79,6 +81,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, RandomState>,
) -> 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