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