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