1use crate::{
2 debug::handle_traces,
3 rpc_trace::{
4 call_frame_to_arena_with_root_address, is_method_not_found_error, is_missing_state_error,
5 },
6 traces::TraceKind,
7 utils::{apply_chain_and_block_specific_env_changes, block_env_from_header},
8};
9use alloy_consensus::{BlockHeader, Transaction, transaction::SignerRecoverable};
10
11use alloy_evm::FromRecoveredTx;
12use alloy_network::{BlockResponse, Network, ReceiptResponse, TransactionResponse};
13use alloy_primitives::{
14 Address, B256, Bytes, U256,
15 map::{AddressHashMap, AddressSet},
16};
17use alloy_provider::{Provider, ext::DebugApi};
18use alloy_rpc_types::{
19 BlockId, BlockTransactions,
20 trace::geth::{CallConfig, GethDebugTracingOptions, GethTrace, PreStateConfig},
21};
22use clap::Parser;
23use eyre::{Result, WrapErr};
24use foundry_cli::{
25 opts::{EtherscanOpts, RpcOpts, TracingArgs},
26 utils::{TraceResult, init_progress, load_config_from_provider},
27};
28use foundry_common::{
29 SYSTEM_TRANSACTION_TYPE, is_known_system_sender, provider::ProviderBuilder, shell,
30};
31use foundry_compilers::artifacts::EvmVersion;
32use foundry_config::{
33 Config, TracingConfig,
34 figment::{
35 self, Metadata, Profile,
36 value::{Dict, Map},
37 },
38};
39#[cfg(feature = "optimism")]
40use foundry_evm::core::evm::OpEvmNetwork;
41use foundry_evm::{
42 core::{
43 FoundryBlock as _,
44 evm::{EthEvmNetwork, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor},
45 },
46 executors::{EvmError, Executor, TracingExecutor},
47 hardforks::{ExecutionSpec, FoundryHardfork},
48 opts::EvmOpts,
49 traces::{InternalTraceMode, SparsedTraceArena, TraceRequirements, Traces},
50};
51use futures::TryFutureExt;
52use revm::{DatabaseRef, context::Block, primitives::hardfork::SpecId};
53
54#[derive(Clone, Debug, Parser)]
56pub struct RunArgs {
57 tx_hash: String,
59
60 #[arg(long, short)]
62 debug: bool,
63
64 #[arg(long, short)]
66 trace_printer: bool,
67
68 #[arg(long)]
72 quick: bool,
73
74 #[arg(long, alias = "sys")]
76 replay_system_txes: bool,
77
78 #[arg(long, default_value_t = false)]
84 prestate_tracer: bool,
85
86 #[arg(
97 long,
98 default_value_t = false,
99 conflicts_with_all = ["debug", "decode_internal", "trace_printer", "quick", "prestate_tracer", "evm_version"]
100 )]
101 debug_trace_transaction: bool,
102
103 #[command(flatten)]
104 tracing: TracingArgs,
105
106 #[arg(short = 'l', value_name = "ADDRESS:LABEL", hide = true)]
108 legacy_labels: Vec<String>,
109
110 #[command(flatten)]
111 etherscan: EtherscanOpts,
112
113 #[command(flatten)]
114 rpc: RpcOpts,
115
116 #[arg(long)]
120 evm_version: Option<EvmVersion>,
121
122 #[arg(long, visible_alias = "la")]
124 pub with_local_artifacts: bool,
125
126 #[arg(long)]
128 pub disable_block_gas_limit: bool,
129
130 #[arg(long)]
132 pub enable_tx_gas_limit: bool,
133}
134
135impl RunArgs {
136 fn resolve_tracing(&self, config: &TracingConfig, verbosity: u8) -> TracingConfig {
137 if self.debug_trace_transaction {
138 self.tracing.resolve_call_tracer(config, verbosity)
139 } else {
140 self.tracing.resolve(config, verbosity)
141 }
142 }
143
144 pub async fn run(self) -> Result<()> {
150 let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self);
151 let mut evm_opts = figment.extract::<EvmOpts>()?;
152
153 evm_opts.infer_network_from_fork().await;
155
156 if evm_opts.networks.is_tempo() {
157 return self.run_with_evm::<TempoEvmNetwork>().await;
158 }
159
160 #[cfg(feature = "optimism")]
161 if evm_opts.networks.is_optimism() {
162 return self.run_with_evm::<OpEvmNetwork>().await;
163 }
164
165 self.run_with_evm::<EthEvmNetwork>().await
166 }
167
168 async fn run_with_evm<FEN: FoundryEvmNetwork>(mut self) -> Result<()> {
169 let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self);
170 let evm_opts = figment.extract::<EvmOpts>()?;
171 let mut config = load_config_from_provider(figment)?;
172 self.tracing.labels.append(&mut self.legacy_labels);
173 let tracing = self.resolve_tracing(&config.tracing, shell::verbosity());
174
175 let with_local_artifacts = self.with_local_artifacts;
176 let debug = self.debug;
177 let compute_units_per_second = if self.rpc.common.no_rpc_rate_limit {
178 Some(u64::MAX)
179 } else {
180 self.rpc.common.compute_units_per_second
181 };
182
183 let provider = ProviderBuilder::<FEN::Network>::from_config(&config)?
184 .compute_units_per_second_opt(compute_units_per_second)
185 .build()?;
186
187 let tx_hash = self.tx_hash.parse().wrap_err("invalid tx hash")?;
188 let tx = provider
189 .get_transaction_by_hash(tx_hash)
190 .await
191 .wrap_err_with(|| format!("tx not found: {tx_hash:?}"))?
192 .ok_or_else(|| eyre::eyre!("tx not found: {:?}", tx_hash))?;
193
194 if self.debug_trace_transaction {
199 let tx_block_number = tx
200 .block_number()
201 .ok_or_else(|| eyre::eyre!("tx may still be pending: {:?}", tx_hash))?;
202
203 let geth_trace = provider
204 .debug_trace_transaction(
205 tx_hash,
206 GethDebugTracingOptions::call_tracer(CallConfig::default().with_log()),
207 )
208 .await
209 .map_err(|err| -> eyre::Report {
210 if is_method_not_found_error(&err) {
215 eyre::eyre!(
216 "the RPC endpoint does not support `debug_traceTransaction` (method not found); use a node with the `debug` namespace enabled (e.g. a local anvil/reth or an archive endpoint), or drop `--debug-trace-transaction` to re-execute the transaction locally"
217 )
218 } else if is_missing_state_error(&err) {
219 eyre::eyre!(
220 "the RPC endpoint does not have the historical state for the transaction's block; use an archive endpoint"
221 )
222 } else {
223 err.into()
224 }
225 })?;
226 let GethTrace::CallTracer(frame) = geth_trace else {
227 eyre::bail!(
228 "`debug_traceTransaction` did not return a callTracer frame; the RPC endpoint \
229 may not support the `callTracer`"
230 );
231 };
232
233 let receipt = provider
234 .get_transaction_receipt(tx_hash)
235 .await?
236 .ok_or_else(|| eyre::eyre!("tx receipt not found: {:?}", tx_hash))?;
237
238 let success = receipt.status();
239 let gas_used = receipt.gas_used();
240 let root_create_address = Transaction::to(&tx).is_none().then(|| {
241 receipt.contract_address().unwrap_or_else(|| tx.from().create(tx.nonce()))
242 });
243 let arena = SparsedTraceArena {
244 arena: call_frame_to_arena_with_root_address(&frame, root_create_address),
245 ignored: Default::default(),
246 };
247 let result = TraceResult {
248 success,
249 traces: Some(vec![(TraceKind::Execution, arena)]),
250 gas_used,
251 };
252
253 let contracts_bytecode = if with_local_artifacts {
258 fetch_transaction_contracts_bytecode_via_rpc(
259 &provider,
260 &result,
261 tx_hash,
262 tx_block_number.into(),
263 )
264 .await?
265 } else {
266 Default::default()
267 };
268
269 let chain = alloy_chains::Chain::from_id(provider.get_chain_id().await?);
270 handle_traces(
271 result,
272 &config,
273 chain,
274 &contracts_bytecode,
275 &tracing,
276 with_local_artifacts,
277 false,
278 config.hardfork.and_then(|hardfork| match hardfork {
279 FoundryHardfork::Tempo(hardfork) => Some(hardfork),
280 _ => None,
281 }),
282 )
283 .await?;
284
285 return Ok(());
286 }
287
288 if !self.replay_system_txes
290 && (is_known_system_sender(tx.from())
291 || tx.transaction_type() == Some(SYSTEM_TRANSACTION_TYPE))
292 {
293 return Err(eyre::eyre!(
294 "{:?} is a system transaction.\nReplaying system transactions is currently not supported.",
295 tx.tx_hash()
296 ));
297 }
298
299 let tx_block_number = tx
300 .block_number()
301 .ok_or_else(|| eyre::eyre!("tx may still be pending: {:?}", tx_hash))?;
302
303 config.fork_block_number = Some(tx_block_number - 1);
305
306 let create2_deployer = evm_opts.create2_deployer;
307 let verbosity = tracing.verbosity;
308 let (block, (mut evm_env, tx_env, fork, chain, networks)) = tokio::try_join!(
309 provider.get_block(tx_block_number.into()).full().into_future().map_err(Into::into),
311 TracingExecutor::<FEN>::get_fork_material(&mut config, evm_opts)
312 )?;
313
314 let mut evm_version = self.evm_version;
315 let mut resolved_tempo_hardfork = config
316 .hardfork
317 .and_then(|hardfork| match hardfork {
318 FoundryHardfork::Tempo(hardfork) => Some(hardfork),
319 _ => None,
320 })
321 .or_else(|| (networks.is_tempo() || chain.is_tempo()).then(|| config.evm_spec_id()));
322
323 evm_env.cfg_env.disable_block_gas_limit = self.disable_block_gas_limit;
324
325 if !self.enable_tx_gas_limit {
328 evm_env.cfg_env.tx_gas_limit_cap = Some(u64::MAX);
329 }
330
331 evm_env.cfg_env.limit_contract_code_size = None;
332 evm_env.block_env.set_number(U256::from(tx_block_number));
333 let configured_spec =
334 config.hardfork.and_then(<SpecFor<FEN> as ExecutionSpec>::from_foundry_hardfork);
335 if let Some(spec) = configured_spec {
336 evm_env.cfg_env.set_spec_and_mainnet_gas_params(spec);
337 }
338
339 let mut parent_beacon_block_root = None;
340 if let Some(block) = &block {
341 evm_env.block_env = block_env_from_header(block.header());
342 parent_beacon_block_root = block.header().parent_beacon_block_root();
343
344 if evm_version.is_none() && configured_spec.is_none() {
348 if let Some(hardfork) = FoundryHardfork::from_chain_and_timestamp(
349 evm_env.cfg_env.chain_id,
350 block.header().timestamp(),
351 ) {
352 if let FoundryHardfork::Tempo(hardfork) = hardfork {
353 resolved_tempo_hardfork = Some(hardfork);
354 }
355 evm_env.cfg_env.set_spec_and_mainnet_gas_params(hardfork.into());
356 } else if block.header().excess_blob_gas().is_some() {
357 evm_version = Some(EvmVersion::Cancun);
359 }
360 }
361 apply_chain_and_block_specific_env_changes::<FEN::Network, _, _>(
362 &mut evm_env,
363 block,
364 config.networks,
365 );
366 }
367
368 let trace_requirements = TraceRequirements::none()
369 .with_calls(true)
370 .with_debug(self.debug)
371 .with_decode_internal(if tracing.decode_internal {
372 InternalTraceMode::Full
373 } else {
374 InternalTraceMode::None
375 })
376 .with_state_changes(verbosity > 4);
377 let mut executor = TracingExecutor::<FEN>::new(
378 (evm_env.clone(), tx_env),
379 fork,
380 evm_version,
381 trace_requirements,
382 networks,
383 create2_deployer,
384 None,
385 )?;
386
387 evm_env.cfg_env.set_spec_and_mainnet_gas_params(executor.spec_id());
388
389 let spec_id = (*evm_env.cfg_env.spec()).into();
390
391 if let Some(parent_beacon_block_root) =
392 parent_beacon_block_root_for_spec(spec_id, parent_beacon_block_root)?
393 {
394 executor.apply_beacon_root(parent_beacon_block_root)?;
395 }
396
397 let mut prestate_applied = false;
404 if !self.quick && self.prestate_tracer {
405 trace!(?tx_hash, "attempting to fetch prestate via debug_traceTransaction");
406 match provider
407 .debug_trace_transaction(
408 tx_hash,
409 GethDebugTracingOptions::prestate_tracer(PreStateConfig::default()),
410 )
411 .await
412 {
413 Ok(trace) => match trace.try_into_pre_state_frame() {
414 Ok(pre_state_frame) => {
415 executor.apply_prestate_trace(pre_state_frame.into_pre_state())?;
416 prestate_applied = true;
417 trace!("prestate trace applied successfully, skipping block replay");
418 }
419 Err(err) => {
420 trace!(%err, "failed to parse prestate trace response");
421 }
422 },
423 Err(err) => {
424 trace!(?err, "debug_traceTransaction failed, falling back to block replay");
425 }
426 }
427 }
428
429 if !self.quick && !prestate_applied {
431 sh_status!("Executing previous transactions from the block.")?;
432
433 if let Some(block) = block {
434 let pb = init_progress(block.transactions().len() as u64, "tx");
435 pb.set_position(0);
436
437 let BlockTransactions::Full(ref txs) = *block.transactions() else {
438 return Err(eyre::eyre!("Could not get block txs"));
439 };
440
441 for (index, tx) in txs.iter().enumerate() {
442 if !self.replay_system_txes
446 && (is_known_system_sender(tx.from())
447 || tx.transaction_type() == Some(SYSTEM_TRANSACTION_TYPE))
448 {
449 pb.set_position((index + 1) as u64);
450 continue;
451 }
452 if tx.tx_hash() == tx_hash {
453 break;
454 }
455
456 let tx_env = TxEnvFor::<FEN>::from_recovered_tx(tx.as_ref(), tx.from());
457
458 evm_env.cfg_env.disable_balance_check = true;
459
460 if let Some(to) = Transaction::to(tx) {
461 trace!(tx=?tx.tx_hash(),?to, "executing previous call transaction");
462 executor.transact_with_env(evm_env.clone(), tx_env.clone()).wrap_err_with(
463 || {
464 format!(
465 "Failed to execute transaction: {:?} in block {}",
466 tx.tx_hash(),
467 evm_env.block_env.number()
468 )
469 },
470 )?;
471 } else {
472 trace!(tx=?tx.tx_hash(), "executing previous create transaction");
473 if let Err(error) =
474 executor.deploy_with_env(evm_env.clone(), tx_env.clone(), None)
475 {
476 match error {
477 EvmError::Execution(_) => (),
479 error => {
480 return Err(error).wrap_err_with(|| {
481 format!(
482 "Failed to deploy transaction: {:?} in block {}",
483 tx.tx_hash(),
484 evm_env.block_env.number()
485 )
486 });
487 }
488 }
489 }
490 }
491
492 pb.set_position((index + 1) as u64);
493 }
494 }
495 }
496
497 let result = {
499 executor.set_trace_printer(self.trace_printer);
500
501 let tx_env = TxEnvFor::<FEN>::from_recovered_tx(tx.as_ref(), tx.from());
502
503 if tx.as_ref().recover_signer().is_ok_and(|signer| signer != tx.from()) {
504 evm_env.cfg_env.disable_balance_check = true;
505 }
506
507 if let Some(to) = Transaction::to(&tx) {
508 trace!(tx=?tx.tx_hash(), to=?to, "executing call transaction");
509 TraceResult::from(executor.transact_with_env(evm_env, tx_env)?)
510 } else {
511 trace!(tx=?tx.tx_hash(), "executing create transaction");
512 TraceResult::try_from(executor.deploy_with_env(evm_env, tx_env, None))?
513 }
514 };
515
516 let contracts_bytecode = fetch_contracts_bytecode_from_trace(&executor, &result)?;
517 handle_traces(
518 result,
519 &config,
520 chain,
521 &contracts_bytecode,
522 &tracing,
523 with_local_artifacts,
524 debug,
525 resolved_tempo_hardfork,
526 )
527 .await?;
528
529 Ok(())
530 }
531}
532
533fn parent_beacon_block_root_for_spec(
534 spec_id: SpecId,
535 parent_beacon_block_root: Option<B256>,
536) -> Result<Option<B256>> {
537 if !spec_id.is_enabled_in(SpecId::CANCUN) {
538 return Ok(None);
539 }
540
541 parent_beacon_block_root.map(Some).ok_or_else(|| {
542 eyre::eyre!(
543 "MissingParentBeaconBlockRoot: missing parent beacon block root for Cancun block"
544 )
545 })
546}
547
548pub fn fetch_contracts_bytecode_from_trace<FEN: FoundryEvmNetwork>(
549 executor: &Executor<FEN>,
550 result: &TraceResult,
551) -> Result<AddressHashMap<Bytes>> {
552 let mut contracts_bytecode = AddressHashMap::default();
553 if let Some(ref traces) = result.traces {
554 contracts_bytecode.extend(gather_trace_addresses(traces).filter_map(|addr| {
555 let code = executor
557 .backend()
558 .basic_ref(addr)
559 .inspect_err(|e| _ = sh_warn!("Failed to fetch code for {addr}: {e}"))
560 .ok()??
561 .code?
562 .bytes();
563 if code.is_empty() {
564 return None;
565 }
566 Some((addr, code))
567 }));
568 }
569 Ok(contracts_bytecode)
570}
571
572pub async fn fetch_contracts_bytecode_via_rpc<N: Network, P: Provider<N>>(
578 provider: &P,
579 result: &TraceResult,
580 block: BlockId,
581) -> Result<AddressHashMap<Bytes>> {
582 let mut contracts_bytecode = AddressHashMap::default();
583 if let Some(ref traces) = result.traces {
584 for addr in gather_trace_addresses(traces) {
585 match provider.get_code_at(addr).block_id(block).await {
586 Ok(code) if !code.is_empty() => {
587 contracts_bytecode.insert(addr, code);
588 }
589 Ok(_) => {}
590 Err(err) => {
591 let _ = sh_warn!("Failed to fetch code for {addr}: {err}");
592 }
593 }
594 }
595 }
596 Ok(contracts_bytecode)
597}
598
599async fn fetch_transaction_contracts_bytecode_via_rpc<N: Network, P: Provider<N>>(
606 provider: &P,
607 result: &TraceResult,
608 tx_hash: B256,
609 block: BlockId,
610) -> Result<AddressHashMap<Bytes>> {
611 let mut contracts_bytecode = AddressHashMap::default();
612 let prestate_config = PreStateConfig { disable_storage: Some(true), ..Default::default() };
613 match provider
614 .debug_trace_transaction(tx_hash, GethDebugTracingOptions::prestate_tracer(prestate_config))
615 .await
616 {
617 Ok(trace) => match trace.try_into_pre_state_frame() {
618 Ok(prestate) => {
619 for (&address, account) in prestate.pre_state() {
620 if let Some(code) = account.code.clone().filter(|code| !code.is_empty()) {
621 contracts_bytecode.insert(address, code);
622 }
623 }
624 }
625 Err(err) => {
626 let _ = sh_warn!("Failed to parse transaction prestate for local artifacts: {err}");
627 }
628 },
629 Err(err) => {
630 let _ = sh_warn!("Failed to fetch transaction prestate for local artifacts: {err}");
631 }
632 }
633
634 if let Some(ref traces) = result.traces {
635 for address in gather_trace_addresses(traces) {
636 if contracts_bytecode.contains_key(&address) {
637 continue;
638 }
639 match provider.get_code_at(address).block_id(block).await {
640 Ok(code) if !code.is_empty() => {
641 contracts_bytecode.insert(address, code);
642 }
643 Ok(_) => {}
644 Err(err) => {
645 let _ = sh_warn!("Failed to fetch code for {address}: {err}");
646 }
647 }
648 }
649 }
650 Ok(contracts_bytecode)
651}
652
653fn gather_trace_addresses(traces: &Traces) -> impl Iterator<Item = Address> {
654 let mut addresses = AddressSet::default();
655 for (_, trace) in traces {
656 for node in trace.arena.nodes() {
657 if !node.trace.address.is_zero() {
658 addresses.insert(node.trace.address);
659 }
660 if !node.trace.caller.is_zero() {
661 addresses.insert(node.trace.caller);
662 }
663 }
664 }
665 addresses.into_iter()
666}
667
668impl figment::Provider for RunArgs {
669 fn metadata(&self) -> Metadata {
670 Metadata::named("RunArgs")
671 }
672
673 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
674 let mut map = Map::new();
675
676 if let Some(api_key) = &self.etherscan.key {
677 map.insert("etherscan_api_key".into(), api_key.as_str().into());
678 }
679
680 if let Some(evm_version) = self.evm_version {
681 map.insert("evm_version".into(), figment::value::Value::serialize(evm_version)?);
682 }
683
684 Ok(Map::from([(Config::selected_profile(), map)]))
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691 use alloy_primitives::address;
692
693 #[test]
694 fn parses_legacy_short_label_alias() {
695 let address = address!("0x0000000000000000000000000000000000000001");
696 let label = format!("{address}:alice");
697 let args = RunArgs::parse_from(["cast run", "0x00", "-l", &label]);
698
699 assert_eq!(args.legacy_labels, vec![label]);
700 }
701
702 #[test]
703 fn debug_trace_transaction_rejects_local_execution_flags() {
704 for flag in
705 ["--debug", "--decode-internal", "--trace-printer", "--quick", "--prestate-tracer"]
706 {
707 let result = RunArgs::try_parse_from([
708 "foundry-cli",
709 "--debug-trace-transaction",
710 "0x0000000000000000000000000000000000000000000000000000000000000000",
711 flag,
712 ]);
713 assert!(result.is_err(), "--debug-trace-transaction must reject {flag}");
714 }
715 let result = RunArgs::try_parse_from([
717 "foundry-cli",
718 "--debug-trace-transaction",
719 "0x0000000000000000000000000000000000000000000000000000000000000000",
720 "--evm-version",
721 "shanghai",
722 ]);
723 assert!(result.is_err(), "--debug-trace-transaction must reject --evm-version");
724 }
725
726 #[test]
727 fn debug_trace_transaction_accepts_label_and_render_flags() {
728 let args = RunArgs::try_parse_from([
729 "foundry-cli",
730 "--debug-trace-transaction",
731 "0x0000000000000000000000000000000000000000000000000000000000000000",
732 "--label",
733 "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045:vitalik.eth",
734 "--disable-labels",
735 "--trace-depth",
736 "2",
737 "--with-local-artifacts",
738 ]);
739 assert!(args.is_ok(), "--debug-trace-transaction must accept label/rendering flags");
740 }
741
742 #[test]
743 fn parent_beacon_block_root_is_required_for_cancun() {
744 let err = parent_beacon_block_root_for_spec(SpecId::CANCUN, None).unwrap_err();
745 assert!(err.to_string().contains("MissingParentBeaconBlockRoot"));
746
747 let root = B256::repeat_byte(0x42);
748 assert_eq!(
749 parent_beacon_block_root_for_spec(SpecId::CANCUN, Some(root)).unwrap(),
750 Some(root),
751 );
752 assert_eq!(parent_beacon_block_root_for_spec(SpecId::SHANGHAI, Some(root)).unwrap(), None);
753 assert_eq!(parent_beacon_block_root_for_spec(SpecId::SHANGHAI, None).unwrap(), None);
754 }
755
756 #[test]
757 fn debug_trace_transaction_ignores_configured_internal_decoding() {
758 let args = RunArgs::parse_from(["cast run", "0x00", "--debug-trace-transaction"]);
759 let config = TracingConfig { decode_internal: true, ..Default::default() };
760
761 assert!(!args.resolve_tracing(&config, 0).decode_internal);
762 }
763}