1#![recursion_limit = "256"]
6#![cfg_attr(not(test), warn(unused_crate_dependencies))]
7#![cfg_attr(docsrs, feature(doc_cfg))]
8
9#[macro_use]
10extern crate foundry_common;
11
12#[macro_use]
13extern crate tracing;
14
15use crate::{broadcast::BundledState, runner::ScriptRunner, simulate::PreSimulationState};
16use alloy_json_abi::{Function, JsonAbi};
17use alloy_network::Network;
18use alloy_primitives::{
19 Address, Bytes, Log, U256, hex,
20 map::{AddressHashMap, HashMap},
21};
22use alloy_signer::Signer;
23use broadcast::next_nonce_resolved;
24use build::PreprocessedState;
25use clap::{Parser, ValueHint, builder::RangedU64ValueParser};
26use dialoguer::Confirm;
27use eyre::{ContextCompat, Result};
28use forge_script_sequence::{AdditionalContract, NestedValue};
29use forge_verify::{RetryArgs, VerifierArgs};
30use foundry_cli::{
31 opts::{BuildOpts, EvmArgs, GlobalArgs, TempoOpts, TracingArgs},
32 utils::LoadConfig,
33};
34use foundry_common::{
35 ContractsByArtifact, SELECTOR_LEN,
36 abi::{encode_function_args, get_func},
37 compile::ContractSizeLimits,
38 shell,
39};
40use foundry_compilers::{ArtifactId, artifacts::output_selection::ContractOutputSelection};
41use foundry_config::{
42 Config, Eip1559FeeEstimatePreset, FoundryHardfork, figment,
43 figment::{
44 Metadata, Profile, Provider,
45 value::{Dict, Map},
46 },
47};
48use foundry_debugger::DebuggerLayout;
49use foundry_evm::{
50 backend::Backend,
51 core::{
52 Breakpoints, FoundryTransaction,
53 evm::{EthEvmNetwork, EvmEnvFor, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor},
54 fork::ResolvedFork,
55 },
56 executors::ExecutorBuilder,
57 inspectors::{
58 CheatsConfig,
59 cheatcodes::{BroadcastableTransactions, Wallets},
60 },
61 opts::{EvmOpts, ExecutionSpecContext, resolve_execution_spec},
62 revm::interpreter::InstructionResult,
63 traces::{InternalTraceMode, TraceRequirements, Traces},
64};
65use foundry_evm_networks::NetworkConfigs;
66use foundry_wallets::MultiWalletOpts;
67use serde::Serialize;
68use std::path::PathBuf;
69
70#[cfg(feature = "base")]
71use foundry_evm::core::evm::BaseEvmNetwork;
72
73#[cfg(feature = "monad")]
74use foundry_evm::core::evm::MonadEvmNetwork;
75
76#[cfg(feature = "optimism")]
77use foundry_evm::core::evm::OpEvmNetwork;
78
79mod broadcast;
80mod build;
81mod execute;
82mod library_deployments;
83mod multi_sequence;
84mod progress;
85mod providers;
86mod receipts;
87mod runner;
88mod sequence;
89mod session;
90mod simulate;
91mod transaction;
92mod verify;
93mod wallet_session;
94
95pub use wallet_session::ScriptWalletSessionArgs;
96
97foundry_config::merge_impl_figment_convert!(ScriptArgs, build, evm);
99
100const DEFAULT_CONFIRMATIONS: u64 = 1;
101
102enum PreparedScript<FEN: FoundryEvmNetwork> {
104 Resume(Box<BundledState<FEN>>),
105 Simulate(Box<PreSimulationState<FEN>>),
106}
107
108#[derive(Clone, Debug, Default, Parser)]
110pub struct ScriptArgs {
111 #[command(flatten)]
113 pub global: GlobalArgs,
114
115 #[arg(value_hint = ValueHint::FilePath)]
120 pub path: String,
121
122 pub args: Vec<String>,
124
125 #[arg(long, visible_alias = "tc", value_name = "CONTRACT_NAME")]
127 pub target_contract: Option<String>,
128
129 #[arg(long, short, default_value = "run")]
131 pub sig: String,
132
133 #[arg(
135 long,
136 env = "ETH_PRIORITY_GAS_PRICE",
137 value_parser = foundry_cli::utils::parse_ether_value,
138 value_name = "PRICE"
139 )]
140 pub priority_gas_price: Option<U256>,
141
142 #[arg(long)]
146 pub legacy: bool,
147
148 #[arg(long = "estimate", value_name = "PRESET")]
156 pub eip1559_fee_estimate: Option<Eip1559FeeEstimatePreset>,
157
158 #[arg(long)]
160 pub broadcast: bool,
161
162 #[arg(long)]
168 pub batch: bool,
169
170 #[command(flatten)]
172 pub tempo: TempoOpts,
173
174 #[command(flatten)]
176 pub wallet_session: ScriptWalletSessionArgs,
177
178 #[arg(long)]
180 pub skip_simulation: bool,
181
182 #[arg(long, short, default_value = "130")]
184 pub gas_estimate_multiplier: u64,
185
186 #[arg(
188 long,
189 value_name = "NONCE",
190 value_parser = RangedU64ValueParser::<u64>::new().range(..u64::MAX),
191 )]
192 pub sender_nonce: Option<u64>,
193
194 #[arg(
196 long,
197 conflicts_with_all = &["private_key", "private_keys", "ledger", "trezor", "aws", "browser"],
198 )]
199 pub unlocked: bool,
200
201 #[arg(long)]
208 pub resume: bool,
209
210 #[arg(long)]
212 pub multi: bool,
213
214 #[arg(long)]
218 pub debug: bool,
219
220 #[arg(long = "debug-layout", requires = "debug", value_enum)]
222 pub debug_layout: Option<DebuggerLayout>,
223
224 #[arg(
226 long,
227 requires = "debug",
228 value_hint = ValueHint::FilePath,
229 value_name = "PATH"
230 )]
231 pub dump: Option<PathBuf>,
232
233 #[arg(long)]
236 pub slow: bool,
237
238 #[arg(
240 long,
241 default_value_t = DEFAULT_CONFIRMATIONS,
242 value_parser = RangedU64ValueParser::<u64>::new().range(1..),
243 )]
244 pub confirmations: u64,
245
246 #[arg(long)]
250 pub non_interactive: bool,
251
252 #[arg(long)]
254 pub disable_code_size_limit: bool,
255
256 #[command(flatten)]
257 pub tracing: TracingArgs,
258
259 #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
261 pub etherscan_api_key: Option<String>,
262
263 #[arg(long, requires = "broadcast")]
265 pub verify: bool,
266
267 #[arg(long, requires = "verify", conflicts_with_all = ["skip_simulation", "offline"])]
269 pub verify_external: bool,
270
271 #[arg(
276 long,
277 env = "ETH_GAS_PRICE",
278 value_parser = foundry_cli::utils::parse_ether_value,
279 value_name = "PRICE",
280 )]
281 pub with_gas_price: Option<U256>,
282
283 #[arg(long, env = "ETH_TIMEOUT")]
285 pub timeout: Option<u64>,
286
287 #[command(flatten)]
288 pub build: BuildOpts,
289
290 #[command(flatten)]
291 pub wallets: MultiWalletOpts,
292
293 #[command(flatten)]
294 pub evm: EvmArgs,
295
296 #[command(flatten)]
297 pub verifier: VerifierArgs,
298
299 #[command(flatten)]
300 pub retry: RetryArgs,
301}
302
303impl ScriptArgs {
304 fn has_tempo_session(&self) -> Result<bool> {
305 Ok(self.tempo.session_id()?.is_some())
306 }
307
308 async fn resolved_evm_opts(
310 &self,
311 mut config: Config,
312 mut evm_opts: EvmOpts,
313 ) -> Result<(Config, EvmOpts)> {
314 if self.tempo.is_tempo() || self.has_tempo_session()? {
315 if evm_opts.networks.has_network_selection() {
316 let network = evm_opts.networks.execution_network();
317 eyre::ensure!(
318 network.is_tempo(),
319 "Tempo transaction options conflict with configured network `{}`",
320 evm_opts.networks.execution_profile_name()
321 );
322 }
323 evm_opts.networks = NetworkConfigs::with_tempo();
325 }
326 evm_opts.infer_network_from_fork().await?;
329 config.networks = evm_opts.networks;
330
331 Ok((config, evm_opts))
332 }
333
334 async fn preprocess<FEN: FoundryEvmNetwork>(
335 self,
336 mut config: Config,
337 mut evm_opts: EvmOpts,
338 executor_builder: ExecutorBuilder<FEN>,
339 ) -> Result<PreprocessedState<FEN>> {
340 let args = self;
341 let mut tempo = args.tempo.clone();
342
343 let session_sender = if args.resume {
344 None
345 } else {
346 tempo.session_sender_for_multi_wallet(&args.wallets, args.evm.sender)?
349 };
350
351 let script_wallets = Wallets::new(args.wallets.get_multi_wallet().await?, args.evm.sender);
352 let browser_wallet = args.wallets.browser_signer::<FEN::Network>().await?;
353
354 if let Some(sender) = session_sender {
355 evm_opts.sender = sender;
356 } else if let Some(sender) = args.maybe_load_private_key()? {
357 evm_opts.sender = sender;
358 } else if args.evm.sender.is_none() {
359 let addresses = script_wallets.addresses();
363 if addresses.len() == 1 {
364 evm_opts.sender = addresses[0];
365 } else if let Some(signer) = browser_wallet.as_ref().map(|b| b.address()) {
366 evm_opts.sender = signer
367 }
368 }
369
370 tempo.resolve_expires();
371 config.tracing = args.tracing.resolve(&config.tracing, evm_opts.verbosity);
372 if args.debug && !config.extra_output.contains(&ContractOutputSelection::StorageLayout) {
373 config.extra_output.push(ContractOutputSelection::StorageLayout);
374 }
375
376 let script_config = ScriptConfig::new(
377 config,
378 evm_opts,
379 executor_builder,
380 args.batch,
381 tempo,
382 args.sender_nonce,
383 )
384 .await?;
385 Ok(PreprocessedState { args, script_config, script_wallets, browser_wallet })
386 }
387
388 #[allow(clippy::large_stack_frames)]
390 pub async fn run_script(self) -> Result<()> {
391 trace!(target: "script", "executing script command");
392
393 if self.tempo.sponsor_url.is_some() {
394 eyre::bail!(
395 "--sponsor-url is not supported by forge script; use --tempo.sponsor with \
396 --tempo.sponsor-signer or --tempo.sponsor-sig"
397 );
398 }
399
400 if self.wallet_session.enabled {
401 return self.run_wallet_session_wrapper();
402 }
403
404 let (mut config, evm_opts) = self.load_config_and_evm_opts()?;
405 self.install_missing_dependencies(&mut config)?;
406 let (config, evm_opts) = self.resolved_evm_opts(config, evm_opts).await?;
407
408 let is_tempo = evm_opts.networks.is_tempo();
409
410 if self.batch && !is_tempo {
411 eyre::bail!("--batch mode is only supported on Tempo networks");
412 }
413
414 if self.unlocked && self.has_tempo_session()? {
415 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --unlocked");
416 }
417
418 if is_tempo {
421 let batch = self.batch;
422 return Box::pin(async move {
423 let bundled = match self
424 .prepare_bundled::<TempoEvmNetwork>(
425 config,
426 evm_opts,
427 ExecutorBuilder::<TempoEvmNetwork>::new(),
428 )
429 .await?
430 {
431 Some(bundled) => bundled,
432 None => return Ok(()),
433 };
434 let bundled = if batch { bundled } else { bundled.wait_for_pending().await? };
438 let broadcasted = if batch {
439 bundled.broadcast_batch().await?
440 } else {
441 bundled.broadcast().await?
442 };
443 if broadcasted.args.verify {
444 broadcasted.verify().await?;
445 }
446 Ok(())
447 })
448 .await;
449 }
450
451 #[cfg(feature = "base")]
452 if evm_opts.networks.is_base() {
453 return Box::pin(self.run_generic_script::<BaseEvmNetwork>(
454 config,
455 evm_opts,
456 ExecutorBuilder::<BaseEvmNetwork>::new(),
457 ))
458 .await;
459 }
460
461 #[cfg(feature = "monad")]
462 if evm_opts.networks.is_monad() {
463 return Box::pin(async move {
464 let Some(prepared) = self
465 .prepare_script(config, evm_opts, ExecutorBuilder::<MonadEvmNetwork>::new())
466 .await?
467 else {
468 return Ok(());
469 };
470 let bundled = match prepared {
471 PreparedScript::Resume(bundled) => *bundled,
472 PreparedScript::Simulate(state) => {
473 state.fill_monad_metadata().await?.bundle().await?
474 }
475 };
476 let Some(bundled) = Self::finish_bundle(bundled).await? else { return Ok(()) };
477 Self::broadcast_bundle(bundled).await
478 })
479 .await;
480 }
481
482 #[cfg(feature = "optimism")]
483 if evm_opts.networks.is_optimism() {
484 return Box::pin(self.run_generic_script::<OpEvmNetwork>(
485 config,
486 evm_opts,
487 ExecutorBuilder::<OpEvmNetwork>::new(),
488 ))
489 .await;
490 }
491
492 Box::pin(self.run_generic_script::<EthEvmNetwork>(
493 config,
494 evm_opts,
495 ExecutorBuilder::<EthEvmNetwork>::new(),
496 ))
497 .await
498 }
499
500 #[allow(clippy::large_stack_frames)]
504 async fn prepare_bundled<FEN: FoundryEvmNetwork>(
505 self,
506 config: Config,
507 evm_opts: EvmOpts,
508 executor_builder: ExecutorBuilder<FEN>,
509 ) -> Result<Option<BundledState<FEN>>> {
510 let Some(prepared) = self.prepare_script(config, evm_opts, executor_builder).await? else {
511 return Ok(None);
512 };
513 let bundled = match prepared {
514 PreparedScript::Resume(bundled) => *bundled,
515 PreparedScript::Simulate(state) => {
516 state.fill_ordinary_metadata().await?.bundle().await?
517 }
518 };
519 Self::finish_bundle(bundled).await
520 }
521
522 #[allow(clippy::large_stack_frames)]
524 async fn prepare_script<FEN: FoundryEvmNetwork>(
525 self,
526 config: Config,
527 evm_opts: EvmOpts,
528 executor_builder: ExecutorBuilder<FEN>,
529 ) -> Result<Option<PreparedScript<FEN>>> {
530 let state = self.preprocess::<FEN>(config, evm_opts, executor_builder).await?;
531 let create2_deployer = state.script_config.evm_opts.create2_deployer;
532 let compiled = state.compile()?;
533
534 let prepared = if compiled.args.resume {
537 PreparedScript::Resume(Box::new(compiled.resume().await?))
538 } else {
539 let pre_simulation = compiled
541 .link()
542 .await?
543 .prepare_execution()
544 .await?
545 .execute()
546 .await?
547 .prepare_simulation()
548 .await?
549 .optimize_library_deployments()
550 .await?;
551
552 if pre_simulation.args.debug {
553 return match pre_simulation.args.dump.clone() {
554 Some(path) => pre_simulation.dump_debugger(&path).map(|_| None),
555 None => pre_simulation.run_debugger().map(|_| None),
556 };
557 }
558
559 if shell::is_json() {
560 pre_simulation.show_json().await?;
561 } else {
562 pre_simulation.show_traces().await?;
563 }
564
565 if pre_simulation
568 .execution_result
569 .transactions
570 .as_ref()
571 .is_none_or(|txs| txs.is_empty())
572 {
573 if pre_simulation.args.broadcast {
574 sh_warn!("No transactions to broadcast.")?;
575 }
576
577 return Ok(None);
578 }
579
580 if pre_simulation.execution_artifacts.rpc_data.missing_rpc {
582 if !shell::is_json() {
583 sh_println!("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?;
584 }
585
586 return Ok(None);
587 }
588
589 let size_limits = pre_simulation.args.contract_size_limits::<FEN>(
590 &pre_simulation.script_config.config,
591 &pre_simulation.script_config.evm_opts,
592 );
593 pre_simulation.args.check_contract_sizes(
594 size_limits,
595 &pre_simulation.execution_result,
596 &pre_simulation.build_data.known_contracts,
597 create2_deployer,
598 )?;
599
600 PreparedScript::Simulate(Box::new(pre_simulation))
601 };
602 Ok(Some(prepared))
603 }
604
605 async fn finish_bundle<FEN: FoundryEvmNetwork>(
607 bundled: BundledState<FEN>,
608 ) -> Result<Option<BundledState<FEN>>> {
609 if !bundled.args.should_broadcast() {
611 if !shell::is_json() {
612 if shell::verbosity() >= 4 {
613 sh_println!("\n=== Transactions that will be broadcast ===\n")?;
614 bundled.sequence.show_transactions()?;
615 }
616
617 sh_println!(
618 "\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more."
619 )?;
620 }
621 return Ok(None);
622 }
623
624 if bundled.args.verify {
626 bundled.verify_preflight_check().await?;
627 }
628
629 Ok(Some(bundled))
630 }
631
632 async fn run_generic_script<FEN: FoundryEvmNetwork>(
633 self,
634 config: Config,
635 evm_opts: EvmOpts,
636 executor_builder: ExecutorBuilder<FEN>,
637 ) -> Result<()> {
638 let bundled = match self.prepare_bundled::<FEN>(config, evm_opts, executor_builder).await? {
639 Some(bundled) => bundled,
640 None => return Ok(()),
641 };
642
643 Self::broadcast_bundle(bundled).await
645 }
646
647 async fn broadcast_bundle<FEN: FoundryEvmNetwork>(bundled: BundledState<FEN>) -> Result<()> {
648 let broadcasted = bundled.wait_for_pending().await?.broadcast().await?;
649
650 if broadcasted.args.verify {
651 broadcasted.verify().await?;
652 }
653
654 Ok(())
655 }
656
657 fn maybe_load_private_key(&self) -> Result<Option<Address>> {
660 if let Some(turnkey_address) = self.wallets.turnkey_address() {
661 return Ok(Some(turnkey_address));
662 }
663
664 let maybe_sender = self
665 .wallets
666 .private_keys()?
667 .filter(|pks| pks.len() == 1)
668 .map(|pks| pks.first().unwrap().address());
669 Ok(maybe_sender)
670 }
671
672 fn get_method_and_calldata(&self, abi: &JsonAbi) -> Result<(Function, Bytes)> {
680 if let Ok(decoded) = hex::decode(&self.sig) {
681 let selector = &decoded[..SELECTOR_LEN];
682 let func =
683 abi.functions().find(|func| selector == &func.selector()[..]).ok_or_else(|| {
684 eyre::eyre!(
685 "Function selector `{}` not found in the ABI",
686 hex::encode(selector)
687 )
688 })?;
689 return Ok((func.clone(), decoded.into()));
690 }
691
692 let func = if self.sig.contains('(') {
693 let func = get_func(&self.sig)?;
694 abi.functions()
695 .find(|&abi_func| abi_func.selector() == func.selector())
696 .wrap_err(format!("Function `{}` is not implemented in your script.", self.sig))?
697 } else {
698 let matching_functions =
699 abi.functions().filter(|func| func.name == self.sig).collect::<Vec<_>>();
700 match matching_functions.len() {
701 0 => {
702 eyre::bail!("Function `{}` not found in the ABI", self.sig);
703 }
704 1 => matching_functions[0],
705 2.. => {
706 eyre::bail!(
707 "Multiple functions with the same name `{}` found in the ABI",
708 self.sig
709 );
710 }
711 }
712 };
713 let data = encode_function_args(func, &self.args)?;
714
715 Ok((func.clone(), data.into()))
716 }
717
718 fn check_contract_sizes<N: Network>(
724 &self,
725 size_limits: ContractSizeLimits,
726 result: &ScriptResult<N>,
727 known_contracts: &ContractsByArtifact,
728 create2_deployer: Address,
729 ) -> Result<()> {
730 if self.disable_code_size_limit {
732 return Ok(());
733 }
734
735 let mut bytecodes: Vec<(String, &[u8], &[u8])> = vec![];
737
738 for (artifact, contract) in known_contracts.iter() {
740 let Some(bytecode) = contract.bytecode() else { continue };
741 let Some(deployed_bytecode) = contract.deployed_bytecode() else { continue };
742 bytecodes.push((artifact.name.clone(), bytecode, deployed_bytecode));
743 }
744
745 let create_nodes = result.traces.iter().flat_map(|(_, traces)| {
747 traces.nodes().iter().filter(|node| node.trace.kind.is_any_create())
748 });
749 let mut unknown_c = 0usize;
750 for node in create_nodes {
751 let init_code = &node.trace.data;
752 let deployed_code = &node.trace.output;
753 if !bytecodes.iter().any(|(_, b, _)| *b == init_code.as_ref()) {
754 bytecodes.push((format!("Unknown{unknown_c}"), init_code, deployed_code));
755 unknown_c += 1;
756 }
757 }
758
759 let mut prompt_user = false;
760 let max_size = size_limits.runtime;
761
762 for (data, to) in result.transactions.iter().flat_map(|txes| {
763 txes.iter().filter_map(|tx| {
764 tx.transaction
765 .input()
766 .filter(|data| data.len() > max_size)
767 .map(|data| (data, tx.transaction.to()))
768 })
769 }) {
770 let mut offset = 0;
771
772 if let Some(to) = to {
774 if to == create2_deployer {
775 offset = 32;
777 } else {
778 continue;
779 }
780 }
781
782 if let Some((name, _, deployed_code)) =
784 bytecodes.iter().find(|(_, init_code, _)| *init_code == &data[offset..])
785 {
786 let deployment_size = deployed_code.len();
787
788 if deployment_size > max_size {
789 prompt_user = self.should_broadcast();
790 sh_err!(
791 "`{name}` is above the contract size limit ({deployment_size} > {max_size})."
792 )?;
793 }
794 }
795 }
796
797 if prompt_user
799 && !self.non_interactive
800 && !Confirm::new().with_prompt("Do you wish to continue?".to_string()).interact()?
801 {
802 eyre::bail!("User canceled the script.");
803 }
804
805 Ok(())
806 }
807
808 fn contract_size_limits<FEN: FoundryEvmNetwork>(
809 &self,
810 config: &Config,
811 evm_opts: &EvmOpts,
812 ) -> ContractSizeLimits {
813 self.evm
814 .env
815 .code_size_limit
816 .or(evm_opts.env.code_size_limit)
817 .or(config.code_size_limit)
818 .map(ContractSizeLimits::with_runtime_limit)
819 .or_else(|| {
820 evm_opts
821 .networks
822 .contract_size_limits()
823 .map(|limits| ContractSizeLimits::new(limits.runtime, limits.initcode))
824 })
825 .unwrap_or_else(|| {
826 let spec_id: SpecFor<FEN> = config.evm_spec_id();
827 ContractSizeLimits::for_spec_id(spec_id.into())
828 })
829 }
830
831 const fn should_broadcast(&self) -> bool {
833 self.broadcast || self.resume || self.verify
834 }
835}
836
837impl Provider for ScriptArgs {
838 fn metadata(&self) -> Metadata {
839 Metadata::named("Script Args Provider")
840 }
841
842 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
843 let mut dict = Dict::default();
844
845 if let Some(etherscan_api_key) =
846 self.etherscan_api_key.as_ref().filter(|s| !s.trim().is_empty())
847 {
848 dict.insert(
849 "etherscan_api_key".to_string(),
850 figment::value::Value::from(etherscan_api_key.clone()),
851 );
852 }
853
854 if let Some(timeout) = self.timeout {
855 dict.insert("transaction_timeout".to_string(), timeout.into());
856 }
857
858 if let Some(preset) = self.eip1559_fee_estimate {
859 dict.insert(
860 "eip1559_fee_estimate".to_string(),
861 figment::value::Value::from(preset.to_string()),
862 );
863 }
864
865 Ok(Map::from([(Config::selected_profile(), dict)]))
866 }
867}
868
869#[derive(Serialize, Clone)]
870#[serde(bound = "")]
871pub struct ScriptResult<N: Network> {
872 pub success: bool,
873 #[serde(rename = "raw_logs")]
874 pub logs: Vec<Log>,
875 pub traces: Traces,
876 pub gas_used: u64,
877 pub labeled_addresses: AddressHashMap<String>,
878 #[serde(skip)]
879 pub debug_bytecodes: AddressHashMap<Bytes>,
880 #[serde(skip)]
881 pub transactions: Option<BroadcastableTransactions<N>>,
882 pub returned: Bytes,
883 #[serde(skip)]
884 pub exit_reason: Option<InstructionResult>,
885 pub address: Option<Address>,
886 #[serde(skip)]
887 pub breakpoints: Breakpoints,
888}
889
890impl<N: Network> Default for ScriptResult<N> {
891 fn default() -> Self {
892 Self {
893 success: Default::default(),
894 logs: Default::default(),
895 traces: Default::default(),
896 gas_used: Default::default(),
897 labeled_addresses: Default::default(),
898 debug_bytecodes: Default::default(),
899 transactions: Default::default(),
900 returned: Default::default(),
901 exit_reason: Default::default(),
902 address: Default::default(),
903 breakpoints: Default::default(),
904 }
905 }
906}
907
908impl<N: Network> ScriptResult<N> {
909 pub fn get_created_contracts(
910 &self,
911 known_contracts: &ContractsByArtifact,
912 ) -> Vec<AdditionalContract> {
913 self.traces
914 .iter()
915 .flat_map(|(_, traces)| {
916 let nodes = traces.nodes();
917 nodes.iter().filter_map(|node| {
918 if !node.trace.kind.is_any_create() || !node.trace.success {
919 return None;
920 }
921
922 let mut creator_code_addresses = Vec::new();
923 let mut parent = node.parent;
924 while let Some(parent_idx) = parent {
925 let ancestor = &nodes[parent_idx];
926 if !ancestor.trace.success {
927 return None;
928 }
929 if !ancestor.trace.kind.is_any_create()
930 && !creator_code_addresses.contains(&ancestor.trace.address)
931 {
932 creator_code_addresses.push(ancestor.trace.address);
933 }
934 parent = ancestor.parent;
935 }
936
937 let init_code = node.trace.data.clone();
938 let contract_name = known_contracts
939 .find_by_creation_code(init_code.as_ref())
940 .map(|artifact| artifact.0.name.clone());
941 Some(AdditionalContract {
942 call_kind: node.trace.kind,
943 address: node.trace.address,
944 contract_name,
945 init_code,
946 creator_code_addresses,
947 })
948 })
949 })
950 .collect()
951 }
952}
953
954#[derive(Serialize)]
955#[serde(bound = "")]
956struct JsonResult<'a, N: Network> {
957 logs: Vec<String>,
958 returns: &'a HashMap<String, NestedValue>,
959 #[serde(flatten)]
960 result: &'a ScriptResult<N>,
961}
962
963#[derive(Clone, Debug)]
964pub struct ScriptConfig<FEN: FoundryEvmNetwork> {
965 pub config: Config,
966 pub evm_opts: EvmOpts,
967 pub executor_builder: ExecutorBuilder<FEN>,
969 pub hardfork: Option<FoundryHardfork>,
971 pub source_chain_id: Option<u64>,
973 pub sender_nonce: u64,
974 sender_nonce_override: Option<u64>,
975 resolved_fork: Option<ResolvedFork>,
976 backends: HashMap<ResolvedFork, Backend<FEN>>,
978 pub batch: bool,
980 pub tempo: TempoOpts,
982}
983
984async fn resolve_script_fork(
985 config: &mut Config,
986 evm_opts: &mut EvmOpts,
987 active_networks: Option<NetworkConfigs>,
988) -> Result<Option<ResolvedFork>> {
989 if evm_opts.fork_url.is_none() {
990 return Ok(None);
991 }
992 if evm_opts.fork_endpoint.is_none() {
993 evm_opts.infer_network_from_fork().await?;
994 }
995 if let Some(active_networks) = active_networks
996 && !active_networks.supports_fork_source(&evm_opts.networks)
997 {
998 eyre::bail!(
999 "fork network `{}` is incompatible with the active EVM",
1000 evm_opts.networks.execution_network()
1001 );
1002 }
1003 config.networks = evm_opts.networks;
1004 if let Some(identity) = evm_opts.fork_endpoint.clone() {
1005 let network_is_inferred = evm_opts.fork_network_is_inferred;
1006 evm_opts.expect_fork_endpoint(identity, network_is_inferred);
1007 }
1008 evm_opts.resolve_fork().await
1009}
1010
1011impl<FEN: FoundryEvmNetwork> ScriptConfig<FEN> {
1012 pub(crate) async fn new(
1013 mut config: Config,
1014 mut evm_opts: EvmOpts,
1015 executor_builder: ExecutorBuilder<FEN>,
1016 batch: bool,
1017 tempo: TempoOpts,
1018 sender_nonce_override: Option<u64>,
1019 ) -> Result<Self> {
1020 let resolved_fork = resolve_script_fork(&mut config, &mut evm_opts, None).await?;
1023 let sender_nonce = if let Some(sender_nonce) = sender_nonce_override {
1024 sender_nonce
1025 } else if evm_opts.fork_url.is_some() {
1026 let fork = resolved_fork.as_ref().context("fork must be resolved")?;
1027 next_nonce_resolved(evm_opts.sender, &evm_opts, fork).await?
1028 } else {
1029 1
1031 };
1032
1033 Ok(Self {
1034 config,
1035 evm_opts,
1036 executor_builder,
1037 hardfork: None,
1038 source_chain_id: None,
1039 sender_nonce,
1040 sender_nonce_override,
1041 resolved_fork,
1042 backends: HashMap::default(),
1043 batch,
1044 tempo,
1045 })
1046 }
1047
1048 pub async fn update_sender(&mut self, sender: Address) -> Result<()> {
1049 self.sender_nonce = if let Some(sender_nonce) = self.sender_nonce_override {
1050 sender_nonce
1051 } else if self.evm_opts.fork_url.is_some() {
1052 let fork = self.ensure_resolved_fork().await?.context("fork must be resolved")?;
1053 next_nonce_resolved(sender, &self.evm_opts, &fork).await?
1054 } else {
1055 1
1057 };
1058 self.evm_opts.sender = sender;
1059 Ok(())
1060 }
1061
1062 pub fn resolved_fork(&self) -> Result<Option<&ResolvedFork>> {
1064 match (&self.evm_opts.fork_url, &self.resolved_fork) {
1065 (None, None) => Ok(None),
1066 (None, Some(_)) => Err(eyre::eyre!("resolved fork exists without a configured fork")),
1067 (Some(_), Some(fork)) if self.evm_opts.resolved_fork_matches(fork) => Ok(Some(fork)),
1068 (Some(_), Some(_)) => {
1069 Err(eyre::eyre!("resolved fork does not match the configured source and selector"))
1070 }
1071 (Some(_), None) => Err(eyre::eyre!("fork must be resolved")),
1072 }
1073 }
1074
1075 fn set_fork_url(&mut self, fork_url: String) {
1076 self.evm_opts.set_fork_url(fork_url);
1077 }
1078
1079 async fn ensure_resolved_fork(&mut self) -> Result<Option<ResolvedFork>> {
1080 if self.evm_opts.fork_url.is_none() {
1081 if self.resolved_fork.take().is_some() {
1082 self.backends.clear();
1083 }
1084 return Ok(None);
1085 }
1086 if let Some(fork) = &self.resolved_fork
1087 && self.evm_opts.resolved_fork_matches(fork)
1088 {
1089 return Ok(Some(fork.clone()));
1090 }
1091 let active_networks = Some(self.config.networks);
1094 if let Some(fork) = &self.resolved_fork {
1095 self.evm_opts.invalidate_fork_endpoint_if_source_changed(fork);
1096 }
1097
1098 let fork =
1099 resolve_script_fork(&mut self.config, &mut self.evm_opts, active_networks).await?;
1100 self.resolved_fork = fork.clone();
1101 Ok(fork)
1102 }
1103
1104 pub(crate) async fn update_tempo_session_sender(
1105 &mut self,
1106 wallets: &MultiWalletOpts,
1107 expected_sender: Option<Address>,
1108 ) -> Result<()> {
1109 if let Some(sender) =
1110 self.tempo.session_sender_for_multi_wallet(wallets, expected_sender)?
1111 {
1112 self.update_sender(sender).await?;
1113 }
1114 Ok(())
1115 }
1116
1117 async fn get_runner_with_cheatcodes(
1118 &mut self,
1119 known_contracts: ContractsByArtifact,
1120 script_wallets: Wallets,
1121 debug: bool,
1122 target: ArtifactId,
1123 restricted: bool,
1124 ) -> Result<ScriptRunner<FEN>> {
1125 let mut runner = self
1126 ._get_runner(Some((known_contracts, script_wallets, target)), debug, restricted)
1127 .await?;
1128
1129 if self.evm_opts.networks.is_tempo() {
1132 runner.executor.evm_env_mut().cfg_env.disable_fee_charge = true;
1133 }
1134
1135 Ok(runner)
1136 }
1137
1138 async fn _get_runner(
1139 &mut self,
1140 cheats_data: Option<(ContractsByArtifact, Wallets, ArtifactId)>,
1141 debug: bool,
1142 restricted: bool,
1143 ) -> Result<ScriptRunner<FEN>> {
1144 trace!("preparing script runner");
1145 let (resolved, evm_env, mut tx_env) = self.resolve_execution_env().await?;
1146
1147 let db = if self.evm_opts.fork_url.is_some() {
1148 let resolved = resolved.context("fork must be resolved")?;
1149 if let Some(backend) = self.backends.get(&resolved) {
1150 backend.clone()
1151 } else {
1152 let fork = self.evm_opts.get_fork_resolved(
1153 &self.config,
1154 evm_env.cfg_env.chain_id,
1155 Some(&resolved),
1156 );
1157 let backend = Backend::spawn(fork)?;
1158 self.backends.insert(resolved, backend.clone());
1159 backend
1160 }
1161 } else {
1162 Backend::spawn(None)?
1166 };
1167
1168 let mut builder = self
1170 .executor_builder
1171 .clone()
1172 .inspectors(|stack| {
1173 stack
1174 .logs(self.config.live_logs)
1175 .trace_requirements(script_trace_requirements(&self.config, debug))
1176 .create2_deployer(self.evm_opts.create2_deployer)
1177 })
1178 .gas_limit(self.evm_opts.gas_limit())
1179 .legacy_assertions(self.config.legacy_assertions);
1180
1181 if let Some((known_contracts, script_wallets, target)) = cheats_data {
1182 builder = builder.inspectors(|stack| {
1183 let mut cheats_config = CheatsConfig::new(
1184 &self.config,
1185 self.evm_opts.clone(),
1186 Some(known_contracts),
1187 Some(target),
1188 self.batch,
1189 );
1190 if restricted {
1191 cheats_config.blocked_cheatcodes =
1192 library_deployments::rerun_unsafe_cheatcode_selectors();
1193 }
1194 stack
1195 .cheatcodes(cheats_config.into())
1196 .wallets(script_wallets)
1197 .enable_isolation(self.evm_opts.isolate)
1198 });
1199 }
1200
1201 tx_env.set_fee_token(self.tempo.fee_token);
1204
1205 let mut runner = ScriptRunner::new(
1206 builder.build(evm_env, tx_env, db, self.evm_opts.networks),
1207 self.evm_opts.clone(),
1208 )
1209 .with_debug_bytecodes(debug);
1210
1211 if self.sender_nonce_override.is_some() {
1212 runner.executor.set_nonce(self.evm_opts.sender, self.sender_nonce)?;
1213 }
1214
1215 Ok(runner)
1216 }
1217
1218 async fn resolve_execution_env(
1220 &mut self,
1221 ) -> Result<(Option<ResolvedFork>, EvmEnvFor<FEN>, TxEnvFor<FEN>)> {
1222 let resolved = self.ensure_resolved_fork().await?;
1223 let (mut evm_env, tx_env) =
1224 self.evm_opts.env_with_resolved_fork::<_, _, TxEnvFor<FEN>>(resolved.as_ref()).await?;
1225 let fork_context = resolved.as_ref().map(ResolvedFork::context);
1226 let fork_chain_id = fork_context.map(|context| context.source_chain_id);
1227 let fork_hardfork = fork_context.and_then(|context| context.hardfork);
1228 self.source_chain_id = fork_chain_id;
1229 self.hardfork = resolve_execution_spec(
1230 self.config.evm_version,
1231 self.config.hardfork,
1232 &mut evm_env,
1233 ExecutionSpecContext::local_or_fork(fork_chain_id, fork_hardfork),
1234 None,
1235 );
1236 Ok((resolved, evm_env, tx_env))
1237 }
1238}
1239
1240const fn script_trace_requirements(config: &Config, debug: bool) -> TraceRequirements {
1241 TraceRequirements::none()
1242 .with_calls(true)
1243 .with_debug(debug)
1244 .with_verbosity(config.tracing.verbosity)
1245 .with_decode_internal(if config.tracing.decode_internal {
1246 InternalTraceMode::Full
1247 } else {
1248 InternalTraceMode::None
1249 })
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use super::*;
1255 use alloy_chains::NamedChain;
1256 use alloy_eips::BlockId;
1257 use alloy_network::Ethereum;
1258 use alloy_primitives::{B256, address};
1259 use alloy_provider::Provider as _;
1260 use alloy_rpc_types::TransactionRequest;
1261 use alloy_signer::SignerSync as _;
1262 use anvil::{NodeConfig, spawn};
1263 use foundry_cli::opts::TEMPO_SESSION_ID_ENV;
1264 use foundry_common::tempo::{
1265 GeneratedSessionKey, SessionAuthorizationRequest, SessionEntry, TEMPO_HOME_ENV,
1266 upsert_session_entry,
1267 };
1268 use foundry_config::UnresolvedEnvVarError;
1269 use foundry_evm::{
1270 revm::context::Block as _,
1271 traces::{
1272 CallKind, CallTrace, CallTraceArena, CallTraceNode, SparsedTraceArena, TraceKind,
1273 },
1274 };
1275 use semver::Version;
1276 use std::{fs, num::NonZeroU64, sync::LazyLock};
1277 use tempfile::tempdir;
1278 use tokio::sync::{Mutex, MutexGuard};
1279
1280 const SESSION_PRIVATE_KEY: &str =
1281 "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
1282 const SESSION_ROOT_PRIVATE_KEY: &str =
1283 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
1284 const SESSION_ID_HEX: &str =
1285 "0x1111111111111111111111111111111111111111111111111111111111111111";
1286 const SESSION_ROOT_ADDRESS: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
1287 static TEMPO_HOME_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
1288
1289 fn assert_missing_orphaned_block(error: &eyre::Report) {
1290 let message = format!("{error:#}").to_ascii_lowercase();
1291 assert!(
1292 message.contains("failed to get block hash")
1293 || message.contains("not found")
1294 || message.contains("unknown block"),
1295 "unexpected orphaned block lookup error: {error:#}"
1296 );
1297 }
1298
1299 #[tokio::test(flavor = "multi_thread")]
1300 async fn script_fork_context_is_re_resolved_for_a_new_rpc() {
1301 let (api_a, handle_a) = spawn(NodeConfig::test()).await;
1302 let (api_b, handle_b) = spawn(NodeConfig::test()).await;
1303 api_a.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1304 api_b.anvil_mine(Some(U256::from(3)), None).await.unwrap();
1305
1306 let evm_opts = EvmOpts {
1307 fork_url: Some(handle_a.http_endpoint()),
1308 sender: handle_a.dev_accounts().next().unwrap(),
1309 ..Default::default()
1310 };
1311 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1312 Config::default(),
1313 evm_opts,
1314 ExecutorBuilder::<EthEvmNetwork>::new(),
1315 false,
1316 TempoOpts::default(),
1317 None,
1318 )
1319 .await
1320 .unwrap();
1321 let first = config.resolved_fork.clone().unwrap();
1322 assert_eq!(first.number(), 1);
1323 assert_eq!(config.evm_opts.fork_block_number, None);
1324 assert!(config.evm_opts.resolved_fork_matches(&first));
1325
1326 config._get_runner(None, false, false).await.unwrap();
1327
1328 config.set_fork_url(handle_b.http_endpoint());
1329 let second = config.ensure_resolved_fork().await.unwrap().unwrap();
1330
1331 assert_eq!(second.number(), 3);
1332 assert_eq!(config.evm_opts.fork_block_number, None);
1333 assert!(config.evm_opts.resolved_fork_matches(&second));
1334 assert_ne!(first, second);
1335 }
1336
1337 #[cfg(feature = "monad")]
1338 #[tokio::test(flavor = "multi_thread")]
1339 async fn script_explicit_network_is_preserved_for_a_new_rpc() {
1340 let (api_a, handle_a) = spawn(NodeConfig::test_monad()).await;
1341 let (api_b, handle_b) = spawn(NodeConfig::test_monad()).await;
1342 api_a.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1343 api_b.anvil_mine(Some(U256::from(3)), None).await.unwrap();
1344
1345 let evm_opts = EvmOpts {
1346 fork_url: Some(handle_a.http_endpoint()),
1347 sender: handle_a.dev_accounts().next().unwrap(),
1348 networks: NetworkConfigs::with_ethereum(),
1349 ..Default::default()
1350 };
1351 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1352 Config::default(),
1353 evm_opts,
1354 ExecutorBuilder::<EthEvmNetwork>::new(),
1355 false,
1356 TempoOpts::default(),
1357 None,
1358 )
1359 .await
1360 .unwrap();
1361 assert_eq!(config.evm_opts.networks, NetworkConfigs::with_ethereum());
1362
1363 config.set_fork_url(handle_b.http_endpoint());
1364 let second = config.ensure_resolved_fork().await.unwrap().unwrap();
1365
1366 assert_eq!(second.number(), 3);
1367 assert_eq!(second.context().network_profile, NetworkConfigs::with_monad());
1368 assert_eq!(config.evm_opts.networks, NetworkConfigs::with_ethereum());
1369 }
1370
1371 #[tokio::test(flavor = "multi_thread")]
1372 async fn script_explicit_fork_block_is_preserved_for_a_new_rpc() {
1373 let (api_a, handle_a) = spawn(NodeConfig::test()).await;
1374 let (api_b, handle_b) = spawn(NodeConfig::test()).await;
1375 api_a.anvil_mine(Some(U256::from(2)), None).await.unwrap();
1376 api_b.anvil_mine(Some(U256::from(4)), None).await.unwrap();
1377
1378 let evm_opts = EvmOpts {
1379 fork_url: Some(handle_a.http_endpoint()),
1380 fork_block_number: Some(1),
1381 sender: handle_a.dev_accounts().next().unwrap(),
1382 ..Default::default()
1383 };
1384 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1385 Config::default(),
1386 evm_opts,
1387 ExecutorBuilder::<EthEvmNetwork>::new(),
1388 false,
1389 TempoOpts::default(),
1390 None,
1391 )
1392 .await
1393 .unwrap();
1394 config._get_runner(None, false, false).await.unwrap();
1395
1396 config.set_fork_url(handle_b.http_endpoint());
1397 let context = config.ensure_resolved_fork().await.unwrap().unwrap();
1398
1399 assert_eq!(context.number(), 1);
1400 assert_eq!(config.evm_opts.fork_block_number, Some(1));
1401 assert!(config.evm_opts.resolved_fork_matches(&context));
1402 }
1403
1404 #[tokio::test(flavor = "multi_thread")]
1405 async fn script_explicit_fork_block_can_equal_previous_latest() {
1406 let (api_a, handle_a) = spawn(NodeConfig::test()).await;
1407 let (api_b, handle_b) = spawn(NodeConfig::test()).await;
1408 api_a.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1409 api_b.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1410
1411 let evm_opts = EvmOpts {
1412 fork_url: Some(handle_a.http_endpoint()),
1413 sender: handle_a.dev_accounts().next().unwrap(),
1414 ..Default::default()
1415 };
1416 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1417 Config::default(),
1418 evm_opts,
1419 ExecutorBuilder::<EthEvmNetwork>::new(),
1420 false,
1421 TempoOpts::default(),
1422 None,
1423 )
1424 .await
1425 .unwrap();
1426 let latest = config.resolved_fork.clone().unwrap();
1427 assert_eq!(config.evm_opts.fork_block_number, None);
1428 config._get_runner(None, false, false).await.unwrap();
1429 assert!(config.backends.contains_key(&latest));
1430
1431 config.set_fork_url(handle_b.http_endpoint());
1432 config.evm_opts.fork_block_number = Some(1);
1433 let explicit = config.ensure_resolved_fork().await.unwrap().unwrap();
1434
1435 assert_eq!(explicit.number(), 1);
1436 assert_eq!(config.evm_opts.fork_block_number, Some(1));
1437 assert!(config.evm_opts.resolved_fork_matches(&explicit));
1438 assert_ne!(latest, explicit);
1439 assert!(config.backends.contains_key(&latest));
1440 assert!(!config.backends.contains_key(&explicit));
1441
1442 config._get_runner(None, false, false).await.unwrap();
1443 assert!(config.backends.contains_key(&latest));
1444 assert!(config.backends.contains_key(&explicit));
1445 assert_eq!(config.backends.len(), 2);
1446 }
1447
1448 #[tokio::test(flavor = "multi_thread")]
1449 async fn script_fork_cache_distinguishes_same_height_contexts() {
1450 let (api_a, handle_a) = spawn(NodeConfig::test()).await;
1451 let (api_b, handle_b) = spawn(NodeConfig::test()).await;
1452 let state_address = address!("0000000000000000000000000000000000001337");
1453 let balance_a = U256::from(1);
1454 let balance_b = U256::from(2);
1455 api_a.anvil_set_balance(state_address, balance_a).await.unwrap();
1456 api_b.anvil_set_balance(state_address, balance_b).await.unwrap();
1457 let original_prevrandao = B256::with_last_byte(0x42);
1458 api_a.anvil_set_next_block_prevrandao(original_prevrandao).await.unwrap();
1459 api_a.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1460 api_b.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1461 let url_a = handle_a.http_endpoint();
1462 let url_b = handle_b.http_endpoint();
1463 let sender = handle_a.dev_accounts().next().unwrap();
1464
1465 let evm_opts = EvmOpts { fork_url: Some(url_a.clone()), sender, ..Default::default() };
1466 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1467 Config::default(),
1468 evm_opts,
1469 ExecutorBuilder::<EthEvmNetwork>::new(),
1470 false,
1471 TempoOpts::default(),
1472 None,
1473 )
1474 .await
1475 .unwrap();
1476 let runner_a = config._get_runner(None, false, false).await.unwrap();
1477 assert_eq!(runner_a.executor.get_balance(state_address).unwrap(), balance_a);
1478 let first_a = config.resolved_fork().unwrap().unwrap().clone();
1479 assert!(config.backends.contains_key(&first_a));
1480
1481 config.set_fork_url(url_b.clone());
1482 let runner_b = config._get_runner(None, false, false).await.unwrap();
1483 assert_eq!(runner_b.executor.get_balance(state_address).unwrap(), balance_b);
1484 let context_b = config.resolved_fork().unwrap().unwrap().clone();
1485 assert_eq!(context_b.number(), 1);
1486 assert!(config.backends.contains_key(&first_a));
1487 assert!(config.backends.contains_key(&context_b));
1488 assert_eq!(config.backends.len(), 2);
1489
1490 let provider_a = handle_a.http_provider();
1491 provider_a
1492 .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
1493 .await
1494 .unwrap();
1495 let replacement = provider_a.get_block_by_number(1.into()).await.unwrap().unwrap();
1496 assert_eq!(replacement.header.number, first_a.number());
1497 assert_ne!(replacement.header.hash, first_a.hash());
1498 assert_ne!(replacement.header.mix_hash, Some(original_prevrandao));
1499
1500 config.set_fork_url(url_a.clone());
1501 let second_a = config.ensure_resolved_fork().await.unwrap().unwrap();
1502 assert_eq!(second_a.number(), first_a.number());
1503 assert_eq!(second_a.hash(), replacement.header.hash);
1504 assert_ne!(second_a.hash(), first_a.hash());
1505 assert!(config.backends.contains_key(&first_a));
1506 assert!(config.backends.contains_key(&context_b));
1507 assert!(!config.backends.contains_key(&second_a));
1508 assert_eq!(config.backends.len(), 2);
1509
1510 let runner = config._get_runner(None, false, false).await.unwrap();
1511 assert!(config.backends.contains_key(&second_a));
1512 assert_eq!(config.backends.len(), 3);
1513 assert_eq!(runner.executor.get_balance(state_address).unwrap(), balance_a);
1514 assert_eq!(runner.executor.evm_env().block_env.prevrandao(), replacement.header.mix_hash);
1515 }
1516
1517 #[tokio::test(flavor = "multi_thread")]
1518 async fn script_same_endpoint_does_not_advance_resolved_fork() {
1519 let (api, handle) = spawn(NodeConfig::test()).await;
1520 api.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1521 let url = handle.http_endpoint();
1522 let evm_opts = EvmOpts {
1523 fork_url: Some(url.clone()),
1524 sender: handle.dev_accounts().next().unwrap(),
1525 ..Default::default()
1526 };
1527 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1528 Config::default(),
1529 evm_opts,
1530 ExecutorBuilder::<EthEvmNetwork>::new(),
1531 false,
1532 TempoOpts::default(),
1533 None,
1534 )
1535 .await
1536 .unwrap();
1537 let resolved = config.resolved_fork.clone().unwrap();
1538 config._get_runner(None, false, false).await.unwrap();
1539
1540 api.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1541 config.set_fork_url(url.clone());
1542 let runner = config._get_runner(None, false, false).await.unwrap();
1543
1544 assert_eq!(config.resolved_fork.as_ref(), Some(&resolved));
1545 assert!(config.backends.contains_key(&resolved));
1546 assert_eq!(config.backends.len(), 1);
1547 assert_eq!(runner.executor.evm_env().block_env.number(), U256::from(1));
1548 assert_eq!(config.evm_opts.fork_block_number, None);
1549 }
1550
1551 #[tokio::test(flavor = "multi_thread")]
1552 async fn script_fork_changed_headers_replace_cached_backend() {
1553 let (api, handle) = spawn(NodeConfig::test()).await;
1554 api.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1555 let url = handle.http_endpoint();
1556 let evm_opts = EvmOpts {
1557 fork_url: Some(url.clone()),
1558 sender: handle.dev_accounts().next().unwrap(),
1559 ..Default::default()
1560 };
1561 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1562 Config::default(),
1563 evm_opts,
1564 ExecutorBuilder::<EthEvmNetwork>::new(),
1565 false,
1566 TempoOpts::default(),
1567 None,
1568 )
1569 .await
1570 .unwrap();
1571 config._get_runner(None, false, false).await.unwrap();
1572 let without_headers = config.resolved_fork().unwrap().unwrap().clone();
1573 assert!(config.backends.contains_key(&without_headers));
1574
1575 config.evm_opts.fork_headers = Some(vec!["x-foundry-test: resolved-fork".to_string()]);
1576 let with_headers = config.ensure_resolved_fork().await.unwrap().unwrap();
1577 assert!(config.backends.contains_key(&without_headers));
1578 assert!(!config.backends.contains_key(&with_headers));
1579 assert_eq!(config.backends.len(), 1);
1580 config._get_runner(None, false, false).await.unwrap();
1581
1582 assert_ne!(without_headers, with_headers);
1583 assert!(config.evm_opts.resolved_fork_matches(&with_headers));
1584 assert!(config.backends.contains_key(&without_headers));
1585 assert!(config.backends.contains_key(&with_headers));
1586 assert_eq!(config.backends.len(), 2);
1587 }
1588
1589 async fn assert_same_url_fork_auth_change_re_resolves(
1590 configure_initial: impl FnOnce(&mut EvmOpts),
1591 configure_changed: impl FnOnce(&mut EvmOpts),
1592 ) {
1593 let (api, handle) = spawn(NodeConfig::test()).await;
1594 let mut evm_opts = EvmOpts {
1595 fork_url: Some(handle.http_endpoint()),
1596 fork_block_number: Some(0),
1597 networks: NetworkConfigs::with_ethereum(),
1598 ..Default::default()
1599 };
1600 evm_opts.env.chain_id = Some(42);
1601 configure_initial(&mut evm_opts);
1602
1603 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1604 Config::default(),
1605 evm_opts,
1606 ExecutorBuilder::<EthEvmNetwork>::new(),
1607 false,
1608 TempoOpts::default(),
1609 None,
1610 )
1611 .await
1612 .unwrap();
1613 config._get_runner(None, false, false).await.unwrap();
1614 let first = config.resolved_fork().unwrap().unwrap().clone();
1615 let first_instance = first.context().instance_id.unwrap();
1616 assert_eq!(config.backends.len(), 1);
1617
1618 api.anvil_reset(None).await.unwrap();
1619 assert_ne!(api.instance_id(), first_instance);
1620 configure_changed(&mut config.evm_opts);
1621
1622 let second = config.ensure_resolved_fork().await.unwrap().unwrap();
1623 assert_ne!(second.context().instance_id, Some(first_instance));
1624 assert_ne!(second, first);
1625 assert_eq!(config.evm_opts.fork_block_number, Some(0));
1626 assert_eq!(config.evm_opts.networks, NetworkConfigs::with_ethereum());
1627 assert_eq!(config.evm_opts.env.chain_id, Some(42));
1628 assert!(config.backends.contains_key(&first));
1629 assert!(!config.backends.contains_key(&second));
1630
1631 config._get_runner(None, false, false).await.unwrap();
1632 assert!(config.backends.contains_key(&first));
1633 assert!(config.backends.contains_key(&second));
1634 assert_eq!(config.backends.len(), 2);
1635 }
1636
1637 #[tokio::test(flavor = "multi_thread")]
1638 async fn script_same_url_rpc_headers_change_adopts_new_fork_identity() {
1639 assert_same_url_fork_auth_change_re_resolves(
1640 |evm_opts| evm_opts.rpc_headers = Some(vec!["x-fork-source: first".to_string()]),
1641 |evm_opts| evm_opts.rpc_headers = Some(vec!["x-fork-source: second".to_string()]),
1642 )
1643 .await;
1644 }
1645
1646 #[tokio::test(flavor = "multi_thread")]
1647 async fn script_same_url_jwt_change_adopts_new_fork_identity() {
1648 const FIRST_JWT: &str = "5c43996d0d150a81f06ae452fce38120d97a4156650aec7487b3384bfe32edae";
1649 const SECOND_JWT: &str = "cabee703106087906e50f3e75a6ddbab60809f980511d1d1548d449d52220795";
1650
1651 assert_same_url_fork_auth_change_re_resolves(
1652 |evm_opts| evm_opts.rpc_jwt = Some(FIRST_JWT.to_string()),
1653 |evm_opts| evm_opts.rpc_jwt = Some(SECOND_JWT.to_string()),
1654 )
1655 .await;
1656 }
1657
1658 #[tokio::test(flavor = "multi_thread")]
1659 async fn script_runner_env_revalidates_resolved_fork_hash() {
1660 let (api, handle) = spawn(NodeConfig::test()).await;
1661 let prevrandao = B256::with_last_byte(0x42);
1662 api.anvil_set_next_block_prevrandao(prevrandao).await.unwrap();
1663 api.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1664
1665 let evm_opts = EvmOpts {
1666 fork_url: Some(handle.http_endpoint()),
1667 sender: handle.dev_accounts().next().unwrap(),
1668 ..Default::default()
1669 };
1670 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1671 Config::default(),
1672 evm_opts,
1673 ExecutorBuilder::<EthEvmNetwork>::new(),
1674 false,
1675 TempoOpts::default(),
1676 None,
1677 )
1678 .await
1679 .unwrap();
1680 assert!(
1681 config
1682 .evm_opts
1683 .can_use_create2_deployer_resolved(config.resolved_fork().unwrap())
1684 .await
1685 .unwrap()
1686 );
1687 config._get_runner(None, false, false).await.unwrap();
1688 assert_eq!(config.backends.len(), 1);
1689
1690 let provider = handle.http_provider();
1691 let pinned = config.resolved_fork.clone().unwrap();
1692 provider
1693 .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
1694 .await
1695 .unwrap();
1696 let replacement = provider.get_block_by_number(1.into()).await.unwrap().unwrap();
1697 assert_ne!(replacement.header.hash, pinned.hash());
1698 assert_ne!(replacement.header.mix_hash, Some(prevrandao));
1699
1700 config.set_fork_url(handle.http_endpoint());
1703 match config._get_runner(None, false, false).await {
1704 Ok(runner) => {
1705 assert_eq!(runner.executor.evm_env().block_env.prevrandao(), Some(prevrandao));
1706 }
1707 Err(error) => assert_missing_orphaned_block(&error),
1708 }
1709 assert_eq!(config.resolved_fork.as_ref(), Some(&pinned));
1710 }
1711
1712 #[test]
1713 fn script_trace_requirements_honor_tracing_verbosity() {
1714 let mut config = Config::default();
1715 config.tracing.verbosity = 5;
1716
1717 let tracing = script_trace_requirements(&config, false).into_config().unwrap();
1718 assert!(tracing.record_state_diff);
1719 }
1720
1721 #[test]
1722 fn created_contracts_include_successful_creator_code_ancestry() {
1723 let root = Address::repeat_byte(0x11);
1724 let implementation = Address::repeat_byte(0x22);
1725 let helper = Address::repeat_byte(0x33);
1726 let created = Address::repeat_byte(0x44);
1727 let init_code = Bytes::from_static(&[0x60, 0x00]);
1728 let mut arena = CallTraceArena::default();
1729 arena.nodes_mut()[0].trace =
1730 CallTrace { success: true, address: root, kind: CallKind::Call, ..Default::default() };
1731 arena.nodes_mut().extend([
1732 CallTraceNode {
1733 parent: Some(0),
1734 idx: 1,
1735 trace: CallTrace {
1736 success: true,
1737 address: implementation,
1738 kind: CallKind::DelegateCall,
1739 ..Default::default()
1740 },
1741 ..Default::default()
1742 },
1743 CallTraceNode {
1744 parent: Some(1),
1745 idx: 2,
1746 trace: CallTrace {
1747 success: true,
1748 address: helper,
1749 kind: CallKind::Call,
1750 ..Default::default()
1751 },
1752 ..Default::default()
1753 },
1754 CallTraceNode {
1755 parent: Some(2),
1756 idx: 3,
1757 trace: CallTrace {
1758 success: true,
1759 address: created,
1760 kind: CallKind::Create2,
1761 data: init_code.clone(),
1762 ..Default::default()
1763 },
1764 ..Default::default()
1765 },
1766 ]);
1767 let result = ScriptResult::<Ethereum> {
1768 traces: vec![(
1769 TraceKind::Execution,
1770 SparsedTraceArena {
1771 arena,
1772 ignored: Default::default(),
1773 diagnostics: Default::default(),
1774 },
1775 )],
1776 ..Default::default()
1777 };
1778
1779 let contracts = result.get_created_contracts(&ContractsByArtifact::default());
1780 assert_eq!(contracts.len(), 1);
1781 assert_eq!(contracts[0].address, created);
1782 assert_eq!(contracts[0].init_code, init_code);
1783 assert_eq!(contracts[0].creator_code_addresses, [helper, implementation, root]);
1784 }
1785
1786 #[test]
1787 fn created_contracts_exclude_creations_rolled_back_by_an_ancestor() {
1788 let mut arena = CallTraceArena::default();
1789 arena.nodes_mut()[0].trace = CallTrace { success: true, ..Default::default() };
1790 arena.nodes_mut().extend([
1791 CallTraceNode {
1792 parent: Some(0),
1793 idx: 1,
1794 trace: CallTrace { success: false, kind: CallKind::Call, ..Default::default() },
1795 ..Default::default()
1796 },
1797 CallTraceNode {
1798 parent: Some(1),
1799 idx: 2,
1800 trace: CallTrace {
1801 success: true,
1802 kind: CallKind::Create,
1803 data: Bytes::from_static(&[0x60, 0x00]),
1804 ..Default::default()
1805 },
1806 ..Default::default()
1807 },
1808 ]);
1809 let result = ScriptResult::<Ethereum> {
1810 traces: vec![(
1811 TraceKind::Execution,
1812 SparsedTraceArena {
1813 arena,
1814 ignored: Default::default(),
1815 diagnostics: Default::default(),
1816 },
1817 )],
1818 ..Default::default()
1819 };
1820
1821 assert!(result.get_created_contracts(&ContractsByArtifact::default()).is_empty());
1822 }
1823
1824 fn active_session_entry(
1825 session_id: B256,
1826 root_account: Address,
1827 chain_id: u64,
1828 ) -> SessionEntry {
1829 let foundry_wallets::WalletSigner::Local(root) =
1830 foundry_wallets::utils::create_private_key_signer(SESSION_ROOT_PRIVATE_KEY).unwrap()
1831 else {
1832 unreachable!("a raw private key always creates a local signer")
1833 };
1834 assert_eq!(root.address(), root_account);
1835 let key = GeneratedSessionKey::from_private_key(SESSION_PRIVATE_KEY).unwrap();
1836 let prepared = SessionAuthorizationRequest {
1837 session_id,
1838 root_account,
1839 chain_id,
1840 key_address: key.address(),
1841 expiry: NonZeroU64::new(u64::MAX).unwrap(),
1842 scope: vec![tempo_primitives::transaction::CallScope {
1843 target: Address::repeat_byte(0xaa),
1844 selector_rules: vec![],
1845 }],
1846 spend_limits: vec![],
1847 }
1848 .prepare(0)
1849 .unwrap();
1850 let signature = root.sign_hash_sync(&prepared.authorization.signature_hash()).unwrap();
1851 let authorization = prepared
1852 .authorization
1853 .clone()
1854 .into_signed(tempo_primitives::transaction::PrimitiveSignature::Secp256k1(signature));
1855 prepared.into_active_entry(key, &authorization).unwrap()
1856 }
1857
1858 struct TempoHomeGuard {
1859 _guard: MutexGuard<'static, ()>,
1860 }
1861
1862 impl TempoHomeGuard {
1863 async fn set(path: &std::path::Path) -> Self {
1864 let guard = TEMPO_HOME_LOCK.lock().await;
1865 unsafe {
1867 std::env::remove_var(TEMPO_SESSION_ID_ENV);
1868 std::env::set_var(TEMPO_HOME_ENV, path);
1869 }
1870 Self { _guard: guard }
1871 }
1872 }
1873
1874 impl Drop for TempoHomeGuard {
1875 fn drop(&mut self) {
1876 unsafe {
1878 std::env::remove_var(TEMPO_HOME_ENV);
1879 std::env::remove_var(TEMPO_SESSION_ID_ENV);
1880 }
1881 }
1882 }
1883
1884 fn session_root() -> Address {
1885 SESSION_ROOT_ADDRESS.parse().unwrap()
1886 }
1887
1888 #[test]
1889 fn can_parse_sig() {
1890 let sig = "0x522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266";
1891 let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--sig", sig]);
1892 assert_eq!(args.sig, sig);
1893 }
1894
1895 #[test]
1896 fn parses_confirmations() {
1897 let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol"]);
1898 assert_eq!(args.confirmations, DEFAULT_CONFIRMATIONS);
1899
1900 let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--confirmations", "6"]);
1901 assert_eq!(args.confirmations, 6);
1902
1903 let err =
1904 ScriptArgs::try_parse_from(["foundry-cli", "Contract.sol", "--confirmations", "0"])
1905 .unwrap_err();
1906 assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
1907 }
1908
1909 #[test]
1910 fn verify_external_requires_verify() {
1911 let err = ScriptArgs::try_parse_from(["foundry-cli", "Contract.sol", "--verify-external"])
1912 .unwrap_err();
1913 assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
1914
1915 let args = ScriptArgs::try_parse_from([
1916 "foundry-cli",
1917 "Contract.sol",
1918 "--broadcast",
1919 "--verify",
1920 "--verify-external",
1921 ])
1922 .unwrap();
1923 assert!(args.verify_external);
1924 }
1925
1926 #[test]
1927 fn verify_external_requires_simulation_and_online_mode() {
1928 for conflicting_arg in ["--skip-simulation", "--offline"] {
1929 let err = ScriptArgs::try_parse_from([
1930 "foundry-cli",
1931 "Contract.sol",
1932 "--broadcast",
1933 "--verify",
1934 "--verify-external",
1935 conflicting_arg,
1936 ])
1937 .unwrap_err();
1938 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
1939 }
1940 }
1941
1942 #[test]
1943 fn rejects_max_sender_nonce() {
1944 let max_valid_nonce = (u64::MAX - 1).to_string();
1945 let args = ScriptArgs::try_parse_from([
1946 "foundry-cli",
1947 "Contract.sol",
1948 "--sender-nonce",
1949 &max_valid_nonce,
1950 ])
1951 .unwrap();
1952 assert_eq!(args.sender_nonce, Some(u64::MAX - 1));
1953
1954 let max_nonce = u64::MAX.to_string();
1955 let err = ScriptArgs::try_parse_from([
1956 "foundry-cli",
1957 "Contract.sol",
1958 "--sender-nonce",
1959 &max_nonce,
1960 ])
1961 .unwrap_err();
1962 assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
1963 }
1964
1965 #[tokio::test(flavor = "multi_thread")]
1966 async fn update_sender_fork_does_not_read_nonce_from_replacement_block() {
1967 let (_api, handle) = spawn(NodeConfig::test()).await;
1968 let accounts = handle.dev_wallets().collect::<Vec<_>>();
1969 let original_sender = accounts[0].address();
1970 let replacement_sender = accounts[1].address();
1971 let provider = handle.http_provider();
1972 let tx = TransactionRequest::default()
1973 .from(replacement_sender)
1974 .to(original_sender)
1975 .value(U256::from(1));
1976 provider.send_transaction(tx.into()).await.unwrap().get_receipt().await.unwrap();
1977
1978 let evm_opts = EvmOpts {
1979 fork_url: Some(handle.http_endpoint()),
1980 sender: original_sender,
1981 ..Default::default()
1982 };
1983 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1984 Config::default(),
1985 evm_opts,
1986 ExecutorBuilder::<EthEvmNetwork>::new(),
1987 false,
1988 TempoOpts::default(),
1989 None,
1990 )
1991 .await
1992 .unwrap();
1993 let resolved = config.resolved_fork.clone().unwrap();
1994 assert_eq!(config.evm_opts.fork_block_number, None);
1995 assert_eq!(config.sender_nonce, 0);
1996
1997 provider
1998 .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
1999 .await
2000 .unwrap();
2001 assert_eq!(
2002 provider
2003 .get_transaction_count(replacement_sender)
2004 .block_id(BlockId::number(1))
2005 .await
2006 .unwrap(),
2007 0
2008 );
2009
2010 match config.update_sender(replacement_sender).await {
2011 Ok(()) => assert_eq!(config.sender_nonce, 1),
2012 Err(error) => assert_missing_orphaned_block(&error),
2013 }
2014 assert_eq!(config.resolved_fork.as_ref(), Some(&resolved));
2015 }
2016
2017 #[tokio::test(flavor = "multi_thread")]
2018 async fn sender_nonce_override_does_not_refresh_fork() {
2019 let (_api, handle) = spawn(NodeConfig::test()).await;
2020 let sender = handle.dev_accounts().next().unwrap();
2021 let evm_opts =
2022 EvmOpts { fork_url: Some(handle.http_endpoint()), sender, ..Default::default() };
2023 let mut config = ScriptConfig::<EthEvmNetwork>::new(
2024 Config::default(),
2025 evm_opts,
2026 ExecutorBuilder::<EthEvmNetwork>::new(),
2027 false,
2028 TempoOpts::default(),
2029 Some(7),
2030 )
2031 .await
2032 .unwrap();
2033
2034 config.set_fork_url("http://127.0.0.1:1".to_string());
2035 config.update_sender(Address::with_last_byte(0x42)).await.unwrap();
2036 assert_eq!(config.sender_nonce, 7);
2037 }
2038
2039 #[tokio::test(flavor = "multi_thread")]
2040 #[cfg(feature = "monad")]
2041 async fn script_runner_preserves_nested_fork_source_chain() {
2042 let (_origin_api, origin_handle) = spawn(
2043 NodeConfig::test_monad()
2044 .with_chain_id(Some(NamedChain::MonadTestnet as u64))
2045 .with_hardfork(Some(foundry_evm::hardforks::MonadHardfork::MonadNine.into())),
2046 )
2047 .await;
2048 let (_fork_api, fork_handle) = spawn(
2049 NodeConfig::test_monad()
2050 .with_chain_id(Some(NamedChain::Mainnet as u64))
2051 .with_no_storage_caching(true)
2052 .with_eth_rpc_url(Some(origin_handle.http_endpoint()))
2053 .with_fork_block_number(Some(0u64)),
2054 )
2055 .await;
2056 let mut evm_opts =
2057 EvmOpts { fork_url: Some(fork_handle.http_endpoint()), ..Default::default() };
2058 evm_opts.infer_network_from_fork().await.unwrap();
2059 let config = Config { networks: evm_opts.networks, ..Default::default() };
2060 let mut script = ScriptConfig::<MonadEvmNetwork>::new(
2061 config,
2062 evm_opts,
2063 ExecutorBuilder::<MonadEvmNetwork>::new(),
2064 false,
2065 TempoOpts::default(),
2066 None,
2067 )
2068 .await
2069 .unwrap();
2070
2071 let _runner = script._get_runner(None, false, false).await.unwrap();
2072
2073 assert_eq!(script.source_chain_id, Some(NamedChain::MonadTestnet as u64));
2074 assert_eq!(
2075 script.hardfork,
2076 Some(FoundryHardfork::Monad(foundry_evm::hardforks::MonadHardfork::MonadNine))
2077 );
2078 }
2079
2080 #[tokio::test(flavor = "multi_thread")]
2081 async fn tempo_runner_fee_charge_matches_execution_context() {
2082 let (_api, handle) = spawn(NodeConfig::test_tempo()).await;
2083 let networks = NetworkConfigs::with_tempo();
2084 let evm_opts = EvmOpts {
2085 fork_url: Some(handle.http_endpoint()),
2086 sender: handle.dev_accounts().next().unwrap(),
2087 networks,
2088 ..Default::default()
2089 };
2090 let config = Config { networks, ..Default::default() };
2091 let mut script = ScriptConfig::<TempoEvmNetwork>::new(
2092 config,
2093 evm_opts,
2094 ExecutorBuilder::<TempoEvmNetwork>::new(),
2095 false,
2096 TempoOpts::default(),
2097 None,
2098 )
2099 .await
2100 .unwrap();
2101
2102 let rpc_runner = script._get_runner(None, false, false).await.unwrap();
2103 assert!(!rpc_runner.executor.evm_env().cfg_env.disable_fee_charge);
2104
2105 let target = ArtifactId {
2106 path: PathBuf::from("Script.json"),
2107 name: "Script".to_string(),
2108 source: PathBuf::from("Script.sol"),
2109 version: Version::new(0, 8, 30),
2110 build_id: String::new(),
2111 profile: "default".to_string(),
2112 };
2113 let synthetic_runner = script
2114 .get_runner_with_cheatcodes(
2115 ContractsByArtifact::default(),
2116 Wallets::new(Default::default(), None),
2117 false,
2118 target,
2119 false,
2120 )
2121 .await
2122 .unwrap();
2123 assert!(synthetic_runner.executor.evm_env().cfg_env.disable_fee_charge);
2124 }
2125
2126 #[test]
2127 fn can_parse_shared_tempo_opts() {
2128 let args = ScriptArgs::parse_from([
2129 "foundry-cli",
2130 "Contract.sol",
2131 "--tempo.fee-token",
2132 "1",
2133 "--tempo.expires",
2134 "10",
2135 ]);
2136
2137 assert_eq!(
2138 args.tempo.fee_token,
2139 Some(address!("0x20C0000000000000000000000000000000000001"))
2140 );
2141 assert_eq!(args.tempo.expires, Some(10));
2142 }
2143
2144 #[test]
2145 fn can_parse_sponsor_tempo_opts() {
2146 let args = ScriptArgs::parse_from([
2147 "foundry-cli",
2148 "Contract.sol",
2149 "--tempo.sponsor",
2150 SESSION_ROOT_ADDRESS,
2151 "--tempo.sponsor-signer",
2152 "env://TEMPO_SPONSOR_PK",
2153 ]);
2154
2155 assert_eq!(args.tempo.sponsor, Some(session_root()));
2156 assert_eq!(args.tempo.sponsor_signer.as_deref(), Some("env://TEMPO_SPONSOR_PK"));
2157 }
2158
2159 #[test]
2160 fn can_parse_full_tempo_opts() {
2161 let args =
2162 ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--tempo.nonce-key", "1"]);
2163
2164 assert_eq!(args.tempo.nonce_key, Some(U256::from(1)));
2165 }
2166
2167 #[test]
2168 fn can_parse_tempo_session_opt() {
2169 let args = ScriptArgs::parse_from([
2170 "foundry-cli",
2171 "Contract.sol",
2172 "--tempo.session",
2173 SESSION_ID_HEX,
2174 ]);
2175
2176 assert_eq!(args.tempo.session, Some(B256::from([0x11; 32])),);
2177 }
2178
2179 #[tokio::test]
2180 async fn tempo_session_sets_script_sender_to_root_account() {
2181 let temp = tempdir().unwrap();
2182 let session_id = B256::from([0x22; 32]);
2183 let root = session_root();
2184 let chain_id = foundry_common::DEV_CHAIN_ID;
2185
2186 let _guard = TempoHomeGuard::set(temp.path()).await;
2187 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
2188
2189 let args = ScriptArgs::parse_from([
2190 "foundry-cli",
2191 "Contract.sol",
2192 "--tempo.session",
2193 &format!("{session_id:?}"),
2194 ]);
2195 let evm_opts = EvmOpts {
2196 networks: NetworkConfigs::with_tempo(),
2197 env: foundry_evm::opts::Env { chain_id: Some(chain_id), ..Default::default() },
2198 ..Default::default()
2199 };
2200
2201 let state = args
2202 .preprocess::<TempoEvmNetwork>(
2203 Config::default(),
2204 evm_opts,
2205 ExecutorBuilder::<TempoEvmNetwork>::new(),
2206 )
2207 .await
2208 .unwrap();
2209 assert_eq!(state.script_config.evm_opts.sender, root);
2210 }
2211
2212 #[tokio::test]
2213 async fn tempo_session_resume_multi_defers_session_sender_until_reexecution() {
2214 let temp = tempdir().unwrap();
2215 let session_id = B256::from([0x55; 32]);
2216 let root = session_root();
2217 let chain_id = 4217;
2218
2219 let _guard = TempoHomeGuard::set(temp.path()).await;
2220 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
2221
2222 let args = ScriptArgs::parse_from([
2223 "foundry-cli",
2224 "Contract.sol",
2225 "--resume",
2226 "--multi",
2227 "--tempo.session",
2228 &format!("{session_id:?}"),
2229 ]);
2230 let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
2231
2232 let state = args
2233 .preprocess::<TempoEvmNetwork>(
2234 Config::default(),
2235 evm_opts,
2236 ExecutorBuilder::<TempoEvmNetwork>::new(),
2237 )
2238 .await
2239 .unwrap();
2240 assert_ne!(state.script_config.evm_opts.sender, root);
2241 }
2242
2243 #[tokio::test]
2244 async fn tempo_session_resume_defers_session_sender_until_reexecution() {
2245 let temp = tempdir().unwrap();
2246 let session_id = B256::from([0x77; 32]);
2247 let root = session_root();
2248 let chain_id = 4217;
2249
2250 let _guard = TempoHomeGuard::set(temp.path()).await;
2251 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
2252
2253 let args = ScriptArgs::parse_from([
2254 "foundry-cli",
2255 "Contract.sol",
2256 "--resume",
2257 "--tempo.session",
2258 &format!("{session_id:?}"),
2259 ]);
2260 let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
2261
2262 let state = args
2263 .preprocess::<TempoEvmNetwork>(
2264 Config::default(),
2265 evm_opts,
2266 ExecutorBuilder::<TempoEvmNetwork>::new(),
2267 )
2268 .await
2269 .unwrap();
2270 assert_ne!(state.script_config.evm_opts.sender, root);
2271 }
2272
2273 #[tokio::test]
2274 async fn tempo_session_non_resume_multi_sets_sender_without_chain_validation() {
2275 let temp = tempdir().unwrap();
2276 let session_id = B256::from([0x66; 32]);
2277 let root = session_root();
2278 let chain_id = 4217;
2279
2280 let _guard = TempoHomeGuard::set(temp.path()).await;
2281 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
2282
2283 let args = ScriptArgs::parse_from([
2284 "foundry-cli",
2285 "Contract.sol",
2286 "--multi",
2287 "--tempo.session",
2288 &format!("{session_id:?}"),
2289 ]);
2290 let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
2291
2292 let state = args
2293 .preprocess::<TempoEvmNetwork>(
2294 Config::default(),
2295 evm_opts,
2296 ExecutorBuilder::<TempoEvmNetwork>::new(),
2297 )
2298 .await
2299 .unwrap();
2300 assert_eq!(state.script_config.evm_opts.sender, root);
2301 }
2302
2303 #[tokio::test]
2304 async fn tempo_session_initial_broadcast_sets_sender_without_chain_validation() {
2305 let temp = tempdir().unwrap();
2306 let session_id = B256::from([0x88; 32]);
2307 let root = session_root();
2308 let chain_id = 4217;
2309
2310 let _guard = TempoHomeGuard::set(temp.path()).await;
2311 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
2312
2313 let args = ScriptArgs::parse_from([
2314 "foundry-cli",
2315 "Contract.sol",
2316 "--broadcast",
2317 "--tempo.session",
2318 &format!("{session_id:?}"),
2319 ]);
2320 let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
2321
2322 let state = args
2323 .preprocess::<TempoEvmNetwork>(
2324 Config::default(),
2325 evm_opts,
2326 ExecutorBuilder::<TempoEvmNetwork>::new(),
2327 )
2328 .await
2329 .unwrap();
2330 assert_eq!(state.script_config.evm_opts.sender, root);
2331 }
2332
2333 #[tokio::test]
2334 async fn tempo_options_preserve_explicit_script_network() {
2335 let temp = tempdir().unwrap();
2336 let _guard = TempoHomeGuard::set(temp.path()).await;
2337 for options in [
2338 vec!["--tempo.fee-token", "0x20c0000000000000000000000000000000000000"],
2339 vec![
2340 "--tempo.session",
2341 "0x4444444444444444444444444444444444444444444444444444444444444444",
2342 ],
2343 ] {
2344 let args =
2345 ScriptArgs::parse_from(["foundry-cli", "Contract.sol"].into_iter().chain(options));
2346 for (networks, name) in [
2347 (NetworkConfigs::with_ethereum(), "ethereum"),
2348 (NetworkConfigs::with_celo(), "celo"),
2349 #[cfg(feature = "base")]
2350 (NetworkConfigs::with_base(), "base"),
2351 ] {
2352 let evm_opts = EvmOpts { networks, ..Default::default() };
2353 let err = args.resolved_evm_opts(Config::default(), evm_opts).await.unwrap_err();
2354 assert_eq!(
2355 err.to_string(),
2356 format!("Tempo transaction options conflict with configured network `{name}`")
2357 );
2358 }
2359 }
2360 }
2361
2362 #[tokio::test]
2363 async fn tempo_session_env_selects_tempo_network() {
2364 let temp = tempdir().unwrap();
2365 let _guard = TempoHomeGuard::set(temp.path()).await;
2366 let session_id = B256::from([0x44; 32]);
2367 unsafe { std::env::set_var(TEMPO_SESSION_ID_ENV, format!("{session_id:?}")) };
2369
2370 let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol"]);
2371 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
2372 let (_, evm_opts) = args.resolved_evm_opts(config, evm_opts).await.unwrap();
2373
2374 assert!(evm_opts.networks.is_tempo());
2375 }
2376
2377 #[tokio::test]
2378 async fn tempo_session_rejects_explicit_script_wallet_signer() {
2379 let temp = tempdir().unwrap();
2380 let session_id = B256::from([0x33; 32]);
2381 let root = session_root();
2382 let chain_id = foundry_common::DEV_CHAIN_ID;
2383
2384 let _guard = TempoHomeGuard::set(temp.path()).await;
2385 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
2386
2387 let args = ScriptArgs::parse_from([
2388 "foundry-cli",
2389 "Contract.sol",
2390 "--tempo.session",
2391 &format!("{session_id:?}"),
2392 "--private-key",
2393 SESSION_PRIVATE_KEY,
2394 ]);
2395 let evm_opts = EvmOpts {
2396 networks: NetworkConfigs::with_tempo(),
2397 env: foundry_evm::opts::Env { chain_id: Some(chain_id), ..Default::default() },
2398 ..Default::default()
2399 };
2400
2401 let err = match args
2402 .preprocess::<TempoEvmNetwork>(
2403 Config::default(),
2404 evm_opts,
2405 ExecutorBuilder::<TempoEvmNetwork>::new(),
2406 )
2407 .await
2408 {
2409 Ok(_) => panic!("expected --tempo.session with --private-key to fail"),
2410 Err(err) => err,
2411 };
2412 assert!(err.to_string().contains("explicit wallet signer"), "{err}");
2413 }
2414
2415 #[test]
2416 fn can_parse_unlocked() {
2417 let args = ScriptArgs::parse_from([
2418 "foundry-cli",
2419 "Contract.sol",
2420 "--sender",
2421 "0x4e59b44847b379578588920ca78fbf26c0b4956c",
2422 "--unlocked",
2423 ]);
2424 assert!(args.unlocked);
2425
2426 let key = U256::ZERO;
2427 let args = ScriptArgs::try_parse_from([
2428 "foundry-cli",
2429 "Contract.sol",
2430 "--sender",
2431 "0x4e59b44847b379578588920ca78fbf26c0b4956c",
2432 "--unlocked",
2433 "--private-key",
2434 &key.to_string(),
2435 ]);
2436 assert!(args.is_err());
2437 }
2438
2439 #[test]
2440 fn can_merge_script_config() {
2441 let args = ScriptArgs::parse_from([
2442 "foundry-cli",
2443 "Contract.sol",
2444 "--etherscan-api-key",
2445 "goerli",
2446 ]);
2447 let config = args.load_config().unwrap();
2448 assert_eq!(config.etherscan_api_key, Some("goerli".to_string()));
2449 }
2450
2451 #[test]
2452 fn can_disable_code_size_limit() {
2453 let args =
2454 ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--disable-code-size-limit"]);
2455 assert!(args.disable_code_size_limit);
2456
2457 let result = ScriptResult::<Ethereum>::default();
2458 let contracts = ContractsByArtifact::default();
2459 let create = Address::ZERO;
2460 assert!(
2461 args.check_contract_sizes(ContractSizeLimits::default(), &result, &contracts, create)
2462 .is_ok()
2463 );
2464 }
2465
2466 #[test]
2467 fn can_parse_verifier_url() {
2468 let args = ScriptArgs::parse_from([
2469 "foundry-cli",
2470 "script",
2471 "script/Test.s.sol:TestScript",
2472 "--fork-url",
2473 "http://localhost:8545",
2474 "--verifier-url",
2475 "http://localhost:3000/api/verify",
2476 "--etherscan-api-key",
2477 "blacksmith",
2478 "--broadcast",
2479 "--verify",
2480 "-vvvvv",
2481 ]);
2482 assert_eq!(
2483 args.verifier.verifier_url,
2484 Some("http://localhost:3000/api/verify".to_string())
2485 );
2486 }
2487
2488 #[test]
2489 fn can_extract_code_size_limit() {
2490 let args = ScriptArgs::parse_from([
2491 "foundry-cli",
2492 "script",
2493 "script/Test.s.sol:TestScript",
2494 "--fork-url",
2495 "http://localhost:8545",
2496 "--broadcast",
2497 "--code-size-limit",
2498 "50000",
2499 ]);
2500 assert_eq!(args.evm.env.code_size_limit, Some(50000));
2501 }
2502
2503 #[test]
2506 fn cli_code_size_limit_is_honoured_by_check() {
2507 let args = ScriptArgs::parse_from([
2508 "foundry-cli",
2509 "script",
2510 "script/Test.s.sol:TestScript",
2511 "--code-size-limit",
2512 "2147483647",
2513 ]);
2514 assert_eq!(args.evm.env.code_size_limit, Some(2147483647));
2517 }
2518
2519 #[test]
2520 #[cfg(feature = "monad")]
2521 fn contract_size_limits_use_resolved_monad_network() {
2522 let args =
2523 ScriptArgs::parse_from(["foundry-cli", "script", "script/Test.s.sol:TestScript"]);
2524 let evm_opts = EvmOpts { networks: NetworkConfigs::with_monad(), ..Default::default() };
2525
2526 let limits = args.contract_size_limits::<MonadEvmNetwork>(&Config::default(), &evm_opts);
2527
2528 assert!(limits.runtime > ContractSizeLimits::default().runtime);
2529 assert!(limits.initcode > ContractSizeLimits::default().initcode);
2530 }
2531
2532 #[test]
2533 #[cfg(feature = "monad")]
2534 fn contract_size_limits_prefer_cli_then_config_over_network() {
2535 let args = ScriptArgs::parse_from([
2536 "foundry-cli",
2537 "script",
2538 "script/Test.s.sol:TestScript",
2539 "--code-size-limit",
2540 "64",
2541 ]);
2542 let mut config = Config { code_size_limit: Some(128), ..Default::default() };
2543 let evm_opts = EvmOpts { networks: NetworkConfigs::with_monad(), ..Default::default() };
2544 assert_eq!(
2545 args.contract_size_limits::<MonadEvmNetwork>(&config, &evm_opts,),
2546 ContractSizeLimits::with_runtime_limit(64)
2547 );
2548
2549 let args =
2550 ScriptArgs::parse_from(["foundry-cli", "script", "script/Test.s.sol:TestScript"]);
2551 config.code_size_limit = Some(128);
2552 assert_eq!(
2553 args.contract_size_limits::<MonadEvmNetwork>(&config, &evm_opts,),
2554 ContractSizeLimits::with_runtime_limit(128)
2555 );
2556 }
2557
2558 #[test]
2559 fn can_extract_script_etherscan_key() {
2560 let temp = tempdir().unwrap();
2561 let root = temp.path();
2562
2563 let config = r#"
2564 [profile.default]
2565 etherscan_api_key = "amoy"
2566
2567 [etherscan]
2568 amoy = { key = "https://etherscan-amoy.com/" }
2569 "#;
2570
2571 let toml_file = root.join(Config::FILE_NAME);
2572 fs::write(toml_file, config).unwrap();
2573 let args = ScriptArgs::parse_from([
2574 "foundry-cli",
2575 "Contract.sol",
2576 "--etherscan-api-key",
2577 "amoy",
2578 "--root",
2579 root.as_os_str().to_str().unwrap(),
2580 ]);
2581
2582 let config = args.load_config().unwrap();
2583 let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
2584 assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
2585 }
2586
2587 #[test]
2588 fn can_extract_script_rpc_alias() {
2589 let temp = tempdir().unwrap();
2590 let root = temp.path();
2591
2592 let config = r#"
2593 [profile.default]
2594
2595 [rpc_endpoints]
2596 polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_CAN_EXTRACT_RPC_ALIAS}"
2597 "#;
2598
2599 let toml_file = root.join(Config::FILE_NAME);
2600 fs::write(toml_file, config).unwrap();
2601 let args = ScriptArgs::parse_from([
2602 "foundry-cli",
2603 "DeployV1",
2604 "--rpc-url",
2605 "polygonAmoy",
2606 "--root",
2607 root.as_os_str().to_str().unwrap(),
2608 ]);
2609
2610 let err = args.load_config_and_evm_opts().unwrap_err();
2611
2612 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
2613
2614 unsafe {
2615 std::env::set_var("_CAN_EXTRACT_RPC_ALIAS", "123456");
2616 }
2617 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
2618 assert_eq!(config.eth_rpc_url, Some("polygonAmoy".to_string()));
2619 assert_eq!(
2620 evm_opts.fork_url,
2621 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
2622 );
2623 }
2624
2625 #[test]
2626 fn can_extract_script_rpc_and_etherscan_alias() {
2627 let temp = tempdir().unwrap();
2628 let root = temp.path();
2629
2630 let config = r#"
2631 [profile.default]
2632
2633 [rpc_endpoints]
2634 amoy = "https://polygon-amoy.g.alchemy.com/v2/${_EXTRACT_RPC_ALIAS}"
2635
2636 [etherscan]
2637 amoy = { key = "${_ETHERSCAN_API_KEY}", chain = 80002, url = "https://amoy.polygonscan.com/" }
2638 "#;
2639
2640 let toml_file = root.join(Config::FILE_NAME);
2641 fs::write(toml_file, config).unwrap();
2642 let args = ScriptArgs::parse_from([
2643 "foundry-cli",
2644 "DeployV1",
2645 "--rpc-url",
2646 "amoy",
2647 "--etherscan-api-key",
2648 "amoy",
2649 "--root",
2650 root.as_os_str().to_str().unwrap(),
2651 ]);
2652 let err = args.load_config_and_evm_opts().unwrap_err();
2653
2654 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
2655
2656 unsafe {
2657 std::env::set_var("_EXTRACT_RPC_ALIAS", "123456");
2658 }
2659 unsafe {
2660 std::env::set_var("_ETHERSCAN_API_KEY", "etherscan_api_key");
2661 }
2662 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
2663 assert_eq!(config.eth_rpc_url, Some("amoy".to_string()));
2664 assert_eq!(
2665 evm_opts.fork_url,
2666 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
2667 );
2668 let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
2669 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
2670 let etherscan = config.get_etherscan_api_key(None);
2671 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
2672 }
2673
2674 #[test]
2675 fn can_extract_script_rpc_and_sole_etherscan_alias() {
2676 let temp = tempdir().unwrap();
2677 let root = temp.path();
2678
2679 let config = r#"
2680 [profile.default]
2681
2682 [rpc_endpoints]
2683 amoy = "https://polygon-amoy.g.alchemy.com/v2/${_SOLE_EXTRACT_RPC_ALIAS}"
2684
2685 [etherscan]
2686 amoy = { key = "${_SOLE_ETHERSCAN_API_KEY}" }
2687 "#;
2688
2689 let toml_file = root.join(Config::FILE_NAME);
2690 fs::write(toml_file, config).unwrap();
2691 let args = ScriptArgs::parse_from([
2692 "foundry-cli",
2693 "DeployV1",
2694 "--rpc-url",
2695 "amoy",
2696 "--root",
2697 root.as_os_str().to_str().unwrap(),
2698 ]);
2699 let err = args.load_config_and_evm_opts().unwrap_err();
2700
2701 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
2702
2703 unsafe {
2704 std::env::set_var("_SOLE_EXTRACT_RPC_ALIAS", "123456");
2705 }
2706 unsafe {
2707 std::env::set_var("_SOLE_ETHERSCAN_API_KEY", "etherscan_api_key");
2708 }
2709 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
2710 assert_eq!(
2711 evm_opts.fork_url,
2712 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
2713 );
2714 let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
2715 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
2716 let etherscan = config.get_etherscan_api_key(None);
2717 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
2718 }
2719
2720 #[test]
2722 fn test_5923() {
2723 let args =
2724 ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--priority-gas-price", "100"]);
2725 assert!(args.priority_gas_price.is_some());
2726 }
2727
2728 #[test]
2729 fn test_eip1559_fee_estimate() {
2730 let args = ScriptArgs::parse_from(["foundry-cli", "DeployV1"]);
2732 assert!(args.eip1559_fee_estimate.is_none());
2733
2734 let args = ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--estimate", "aggressive"]);
2735 assert_eq!(args.eip1559_fee_estimate, Some(Eip1559FeeEstimatePreset::Aggressive));
2736 }
2737
2738 #[test]
2740 fn test_5910() {
2741 let args = ScriptArgs::parse_from([
2742 "foundry-cli",
2743 "--broadcast",
2744 "--with-gas-price",
2745 "0",
2746 "SolveTutorial",
2747 ]);
2748 assert!(args.with_gas_price.unwrap().is_zero());
2749 }
2750
2751 #[test]
2752 fn test_priority_gas_price_cannot_exceed_gas_price() {
2753 let args = ScriptArgs::parse_from([
2754 "foundry-cli",
2755 "--broadcast",
2756 "--with-gas-price",
2757 "100",
2758 "--priority-gas-price",
2759 "200",
2760 "Script",
2761 ]);
2762 assert!(args.priority_gas_price.unwrap() > args.with_gas_price.unwrap());
2764 }
2765}