Skip to main content

cast/cmd/
run.rs

1use crate::{
2    MAX_CONCURRENT_RPC_REQUESTS,
3    debug::{ensure_remote_trace_context_unchanged, handle_traces, select_remote_trace_hardfork},
4    rpc_trace::{
5        call_frame_to_arena_with_root_address, is_method_not_found_error, is_missing_state_error,
6    },
7    traces::TraceKind,
8    utils::{
9        apply_chain_and_block_specific_env_changes_for_chain,
10        apply_chain_specific_tx_replay_env_changes_for_chain, block_env_from_header,
11    },
12};
13use alloy_consensus::{BlockHeader, Transaction, transaction::SignerRecoverable};
14use alloy_eips::BlockNumHash;
15use alloy_network::{
16    AnyNetwork, AnyTxEnvelope, BlockResponse, Network, ReceiptResponse, TransactionResponse,
17    primitives::HeaderResponse,
18};
19use alloy_primitives::{
20    Address, B256, Bytes, U256,
21    map::{AddressHashMap, AddressSet},
22};
23use alloy_provider::{Provider, ext::DebugApi};
24use alloy_rpc_types::{
25    BlockId, BlockTransactions,
26    trace::geth::{CallConfig, GethDebugTracingOptions, GethTrace, PreStateConfig},
27};
28use clap::Parser;
29use eyre::{Result, WrapErr};
30use foundry_cli::{
31    opts::{EtherscanOpts, RpcOpts, TracingArgs},
32    utils::{TraceResult, init_progress},
33};
34use foundry_common::{
35    SYSTEM_TRANSACTION_TYPE, is_known_system_sender, provider::ProviderBuilder, shell,
36};
37use foundry_compilers::artifacts::EvmVersion;
38use foundry_config::{
39    Config, TracingConfig,
40    figment::{
41        self, Metadata, Profile,
42        value::{Dict, Map},
43    },
44};
45#[cfg(feature = "optimism")]
46use foundry_evm::core::evm::OpEvmNetwork;
47use foundry_evm::{
48    core::{
49        FoundryBlock as _, FoundryChain,
50        env::FromAnyRpcTransaction as _,
51        evm::{
52            BlockContext, ChainFor, EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor,
53        },
54    },
55    executors::{EvmError, Executor, TracingExecutor},
56    hardforks::FoundryHardfork,
57    opts::EvmOpts,
58    traces::{InternalTraceMode, SparsedTraceArena, TraceRequirements, Traces},
59};
60use foundry_evm_networks::NetworkConfigs;
61use futures::{StreamExt, TryFutureExt};
62use revm::{DatabaseRef, context::Block, primitives::hardfork::SpecId};
63
64/// CLI arguments for `cast run`.
65#[derive(Clone, Debug, Parser)]
66pub struct RunArgs {
67    /// The transaction hash.
68    tx_hash: String,
69
70    /// Opens the transaction in the debugger.
71    #[arg(long, short)]
72    debug: bool,
73
74    /// Print out opcode traces.
75    #[arg(long, short)]
76    trace_printer: bool,
77
78    /// Executes the transaction only with the state from the previous block.
79    ///
80    /// May result in different results than the live execution!
81    #[arg(long)]
82    quick: bool,
83
84    /// Whether to replay system transactions.
85    #[arg(long, alias = "sys")]
86    replay_system_txes: bool,
87
88    /// Use debug_traceTransaction to fetch the prestate instead of replaying the block.
89    ///
90    /// This is significantly faster than replaying all previous transactions in the block, but
91    /// requires the node to expose the `debug_` namespace (most public RPCs don't). If the call
92    /// or response can't be used, cast silently falls back to replaying the block.
93    #[arg(long, default_value_t = false)]
94    prestate_tracer: bool,
95
96    /// Fetch the transaction's trace from the node via `debug_traceTransaction` (callTracer) and
97    /// render it, instead of re-executing the transaction locally.
98    ///
99    /// This skips the block replay entirely, so it is fast and reflects exactly what happened
100    /// on-chain, including chain-specific EVM behavior a local replay may not reproduce, but it
101    /// requires the node to expose the `debug_` namespace. The result is a call-tree view:
102    /// nested calls, value, gas, emitted logs and revert data. It does not provide the
103    /// opcode-level detail of a local run, so the local-execution-only flags (`--debug`,
104    /// `--decode-internal`, `--trace-printer`, `--quick`, `--prestate-tracer`, `--evm-version`)
105    /// do not apply.
106    #[arg(
107        long,
108        default_value_t = false,
109        conflicts_with_all = ["debug", "decode_internal", "trace_printer", "quick", "prestate_tracer", "evm_version"]
110    )]
111    debug_trace_transaction: bool,
112
113    #[command(flatten)]
114    tracing: TracingArgs,
115
116    /// Deprecated short alias for `--labels`.
117    #[arg(short = 'l', value_name = "ADDRESS:LABEL", hide = true)]
118    legacy_labels: Vec<String>,
119
120    #[command(flatten)]
121    etherscan: EtherscanOpts,
122
123    #[command(flatten)]
124    rpc: RpcOpts,
125
126    /// The EVM version to use.
127    ///
128    /// Overrides the version specified in the config.
129    #[arg(long)]
130    evm_version: Option<EvmVersion>,
131
132    /// Use current project artifacts for trace decoding.
133    #[arg(long, visible_alias = "la")]
134    pub with_local_artifacts: bool,
135
136    /// Disable block gas limit check.
137    ///
138    /// Always implied: a mined transaction already passed its chain's own check.
139    #[arg(long)]
140    pub disable_block_gas_limit: bool,
141
142    /// Enable the tx gas limit checks as imposed by Osaka (EIP-7825).
143    #[arg(long)]
144    pub enable_tx_gas_limit: bool,
145}
146
147impl RunArgs {
148    fn resolve_tracing(&self, config: &TracingConfig, verbosity: u8) -> TracingConfig {
149        if self.debug_trace_transaction {
150            self.tracing.resolve_call_tracer(config, verbosity)
151        } else {
152            self.tracing.resolve(config, verbosity)
153        }
154    }
155
156    /// Executes the transaction by replaying it
157    ///
158    /// This replays the entire block the transaction was mined in unless `quick` is set to true
159    ///
160    /// Note: This executes the transaction(s) as is: Cheatcodes are disabled
161    pub async fn run(self) -> Result<()> {
162        let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self);
163        let (config, mut evm_opts) = super::load_cast_config_and_evm_opts(figment)?;
164        evm_opts.fork_url = Some(config.get_rpc_url_or_localhost_http()?.into_owned());
165
166        // Auto-detect network from fork chain ID when not explicitly configured.
167        evm_opts.infer_network_from_fork().await?;
168
169        if evm_opts.networks.is_tempo() {
170            return self.run_with_evm::<TempoEvmNetwork>(config, evm_opts).await;
171        }
172
173        #[cfg(feature = "monad")]
174        if evm_opts.networks.is_monad() {
175            return self
176                .run_with_evm::<foundry_evm::core::evm::MonadEvmNetwork>(config, evm_opts)
177                .await;
178        }
179
180        #[cfg(feature = "optimism")]
181        if evm_opts.networks.is_optimism() {
182            return self.run_with_evm::<OpEvmNetwork>(config, evm_opts).await;
183        }
184
185        self.run_with_evm::<EthEvmNetwork>(config, evm_opts).await
186    }
187
188    async fn run_with_evm<FEN: FoundryEvmNetwork>(
189        mut self,
190        mut config: Box<Config>,
191        evm_opts: EvmOpts,
192    ) -> Result<()> {
193        config.networks = evm_opts.networks;
194        self.tracing.labels.append(&mut self.legacy_labels);
195        config.tracing = self.resolve_tracing(&config.tracing, shell::verbosity());
196        let tracing = config.tracing.clone();
197
198        let with_local_artifacts = self.with_local_artifacts;
199        let debug = self.debug;
200        let compute_units_per_second = if self.rpc.common.no_rpc_rate_limit {
201            Some(u64::MAX)
202        } else {
203            self.rpc.common.compute_units_per_second
204        };
205
206        // `AnyNetwork` rather than `FEN::Network`: chains such as Arbitrum, Celo and the
207        // OP-stack forks Foundry does not route to a dedicated network put transaction types the
208        // strict Ethereum envelope cannot decode into every block, which would fail the full
209        // block fetch below for the whole chain. Execution still uses `FEN`.
210        let provider = ProviderBuilder::<AnyNetwork>::from_config(&config)?
211            .compute_units_per_second_opt(compute_units_per_second)
212            .build()?;
213
214        let tx_hash = self.tx_hash.parse().wrap_err("invalid tx hash")?;
215        let endpoint_identity = if self.debug_trace_transaction {
216            Some(evm_opts.discover_fork_endpoint().await?)
217        } else {
218            None
219        };
220        let tx = provider
221            .get_transaction_by_hash(tx_hash)
222            .await
223            .wrap_err_with(|| format!("tx not found: {tx_hash:?}"))?
224            .ok_or_else(|| eyre::eyre!("tx not found: {:?}", tx_hash))?;
225
226        // Fetch the trace from the node via `debug_traceTransaction` (callTracer) instead of
227        // re-executing the transaction locally. The node already holds the transaction's exact
228        // pre-state and EVM rules, so this needs no block replay and no local executor; it also
229        // handles system transactions, so this path comes before the system transaction guard.
230        if self.debug_trace_transaction {
231            let endpoint_identity = endpoint_identity
232                .as_ref()
233                .ok_or_else(|| eyre::eyre!("remote trace endpoint identity was not captured"))?;
234            let tx_inclusion = tx
235                .block_hash_num()
236                .ok_or_else(|| eyre::eyre!("tx may still be pending: {:?}", tx_hash))?;
237            let tx_block_number = tx_inclusion.number;
238            let tx_block_hash = tx_inclusion.hash;
239
240            let geth_trace = provider
241                .debug_trace_transaction(
242                    tx_hash,
243                    GethDebugTracingOptions::call_tracer(CallConfig::default().with_log()),
244                )
245                .await
246                .map_err(|err| -> eyre::Report {
247                    // Two RPC rejections deserve an actionable hint instead of the raw transport
248                    // error, and they need different fixes: a disabled `debug` namespace, and
249                    // missing historical state, hit whenever the transaction's block has been
250                    // pruned by a full node.
251                    if is_method_not_found_error(&err) {
252                        eyre::eyre!(
253                            "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"
254                        )
255                    } else if is_missing_state_error(&err) {
256                        eyre::eyre!(
257                            "the RPC endpoint does not have the historical state for the transaction's block; use an archive endpoint"
258                        )
259                    } else {
260                        err.into()
261                    }
262                })?;
263            let GethTrace::CallTracer(frame) = geth_trace else {
264                eyre::bail!(
265                    "`debug_traceTransaction` did not return a callTracer frame; the RPC endpoint \
266                     may not support the `callTracer`"
267                );
268            };
269
270            let receipt = provider
271                .get_transaction_receipt(tx_hash)
272                .await?
273                .ok_or_else(|| eyre::eyre!("tx receipt not found: {:?}", tx_hash))?;
274            ensure_remote_transaction_inclusion(
275                tx_hash,
276                tx_inclusion,
277                receipt.block_hash_num(),
278                "transaction receipt",
279            )?;
280
281            let Some(transaction_block) = provider.get_block_by_hash(tx_block_hash).await? else {
282                return ensure_remote_transaction_inclusion(
283                    tx_hash,
284                    tx_inclusion,
285                    None,
286                    "block fetched by hash",
287                );
288            };
289            ensure_remote_transaction_inclusion(
290                tx_hash,
291                tx_inclusion,
292                Some(BlockNumHash::new(
293                    transaction_block.header().number(),
294                    transaction_block.header().hash(),
295                )),
296                "block fetched by hash",
297            )?;
298
299            let success = receipt.status();
300            let gas_used = receipt.gas_used();
301            let root_create_address = Transaction::to(&tx).is_none().then(|| {
302                receipt.contract_address().unwrap_or_else(|| tx.from().create(tx.nonce()))
303            });
304            let arena = SparsedTraceArena {
305                arena: call_frame_to_arena_with_root_address(&frame, root_create_address),
306                ignored: Default::default(),
307                diagnostics: Default::default(),
308            };
309            let result = TraceResult {
310                success,
311                traces: Some(vec![(TraceKind::Execution, arena)]),
312                gas_used,
313            };
314
315            // Local-artifact labeling matches deployed runtime bytecode against the project
316            // artifacts. There is no local executor on this path, so fetch the code over RPC
317            // for the addresses in the trace, at the transaction's block. Skip the extra
318            // round-trips unless local artifacts were requested.
319            let contracts_bytecode = if with_local_artifacts {
320                fetch_transaction_contracts_bytecode_via_rpc(
321                    &provider,
322                    &result,
323                    tx_hash,
324                    BlockId::hash(tx_block_hash),
325                )
326                .await?
327            } else {
328                Default::default()
329            };
330
331            // The remote node executed this trace, so its reported family is authoritative for
332            // decoding even when the caller selected a compatible local EVM implementation.
333            let execution_network = endpoint_identity.network;
334            let chain = alloy_chains::Chain::from_id(endpoint_identity.source_chain_id);
335            // A configured hardfork is an explicit trace-decoding override. Otherwise honor an
336            // Anvil endpoint's exact execution hardfork before consulting the source schedule.
337            let resolved_hardfork = if let Some(hardfork) = select_remote_trace_hardfork(
338                config.hardfork,
339                endpoint_identity.hardfork,
340                execution_network,
341            ) {
342                Some(hardfork)
343            } else {
344                FoundryHardfork::from_chain_and_timestamp(
345                    chain.id(),
346                    transaction_block.header().timestamp(),
347                )
348            };
349            let final_endpoint_identity = evm_opts.discover_fork_endpoint().await?;
350            ensure_remote_trace_context_unchanged(endpoint_identity, &final_endpoint_identity)?;
351
352            let current_tx = provider.get_transaction_by_hash(tx_hash).await?;
353            ensure_remote_transaction_inclusion(
354                tx_hash,
355                tx_inclusion,
356                current_tx.and_then(|tx| tx.block_hash_num()),
357                "transaction lookup",
358            )?;
359            let canonical_block = provider.get_block_by_number(tx_block_number.into()).await?;
360            ensure_remote_transaction_inclusion(
361                tx_hash,
362                tx_inclusion,
363                canonical_block
364                    .map(|block| BlockNumHash::new(block.header().number(), block.header().hash())),
365                "canonical block lookup",
366            )?;
367            handle_traces(
368                result,
369                &config,
370                chain,
371                &contracts_bytecode,
372                &tracing,
373                with_local_artifacts,
374                false,
375                resolved_hardfork,
376                endpoint_identity.network_profile,
377            )
378            .await?;
379
380            return Ok(());
381        }
382
383        let target_is_system = is_known_system_sender(tx.from())
384            || tx.transaction_type() == Some(SYSTEM_TRANSACTION_TYPE);
385        // Report an unsupported system transaction before decoding it: the envelopes a chain
386        // reserves for itself, such as Arbitrum's internal transaction, are exactly the ones this
387        // build may not be able to decode.
388        if target_is_system && !self.replay_system_txes && !evm_opts.networks.is_monad() {
389            return Err(eyre::eyre!(
390                "{tx_hash:?} is a system transaction.\nReplaying system transactions is currently not supported."
391            ));
392        }
393        let target_tx_env = TxEnvFor::<FEN>::from_any_rpc_transaction(&tx)?;
394
395        let tx_block_number = tx
396            .block_number()
397            .ok_or_else(|| eyre::eyre!("tx may still be pending: {:?}", tx_hash))?;
398
399        // we need to fork off the parent block
400        config.fork_block_number = Some(tx_block_number - 1);
401
402        let create2_deployer = evm_opts.create2_deployer;
403        let verbosity = tracing.verbosity;
404        let (block, (mut evm_env, tx_env, fork, chain, networks, endpoint_hardfork)) = tokio::try_join!(
405            // fetch the block the transaction was mined in
406            provider.get_block(tx_block_number.into()).full().into_future().map_err(Into::into),
407            TracingExecutor::<FEN>::get_fork_material(&mut config, evm_opts)
408        )?;
409
410        let mut evm_version = self.evm_version;
411        // Mined transactions already passed the block gas limit check their chain applies, and
412        // some chains admit transactions whose gas limit exceeds it: BSC validator transactions
413        // carry a gas limit of `i64::MAX`. Re-applying the check can only reject a transaction
414        // the chain accepted.
415        evm_env.cfg_env.disable_block_gas_limit = true;
416
417        // By default do not enforce transaction gas limits imposed by Osaka (EIP-7825).
418        // Users can opt-in to enable these limits by setting `enable_tx_gas_limit` to true.
419        if !self.enable_tx_gas_limit {
420            evm_env.cfg_env.tx_gas_limit_cap = Some(u64::MAX);
421        }
422
423        evm_env.cfg_env.limit_contract_code_size = None;
424        evm_env.block_env.set_number(U256::from(tx_block_number));
425
426        let mut parent_beacon_block_root = None;
427        if let Some(block) = &block {
428            evm_env.block_env = block_env_from_header(block.header());
429            parent_beacon_block_root = block.header().parent_beacon_block_root();
430
431            // Unless explicitly configured, resolve the correct spec for the block using the same
432            // approach as reth: walk known chain activation conditions to find the latest active
433            // fork. Falls back to a blob-gas heuristic for unknown chains.
434            if evm_version.is_none()
435                && config.hardfork.is_none()
436                && FoundryHardfork::from_chain_and_timestamp(chain.id(), block.header().timestamp())
437                    .is_none()
438                && block.header().excess_blob_gas().is_some()
439            {
440                // TODO: add glamsterdam header field checks in the future
441                evm_version = Some(EvmVersion::Cancun);
442            }
443            apply_chain_and_block_specific_env_changes_for_chain::<AnyNetwork, _, _>(
444                &mut evm_env,
445                block,
446                chain.id(),
447                config.networks,
448            );
449        }
450        let resolved_hardfork = TracingExecutor::<FEN>::resolve_spec_for_chain(
451            &config,
452            networks,
453            chain.id(),
454            endpoint_hardfork,
455            &mut evm_env,
456            evm_version,
457        );
458        TracingExecutor::<FEN>::extend_precompile_labels(&mut config, networks, resolved_hardfork);
459
460        let block_context = if networks.is_monad() {
461            // `BlockContext` is typed to `FEN::Network`. Monad blocks only carry standard
462            // envelopes, so a typed provider can serve this path while the rest of the command
463            // stays on `AnyNetwork`.
464            let typed_provider = ProviderBuilder::<FEN::Network>::from_config(&config)?
465                .compute_units_per_second_opt(compute_units_per_second)
466                .build()?;
467            let block = typed_provider.get_block(tx_block_number.into()).full().await?.ok_or_else(
468                || {
469                    eyre::eyre!(
470                        "block {tx_block_number} is required to reconstruct transaction context"
471                    )
472                },
473            )?;
474            Some(BlockContext::<FEN>::fetch(&typed_provider, &block).await?)
475        } else {
476            None
477        };
478        apply_chain_specific_tx_replay_env_changes_for_chain(&mut evm_env, chain.id());
479
480        let mut executor = TracingExecutor::<FEN>::new(
481            (evm_env.clone(), tx_env),
482            fork,
483            evm_version,
484            TraceRequirements::none(),
485            networks,
486            create2_deployer,
487            None,
488        )?;
489
490        evm_env.cfg_env.set_spec_and_mainnet_gas_params(executor.spec_id());
491
492        let spec_id = (*evm_env.cfg_env.spec()).into();
493
494        if let Some(parent_beacon_block_root) =
495            parent_beacon_block_root_for_network(networks, spec_id, parent_beacon_block_root)
496        {
497            executor.apply_beacon_root(parent_beacon_block_root)?;
498        }
499
500        // Set the state to the moment right before the transaction.
501        //
502        // When `--prestate-tracer` is set, opportunistically try to fetch the prestate directly
503        // via `debug_traceTransaction` (much faster than replaying the block). This requires the
504        // `debug_` namespace, which most nodes don't expose, so it is opt-in and silently falls
505        // back to replaying previous transactions in the block if the call or parsing fails.
506        let mut prestate_applied = false;
507        if !self.quick && self.prestate_tracer {
508            trace!(?tx_hash, "attempting to fetch prestate via debug_traceTransaction");
509            match provider
510                .debug_trace_transaction(
511                    tx_hash,
512                    GethDebugTracingOptions::prestate_tracer(PreStateConfig::default()),
513                )
514                .await
515            {
516                Ok(trace) => match trace.try_into_pre_state_frame() {
517                    Ok(pre_state_frame) => {
518                        executor.apply_prestate_trace(pre_state_frame.into_pre_state())?;
519                        prestate_applied = true;
520                        trace!("prestate trace applied successfully, skipping block replay");
521                    }
522                    Err(err) => {
523                        trace!(%err, "failed to parse prestate trace response");
524                    }
525                },
526                Err(err) => {
527                    trace!(?err, "debug_traceTransaction failed, falling back to block replay");
528                }
529            }
530        }
531
532        // Fall back to replaying previous transactions if prestate trace wasn't applied.
533        if !self.quick && !prestate_applied {
534            sh_status!("Executing previous transactions from the block.")?;
535
536            if let Some(block) = &block {
537                let pb = init_progress(block.transactions().len() as u64, "tx");
538                pb.set_position(0);
539
540                let BlockTransactions::Full(ref txs) = *block.transactions() else {
541                    return Err(eyre::eyre!("Could not get block txs"));
542                };
543
544                for (index, tx) in txs.iter().enumerate() {
545                    if tx.tx_hash() == tx_hash {
546                        break;
547                    }
548
549                    let is_system = is_known_system_sender(tx.from())
550                        || tx.transaction_type() == Some(SYSTEM_TRANSACTION_TYPE);
551                    // Classify before converting: a chain's own system envelopes are exactly the
552                    // ones this build may not be able to decode, and they are skipped below.
553                    if is_system && !self.replay_system_txes && !networks.is_monad() {
554                        pb.set_position((index + 1) as u64);
555                        continue;
556                    }
557                    let tx_env = TxEnvFor::<FEN>::from_any_rpc_transaction(tx)?;
558                    let chain_context = block_context.as_ref().map_or_else(
559                        || ChainFor::<FEN>::for_transaction(&tx_env),
560                        |context| context.transaction(index),
561                    );
562
563                    evm_env.cfg_env.disable_balance_check = true;
564
565                    if is_system {
566                        #[cfg(feature = "monad")]
567                        if executor
568                            .try_transact_system_replay_with_env_and_context(
569                                evm_env.clone(),
570                                tx_env.clone(),
571                                chain_context.clone(),
572                            )
573                            .wrap_err_with(|| {
574                                format!(
575                                    "Failed to replay system transaction: {:?} in block {}",
576                                    tx.tx_hash(),
577                                    evm_env.block_env.number()
578                                )
579                            })?
580                            .is_some()
581                        {
582                            trace!(tx=?tx.tx_hash(), "executed previous canonical system transaction");
583                            pb.set_position((index + 1) as u64);
584                            continue;
585                        }
586                        if !self.replay_system_txes {
587                            pb.set_position((index + 1) as u64);
588                            continue;
589                        }
590                    }
591
592                    if let Some(to) = Transaction::to(tx) {
593                        trace!(tx=?tx.tx_hash(),?to, "executing previous call transaction");
594                        executor
595                            .transact_with_env_and_context(
596                                evm_env.clone(),
597                                tx_env.clone(),
598                                chain_context,
599                            )
600                            .wrap_err_with(|| {
601                                format!(
602                                    "Failed to execute transaction: {:?} in block {}",
603                                    tx.tx_hash(),
604                                    evm_env.block_env.number()
605                                )
606                            })?;
607                    } else {
608                        trace!(tx=?tx.tx_hash(), "executing previous create transaction");
609                        if let Err(error) = executor.deploy_with_env_and_context(
610                            evm_env.clone(),
611                            tx_env.clone(),
612                            chain_context,
613                            None,
614                        ) {
615                            match error {
616                                // Reverted transactions should be skipped
617                                EvmError::Execution(_) => (),
618                                error => {
619                                    return Err(error).wrap_err_with(|| {
620                                        format!(
621                                            "Failed to deploy transaction: {:?} in block {}",
622                                            tx.tx_hash(),
623                                            evm_env.block_env.number()
624                                        )
625                                    });
626                                }
627                            }
628                        }
629                    }
630
631                    pb.set_position((index + 1) as u64);
632                }
633            }
634        }
635
636        // Execute our transaction
637        let result = {
638            // Enable tracing only for the target transaction; the prefix replay above ran with
639            // tracing disabled.
640            let target_trace_requirements = TraceRequirements::none()
641                .with_calls(true)
642                .with_debug(self.debug)
643                .with_decode_internal(if tracing.decode_internal {
644                    InternalTraceMode::Full
645                } else {
646                    InternalTraceMode::None
647                })
648                .with_state_changes(verbosity > 4);
649            executor.set_trace_requirements(target_trace_requirements);
650            executor.set_trace_printer(self.trace_printer);
651
652            let tx_env = target_tx_env;
653            let target_index = if let Some(block) = &block {
654                let BlockTransactions::Full(transactions) = block.transactions() else {
655                    return Err(eyre::eyre!("Could not get block txs"));
656                };
657                transactions
658                    .iter()
659                    .position(|candidate| candidate.tx_hash() == tx_hash)
660                    .ok_or_else(|| {
661                        eyre::eyre!("transaction {tx_hash:?} is missing from its block")
662                    })?
663            } else {
664                0
665            };
666            let chain_context = block_context.as_ref().map_or_else(
667                || ChainFor::<FEN>::for_transaction(&tx_env),
668                |context| context.transaction(target_index),
669            );
670
671            // A recovered signer that disagrees with the `from` the node reports marks a
672            // transaction the chain injected rather than one a key signed, such as a HyperCore
673            // credit. Envelopes this build cannot decode are in the same category.
674            let sender_is_forged = match &*tx.inner.inner {
675                AnyTxEnvelope::Ethereum(inner) => {
676                    inner.recover_signer().is_ok_and(|signer| signer != tx.from())
677                }
678                AnyTxEnvelope::Unknown(_) => true,
679            };
680            if sender_is_forged {
681                evm_env.cfg_env.disable_balance_check = true;
682            }
683
684            #[cfg(feature = "monad")]
685            let replay_result = if target_is_system {
686                executor.try_transact_system_replay_with_env_and_context(
687                    evm_env.clone(),
688                    tx_env.clone(),
689                    chain_context.clone(),
690                )?
691            } else {
692                None
693            };
694            #[cfg(not(feature = "monad"))]
695            let replay_result: Option<foundry_evm::executors::RawCallResult<FEN>> = None;
696
697            if let Some(result) = replay_result {
698                trace!(tx=?tx.tx_hash(), "executed canonical system transaction");
699                TraceResult::from(result)
700            } else {
701                if target_is_system && !self.replay_system_txes {
702                    return Err(eyre::eyre!(
703                        "{:?} is a system transaction.\nReplaying system transactions is currently not supported.",
704                        tx.tx_hash()
705                    ));
706                }
707
708                if let Some(to) = Transaction::to(&tx) {
709                    trace!(tx=?tx.tx_hash(), to=?to, "executing call transaction");
710                    TraceResult::from(executor.transact_with_env_and_context(
711                        evm_env,
712                        tx_env,
713                        chain_context,
714                    )?)
715                } else {
716                    trace!(tx=?tx.tx_hash(), "executing create transaction");
717                    TraceResult::try_from(executor.deploy_with_env_and_context(
718                        evm_env,
719                        tx_env,
720                        chain_context,
721                        None,
722                    ))?
723                }
724            }
725        };
726
727        let contracts_bytecode = fetch_contracts_bytecode_from_trace(&executor, &result)?;
728        handle_traces(
729            result,
730            &config,
731            chain,
732            &contracts_bytecode,
733            &tracing,
734            with_local_artifacts,
735            debug,
736            resolved_hardfork,
737            networks,
738        )
739        .await?;
740
741        Ok(())
742    }
743}
744
745fn ensure_remote_transaction_inclusion(
746    tx_hash: B256,
747    expected: BlockNumHash,
748    actual: Option<BlockNumHash>,
749    source: &str,
750) -> Result<()> {
751    let Some(actual) = actual else {
752        eyre::bail!(
753            "transaction {tx_hash} changed inclusion while collecting its remote trace: {source} no longer reports it as mined; retry the command"
754        );
755    };
756    if actual != expected {
757        eyre::bail!(
758            "transaction {tx_hash} changed inclusion while collecting its remote trace: expected block {} at {}, but {source} reported block {} at {}; retry the command",
759            expected.hash,
760            expected.number,
761            actual.hash,
762            actual.number,
763        );
764    }
765
766    Ok(())
767}
768
769const fn parent_beacon_block_root_for_network(
770    networks: NetworkConfigs,
771    spec_id: SpecId,
772    parent_beacon_block_root: Option<B256>,
773) -> Option<B256> {
774    if networks.is_monad() || !spec_id.is_enabled_in(SpecId::CANCUN) {
775        return None;
776    }
777
778    // Chains that run a Cancun or later EVM without Ethereum's beacon chain, such as Polygon and
779    // Scroll, never populate this header field and never deploy the EIP-4788 contract, so there
780    // is no root to apply. Requiring one makes their blocks unreplayable.
781    parent_beacon_block_root
782}
783
784pub fn fetch_contracts_bytecode_from_trace<FEN: FoundryEvmNetwork>(
785    executor: &Executor<FEN>,
786    result: &TraceResult,
787) -> Result<AddressHashMap<Bytes>> {
788    let mut contracts_bytecode = AddressHashMap::default();
789    if let Some(ref traces) = result.traces {
790        contracts_bytecode.extend(gather_trace_addresses(traces).filter_map(|addr| {
791            // All relevant bytecodes should already be cached in the executor.
792            let code = executor
793                .backend()
794                .basic_ref(addr)
795                .inspect_err(|e| _ = sh_warn!("Failed to fetch code for {addr}: {e}"))
796                .ok()??
797                .code?
798                .bytes();
799            if code.is_empty() {
800                return None;
801            }
802            Some((addr, code))
803        }));
804    }
805    Ok(contracts_bytecode)
806}
807
808/// Fetches the runtime bytecode of the addresses seen in `result` over RPC.
809///
810/// The RPC trace path (`cast call --debug-trace-call`) has no local executor to read code
811/// from, so the bytecode needed to match local artifacts is fetched from the node with
812/// `eth_getCode`. Addresses whose code cannot be fetched are skipped with a warning.
813pub async fn fetch_contracts_bytecode_via_rpc<N: Network, P: Provider<N>>(
814    provider: &P,
815    result: &TraceResult,
816    block: BlockId,
817) -> Result<AddressHashMap<Bytes>> {
818    let mut contracts_bytecode = AddressHashMap::default();
819    if let Some(ref traces) = result.traces {
820        let mut requests =
821            futures::stream::iter(gather_trace_addresses(traces))
822                .map(|address| async move {
823                    (address, provider.get_code_at(address).block_id(block).await)
824                })
825                .buffer_unordered(MAX_CONCURRENT_RPC_REQUESTS);
826        while let Some((address, code)) = requests.next().await {
827            match code {
828                Ok(code) if !code.is_empty() => {
829                    contracts_bytecode.insert(address, code);
830                }
831                Ok(_) => {}
832                Err(err) => {
833                    let _ = sh_warn!("Failed to fetch code for {address}: {err}");
834                }
835            }
836        }
837    }
838    Ok(contracts_bytecode)
839}
840
841/// Fetches bytecode for a mined transaction at its exact transaction index.
842///
843/// The prestate tracer provides the code that existed immediately before the transaction, which
844/// avoids reading end-of-block state for contracts changed or removed by later transactions. Any
845/// address absent from the prestate (for example, a contract created by this transaction) falls
846/// back to `eth_getCode` at the transaction's block.
847async fn fetch_transaction_contracts_bytecode_via_rpc<N: Network, P: Provider<N>>(
848    provider: &P,
849    result: &TraceResult,
850    tx_hash: B256,
851    block: BlockId,
852) -> Result<AddressHashMap<Bytes>> {
853    let mut contracts_bytecode = AddressHashMap::default();
854    let prestate_config = PreStateConfig { disable_storage: Some(true), ..Default::default() };
855    match provider
856        .debug_trace_transaction(tx_hash, GethDebugTracingOptions::prestate_tracer(prestate_config))
857        .await
858    {
859        Ok(trace) => match trace.try_into_pre_state_frame() {
860            Ok(prestate) => {
861                for (&address, account) in prestate.pre_state() {
862                    if let Some(code) = account.code.clone().filter(|code| !code.is_empty()) {
863                        contracts_bytecode.insert(address, code);
864                    }
865                }
866            }
867            Err(err) => {
868                let _ = sh_warn!("Failed to parse transaction prestate for local artifacts: {err}");
869            }
870        },
871        Err(err) => {
872            let _ = sh_warn!("Failed to fetch transaction prestate for local artifacts: {err}");
873        }
874    }
875
876    if let Some(ref traces) = result.traces {
877        let missing_addresses = gather_trace_addresses(traces)
878            .filter(|address| !contracts_bytecode.contains_key(address))
879            .collect::<Vec<_>>();
880        let mut requests =
881            futures::stream::iter(missing_addresses)
882                .map(|address| async move {
883                    (address, provider.get_code_at(address).block_id(block).await)
884                })
885                .buffer_unordered(MAX_CONCURRENT_RPC_REQUESTS);
886        while let Some((address, code)) = requests.next().await {
887            match code {
888                Ok(code) if !code.is_empty() => {
889                    contracts_bytecode.insert(address, code);
890                }
891                Ok(_) => {}
892                Err(err) => {
893                    let _ = sh_warn!("Failed to fetch code for {address}: {err}");
894                }
895            }
896        }
897    }
898    Ok(contracts_bytecode)
899}
900
901fn gather_trace_addresses(traces: &Traces) -> impl Iterator<Item = Address> {
902    let mut addresses = AddressSet::default();
903    for (_, trace) in traces {
904        for node in trace.arena.nodes() {
905            if !node.trace.address.is_zero() {
906                addresses.insert(node.trace.address);
907            }
908            if !node.trace.caller.is_zero() {
909                addresses.insert(node.trace.caller);
910            }
911        }
912    }
913    addresses.into_iter()
914}
915
916impl figment::Provider for RunArgs {
917    fn metadata(&self) -> Metadata {
918        Metadata::named("RunArgs")
919    }
920
921    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
922        let mut map = Map::new();
923
924        if let Some(api_key) = &self.etherscan.key {
925            map.insert("etherscan_api_key".into(), api_key.as_str().into());
926        }
927
928        if let Some(evm_version) = self.evm_version {
929            map.insert("evm_version".into(), figment::value::Value::serialize(evm_version)?);
930        }
931
932        Ok(Map::from([(Config::selected_profile(), map)]))
933    }
934}
935
936#[cfg(test)]
937mod tests {
938    use super::*;
939    use alloy_primitives::address;
940
941    #[test]
942    fn remote_transaction_inclusion_must_remain_stable() {
943        let tx_hash = B256::repeat_byte(0x11);
944        let expected = BlockNumHash::new(42, B256::repeat_byte(0x22));
945
946        ensure_remote_transaction_inclusion(tx_hash, expected, Some(expected), "receipt").unwrap();
947
948        let err =
949            ensure_remote_transaction_inclusion(tx_hash, expected, None, "receipt").unwrap_err();
950        assert!(err.to_string().contains("no longer reports it as mined"));
951
952        for actual in [
953            BlockNumHash::new(43, expected.hash),
954            BlockNumHash::new(expected.number, B256::repeat_byte(0x33)),
955        ] {
956            let err =
957                ensure_remote_transaction_inclusion(tx_hash, expected, Some(actual), "receipt")
958                    .unwrap_err();
959            assert!(err.to_string().contains("changed inclusion"));
960        }
961    }
962
963    #[test]
964    fn parses_legacy_short_label_alias() {
965        let address = address!("0x0000000000000000000000000000000000000001");
966        let label = format!("{address}:alice");
967        let args = RunArgs::parse_from(["cast run", "0x00", "-l", &label]);
968
969        assert_eq!(args.legacy_labels, vec![label]);
970    }
971
972    #[test]
973    fn debug_trace_transaction_rejects_local_execution_flags() {
974        for flag in
975            ["--debug", "--decode-internal", "--trace-printer", "--quick", "--prestate-tracer"]
976        {
977            let result = RunArgs::try_parse_from([
978                "foundry-cli",
979                "--debug-trace-transaction",
980                "0x0000000000000000000000000000000000000000000000000000000000000000",
981                flag,
982            ]);
983            assert!(result.is_err(), "--debug-trace-transaction must reject {flag}");
984        }
985        // --evm-version takes a value, so it is checked separately from the boolean flags above.
986        let result = RunArgs::try_parse_from([
987            "foundry-cli",
988            "--debug-trace-transaction",
989            "0x0000000000000000000000000000000000000000000000000000000000000000",
990            "--evm-version",
991            "shanghai",
992        ]);
993        assert!(result.is_err(), "--debug-trace-transaction must reject --evm-version");
994    }
995
996    #[test]
997    fn debug_trace_transaction_accepts_label_and_render_flags() {
998        let args = RunArgs::try_parse_from([
999            "foundry-cli",
1000            "--debug-trace-transaction",
1001            "0x0000000000000000000000000000000000000000000000000000000000000000",
1002            "--label",
1003            "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045:vitalik.eth",
1004            "--disable-labels",
1005            "--trace-depth",
1006            "2",
1007            "--with-local-artifacts",
1008        ]);
1009        assert!(args.is_ok(), "--debug-trace-transaction must accept label/rendering flags");
1010    }
1011
1012    #[test]
1013    fn parent_beacon_block_root_is_applied_only_when_the_header_has_one() {
1014        let networks = NetworkConfigs::default();
1015        // Polygon and Scroll run a Cancun or later EVM without populating the header field.
1016        assert_eq!(parent_beacon_block_root_for_network(networks, SpecId::CANCUN, None), None);
1017
1018        let root = B256::repeat_byte(0x42);
1019        assert_eq!(
1020            parent_beacon_block_root_for_network(networks, SpecId::CANCUN, Some(root)),
1021            Some(root),
1022        );
1023        assert_eq!(
1024            parent_beacon_block_root_for_network(networks, SpecId::SHANGHAI, Some(root)),
1025            None,
1026        );
1027        assert_eq!(parent_beacon_block_root_for_network(networks, SpecId::SHANGHAI, None), None,);
1028    }
1029
1030    #[cfg(feature = "monad")]
1031    #[test]
1032    fn parent_beacon_block_root_is_not_used_by_monad() {
1033        let networks = NetworkConfigs::with_monad();
1034        for spec_id in [SpecId::PRAGUE, SpecId::OSAKA] {
1035            assert_eq!(parent_beacon_block_root_for_network(networks, spec_id, None), None,);
1036            assert_eq!(
1037                parent_beacon_block_root_for_network(
1038                    networks,
1039                    spec_id,
1040                    Some(B256::repeat_byte(0x42)),
1041                ),
1042                None,
1043            );
1044        }
1045    }
1046
1047    #[test]
1048    fn debug_trace_transaction_ignores_configured_internal_decoding() {
1049        let args = RunArgs::parse_from(["cast run", "0x00", "--debug-trace-transaction"]);
1050        let config = TracingConfig { decode_internal: true, ..Default::default() };
1051
1052        assert!(!args.resolve_tracing(&config, 0).decode_internal);
1053    }
1054}