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
29pub 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 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 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 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 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 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 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 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 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
244pub 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 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 let mut new_sequence = VecDeque::new();
273 let mut manager = ProvidersManager::default();
274 let mut sequences = vec![];
275
276 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 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 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 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 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 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 for (rpc, total_gas) in total_gas_per_rpc {
348 let provider_info = manager.get(&rpc).expect("provider is set.");
349
350 let token_symbol = NamedChain::try_from(provider_info.chain)
352 .unwrap_or_default()
353 .native_currency_symbol()
354 .unwrap_or("ETH");
355
356 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 fn create_sequence(
419 &self,
420 multi: bool,
421 chain: u64,
422 transactions: VecDeque<TransactionWithMetadata>,
423 ) -> Result<ScriptSequence> {
424 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}