forge_script/
simulate.rs

1use super::{
2    multi_sequence::MultiChainSequence, providers::ProvidersManager, runner::ScriptRunner,
3    sequence::ScriptSequenceKind, transaction::ScriptTransactionBuilder,
4};
5use crate::{
6    ScriptArgs, ScriptConfig, ScriptResult,
7    broadcast::{BundledState, estimate_gas},
8    build::LinkedBuildData,
9    execute::{ExecutionArtifacts, ExecutionData},
10    sequence::get_commit_hash,
11};
12use alloy_chains::NamedChain;
13use alloy_network::TransactionBuilder;
14use alloy_primitives::{Address, TxKind, U256, map::HashMap, utils::format_units};
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::{ContractData, shell};
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().cloned(),
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().evm_env.block_env.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(
153                        &result,
154                        self.args.gas_estimate_multiplier,
155                        &self.build_data,
156                    )
157                    .build();
158
159                eyre::Ok((Some(transaction), is_noop_tx, result.traces))
160            })
161            .collect::<Vec<_>>();
162
163        if !shell::is_json() && self.script_config.evm_opts.verbosity > 3 {
164            sh_println!("==========================")?;
165            sh_println!("Simulated On-chain Traces:\n")?;
166        }
167
168        let mut abort = false;
169        for res in join_all(futs).await {
170            let (tx, is_noop_tx, mut traces) = res?;
171
172            // Transaction will be `None`, if execution didn't pass.
173            if tx.is_none() || self.script_config.evm_opts.verbosity > 3 {
174                for (_, trace) in &mut traces {
175                    decode_trace_arena(trace, &self.execution_artifacts.decoder).await;
176                    sh_println!("{}", render_trace_arena(trace))?;
177                }
178            }
179
180            if let Some(tx) = tx {
181                if is_noop_tx {
182                    let to = tx.contract_address.unwrap();
183                    sh_warn!(
184                        "Script contains a transaction to {to} which does not contain any code."
185                    )?;
186
187                    // Only prompt if we're broadcasting and we've not disabled interactivity.
188                    if self.args.should_broadcast()
189                        && !self.args.non_interactive
190                        && !Confirm::new()
191                            .with_prompt("Do you wish to continue?".to_string())
192                            .interact()?
193                    {
194                        eyre::bail!("User canceled the script.");
195                    }
196                }
197
198                final_txs.push_back(tx);
199            } else {
200                abort = true;
201            }
202        }
203
204        if abort {
205            eyre::bail!("Simulated execution failed.")
206        }
207
208        Ok(final_txs)
209    }
210
211    /// Build mapping from contract address to its ABI, code and contract name.
212    fn build_address_to_abi_map(&self) -> BTreeMap<Address, &ContractData> {
213        self.execution_artifacts
214            .decoder
215            .contracts
216            .iter()
217            .filter_map(move |(addr, contract_id)| {
218                if let Ok(Some((_, data))) =
219                    self.build_data.known_contracts.find_by_name_or_identifier(contract_id)
220                {
221                    return Some((*addr, data));
222                }
223                None
224            })
225            .collect()
226    }
227
228    /// Build [ScriptRunner] forking given RPC for each RPC used in the script.
229    async fn build_runners(&self) -> Result<Vec<(String, ScriptRunner)>> {
230        let rpcs = self.execution_artifacts.rpc_data.total_rpcs.clone();
231
232        if !shell::is_json() {
233            let n = rpcs.len();
234            let s = if n != 1 { "s" } else { "" };
235            sh_println!("\n## Setting up {n} EVM{s}.")?;
236        }
237
238        let futs = rpcs.into_iter().map(|rpc| async move {
239            let mut script_config = self.script_config.clone();
240            script_config.evm_opts.fork_url = Some(rpc.clone());
241            let runner = script_config.get_runner().await?;
242            Ok((rpc.clone(), runner))
243        });
244        try_join_all(futs).await
245    }
246}
247
248/// At this point we have converted transactions collected during script execution to
249/// [TransactionWithMetadata] objects which contain additional metadata needed for broadcasting and
250/// verification.
251pub struct FilledTransactionsState {
252    pub args: ScriptArgs,
253    pub script_config: ScriptConfig,
254    pub script_wallets: Wallets,
255    pub build_data: LinkedBuildData,
256    pub execution_artifacts: ExecutionArtifacts,
257    pub transactions: VecDeque<TransactionWithMetadata>,
258}
259
260impl FilledTransactionsState {
261    /// Bundles all transactions of the [`TransactionWithMetadata`] type in a list of
262    /// [`ScriptSequence`]. List length will be higher than 1, if we're dealing with a multi
263    /// chain deployment.
264    ///
265    /// Each transaction will be added with the correct transaction type and gas estimation.
266    pub async fn bundle(self) -> Result<BundledState> {
267        let is_multi_deployment = self.execution_artifacts.rpc_data.total_rpcs.len() > 1;
268
269        if is_multi_deployment && !self.build_data.libraries.is_empty() {
270            eyre::bail!("Multi-chain deployment is not supported with libraries.");
271        }
272
273        let mut total_gas_per_rpc: HashMap<String, u128> = HashMap::default();
274
275        // Batches sequence of transactions from different rpcs.
276        let mut new_sequence = VecDeque::new();
277        let mut manager = ProvidersManager::default();
278        let mut sequences = vec![];
279
280        // Peeking is used to check if the next rpc url is different. If so, it creates a
281        // [`ScriptSequence`] from all the collected transactions up to this point.
282        let mut txes_iter = self.transactions.clone().into_iter().peekable();
283
284        while let Some(mut tx) = txes_iter.next() {
285            let tx_rpc = tx.rpc.to_owned();
286            let provider_info = manager.get_or_init_provider(&tx.rpc, self.args.legacy).await?;
287
288            if let Some(tx) = tx.tx_mut().as_unsigned_mut() {
289                // Handles chain specific requirements for unsigned transactions.
290                tx.set_chain_id(provider_info.chain);
291            }
292
293            if !self.args.skip_simulation {
294                let tx = tx.tx_mut();
295
296                if has_different_gas_calc(provider_info.chain) {
297                    // only estimate gas for unsigned transactions
298                    if let Some(tx) = tx.as_unsigned_mut() {
299                        trace!("estimating with different gas calculation");
300                        let gas = tx.gas.expect("gas is set by simulation.");
301
302                        // We are trying to show the user an estimation of the total gas usage.
303                        //
304                        // However, some transactions might depend on previous ones. For
305                        // example, tx1 might deploy a contract that tx2 uses. That
306                        // will result in the following `estimate_gas` call to fail,
307                        // since tx1 hasn't been broadcasted yet.
308                        //
309                        // Not exiting here will not be a problem when actually broadcasting,
310                        // because for chains where `has_different_gas_calc`
311                        // returns true, we await each transaction before
312                        // broadcasting the next one.
313                        if let Err(err) = estimate_gas(
314                            tx,
315                            &provider_info.provider,
316                            self.args.gas_estimate_multiplier,
317                        )
318                        .await
319                        {
320                            trace!("gas estimation failed: {err}");
321
322                            // Restore gas value, since `estimate_gas` will remove it.
323                            tx.set_gas_limit(gas);
324                        }
325                    }
326                }
327
328                let total_gas = total_gas_per_rpc.entry(tx_rpc.clone()).or_insert(0);
329                *total_gas += tx.gas().expect("gas is set");
330            }
331
332            new_sequence.push_back(tx);
333            // We only create a [`ScriptSequence`] object when we collect all the rpc related
334            // transactions.
335            if let Some(next_tx) = txes_iter.peek()
336                && next_tx.rpc == tx_rpc
337            {
338                continue;
339            }
340
341            let sequence =
342                self.create_sequence(is_multi_deployment, provider_info.chain, new_sequence)?;
343
344            sequences.push(sequence);
345
346            new_sequence = VecDeque::new();
347        }
348
349        if !self.args.skip_simulation {
350            // Present gas information on a per RPC basis.
351            for (rpc, total_gas) in total_gas_per_rpc {
352                let provider_info = manager.get(&rpc).expect("provider is set.");
353
354                // Get the native token symbol for the chain using NamedChain
355                let token_symbol = NamedChain::try_from(provider_info.chain)
356                    .unwrap_or_default()
357                    .native_currency_symbol()
358                    .unwrap_or("ETH");
359
360                // We don't store it in the transactions, since we want the most updated value.
361                // Right before broadcasting.
362                let per_gas = if let Some(gas_price) = self.args.with_gas_price {
363                    gas_price.to()
364                } else {
365                    provider_info.gas_price()?
366                };
367
368                let estimated_gas_price_raw = format_units(per_gas, 9)
369                    .unwrap_or_else(|_| "[Could not calculate]".to_string());
370                let estimated_gas_price =
371                    estimated_gas_price_raw.trim_end_matches('0').trim_end_matches('.');
372
373                let estimated_amount_raw = format_units(total_gas.saturating_mul(per_gas), 18)
374                    .unwrap_or_else(|_| "[Could not calculate]".to_string());
375                let estimated_amount = estimated_amount_raw.trim_end_matches('0');
376
377                if !shell::is_json() {
378                    sh_println!("\n==========================")?;
379                    sh_println!("\nChain {}", provider_info.chain)?;
380
381                    sh_println!("\nEstimated gas price: {} gwei", estimated_gas_price)?;
382                    sh_println!("\nEstimated total gas used for script: {total_gas}")?;
383                    sh_println!("\nEstimated amount required: {estimated_amount} {token_symbol}")?;
384                    sh_println!("\n==========================")?;
385                } else {
386                    sh_println!(
387                        "{}",
388                        serde_json::json!({
389                            "chain": provider_info.chain,
390                            "estimated_gas_price": estimated_gas_price,
391                            "estimated_total_gas_used": total_gas,
392                            "estimated_amount_required": estimated_amount,
393                            "token_symbol": token_symbol,
394                        })
395                    )?;
396                }
397            }
398        }
399
400        let sequence = if sequences.len() == 1 {
401            ScriptSequenceKind::Single(sequences.pop().expect("empty sequences"))
402        } else {
403            ScriptSequenceKind::Multi(MultiChainSequence::new(
404                sequences,
405                &self.args.sig,
406                &self.build_data.build_data.target,
407                &self.script_config.config,
408                !self.args.broadcast,
409            )?)
410        };
411
412        Ok(BundledState {
413            args: self.args,
414            script_config: self.script_config,
415            script_wallets: self.script_wallets,
416            build_data: self.build_data,
417            sequence,
418        })
419    }
420
421    /// Creates a [ScriptSequence] object from the given transactions.
422    fn create_sequence(
423        &self,
424        multi: bool,
425        chain: u64,
426        transactions: VecDeque<TransactionWithMetadata>,
427    ) -> Result<ScriptSequence> {
428        // Paths are set to None for multi-chain sequences parts, because they don't need to be
429        // saved to a separate file.
430        let paths = if multi {
431            None
432        } else {
433            Some(ScriptSequence::get_paths(
434                &self.script_config.config,
435                &self.args.sig,
436                &self.build_data.build_data.target,
437                chain,
438                !self.args.broadcast,
439            )?)
440        };
441
442        let commit = get_commit_hash(&self.script_config.config.root);
443
444        let libraries = self
445            .build_data
446            .libraries
447            .libs
448            .iter()
449            .flat_map(|(file, libs)| {
450                libs.iter()
451                    .map(|(name, address)| format!("{}:{name}:{address}", file.to_string_lossy()))
452            })
453            .collect();
454
455        let sequence = ScriptSequence {
456            transactions,
457            returns: self.execution_artifacts.returns.clone(),
458            receipts: vec![],
459            pending: vec![],
460            paths,
461            timestamp: now().as_millis(),
462            libraries,
463            chain,
464            commit,
465        };
466        Ok(sequence)
467    }
468}