Skip to main content

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