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