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