cast/cmd/
run.rs

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