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