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