forge_script/
simulate.rs

1use super::{
2    multi_sequence::MultiChainSequence, providers::ProvidersManager, runner::ScriptRunner,
3    sequence::ScriptSequenceKind, transaction::ScriptTransactionBuilder,
4};
5use crate::{
6    broadcast::{estimate_gas, BundledState},
7    build::LinkedBuildData,
8    execute::{ExecutionArtifacts, ExecutionData},
9    sequence::get_commit_hash,
10    ScriptArgs, ScriptConfig, ScriptResult,
11};
12use alloy_chains::NamedChain;
13use alloy_network::TransactionBuilder;
14use alloy_primitives::{map::HashMap, utils::format_units, Address, Bytes, TxKind, U256};
15use dialoguer::Confirm;
16use eyre::{Context, Result};
17use forge_script_sequence::{ScriptSequence, TransactionWithMetadata};
18use foundry_cheatcodes::Wallets;
19use foundry_cli::utils::{has_different_gas_calc, now};
20use foundry_common::{shell, ContractData};
21use foundry_evm::traces::{decode_trace_arena, render_trace_arena};
22use futures::future::{join_all, try_join_all};
23use parking_lot::RwLock;
24use std::{
25    collections::{BTreeMap, VecDeque},
26    sync::Arc,
27};
28
29/// Same as [ExecutedState](crate::execute::ExecutedState), but also contains [ExecutionArtifacts]
30/// which are obtained from [ScriptResult].
31///
32/// Can be either converted directly to [BundledState] or driven to it through
33/// [FilledTransactionsState].
34pub struct PreSimulationState {
35    pub args: ScriptArgs,
36    pub script_config: ScriptConfig,
37    pub script_wallets: Wallets,
38    pub build_data: LinkedBuildData,
39    pub execution_data: ExecutionData,
40    pub execution_result: ScriptResult,
41    pub execution_artifacts: ExecutionArtifacts,
42}
43
44impl PreSimulationState {
45    /// If simulation is enabled, simulates transactions against fork and fills gas estimation and
46    /// metadata. Otherwise, metadata (e.g. additional contracts, created contract names) is
47    /// left empty.
48    ///
49    /// Both modes will panic if any of the transactions have None for the `rpc` field.
50    pub async fn fill_metadata(self) -> Result<FilledTransactionsState> {
51        let address_to_abi = self.build_address_to_abi_map();
52
53        let mut transactions = self
54            .execution_result
55            .transactions
56            .clone()
57            .unwrap_or_default()
58            .into_iter()
59            .map(|tx| {
60                let rpc = tx.rpc.expect("missing broadcastable tx rpc url");
61                let sender = tx.transaction.from().expect("all transactions should have a sender");
62                let nonce = tx.transaction.nonce().expect("all transactions should have a sender");
63                let to = tx.transaction.to();
64
65                let mut builder = ScriptTransactionBuilder::new(tx.transaction, rpc);
66
67                if let Some(TxKind::Call(_)) = to {
68                    builder.set_call(
69                        &address_to_abi,
70                        &self.execution_artifacts.decoder,
71                        self.script_config.evm_opts.create2_deployer,
72                    )?;
73                } else {
74                    builder.set_create(false, sender.create(nonce), &address_to_abi)?;
75                }
76
77                Ok(builder.build())
78            })
79            .collect::<Result<VecDeque<_>>>()?;
80
81        if self.args.skip_simulation {
82            sh_println!("\nSKIPPING ON CHAIN SIMULATION.")?;
83        } else {
84            transactions = self.simulate_and_fill(transactions).await?;
85        }
86
87        Ok(FilledTransactionsState {
88            args: self.args,
89            script_config: self.script_config,
90            script_wallets: self.script_wallets,
91            build_data: self.build_data,
92            execution_artifacts: self.execution_artifacts,
93            transactions,
94        })
95    }
96
97    /// Builds separate runners and environments for each RPC used in script and executes all
98    /// transactions in those environments.
99    ///
100    /// Collects gas usage and metadata for each transaction.
101    pub async fn simulate_and_fill(
102        &self,
103        transactions: VecDeque<TransactionWithMetadata>,
104    ) -> Result<VecDeque<TransactionWithMetadata>> {
105        trace!(target: "script", "executing onchain simulation");
106
107        let runners = Arc::new(
108            self.build_runners()
109                .await?
110                .into_iter()
111                .map(|(rpc, runner)| (rpc, Arc::new(RwLock::new(runner))))
112                .collect::<HashMap<_, _>>(),
113        );
114
115        let mut final_txs = VecDeque::new();
116
117        // Executes all transactions from the different forks concurrently.
118        let futs = transactions
119            .into_iter()
120            .map(|mut transaction| async {
121                let mut runner = runners.get(&transaction.rpc).expect("invalid rpc url").write();
122                let tx = transaction.tx_mut();
123
124                let to = if let Some(TxKind::Call(to)) = tx.to() { Some(to) } else { None };
125                let result = runner
126                    .simulate(
127                        tx.from()
128                            .expect("transaction doesn't have a `from` address at execution time"),
129                        to,
130                        tx.input().map(Bytes::copy_from_slice),
131                        tx.value(),
132                        tx.authorization_list(),
133                    )
134                    .wrap_err("Internal EVM error during simulation")?;
135
136                if !result.success {
137                    return Ok((None, false, result.traces));
138                }
139
140                // Simulate mining the transaction if the user passes `--slow`.
141                if self.args.slow {
142                    runner.executor.env_mut().block.number += U256::from(1);
143                }
144
145                let is_noop_tx = if let Some(to) = to {
146                    runner.executor.is_empty_code(to)? && tx.value().unwrap_or_default().is_zero()
147                } else {
148                    false
149                };
150
151                let transaction = ScriptTransactionBuilder::from(transaction)
152                    .with_execution_result(&result, self.args.gas_estimate_multiplier)
153                    .build();
154
155                eyre::Ok((Some(transaction), is_noop_tx, result.traces))
156            })
157            .collect::<Vec<_>>();
158
159        if !shell::is_json() && self.script_config.evm_opts.verbosity > 3 {
160            sh_println!("==========================")?;
161            sh_println!("Simulated On-chain Traces:\n")?;
162        }
163
164        let mut abort = false;
165        for res in join_all(futs).await {
166            let (tx, is_noop_tx, mut traces) = res?;
167
168            // Transaction will be `None`, if execution didn't pass.
169            if tx.is_none() || self.script_config.evm_opts.verbosity > 3 {
170                for (_, trace) in &mut traces {
171                    decode_trace_arena(trace, &self.execution_artifacts.decoder).await?;
172                    sh_println!("{}", render_trace_arena(trace))?;
173                }
174            }
175
176            if let Some(tx) = tx {
177                if is_noop_tx {
178                    let to = tx.contract_address.unwrap();
179                    sh_warn!(
180                        "Script contains a transaction to {to} which does not contain any code."
181                    )?;
182
183                    // Only prompt if we're broadcasting and we've not disabled interactivity.
184                    if self.args.should_broadcast() &&
185                        !self.args.non_interactive &&
186                        !Confirm::new()
187                            .with_prompt("Do you wish to continue?".to_string())
188                            .interact()?
189                    {
190                        eyre::bail!("User canceled the script.");
191                    }
192                }
193
194                final_txs.push_back(tx);
195            } else {
196                abort = true;
197            }
198        }
199
200        if abort {
201            eyre::bail!("Simulated execution failed.")
202        }
203
204        Ok(final_txs)
205    }
206
207    /// Build mapping from contract address to its ABI, code and contract name.
208    fn build_address_to_abi_map(&self) -> BTreeMap<Address, &ContractData> {
209        self.execution_artifacts
210            .decoder
211            .contracts
212            .iter()
213            .filter_map(move |(addr, contract_id)| {
214                if let Ok(Some((_, data))) =
215                    self.build_data.known_contracts.find_by_name_or_identifier(contract_id)
216                {
217                    return Some((*addr, data));
218                }
219                None
220            })
221            .collect()
222    }
223
224    /// Build [ScriptRunner] forking given RPC for each RPC used in the script.
225    async fn build_runners(&self) -> Result<Vec<(String, ScriptRunner)>> {
226        let rpcs = self.execution_artifacts.rpc_data.total_rpcs.clone();
227
228        if !shell::is_json() {
229            let n = rpcs.len();
230            let s = if n != 1 { "s" } else { "" };
231            sh_println!("\n## Setting up {n} EVM{s}.")?;
232        }
233
234        let futs = rpcs.into_iter().map(|rpc| async move {
235            let mut script_config = self.script_config.clone();
236            script_config.evm_opts.fork_url = Some(rpc.clone());
237            let runner = script_config.get_runner().await?;
238            Ok((rpc.clone(), runner))
239        });
240        try_join_all(futs).await
241    }
242}
243
244/// At this point we have converted transactions collected during script execution to
245/// [TransactionWithMetadata] objects which contain additional metadata needed for broadcasting and
246/// verification.
247pub struct FilledTransactionsState {
248    pub args: ScriptArgs,
249    pub script_config: ScriptConfig,
250    pub script_wallets: Wallets,
251    pub build_data: LinkedBuildData,
252    pub execution_artifacts: ExecutionArtifacts,
253    pub transactions: VecDeque<TransactionWithMetadata>,
254}
255
256impl FilledTransactionsState {
257    /// Bundles all transactions of the [`TransactionWithMetadata`] type in a list of
258    /// [`ScriptSequence`]. List length will be higher than 1, if we're dealing with a multi
259    /// chain deployment.
260    ///
261    /// Each transaction will be added with the correct transaction type and gas estimation.
262    pub async fn bundle(self) -> Result<BundledState> {
263        let is_multi_deployment = self.execution_artifacts.rpc_data.total_rpcs.len() > 1;
264
265        if is_multi_deployment && !self.build_data.libraries.is_empty() {
266            eyre::bail!("Multi-chain deployment is not supported with libraries.");
267        }
268
269        let mut total_gas_per_rpc: HashMap<String, u128> = HashMap::default();
270
271        // Batches sequence of transactions from different rpcs.
272        let mut new_sequence = VecDeque::new();
273        let mut manager = ProvidersManager::default();
274        let mut sequences = vec![];
275
276        // Peeking is used to check if the next rpc url is different. If so, it creates a
277        // [`ScriptSequence`] from all the collected transactions up to this point.
278        let mut txes_iter = self.transactions.clone().into_iter().peekable();
279
280        while let Some(mut tx) = txes_iter.next() {
281            let tx_rpc = tx.rpc.to_owned();
282            let provider_info = manager.get_or_init_provider(&tx.rpc, self.args.legacy).await?;
283
284            if let Some(tx) = tx.tx_mut().as_unsigned_mut() {
285                // Handles chain specific requirements for unsigned transactions.
286                tx.set_chain_id(provider_info.chain);
287            }
288
289            if !self.args.skip_simulation {
290                let tx = tx.tx_mut();
291
292                if has_different_gas_calc(provider_info.chain) {
293                    // only estimate gas for unsigned transactions
294                    if let Some(tx) = tx.as_unsigned_mut() {
295                        trace!("estimating with different gas calculation");
296                        let gas = tx.gas.expect("gas is set by simulation.");
297
298                        // We are trying to show the user an estimation of the total gas usage.
299                        //
300                        // However, some transactions might depend on previous ones. For
301                        // example, tx1 might deploy a contract that tx2 uses. That
302                        // will result in the following `estimate_gas` call to fail,
303                        // since tx1 hasn't been broadcasted yet.
304                        //
305                        // Not exiting here will not be a problem when actually broadcasting,
306                        // because for chains where `has_different_gas_calc`
307                        // returns true, we await each transaction before
308                        // broadcasting the next one.
309                        if let Err(err) = estimate_gas(
310                            tx,
311                            &provider_info.provider,
312                            self.args.gas_estimate_multiplier,
313                        )
314                        .await
315                        {
316                            trace!("gas estimation failed: {err}");
317
318                            // Restore gas value, since `estimate_gas` will remove it.
319                            tx.set_gas_limit(gas);
320                        }
321                    }
322                }
323
324                let total_gas = total_gas_per_rpc.entry(tx_rpc.clone()).or_insert(0);
325                *total_gas += tx.gas().expect("gas is set");
326            }
327
328            new_sequence.push_back(tx);
329            // We only create a [`ScriptSequence`] object when we collect all the rpc related
330            // transactions.
331            if let Some(next_tx) = txes_iter.peek() {
332                if next_tx.rpc == tx_rpc {
333                    continue;
334                }
335            }
336
337            let sequence =
338                self.create_sequence(is_multi_deployment, provider_info.chain, new_sequence)?;
339
340            sequences.push(sequence);
341
342            new_sequence = VecDeque::new();
343        }
344
345        if !self.args.skip_simulation {
346            // Present gas information on a per RPC basis.
347            for (rpc, total_gas) in total_gas_per_rpc {
348                let provider_info = manager.get(&rpc).expect("provider is set.");
349
350                // Get the native token symbol for the chain using NamedChain
351                let token_symbol = NamedChain::try_from(provider_info.chain)
352                    .unwrap_or_default()
353                    .native_currency_symbol()
354                    .unwrap_or("ETH");
355
356                // We don't store it in the transactions, since we want the most updated value.
357                // Right before broadcasting.
358                let per_gas = if let Some(gas_price) = self.args.with_gas_price {
359                    gas_price.to()
360                } else {
361                    provider_info.gas_price()?
362                };
363
364                let estimated_gas_price_raw = format_units(per_gas, 9)
365                    .unwrap_or_else(|_| "[Could not calculate]".to_string());
366                let estimated_gas_price =
367                    estimated_gas_price_raw.trim_end_matches('0').trim_end_matches('.');
368
369                let estimated_amount_raw = format_units(total_gas.saturating_mul(per_gas), 18)
370                    .unwrap_or_else(|_| "[Could not calculate]".to_string());
371                let estimated_amount = estimated_amount_raw.trim_end_matches('0');
372
373                if !shell::is_json() {
374                    sh_println!("\n==========================")?;
375                    sh_println!("\nChain {}", provider_info.chain)?;
376
377                    sh_println!("\nEstimated gas price: {} gwei", estimated_gas_price)?;
378                    sh_println!("\nEstimated total gas used for script: {total_gas}")?;
379                    sh_println!("\nEstimated amount required: {estimated_amount} {token_symbol}")?;
380                    sh_println!("\n==========================")?;
381                } else {
382                    sh_println!(
383                        "{}",
384                        serde_json::json!({
385                            "chain": provider_info.chain,
386                            "estimated_gas_price": estimated_gas_price,
387                            "estimated_total_gas_used": total_gas,
388                            "estimated_amount_required": estimated_amount,
389                            "token_symbol": token_symbol,
390                        })
391                    )?;
392                }
393            }
394        }
395
396        let sequence = if sequences.len() == 1 {
397            ScriptSequenceKind::Single(sequences.pop().expect("empty sequences"))
398        } else {
399            ScriptSequenceKind::Multi(MultiChainSequence::new(
400                sequences,
401                &self.args.sig,
402                &self.build_data.build_data.target,
403                &self.script_config.config,
404                !self.args.broadcast,
405            )?)
406        };
407
408        Ok(BundledState {
409            args: self.args,
410            script_config: self.script_config,
411            script_wallets: self.script_wallets,
412            build_data: self.build_data,
413            sequence,
414        })
415    }
416
417    /// Creates a [ScriptSequence] object from the given transactions.
418    fn create_sequence(
419        &self,
420        multi: bool,
421        chain: u64,
422        transactions: VecDeque<TransactionWithMetadata>,
423    ) -> Result<ScriptSequence> {
424        // Paths are set to None for multi-chain sequences parts, because they don't need to be
425        // saved to a separate file.
426        let paths = if multi {
427            None
428        } else {
429            Some(ScriptSequence::get_paths(
430                &self.script_config.config,
431                &self.args.sig,
432                &self.build_data.build_data.target,
433                chain,
434                !self.args.broadcast,
435            )?)
436        };
437
438        let commit = get_commit_hash(&self.script_config.config.root);
439
440        let libraries = self
441            .build_data
442            .libraries
443            .libs
444            .iter()
445            .flat_map(|(file, libs)| {
446                libs.iter()
447                    .map(|(name, address)| format!("{}:{name}:{address}", file.to_string_lossy()))
448            })
449            .collect();
450
451        let sequence = ScriptSequence {
452            transactions,
453            returns: self.execution_artifacts.returns.clone(),
454            receipts: vec![],
455            pending: vec![],
456            paths,
457            timestamp: now().as_secs(),
458            libraries,
459            chain,
460            commit,
461        };
462        Ok(sequence)
463    }
464}