Skip to main content

cast/cmd/
run.rs

1use crate::{
2    debug::handle_traces,
3    utils::{apply_chain_and_block_specific_env_changes, block_env_from_header},
4};
5use alloy_consensus::{BlockHeader, Transaction, transaction::SignerRecoverable};
6
7use alloy_evm::FromRecoveredTx;
8use alloy_network::{BlockResponse, TransactionResponse};
9use alloy_primitives::{
10    Address, Bytes, U256,
11    map::{AddressHashMap, AddressSet},
12};
13use alloy_provider::{Provider, ext::DebugApi};
14use alloy_rpc_types::{
15    BlockTransactions,
16    trace::geth::{GethDebugTracingOptions, PreStateConfig},
17};
18use clap::Parser;
19use eyre::{Result, WrapErr};
20use foundry_cli::{
21    opts::{EtherscanOpts, RpcOpts},
22    utils::{TraceResult, init_progress},
23};
24use foundry_common::{
25    SYSTEM_TRANSACTION_TYPE, is_known_system_sender, provider::ProviderBuilder, shell,
26};
27use foundry_compilers::artifacts::EvmVersion;
28use foundry_config::{
29    Config,
30    figment::{
31        self, Metadata, Profile,
32        value::{Dict, Map},
33    },
34};
35#[cfg(feature = "optimism")]
36use foundry_evm::core::evm::OpEvmNetwork;
37use foundry_evm::{
38    core::{
39        FoundryBlock as _,
40        evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor},
41    },
42    executors::{EvmError, Executor, TracingExecutor},
43    hardforks::FoundryHardfork,
44    opts::EvmOpts,
45    traces::{InternalTraceMode, TraceMode, Traces},
46};
47use futures::TryFutureExt;
48use revm::{DatabaseRef, context::Block};
49
50/// CLI arguments for `cast run`.
51#[derive(Clone, Debug, Parser)]
52pub struct RunArgs {
53    /// The transaction hash.
54    tx_hash: String,
55
56    /// Opens the transaction in the debugger.
57    #[arg(long, short)]
58    debug: bool,
59
60    /// Whether to identify internal functions in traces.
61    #[arg(long)]
62    decode_internal: bool,
63
64    /// Defines the depth of a trace
65    #[arg(long)]
66    trace_depth: Option<usize>,
67
68    /// Print out opcode traces.
69    #[arg(long, short)]
70    trace_printer: bool,
71
72    /// Executes the transaction only with the state from the previous block.
73    ///
74    /// May result in different results than the live execution!
75    #[arg(long)]
76    quick: bool,
77
78    /// Whether to replay system transactions.
79    #[arg(long, alias = "sys")]
80    replay_system_txes: bool,
81
82    /// Disables the labels in the traces.
83    #[arg(long, default_value_t = false)]
84    disable_labels: bool,
85
86    /// Use debug_traceTransaction to fetch the prestate instead of replaying the block.
87    ///
88    /// This is significantly faster than replaying all previous transactions in the block, but
89    /// requires the node to expose the `debug_` namespace (most public RPCs don't). If the call
90    /// or response can't be used, cast silently falls back to replaying the block.
91    #[arg(long, default_value_t = false)]
92    prestate_tracer: bool,
93
94    /// Label addresses in the trace.
95    ///
96    /// Example: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045:vitalik.eth
97    #[arg(long, short)]
98    label: Vec<String>,
99
100    #[command(flatten)]
101    etherscan: EtherscanOpts,
102
103    #[command(flatten)]
104    rpc: RpcOpts,
105
106    /// The EVM version to use.
107    ///
108    /// Overrides the version specified in the config.
109    #[arg(long)]
110    evm_version: Option<EvmVersion>,
111
112    /// Use current project artifacts for trace decoding.
113    #[arg(long, visible_alias = "la")]
114    pub with_local_artifacts: bool,
115
116    /// Disable block gas limit check.
117    #[arg(long)]
118    pub disable_block_gas_limit: bool,
119
120    /// Enable the tx gas limit checks as imposed by Osaka (EIP-7825).
121    #[arg(long)]
122    pub enable_tx_gas_limit: bool,
123}
124
125impl RunArgs {
126    /// Executes the transaction by replaying it
127    ///
128    /// This replays the entire block the transaction was mined in unless `quick` is set to true
129    ///
130    /// Note: This executes the transaction(s) as is: Cheatcodes are disabled
131    pub async fn run(self) -> Result<()> {
132        let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self);
133        let mut evm_opts = figment.extract::<EvmOpts>()?;
134
135        // Auto-detect network from fork chain ID when not explicitly configured.
136        evm_opts.infer_network_from_fork().await;
137
138        if evm_opts.networks.is_tempo() {
139            return self.run_with_evm::<TempoEvmNetwork>().await;
140        }
141
142        #[cfg(feature = "optimism")]
143        if evm_opts.networks.is_optimism() {
144            return self.run_with_evm::<OpEvmNetwork>().await;
145        }
146
147        self.run_with_evm::<EthEvmNetwork>().await
148    }
149
150    async fn run_with_evm<FEN: FoundryEvmNetwork>(self) -> Result<()> {
151        let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self);
152        let evm_opts = figment.extract::<EvmOpts>()?;
153        let mut config = Config::from_provider(figment)?.sanitized();
154
155        let label = self.label;
156        let with_local_artifacts = self.with_local_artifacts;
157        let debug = self.debug;
158        let decode_internal = self.decode_internal;
159        let disable_labels = self.disable_labels;
160        let compute_units_per_second = if self.rpc.common.no_rpc_rate_limit {
161            Some(u64::MAX)
162        } else {
163            self.rpc.common.compute_units_per_second
164        };
165
166        let provider = ProviderBuilder::<FEN::Network>::from_config(&config)?
167            .compute_units_per_second_opt(compute_units_per_second)
168            .build()?;
169
170        let tx_hash = self.tx_hash.parse().wrap_err("invalid tx hash")?;
171        let tx = provider
172            .get_transaction_by_hash(tx_hash)
173            .await
174            .wrap_err_with(|| format!("tx not found: {tx_hash:?}"))?
175            .ok_or_else(|| eyre::eyre!("tx not found: {:?}", tx_hash))?;
176
177        // check if the tx is a system transaction
178        if !self.replay_system_txes
179            && (is_known_system_sender(tx.from())
180                || tx.transaction_type() == Some(SYSTEM_TRANSACTION_TYPE))
181        {
182            return Err(eyre::eyre!(
183                "{:?} is a system transaction.\nReplaying system transactions is currently not supported.",
184                tx.tx_hash()
185            ));
186        }
187
188        let tx_block_number = tx
189            .block_number()
190            .ok_or_else(|| eyre::eyre!("tx may still be pending: {:?}", tx_hash))?;
191
192        // we need to fork off the parent block
193        config.fork_block_number = Some(tx_block_number - 1);
194
195        let create2_deployer = evm_opts.create2_deployer;
196        let (block, (mut evm_env, tx_env, fork, chain, networks)) = tokio::try_join!(
197            // fetch the block the transaction was mined in
198            provider.get_block(tx_block_number.into()).full().into_future().map_err(Into::into),
199            TracingExecutor::<FEN>::get_fork_material(&mut config, evm_opts)
200        )?;
201
202        let mut evm_version = self.evm_version;
203        let mut resolved_tempo_hardfork = chain.is_tempo().then(|| config.evm_spec_id());
204
205        evm_env.cfg_env.disable_block_gas_limit = self.disable_block_gas_limit;
206
207        // By default do not enforce transaction gas limits imposed by Osaka (EIP-7825).
208        // Users can opt-in to enable these limits by setting `enable_tx_gas_limit` to true.
209        if !self.enable_tx_gas_limit {
210            evm_env.cfg_env.tx_gas_limit_cap = Some(u64::MAX);
211        }
212
213        evm_env.cfg_env.limit_contract_code_size = None;
214        evm_env.block_env.set_number(U256::from(tx_block_number));
215
216        if let Some(block) = &block {
217            evm_env.block_env = block_env_from_header(block.header());
218
219            // Resolve the correct spec for the block using the same approach as reth: walk
220            // known chain activation conditions to find the latest active fork. Falls back
221            // to a blob-gas heuristic for unknown chains.
222            if evm_version.is_none() {
223                if let Some(hardfork) = FoundryHardfork::from_chain_and_timestamp(
224                    evm_env.cfg_env.chain_id,
225                    block.header().timestamp(),
226                ) {
227                    if let FoundryHardfork::Tempo(hardfork) = hardfork {
228                        resolved_tempo_hardfork = Some(hardfork);
229                    }
230                    evm_env.cfg_env.set_spec_and_mainnet_gas_params(hardfork.into());
231                } else if block.header().excess_blob_gas().is_some() {
232                    // TODO: add glamsterdam header field checks in the future
233                    evm_version = Some(EvmVersion::Cancun);
234                }
235            }
236            apply_chain_and_block_specific_env_changes::<FEN::Network, _, _>(
237                &mut evm_env,
238                block,
239                config.networks,
240            );
241        }
242
243        let trace_mode = TraceMode::Call
244            .with_debug(self.debug)
245            .with_decode_internal(if self.decode_internal {
246                InternalTraceMode::Full
247            } else {
248                InternalTraceMode::None
249            })
250            .with_state_changes(shell::verbosity() > 4);
251        let mut executor = TracingExecutor::<FEN>::new(
252            (evm_env.clone(), tx_env),
253            fork,
254            evm_version,
255            trace_mode,
256            networks,
257            create2_deployer,
258            None,
259        )?;
260
261        evm_env.cfg_env.set_spec_and_mainnet_gas_params(executor.spec_id());
262
263        // Set the state to the moment right before the transaction.
264        //
265        // When `--prestate-tracer` is set, opportunistically try to fetch the prestate directly
266        // via `debug_traceTransaction` (much faster than replaying the block). This requires the
267        // `debug_` namespace, which most nodes don't expose, so it is opt-in and silently falls
268        // back to replaying previous transactions in the block if the call or parsing fails.
269        let mut prestate_applied = false;
270        if !self.quick && self.prestate_tracer {
271            trace!(?tx_hash, "attempting to fetch prestate via debug_traceTransaction");
272            match provider
273                .debug_trace_transaction(
274                    tx_hash,
275                    GethDebugTracingOptions::prestate_tracer(PreStateConfig::default()),
276                )
277                .await
278            {
279                Ok(trace) => match trace.try_into_pre_state_frame() {
280                    Ok(pre_state_frame) => {
281                        executor.apply_prestate_trace(pre_state_frame.into_pre_state())?;
282                        prestate_applied = true;
283                        trace!("prestate trace applied successfully, skipping block replay");
284                    }
285                    Err(err) => {
286                        trace!(%err, "failed to parse prestate trace response");
287                    }
288                },
289                Err(err) => {
290                    trace!(?err, "debug_traceTransaction failed, falling back to block replay");
291                }
292            }
293        }
294
295        // Fall back to replaying previous transactions if prestate trace wasn't applied.
296        if !self.quick && !prestate_applied {
297            sh_status!("Executing previous transactions from the block.")?;
298
299            if let Some(block) = block {
300                let pb = init_progress(block.transactions().len() as u64, "tx");
301                pb.set_position(0);
302
303                let BlockTransactions::Full(ref txs) = *block.transactions() else {
304                    return Err(eyre::eyre!("Could not get block txs"));
305                };
306
307                for (index, tx) in txs.iter().enumerate() {
308                    // Replay system transactions only if running with `sys` option.
309                    // System transactions such as on L2s don't contain any pricing info so it
310                    // could cause reverts.
311                    if !self.replay_system_txes
312                        && (is_known_system_sender(tx.from())
313                            || tx.transaction_type() == Some(SYSTEM_TRANSACTION_TYPE))
314                    {
315                        pb.set_position((index + 1) as u64);
316                        continue;
317                    }
318                    if tx.tx_hash() == tx_hash {
319                        break;
320                    }
321
322                    let tx_env = TxEnvFor::<FEN>::from_recovered_tx(tx.as_ref(), tx.from());
323
324                    evm_env.cfg_env.disable_balance_check = true;
325
326                    if let Some(to) = Transaction::to(tx) {
327                        trace!(tx=?tx.tx_hash(),?to, "executing previous call transaction");
328                        executor.transact_with_env(evm_env.clone(), tx_env.clone()).wrap_err_with(
329                            || {
330                                format!(
331                                    "Failed to execute transaction: {:?} in block {}",
332                                    tx.tx_hash(),
333                                    evm_env.block_env.number()
334                                )
335                            },
336                        )?;
337                    } else {
338                        trace!(tx=?tx.tx_hash(), "executing previous create transaction");
339                        if let Err(error) =
340                            executor.deploy_with_env(evm_env.clone(), tx_env.clone(), None)
341                        {
342                            match error {
343                                // Reverted transactions should be skipped
344                                EvmError::Execution(_) => (),
345                                error => {
346                                    return Err(error).wrap_err_with(|| {
347                                        format!(
348                                            "Failed to deploy transaction: {:?} in block {}",
349                                            tx.tx_hash(),
350                                            evm_env.block_env.number()
351                                        )
352                                    });
353                                }
354                            }
355                        }
356                    }
357
358                    pb.set_position((index + 1) as u64);
359                }
360            }
361        }
362
363        // Execute our transaction
364        let result = {
365            executor.set_trace_printer(self.trace_printer);
366
367            let tx_env = TxEnvFor::<FEN>::from_recovered_tx(tx.as_ref(), tx.from());
368
369            if tx.as_ref().recover_signer().is_ok_and(|signer| signer != tx.from()) {
370                evm_env.cfg_env.disable_balance_check = true;
371            }
372
373            if let Some(to) = Transaction::to(&tx) {
374                trace!(tx=?tx.tx_hash(), to=?to, "executing call transaction");
375                TraceResult::from(executor.transact_with_env(evm_env, tx_env)?)
376            } else {
377                trace!(tx=?tx.tx_hash(), "executing create transaction");
378                TraceResult::try_from(executor.deploy_with_env(evm_env, tx_env, None))?
379            }
380        };
381
382        let contracts_bytecode = fetch_contracts_bytecode_from_trace(&executor, &result)?;
383        handle_traces(
384            result,
385            &config,
386            chain,
387            &contracts_bytecode,
388            label,
389            with_local_artifacts,
390            debug,
391            decode_internal,
392            disable_labels,
393            self.trace_depth,
394            resolved_tempo_hardfork,
395        )
396        .await?;
397
398        Ok(())
399    }
400}
401
402pub fn fetch_contracts_bytecode_from_trace<FEN: FoundryEvmNetwork>(
403    executor: &Executor<FEN>,
404    result: &TraceResult,
405) -> Result<AddressHashMap<Bytes>> {
406    let mut contracts_bytecode = AddressHashMap::default();
407    if let Some(ref traces) = result.traces {
408        contracts_bytecode.extend(gather_trace_addresses(traces).filter_map(|addr| {
409            // All relevant bytecodes should already be cached in the executor.
410            let code = executor
411                .backend()
412                .basic_ref(addr)
413                .inspect_err(|e| _ = sh_warn!("Failed to fetch code for {addr}: {e}"))
414                .ok()??
415                .code?
416                .bytes();
417            if code.is_empty() {
418                return None;
419            }
420            Some((addr, code))
421        }));
422    }
423    Ok(contracts_bytecode)
424}
425
426fn gather_trace_addresses(traces: &Traces) -> impl Iterator<Item = Address> {
427    let mut addresses = AddressSet::default();
428    for (_, trace) in traces {
429        for node in trace.arena.nodes() {
430            if !node.trace.address.is_zero() {
431                addresses.insert(node.trace.address);
432            }
433            if !node.trace.caller.is_zero() {
434                addresses.insert(node.trace.caller);
435            }
436        }
437    }
438    addresses.into_iter()
439}
440
441impl figment::Provider for RunArgs {
442    fn metadata(&self) -> Metadata {
443        Metadata::named("RunArgs")
444    }
445
446    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
447        let mut map = Map::new();
448
449        if let Some(api_key) = &self.etherscan.key {
450            map.insert("etherscan_api_key".into(), api_key.as_str().into());
451        }
452
453        if let Some(evm_version) = self.evm_version {
454            map.insert("evm_version".into(), figment::value::Value::serialize(evm_version)?);
455        }
456
457        Ok(Map::from([(Config::selected_profile(), map)]))
458    }
459}