Skip to main content

cast/cmd/
run.rs

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/// CLI arguments for `cast run`.
55#[derive(Clone, Debug, Parser)]
56pub struct RunArgs {
57    /// The transaction hash.
58    tx_hash: String,
59
60    /// Opens the transaction in the debugger.
61    #[arg(long, short)]
62    debug: bool,
63
64    /// Print out opcode traces.
65    #[arg(long, short)]
66    trace_printer: bool,
67
68    /// Executes the transaction only with the state from the previous block.
69    ///
70    /// May result in different results than the live execution!
71    #[arg(long)]
72    quick: bool,
73
74    /// Whether to replay system transactions.
75    #[arg(long, alias = "sys")]
76    replay_system_txes: bool,
77
78    /// Use debug_traceTransaction to fetch the prestate instead of replaying the block.
79    ///
80    /// This is significantly faster than replaying all previous transactions in the block, but
81    /// requires the node to expose the `debug_` namespace (most public RPCs don't). If the call
82    /// or response can't be used, cast silently falls back to replaying the block.
83    #[arg(long, default_value_t = false)]
84    prestate_tracer: bool,
85
86    /// Fetch the transaction's trace from the node via `debug_traceTransaction` (callTracer) and
87    /// render it, instead of re-executing the transaction locally.
88    ///
89    /// This skips the block replay entirely, so it is fast and reflects exactly what happened
90    /// on-chain, including chain-specific EVM behavior a local replay may not reproduce, but it
91    /// requires the node to expose the `debug_` namespace. The result is a call-tree view:
92    /// nested calls, value, gas, emitted logs and revert data. It does not provide the
93    /// opcode-level detail of a local run, so the local-execution-only flags (`--debug`,
94    /// `--decode-internal`, `--trace-printer`, `--quick`, `--prestate-tracer`, `--evm-version`)
95    /// do not apply.
96    #[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    /// Deprecated short alias for `--labels`.
107    #[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    /// The EVM version to use.
117    ///
118    /// Overrides the version specified in the config.
119    #[arg(long)]
120    evm_version: Option<EvmVersion>,
121
122    /// Use current project artifacts for trace decoding.
123    #[arg(long, visible_alias = "la")]
124    pub with_local_artifacts: bool,
125
126    /// Disable block gas limit check.
127    #[arg(long)]
128    pub disable_block_gas_limit: bool,
129
130    /// Enable the tx gas limit checks as imposed by Osaka (EIP-7825).
131    #[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    /// Executes the transaction by replaying it
145    ///
146    /// This replays the entire block the transaction was mined in unless `quick` is set to true
147    ///
148    /// Note: This executes the transaction(s) as is: Cheatcodes are disabled
149    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        // Auto-detect network from fork chain ID when not explicitly configured.
154        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        // Fetch the trace from the node via `debug_traceTransaction` (callTracer) instead of
195        // re-executing the transaction locally. The node already holds the transaction's exact
196        // pre-state and EVM rules, so this needs no block replay and no local executor; it also
197        // handles system transactions, so this path comes before the system transaction guard.
198        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                    // Two RPC rejections deserve an actionable hint instead of the raw transport
211                    // error, and they need different fixes: a disabled `debug` namespace, and
212                    // missing historical state, hit whenever the transaction's block has been
213                    // pruned by a full node.
214                    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            // Local-artifact labeling matches deployed runtime bytecode against the project
254            // artifacts. There is no local executor on this path, so fetch the code over RPC
255            // for the addresses in the trace, at the transaction's block. Skip the extra
256            // round-trips unless local artifacts were requested.
257            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        // check if the tx is a system transaction
289        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        // we need to fork off the parent block
304        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            // fetch the block the transaction was mined in
310            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        // By default do not enforce transaction gas limits imposed by Osaka (EIP-7825).
326        // Users can opt-in to enable these limits by setting `enable_tx_gas_limit` to true.
327        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            // Unless explicitly configured, resolve the correct spec for the block using the same
345            // approach as reth: walk known chain activation conditions to find the latest active
346            // fork. Falls back to a blob-gas heuristic for unknown chains.
347            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                    // TODO: add glamsterdam header field checks in the future
358                    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        // Set the state to the moment right before the transaction.
398        //
399        // When `--prestate-tracer` is set, opportunistically try to fetch the prestate directly
400        // via `debug_traceTransaction` (much faster than replaying the block). This requires the
401        // `debug_` namespace, which most nodes don't expose, so it is opt-in and silently falls
402        // back to replaying previous transactions in the block if the call or parsing fails.
403        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        // Fall back to replaying previous transactions if prestate trace wasn't applied.
430        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                    // Replay system transactions only if running with `sys` option.
443                    // System transactions such as on L2s don't contain any pricing info so it
444                    // could cause reverts.
445                    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                                // Reverted transactions should be skipped
478                                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        // Execute our transaction
498        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            // All relevant bytecodes should already be cached in the executor.
556            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
572/// Fetches the runtime bytecode of the addresses seen in `result` over RPC.
573///
574/// The RPC trace path (`cast call --debug-trace-call`) has no local executor to read code
575/// from, so the bytecode needed to match local artifacts is fetched from the node with
576/// `eth_getCode`. Addresses whose code cannot be fetched are skipped with a warning.
577pub 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
599/// Fetches bytecode for a mined transaction at its exact transaction index.
600///
601/// The prestate tracer provides the code that existed immediately before the transaction, which
602/// avoids reading end-of-block state for contracts changed or removed by later transactions. Any
603/// address absent from the prestate (for example, a contract created by this transaction) falls
604/// back to `eth_getCode` at the transaction's block.
605async 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        // --evm-version takes a value, so it is checked separately from the boolean flags above.
716        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}