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};
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;
41use foundry_config::{
42 Config, Eip1559FeeEstimatePreset, figment,
43 figment::{
44 Metadata, Profile, Provider,
45 value::{Dict, Map},
46 },
47};
48use foundry_debugger::DebuggerLayout;
49#[cfg(feature = "optimism")]
50use foundry_evm::core::evm::OpEvmNetwork;
51use foundry_evm::{
52 backend::Backend,
53 core::{
54 Breakpoints, FoundryTransaction,
55 evm::{EthEvmNetwork, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor},
56 fork::ResolvedFork,
57 },
58 executors::ExecutorBuilder,
59 inspectors::{
60 CheatsConfig,
61 cheatcodes::{BroadcastableTransactions, Wallets},
62 },
63 opts::EvmOpts,
64 revm::interpreter::InstructionResult,
65 traces::{InternalTraceMode, TraceRequirements, Traces},
66};
67use foundry_evm_networks::NetworkConfigs;
68use foundry_wallets::MultiWalletOpts;
69use serde::Serialize;
70use std::path::PathBuf;
71
72mod broadcast;
73mod build;
74mod execute;
75mod library_deployments;
76mod multi_sequence;
77mod progress;
78mod providers;
79mod receipts;
80mod runner;
81mod sequence;
82mod session;
83mod simulate;
84mod transaction;
85mod verify;
86mod wallet_session;
87
88pub use wallet_session::ScriptWalletSessionArgs;
89
90foundry_config::merge_impl_figment_convert!(ScriptArgs, build, evm);
92
93const DEFAULT_CONFIRMATIONS: u64 = 1;
94
95#[derive(Clone, Debug, Default, Parser)]
97pub struct ScriptArgs {
98 #[command(flatten)]
100 pub global: GlobalArgs,
101
102 #[arg(value_hint = ValueHint::FilePath)]
107 pub path: String,
108
109 pub args: Vec<String>,
111
112 #[arg(long, visible_alias = "tc", value_name = "CONTRACT_NAME")]
114 pub target_contract: Option<String>,
115
116 #[arg(long, short, default_value = "run")]
118 pub sig: String,
119
120 #[arg(
122 long,
123 env = "ETH_PRIORITY_GAS_PRICE",
124 value_parser = foundry_cli::utils::parse_ether_value,
125 value_name = "PRICE"
126 )]
127 pub priority_gas_price: Option<U256>,
128
129 #[arg(long)]
133 pub legacy: bool,
134
135 #[arg(long = "estimate", value_name = "PRESET")]
143 pub eip1559_fee_estimate: Option<Eip1559FeeEstimatePreset>,
144
145 #[arg(long)]
147 pub broadcast: bool,
148
149 #[arg(long)]
155 pub batch: bool,
156
157 #[command(flatten)]
159 pub tempo: TempoOpts,
160
161 #[command(flatten)]
163 pub wallet_session: ScriptWalletSessionArgs,
164
165 #[arg(long)]
167 pub skip_simulation: bool,
168
169 #[arg(long, short, default_value = "130")]
171 pub gas_estimate_multiplier: u64,
172
173 #[arg(
175 long,
176 value_name = "NONCE",
177 value_parser = RangedU64ValueParser::<u64>::new().range(..u64::MAX),
178 )]
179 pub sender_nonce: Option<u64>,
180
181 #[arg(
183 long,
184 conflicts_with_all = &["private_key", "private_keys", "ledger", "trezor", "aws", "browser"],
185 )]
186 pub unlocked: bool,
187
188 #[arg(long)]
195 pub resume: bool,
196
197 #[arg(long)]
199 pub multi: bool,
200
201 #[arg(long)]
205 pub debug: bool,
206
207 #[arg(long = "debug-layout", requires = "debug", value_enum)]
209 pub debug_layout: Option<DebuggerLayout>,
210
211 #[arg(
213 long,
214 requires = "debug",
215 value_hint = ValueHint::FilePath,
216 value_name = "PATH"
217 )]
218 pub dump: Option<PathBuf>,
219
220 #[arg(long)]
223 pub slow: bool,
224
225 #[arg(
227 long,
228 default_value_t = DEFAULT_CONFIRMATIONS,
229 value_parser = RangedU64ValueParser::<u64>::new().range(1..),
230 )]
231 pub confirmations: u64,
232
233 #[arg(long)]
237 pub non_interactive: bool,
238
239 #[arg(long)]
241 pub disable_code_size_limit: bool,
242
243 #[command(flatten)]
244 pub tracing: TracingArgs,
245
246 #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
248 pub etherscan_api_key: Option<String>,
249
250 #[arg(long, requires = "broadcast")]
252 pub verify: bool,
253
254 #[arg(long, requires = "verify", conflicts_with_all = ["skip_simulation", "offline"])]
256 pub verify_external: bool,
257
258 #[arg(
263 long,
264 env = "ETH_GAS_PRICE",
265 value_parser = foundry_cli::utils::parse_ether_value,
266 value_name = "PRICE",
267 )]
268 pub with_gas_price: Option<U256>,
269
270 #[arg(long, env = "ETH_TIMEOUT")]
272 pub timeout: Option<u64>,
273
274 #[command(flatten)]
275 pub build: BuildOpts,
276
277 #[command(flatten)]
278 pub wallets: MultiWalletOpts,
279
280 #[command(flatten)]
281 pub evm: EvmArgs,
282
283 #[command(flatten)]
284 pub verifier: VerifierArgs,
285
286 #[command(flatten)]
287 pub retry: RetryArgs,
288}
289
290impl ScriptArgs {
291 fn has_tempo_session(&self) -> Result<bool> {
292 Ok(self.tempo.session_id()?.is_some())
293 }
294
295 async fn resolved_evm_opts(&self) -> Result<(Config, EvmOpts)> {
297 let (config, mut evm_opts) = self.load_config_and_evm_opts()?;
298
299 if self.tempo.is_tempo() || self.has_tempo_session()? {
300 evm_opts.networks = NetworkConfigs::with_tempo();
302 } else {
303 evm_opts.infer_network_from_fork().await;
305 }
306
307 Ok((config, evm_opts))
308 }
309
310 async fn preprocess<FEN: FoundryEvmNetwork>(
311 self,
312 mut config: Config,
313 mut evm_opts: EvmOpts,
314 ) -> Result<PreprocessedState<FEN>> {
315 let args = self;
316 let mut tempo = args.tempo.clone();
317
318 let session_sender = if args.resume {
319 None
320 } else {
321 tempo.session_sender_for_multi_wallet(&args.wallets, args.evm.sender)?
324 };
325
326 let script_wallets = Wallets::new(args.wallets.get_multi_wallet().await?, args.evm.sender);
327 let browser_wallet = args.wallets.browser_signer::<FEN::Network>().await?;
328
329 if let Some(sender) = session_sender {
330 evm_opts.sender = sender;
331 } else if let Some(sender) = args.maybe_load_private_key()? {
332 evm_opts.sender = sender;
333 } else if args.evm.sender.is_none() {
334 let addresses = script_wallets.addresses();
338 if addresses.len() == 1 {
339 evm_opts.sender = addresses[0];
340 } else if let Some(signer) = browser_wallet.as_ref().map(|b| b.address()) {
341 evm_opts.sender = signer
342 }
343 }
344
345 tempo.resolve_expires();
346 config.tracing = args.tracing.resolve(&config.tracing, evm_opts.verbosity);
347
348 let script_config =
349 ScriptConfig::new(config, evm_opts, args.batch, tempo, args.sender_nonce).await?;
350 Ok(PreprocessedState { args, script_config, script_wallets, browser_wallet })
351 }
352
353 #[allow(clippy::large_stack_frames)]
355 pub async fn run_script(self) -> Result<()> {
356 trace!(target: "script", "executing script command");
357
358 if self.tempo.sponsor_url.is_some() {
359 eyre::bail!(
360 "--sponsor-url is not supported by forge script; use --tempo.sponsor with \
361 --tempo.sponsor-signer or --tempo.sponsor-sig"
362 );
363 }
364
365 if self.wallet_session.enabled {
366 return self.run_wallet_session_wrapper();
367 }
368
369 let (config, evm_opts) = self.resolved_evm_opts().await?;
370
371 let is_tempo = evm_opts.networks.is_tempo();
372
373 if self.batch && !is_tempo {
374 eyre::bail!("--batch mode is only supported on Tempo networks");
375 }
376
377 if self.unlocked && self.has_tempo_session()? {
378 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --unlocked");
379 }
380
381 if is_tempo {
384 let batch = self.batch;
385 return Box::pin(async move {
386 let bundled =
387 match self.prepare_bundled::<TempoEvmNetwork>(config, evm_opts).await? {
388 Some(bundled) => bundled,
389 None => return Ok(()),
390 };
391 let bundled = if batch { bundled } else { bundled.wait_for_pending().await? };
395 let broadcasted = if batch {
396 bundled.broadcast_batch().await?
397 } else {
398 bundled.broadcast().await?
399 };
400 if broadcasted.args.verify {
401 broadcasted.verify().await?;
402 }
403 Ok(())
404 })
405 .await;
406 }
407
408 #[cfg(feature = "optimism")]
409 if evm_opts.networks.is_optimism() {
410 return Box::pin(self.run_generic_script::<OpEvmNetwork>(config, evm_opts)).await;
411 }
412
413 Box::pin(self.run_generic_script::<EthEvmNetwork>(config, evm_opts)).await
414 }
415
416 #[allow(clippy::large_stack_frames)]
420 async fn prepare_bundled<FEN: FoundryEvmNetwork>(
421 self,
422 config: Config,
423 evm_opts: EvmOpts,
424 ) -> Result<Option<BundledState<FEN>>> {
425 let state = self.preprocess::<FEN>(config, evm_opts).await?;
426 let create2_deployer = state.script_config.evm_opts.create2_deployer;
427 let compiled = state.compile()?;
428
429 let bundled = if compiled.args.resume {
432 compiled.resume().await?
433 } else {
434 let pre_simulation = compiled
436 .link()
437 .await?
438 .prepare_execution()
439 .await?
440 .execute()
441 .await?
442 .prepare_simulation()
443 .await?
444 .optimize_library_deployments()
445 .await?;
446
447 if pre_simulation.args.debug {
448 return match pre_simulation.args.dump.clone() {
449 Some(path) => pre_simulation.dump_debugger(&path).map(|_| None),
450 None => pre_simulation.run_debugger().map(|_| None),
451 };
452 }
453
454 if shell::is_json() {
455 pre_simulation.show_json().await?;
456 } else {
457 pre_simulation.show_traces().await?;
458 }
459
460 if pre_simulation
463 .execution_result
464 .transactions
465 .as_ref()
466 .is_none_or(|txs| txs.is_empty())
467 {
468 if pre_simulation.args.broadcast {
469 sh_warn!("No transactions to broadcast.")?;
470 }
471
472 return Ok(None);
473 }
474
475 if pre_simulation.execution_artifacts.rpc_data.missing_rpc {
477 if !shell::is_json() {
478 sh_println!("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?;
479 }
480
481 return Ok(None);
482 }
483
484 let size_limits = pre_simulation
485 .script_config
486 .evm_opts
487 .env
488 .code_size_limit
489 .or(pre_simulation.script_config.config.code_size_limit)
490 .map(ContractSizeLimits::with_runtime_limit)
491 .unwrap_or_else(|| {
492 let spec_id: SpecFor<FEN> = pre_simulation.script_config.config.evm_spec_id();
493 ContractSizeLimits::for_spec_id(spec_id.into())
494 });
495 pre_simulation.args.check_contract_sizes(
496 size_limits,
497 &pre_simulation.execution_result,
498 &pre_simulation.build_data.known_contracts,
499 create2_deployer,
500 )?;
501
502 pre_simulation.fill_metadata().await?.bundle().await?
503 };
504
505 if !bundled.args.should_broadcast() {
507 if !shell::is_json() {
508 if shell::verbosity() >= 4 {
509 sh_println!("\n=== Transactions that will be broadcast ===\n")?;
510 bundled.sequence.show_transactions()?;
511 }
512
513 sh_println!(
514 "\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more."
515 )?;
516 }
517 return Ok(None);
518 }
519
520 if bundled.args.verify {
522 bundled.verify_preflight_check().await?;
523 }
524
525 Ok(Some(bundled))
526 }
527
528 async fn run_generic_script<FEN: FoundryEvmNetwork>(
529 self,
530 config: Config,
531 evm_opts: EvmOpts,
532 ) -> Result<()> {
533 let bundled = match self.prepare_bundled::<FEN>(config, evm_opts).await? {
534 Some(bundled) => bundled,
535 None => return Ok(()),
536 };
537
538 let broadcasted = bundled.wait_for_pending().await?.broadcast().await?;
540
541 if broadcasted.args.verify {
542 broadcasted.verify().await?;
543 }
544
545 Ok(())
546 }
547
548 fn maybe_load_private_key(&self) -> Result<Option<Address>> {
551 if let Some(turnkey_address) = self.wallets.turnkey_address() {
552 return Ok(Some(turnkey_address));
553 }
554
555 let maybe_sender = self
556 .wallets
557 .private_keys()?
558 .filter(|pks| pks.len() == 1)
559 .map(|pks| pks.first().unwrap().address());
560 Ok(maybe_sender)
561 }
562
563 fn get_method_and_calldata(&self, abi: &JsonAbi) -> Result<(Function, Bytes)> {
571 if let Ok(decoded) = hex::decode(&self.sig) {
572 let selector = &decoded[..SELECTOR_LEN];
573 let func =
574 abi.functions().find(|func| selector == &func.selector()[..]).ok_or_else(|| {
575 eyre::eyre!(
576 "Function selector `{}` not found in the ABI",
577 hex::encode(selector)
578 )
579 })?;
580 return Ok((func.clone(), decoded.into()));
581 }
582
583 let func = if self.sig.contains('(') {
584 let func = get_func(&self.sig)?;
585 abi.functions()
586 .find(|&abi_func| abi_func.selector() == func.selector())
587 .wrap_err(format!("Function `{}` is not implemented in your script.", self.sig))?
588 } else {
589 let matching_functions =
590 abi.functions().filter(|func| func.name == self.sig).collect::<Vec<_>>();
591 match matching_functions.len() {
592 0 => {
593 eyre::bail!("Function `{}` not found in the ABI", self.sig);
594 }
595 1 => matching_functions[0],
596 2.. => {
597 eyre::bail!(
598 "Multiple functions with the same name `{}` found in the ABI",
599 self.sig
600 );
601 }
602 }
603 };
604 let data = encode_function_args(func, &self.args)?;
605
606 Ok((func.clone(), data.into()))
607 }
608
609 fn check_contract_sizes<N: Network>(
615 &self,
616 size_limits: ContractSizeLimits,
617 result: &ScriptResult<N>,
618 known_contracts: &ContractsByArtifact,
619 create2_deployer: Address,
620 ) -> Result<()> {
621 if self.disable_code_size_limit {
623 return Ok(());
624 }
625
626 let mut bytecodes: Vec<(String, &[u8], &[u8])> = vec![];
628
629 for (artifact, contract) in known_contracts.iter() {
631 let Some(bytecode) = contract.bytecode() else { continue };
632 let Some(deployed_bytecode) = contract.deployed_bytecode() else { continue };
633 bytecodes.push((artifact.name.clone(), bytecode, deployed_bytecode));
634 }
635
636 let create_nodes = result.traces.iter().flat_map(|(_, traces)| {
638 traces.nodes().iter().filter(|node| node.trace.kind.is_any_create())
639 });
640 let mut unknown_c = 0usize;
641 for node in create_nodes {
642 let init_code = &node.trace.data;
643 let deployed_code = &node.trace.output;
644 if !bytecodes.iter().any(|(_, b, _)| *b == init_code.as_ref()) {
645 bytecodes.push((format!("Unknown{unknown_c}"), init_code, deployed_code));
646 unknown_c += 1;
647 }
648 }
649
650 let mut prompt_user = false;
651 let max_size = size_limits.runtime;
652
653 for (data, to) in result.transactions.iter().flat_map(|txes| {
654 txes.iter().filter_map(|tx| {
655 tx.transaction
656 .input()
657 .filter(|data| data.len() > max_size)
658 .map(|data| (data, tx.transaction.to()))
659 })
660 }) {
661 let mut offset = 0;
662
663 if let Some(to) = to {
665 if to == create2_deployer {
666 offset = 32;
668 } else {
669 continue;
670 }
671 }
672
673 if let Some((name, _, deployed_code)) =
675 bytecodes.iter().find(|(_, init_code, _)| *init_code == &data[offset..])
676 {
677 let deployment_size = deployed_code.len();
678
679 if deployment_size > max_size {
680 prompt_user = self.should_broadcast();
681 sh_err!(
682 "`{name}` is above the contract size limit ({deployment_size} > {max_size})."
683 )?;
684 }
685 }
686 }
687
688 if prompt_user
690 && !self.non_interactive
691 && !Confirm::new().with_prompt("Do you wish to continue?".to_string()).interact()?
692 {
693 eyre::bail!("User canceled the script.");
694 }
695
696 Ok(())
697 }
698
699 const fn should_broadcast(&self) -> bool {
701 self.broadcast || self.resume || self.verify
702 }
703}
704
705impl Provider for ScriptArgs {
706 fn metadata(&self) -> Metadata {
707 Metadata::named("Script Args Provider")
708 }
709
710 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
711 let mut dict = Dict::default();
712
713 if let Some(etherscan_api_key) =
714 self.etherscan_api_key.as_ref().filter(|s| !s.trim().is_empty())
715 {
716 dict.insert(
717 "etherscan_api_key".to_string(),
718 figment::value::Value::from(etherscan_api_key.clone()),
719 );
720 }
721
722 if let Some(timeout) = self.timeout {
723 dict.insert("transaction_timeout".to_string(), timeout.into());
724 }
725
726 if let Some(preset) = self.eip1559_fee_estimate {
727 dict.insert(
728 "eip1559_fee_estimate".to_string(),
729 figment::value::Value::from(preset.to_string()),
730 );
731 }
732
733 Ok(Map::from([(Config::selected_profile(), dict)]))
734 }
735}
736
737#[derive(Serialize, Clone)]
738#[serde(bound = "")]
739pub struct ScriptResult<N: Network> {
740 pub success: bool,
741 #[serde(rename = "raw_logs")]
742 pub logs: Vec<Log>,
743 pub traces: Traces,
744 pub gas_used: u64,
745 pub labeled_addresses: AddressHashMap<String>,
746 #[serde(skip)]
747 pub debug_bytecodes: AddressHashMap<Bytes>,
748 #[serde(skip)]
749 pub transactions: Option<BroadcastableTransactions<N>>,
750 pub returned: Bytes,
751 #[serde(skip)]
752 pub exit_reason: Option<InstructionResult>,
753 pub address: Option<Address>,
754 #[serde(skip)]
755 pub breakpoints: Breakpoints,
756}
757
758impl<N: Network> Default for ScriptResult<N> {
759 fn default() -> Self {
760 Self {
761 success: Default::default(),
762 logs: Default::default(),
763 traces: Default::default(),
764 gas_used: Default::default(),
765 labeled_addresses: Default::default(),
766 debug_bytecodes: Default::default(),
767 transactions: Default::default(),
768 returned: Default::default(),
769 exit_reason: Default::default(),
770 address: Default::default(),
771 breakpoints: Default::default(),
772 }
773 }
774}
775
776impl<N: Network> ScriptResult<N> {
777 pub fn get_created_contracts(
778 &self,
779 known_contracts: &ContractsByArtifact,
780 ) -> Vec<AdditionalContract> {
781 self.traces
782 .iter()
783 .flat_map(|(_, traces)| {
784 let nodes = traces.nodes();
785 nodes.iter().filter_map(|node| {
786 if !node.trace.kind.is_any_create() || !node.trace.success {
787 return None;
788 }
789
790 let mut creator_code_addresses = Vec::new();
791 let mut parent = node.parent;
792 while let Some(parent_idx) = parent {
793 let ancestor = &nodes[parent_idx];
794 if !ancestor.trace.success {
795 return None;
796 }
797 if !ancestor.trace.kind.is_any_create()
798 && !creator_code_addresses.contains(&ancestor.trace.address)
799 {
800 creator_code_addresses.push(ancestor.trace.address);
801 }
802 parent = ancestor.parent;
803 }
804
805 let init_code = node.trace.data.clone();
806 let contract_name = known_contracts
807 .find_by_creation_code(init_code.as_ref())
808 .map(|artifact| artifact.0.name.clone());
809 Some(AdditionalContract {
810 call_kind: node.trace.kind,
811 address: node.trace.address,
812 contract_name,
813 init_code,
814 creator_code_addresses,
815 })
816 })
817 })
818 .collect()
819 }
820}
821
822#[derive(Serialize)]
823#[serde(bound = "")]
824struct JsonResult<'a, N: Network> {
825 logs: Vec<String>,
826 returns: &'a HashMap<String, NestedValue>,
827 #[serde(flatten)]
828 result: &'a ScriptResult<N>,
829}
830
831#[derive(Clone, Debug)]
832pub struct ScriptConfig<FEN: FoundryEvmNetwork> {
833 pub config: Config,
834 pub evm_opts: EvmOpts,
835 pub sender_nonce: u64,
836 sender_nonce_override: Option<u64>,
837 resolved_fork: Option<ResolvedFork>,
838 backends: HashMap<ResolvedFork, Backend<FEN>>,
840 pub batch: bool,
842 pub tempo: TempoOpts,
844}
845
846impl<FEN: FoundryEvmNetwork> ScriptConfig<FEN> {
847 pub async fn new(
848 config: Config,
849 evm_opts: EvmOpts,
850 batch: bool,
851 tempo: TempoOpts,
852 sender_nonce_override: Option<u64>,
853 ) -> Result<Self> {
854 let resolved_fork = evm_opts.resolve_fork().await?;
857 let sender_nonce = if let Some(sender_nonce) = sender_nonce_override {
858 sender_nonce
859 } else if evm_opts.fork_url.is_some() {
860 let fork = resolved_fork.as_ref().context("fork must be resolved")?;
861 next_nonce_resolved(evm_opts.sender, &evm_opts, fork).await?
862 } else {
863 1
865 };
866
867 Ok(Self {
868 config,
869 evm_opts,
870 sender_nonce,
871 sender_nonce_override,
872 resolved_fork,
873 backends: HashMap::default(),
874 batch,
875 tempo,
876 })
877 }
878
879 pub async fn update_sender(&mut self, sender: Address) -> Result<()> {
880 self.sender_nonce = if let Some(sender_nonce) = self.sender_nonce_override {
881 sender_nonce
882 } else if self.evm_opts.fork_url.is_some() {
883 let fork = self.ensure_resolved_fork().await?.context("fork must be resolved")?;
884 next_nonce_resolved(sender, &self.evm_opts, &fork).await?
885 } else {
886 1
888 };
889 self.evm_opts.sender = sender;
890 Ok(())
891 }
892
893 pub fn resolved_fork(&self) -> Result<Option<&ResolvedFork>> {
895 match (&self.evm_opts.fork_url, &self.resolved_fork) {
896 (None, None) => Ok(None),
897 (None, Some(_)) => Err(eyre::eyre!("resolved fork exists without a configured fork")),
898 (Some(_), Some(fork)) if self.evm_opts.resolved_fork_matches(fork) => Ok(Some(fork)),
899 (Some(_), Some(_)) => {
900 Err(eyre::eyre!("resolved fork does not match the configured source and selector"))
901 }
902 (Some(_), None) => Err(eyre::eyre!("fork must be resolved")),
903 }
904 }
905
906 fn set_fork_url(&mut self, fork_url: String) {
907 self.evm_opts.fork_url = Some(fork_url);
908 }
909
910 async fn ensure_resolved_fork(&mut self) -> Result<Option<ResolvedFork>> {
911 if self.evm_opts.fork_url.is_none() {
912 if self.resolved_fork.take().is_some() {
913 self.backends.clear();
914 }
915 return Ok(None);
916 }
917 if let Some(fork) = &self.resolved_fork
918 && self.evm_opts.resolved_fork_matches(fork)
919 {
920 return Ok(Some(fork.clone()));
921 }
922
923 let fork = self.evm_opts.resolve_fork().await?;
924 self.resolved_fork = fork.clone();
925 Ok(fork)
926 }
927
928 pub(crate) async fn update_tempo_session_sender(
929 &mut self,
930 wallets: &MultiWalletOpts,
931 expected_sender: Option<Address>,
932 ) -> Result<()> {
933 if let Some(sender) =
934 self.tempo.session_sender_for_multi_wallet(wallets, expected_sender)?
935 {
936 self.update_sender(sender).await?;
937 }
938 Ok(())
939 }
940
941 async fn get_runner_with_cheatcodes(
942 &mut self,
943 known_contracts: ContractsByArtifact,
944 script_wallets: Wallets,
945 debug: bool,
946 target: ArtifactId,
947 restricted: bool,
948 ) -> Result<ScriptRunner<FEN>> {
949 self._get_runner(Some((known_contracts, script_wallets, target)), debug, restricted).await
950 }
951
952 async fn _get_runner(
953 &mut self,
954 cheats_data: Option<(ContractsByArtifact, Wallets, ArtifactId)>,
955 debug: bool,
956 restricted: bool,
957 ) -> Result<ScriptRunner<FEN>> {
958 trace!("preparing script runner");
959 let resolved = self.ensure_resolved_fork().await?;
960 let (evm_env, mut tx_env) =
961 self.evm_opts.env_with_resolved_fork::<_, _, TxEnvFor<FEN>>(resolved.as_ref()).await?;
962 let fork_block_number = resolved.as_ref().map(ResolvedFork::number);
963
964 let db = if self.evm_opts.fork_url.is_some() {
965 let resolved = resolved.context("fork must be resolved")?;
966 if let Some(backend) = self.backends.get(&resolved) {
967 backend.clone()
968 } else {
969 let fork = self.evm_opts.get_fork(
970 &self.config,
971 evm_env.cfg_env.chain_id,
972 fork_block_number,
973 );
974 let backend = Backend::spawn(fork)?;
975 self.backends.insert(resolved, backend.clone());
976 backend
977 }
978 } else {
979 Backend::spawn(None)?
983 };
984
985 let mut builder = ExecutorBuilder::default()
987 .inspectors(|stack| {
988 stack
989 .logs(self.config.live_logs)
990 .trace_requirements(script_trace_requirements(&self.config, debug))
991 .networks(self.evm_opts.networks)
992 .create2_deployer(self.evm_opts.create2_deployer)
993 })
994 .spec_id(self.config.evm_spec_id())
995 .gas_limit(self.evm_opts.gas_limit())
996 .legacy_assertions(self.config.legacy_assertions);
997
998 if let Some((known_contracts, script_wallets, target)) = cheats_data {
999 builder = builder.inspectors(|stack| {
1000 let mut cheats_config = CheatsConfig::new(
1001 &self.config,
1002 self.evm_opts.clone(),
1003 Some(known_contracts),
1004 Some(target),
1005 self.tempo.fee_token,
1006 self.batch,
1007 );
1008 if restricted {
1009 cheats_config.blocked_cheatcodes =
1010 library_deployments::rerun_unsafe_cheatcode_selectors();
1011 }
1012 stack
1013 .cheatcodes(cheats_config.into())
1014 .wallets(script_wallets)
1015 .enable_isolation(self.evm_opts.isolate)
1016 });
1017 }
1018
1019 tx_env.set_fee_token(self.tempo.fee_token);
1022
1023 let mut runner =
1024 ScriptRunner::new(builder.build(evm_env, tx_env, db), self.evm_opts.clone())
1025 .with_debug_bytecodes(debug);
1026
1027 if self.sender_nonce_override.is_some() {
1028 runner.executor.set_nonce(self.evm_opts.sender, self.sender_nonce)?;
1029 }
1030
1031 Ok(runner)
1032 }
1033}
1034
1035const fn script_trace_requirements(config: &Config, debug: bool) -> TraceRequirements {
1036 TraceRequirements::none()
1037 .with_calls(true)
1038 .with_debug(debug)
1039 .with_verbosity(config.tracing.verbosity)
1040 .with_decode_internal(if config.tracing.decode_internal {
1041 InternalTraceMode::Full
1042 } else {
1043 InternalTraceMode::None
1044 })
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 use super::*;
1050 use alloy_chains::NamedChain;
1051 use alloy_eips::BlockId;
1052 use alloy_network::Ethereum;
1053 use alloy_primitives::{B256, address};
1054 use alloy_provider::Provider as _;
1055 use alloy_rpc_types::TransactionRequest;
1056 use alloy_signer::SignerSync as _;
1057 use anvil::{NodeConfig, spawn};
1058 use foundry_cli::opts::TEMPO_SESSION_ID_ENV;
1059 use foundry_common::tempo::{
1060 GeneratedSessionKey, SessionAuthorizationRequest, SessionEntry, TEMPO_HOME_ENV,
1061 upsert_session_entry,
1062 };
1063 use foundry_config::UnresolvedEnvVarError;
1064 use foundry_evm::{
1065 revm::context::Block as _,
1066 traces::{
1067 CallKind, CallTrace, CallTraceArena, CallTraceNode, SparsedTraceArena, TraceKind,
1068 },
1069 };
1070 use std::{fs, num::NonZeroU64, sync::LazyLock};
1071 use tempfile::tempdir;
1072 use tokio::sync::{Mutex, MutexGuard};
1073
1074 const SESSION_PRIVATE_KEY: &str =
1075 "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
1076 const SESSION_ROOT_PRIVATE_KEY: &str =
1077 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
1078 const SESSION_ID_HEX: &str =
1079 "0x1111111111111111111111111111111111111111111111111111111111111111";
1080 const SESSION_ROOT_ADDRESS: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
1081 static TEMPO_HOME_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
1082
1083 fn assert_missing_orphaned_block(error: &eyre::Report) {
1084 let message = format!("{error:#}").to_ascii_lowercase();
1085 assert!(
1086 message.contains("failed to get block hash")
1087 || message.contains("not found")
1088 || message.contains("unknown block"),
1089 "unexpected orphaned block lookup error: {error:#}"
1090 );
1091 }
1092
1093 #[tokio::test(flavor = "multi_thread")]
1094 async fn script_fork_context_is_re_resolved_for_a_new_rpc() {
1095 let (api_a, handle_a) = spawn(NodeConfig::test()).await;
1096 let (api_b, handle_b) = spawn(NodeConfig::test()).await;
1097 api_a.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1098 api_b.anvil_mine(Some(U256::from(3)), None).await.unwrap();
1099
1100 let evm_opts = EvmOpts {
1101 fork_url: Some(handle_a.http_endpoint()),
1102 sender: handle_a.dev_accounts().next().unwrap(),
1103 ..Default::default()
1104 };
1105 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1106 Config::default(),
1107 evm_opts,
1108 false,
1109 TempoOpts::default(),
1110 None,
1111 )
1112 .await
1113 .unwrap();
1114 let first = config.resolved_fork.clone().unwrap();
1115 assert_eq!(first.number(), 1);
1116 assert_eq!(config.evm_opts.fork_block_number, None);
1117 assert!(config.evm_opts.resolved_fork_matches(&first));
1118
1119 config._get_runner(None, false, false).await.unwrap();
1120
1121 config.set_fork_url(handle_b.http_endpoint());
1122 let second = config.ensure_resolved_fork().await.unwrap().unwrap();
1123
1124 assert_eq!(second.number(), 3);
1125 assert_eq!(config.evm_opts.fork_block_number, None);
1126 assert!(config.evm_opts.resolved_fork_matches(&second));
1127 assert_ne!(first, second);
1128 }
1129
1130 #[tokio::test(flavor = "multi_thread")]
1131 async fn script_explicit_fork_block_is_preserved_for_a_new_rpc() {
1132 let (api_a, handle_a) = spawn(NodeConfig::test()).await;
1133 let (api_b, handle_b) = spawn(NodeConfig::test()).await;
1134 api_a.anvil_mine(Some(U256::from(2)), None).await.unwrap();
1135 api_b.anvil_mine(Some(U256::from(4)), None).await.unwrap();
1136
1137 let evm_opts = EvmOpts {
1138 fork_url: Some(handle_a.http_endpoint()),
1139 fork_block_number: Some(1),
1140 sender: handle_a.dev_accounts().next().unwrap(),
1141 ..Default::default()
1142 };
1143 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1144 Config::default(),
1145 evm_opts,
1146 false,
1147 TempoOpts::default(),
1148 None,
1149 )
1150 .await
1151 .unwrap();
1152 config._get_runner(None, false, false).await.unwrap();
1153
1154 config.set_fork_url(handle_b.http_endpoint());
1155 let context = config.ensure_resolved_fork().await.unwrap().unwrap();
1156
1157 assert_eq!(context.number(), 1);
1158 assert_eq!(config.evm_opts.fork_block_number, Some(1));
1159 assert!(config.evm_opts.resolved_fork_matches(&context));
1160 }
1161
1162 #[tokio::test(flavor = "multi_thread")]
1163 async fn script_explicit_fork_block_can_equal_previous_latest() {
1164 let (api_a, handle_a) = spawn(NodeConfig::test()).await;
1165 let (api_b, handle_b) = spawn(NodeConfig::test()).await;
1166 api_a.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1167 api_b.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1168
1169 let evm_opts = EvmOpts {
1170 fork_url: Some(handle_a.http_endpoint()),
1171 sender: handle_a.dev_accounts().next().unwrap(),
1172 ..Default::default()
1173 };
1174 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1175 Config::default(),
1176 evm_opts,
1177 false,
1178 TempoOpts::default(),
1179 None,
1180 )
1181 .await
1182 .unwrap();
1183 let latest = config.resolved_fork.clone().unwrap();
1184 assert_eq!(config.evm_opts.fork_block_number, None);
1185 config._get_runner(None, false, false).await.unwrap();
1186 assert!(config.backends.contains_key(&latest));
1187
1188 config.set_fork_url(handle_b.http_endpoint());
1189 config.evm_opts.fork_block_number = Some(1);
1190 let explicit = config.ensure_resolved_fork().await.unwrap().unwrap();
1191
1192 assert_eq!(explicit.number(), 1);
1193 assert_eq!(config.evm_opts.fork_block_number, Some(1));
1194 assert!(config.evm_opts.resolved_fork_matches(&explicit));
1195 assert_ne!(latest, explicit);
1196 assert!(config.backends.contains_key(&latest));
1197 assert!(!config.backends.contains_key(&explicit));
1198
1199 config._get_runner(None, false, false).await.unwrap();
1200 assert!(config.backends.contains_key(&latest));
1201 assert!(config.backends.contains_key(&explicit));
1202 assert_eq!(config.backends.len(), 2);
1203 }
1204
1205 #[tokio::test(flavor = "multi_thread")]
1206 async fn script_fork_cache_distinguishes_same_height_contexts() {
1207 let (api_a, handle_a) = spawn(NodeConfig::test()).await;
1208 let (api_b, handle_b) = spawn(NodeConfig::test()).await;
1209 let state_address = address!("0000000000000000000000000000000000001337");
1210 let balance_a = U256::from(1);
1211 let balance_b = U256::from(2);
1212 api_a.anvil_set_balance(state_address, balance_a).await.unwrap();
1213 api_b.anvil_set_balance(state_address, balance_b).await.unwrap();
1214 let original_prevrandao = B256::with_last_byte(0x42);
1215 api_a.anvil_set_next_block_prevrandao(original_prevrandao).await.unwrap();
1216 api_a.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1217 api_b.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1218 let url_a = handle_a.http_endpoint();
1219 let url_b = handle_b.http_endpoint();
1220 let sender = handle_a.dev_accounts().next().unwrap();
1221
1222 let evm_opts = EvmOpts { fork_url: Some(url_a.clone()), sender, ..Default::default() };
1223 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1224 Config::default(),
1225 evm_opts,
1226 false,
1227 TempoOpts::default(),
1228 None,
1229 )
1230 .await
1231 .unwrap();
1232 let runner_a = config._get_runner(None, false, false).await.unwrap();
1233 assert_eq!(runner_a.executor.get_balance(state_address).unwrap(), balance_a);
1234 let first_a = config.resolved_fork().unwrap().unwrap().clone();
1235 assert!(config.backends.contains_key(&first_a));
1236
1237 config.set_fork_url(url_b.clone());
1238 let runner_b = config._get_runner(None, false, false).await.unwrap();
1239 assert_eq!(runner_b.executor.get_balance(state_address).unwrap(), balance_b);
1240 let context_b = config.resolved_fork().unwrap().unwrap().clone();
1241 assert_eq!(context_b.number(), 1);
1242 assert!(config.backends.contains_key(&first_a));
1243 assert!(config.backends.contains_key(&context_b));
1244 assert_eq!(config.backends.len(), 2);
1245
1246 let provider_a = handle_a.http_provider();
1247 provider_a
1248 .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
1249 .await
1250 .unwrap();
1251 let replacement = provider_a.get_block_by_number(1.into()).await.unwrap().unwrap();
1252 assert_eq!(replacement.header.number, first_a.number());
1253 assert_ne!(replacement.header.hash, first_a.hash());
1254 assert_ne!(replacement.header.mix_hash, Some(original_prevrandao));
1255
1256 config.set_fork_url(url_a.clone());
1257 let second_a = config.ensure_resolved_fork().await.unwrap().unwrap();
1258 assert_eq!(second_a.number(), first_a.number());
1259 assert_eq!(second_a.hash(), replacement.header.hash);
1260 assert_ne!(second_a.hash(), first_a.hash());
1261 assert!(config.backends.contains_key(&first_a));
1262 assert!(config.backends.contains_key(&context_b));
1263 assert!(!config.backends.contains_key(&second_a));
1264 assert_eq!(config.backends.len(), 2);
1265
1266 let runner = config._get_runner(None, false, false).await.unwrap();
1267 assert!(config.backends.contains_key(&second_a));
1268 assert_eq!(config.backends.len(), 3);
1269 assert_eq!(runner.executor.get_balance(state_address).unwrap(), balance_a);
1270 assert_eq!(runner.executor.evm_env().block_env.prevrandao(), replacement.header.mix_hash);
1271 }
1272
1273 #[tokio::test(flavor = "multi_thread")]
1274 async fn script_same_endpoint_does_not_advance_resolved_fork() {
1275 let (api, handle) = spawn(NodeConfig::test()).await;
1276 api.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1277 let url = handle.http_endpoint();
1278 let evm_opts = EvmOpts {
1279 fork_url: Some(url.clone()),
1280 sender: handle.dev_accounts().next().unwrap(),
1281 ..Default::default()
1282 };
1283 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1284 Config::default(),
1285 evm_opts,
1286 false,
1287 TempoOpts::default(),
1288 None,
1289 )
1290 .await
1291 .unwrap();
1292 let resolved = config.resolved_fork.clone().unwrap();
1293 config._get_runner(None, false, false).await.unwrap();
1294
1295 api.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1296 config.set_fork_url(url.clone());
1297 let runner = config._get_runner(None, false, false).await.unwrap();
1298
1299 assert_eq!(config.resolved_fork.as_ref(), Some(&resolved));
1300 assert!(config.backends.contains_key(&resolved));
1301 assert_eq!(config.backends.len(), 1);
1302 assert_eq!(runner.executor.evm_env().block_env.number(), U256::from(1));
1303 assert_eq!(config.evm_opts.fork_block_number, None);
1304 }
1305
1306 #[tokio::test(flavor = "multi_thread")]
1307 async fn script_fork_changed_headers_replace_cached_backend() {
1308 let (api, handle) = spawn(NodeConfig::test()).await;
1309 api.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1310 let url = handle.http_endpoint();
1311 let evm_opts = EvmOpts {
1312 fork_url: Some(url.clone()),
1313 sender: handle.dev_accounts().next().unwrap(),
1314 ..Default::default()
1315 };
1316 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1317 Config::default(),
1318 evm_opts,
1319 false,
1320 TempoOpts::default(),
1321 None,
1322 )
1323 .await
1324 .unwrap();
1325 config._get_runner(None, false, false).await.unwrap();
1326 let without_headers = config.resolved_fork().unwrap().unwrap().clone();
1327 assert!(config.backends.contains_key(&without_headers));
1328
1329 config.evm_opts.fork_headers = Some(vec!["x-foundry-test: resolved-fork".to_string()]);
1330 let with_headers = config.ensure_resolved_fork().await.unwrap().unwrap();
1331 assert!(config.backends.contains_key(&without_headers));
1332 assert!(!config.backends.contains_key(&with_headers));
1333 assert_eq!(config.backends.len(), 1);
1334 config._get_runner(None, false, false).await.unwrap();
1335
1336 assert_ne!(without_headers, with_headers);
1337 assert!(config.evm_opts.resolved_fork_matches(&with_headers));
1338 assert!(config.backends.contains_key(&without_headers));
1339 assert!(config.backends.contains_key(&with_headers));
1340 assert_eq!(config.backends.len(), 2);
1341 }
1342
1343 #[tokio::test(flavor = "multi_thread")]
1344 async fn script_runner_env_revalidates_resolved_fork_hash() {
1345 let (api, handle) = spawn(NodeConfig::test()).await;
1346 let prevrandao = B256::with_last_byte(0x42);
1347 api.anvil_set_next_block_prevrandao(prevrandao).await.unwrap();
1348 api.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1349
1350 let evm_opts = EvmOpts {
1351 fork_url: Some(handle.http_endpoint()),
1352 sender: handle.dev_accounts().next().unwrap(),
1353 ..Default::default()
1354 };
1355 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1356 Config::default(),
1357 evm_opts,
1358 false,
1359 TempoOpts::default(),
1360 None,
1361 )
1362 .await
1363 .unwrap();
1364 assert!(
1365 config
1366 .evm_opts
1367 .can_use_create2_deployer_resolved(config.resolved_fork().unwrap())
1368 .await
1369 .unwrap()
1370 );
1371 config._get_runner(None, false, false).await.unwrap();
1372 assert_eq!(config.backends.len(), 1);
1373
1374 let provider = handle.http_provider();
1375 let pinned = config.resolved_fork.clone().unwrap();
1376 provider
1377 .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
1378 .await
1379 .unwrap();
1380 let replacement = provider.get_block_by_number(1.into()).await.unwrap().unwrap();
1381 assert_ne!(replacement.header.hash, pinned.hash());
1382 assert_ne!(replacement.header.mix_hash, Some(prevrandao));
1383
1384 config.set_fork_url(handle.http_endpoint());
1387 match config._get_runner(None, false, false).await {
1388 Ok(runner) => {
1389 assert_eq!(runner.executor.evm_env().block_env.prevrandao(), Some(prevrandao));
1390 }
1391 Err(error) => assert_missing_orphaned_block(&error),
1392 }
1393 assert_eq!(config.resolved_fork.as_ref(), Some(&pinned));
1394 }
1395
1396 #[test]
1397 fn script_trace_requirements_honor_tracing_verbosity() {
1398 let mut config = Config::default();
1399 config.tracing.verbosity = 5;
1400
1401 let tracing = script_trace_requirements(&config, false).into_config().unwrap();
1402 assert!(tracing.record_state_diff);
1403 }
1404
1405 #[test]
1406 fn created_contracts_include_successful_creator_code_ancestry() {
1407 let root = Address::repeat_byte(0x11);
1408 let implementation = Address::repeat_byte(0x22);
1409 let helper = Address::repeat_byte(0x33);
1410 let created = Address::repeat_byte(0x44);
1411 let init_code = Bytes::from_static(&[0x60, 0x00]);
1412 let mut arena = CallTraceArena::default();
1413 arena.nodes_mut()[0].trace =
1414 CallTrace { success: true, address: root, kind: CallKind::Call, ..Default::default() };
1415 arena.nodes_mut().extend([
1416 CallTraceNode {
1417 parent: Some(0),
1418 idx: 1,
1419 trace: CallTrace {
1420 success: true,
1421 address: implementation,
1422 kind: CallKind::DelegateCall,
1423 ..Default::default()
1424 },
1425 ..Default::default()
1426 },
1427 CallTraceNode {
1428 parent: Some(1),
1429 idx: 2,
1430 trace: CallTrace {
1431 success: true,
1432 address: helper,
1433 kind: CallKind::Call,
1434 ..Default::default()
1435 },
1436 ..Default::default()
1437 },
1438 CallTraceNode {
1439 parent: Some(2),
1440 idx: 3,
1441 trace: CallTrace {
1442 success: true,
1443 address: created,
1444 kind: CallKind::Create2,
1445 data: init_code.clone(),
1446 ..Default::default()
1447 },
1448 ..Default::default()
1449 },
1450 ]);
1451 let result = ScriptResult::<Ethereum> {
1452 traces: vec![(
1453 TraceKind::Execution,
1454 SparsedTraceArena {
1455 arena,
1456 ignored: Default::default(),
1457 diagnostics: Default::default(),
1458 },
1459 )],
1460 ..Default::default()
1461 };
1462
1463 let contracts = result.get_created_contracts(&ContractsByArtifact::default());
1464 assert_eq!(contracts.len(), 1);
1465 assert_eq!(contracts[0].address, created);
1466 assert_eq!(contracts[0].init_code, init_code);
1467 assert_eq!(contracts[0].creator_code_addresses, [helper, implementation, root]);
1468 }
1469
1470 #[test]
1471 fn created_contracts_exclude_creations_rolled_back_by_an_ancestor() {
1472 let mut arena = CallTraceArena::default();
1473 arena.nodes_mut()[0].trace = CallTrace { success: true, ..Default::default() };
1474 arena.nodes_mut().extend([
1475 CallTraceNode {
1476 parent: Some(0),
1477 idx: 1,
1478 trace: CallTrace { success: false, kind: CallKind::Call, ..Default::default() },
1479 ..Default::default()
1480 },
1481 CallTraceNode {
1482 parent: Some(1),
1483 idx: 2,
1484 trace: CallTrace {
1485 success: true,
1486 kind: CallKind::Create,
1487 data: Bytes::from_static(&[0x60, 0x00]),
1488 ..Default::default()
1489 },
1490 ..Default::default()
1491 },
1492 ]);
1493 let result = ScriptResult::<Ethereum> {
1494 traces: vec![(
1495 TraceKind::Execution,
1496 SparsedTraceArena {
1497 arena,
1498 ignored: Default::default(),
1499 diagnostics: Default::default(),
1500 },
1501 )],
1502 ..Default::default()
1503 };
1504
1505 assert!(result.get_created_contracts(&ContractsByArtifact::default()).is_empty());
1506 }
1507
1508 fn active_session_entry(
1509 session_id: B256,
1510 root_account: Address,
1511 chain_id: u64,
1512 ) -> SessionEntry {
1513 let foundry_wallets::WalletSigner::Local(root) =
1514 foundry_wallets::utils::create_private_key_signer(SESSION_ROOT_PRIVATE_KEY).unwrap()
1515 else {
1516 unreachable!("a raw private key always creates a local signer")
1517 };
1518 assert_eq!(root.address(), root_account);
1519 let key = GeneratedSessionKey::from_private_key(SESSION_PRIVATE_KEY).unwrap();
1520 let prepared = SessionAuthorizationRequest {
1521 session_id,
1522 root_account,
1523 chain_id,
1524 key_address: key.address(),
1525 expiry: NonZeroU64::new(u64::MAX).unwrap(),
1526 scope: vec![tempo_primitives::transaction::CallScope {
1527 target: Address::repeat_byte(0xaa),
1528 selector_rules: vec![],
1529 }],
1530 spend_limits: vec![],
1531 }
1532 .prepare(0)
1533 .unwrap();
1534 let signature = root.sign_hash_sync(&prepared.authorization.signature_hash()).unwrap();
1535 let authorization = prepared
1536 .authorization
1537 .clone()
1538 .into_signed(tempo_primitives::transaction::PrimitiveSignature::Secp256k1(signature));
1539 prepared.into_active_entry(key, &authorization).unwrap()
1540 }
1541
1542 struct TempoHomeGuard {
1543 _guard: MutexGuard<'static, ()>,
1544 }
1545
1546 impl TempoHomeGuard {
1547 async fn set(path: &std::path::Path) -> Self {
1548 let guard = TEMPO_HOME_LOCK.lock().await;
1549 unsafe {
1551 std::env::remove_var(TEMPO_SESSION_ID_ENV);
1552 std::env::set_var(TEMPO_HOME_ENV, path);
1553 }
1554 Self { _guard: guard }
1555 }
1556 }
1557
1558 impl Drop for TempoHomeGuard {
1559 fn drop(&mut self) {
1560 unsafe {
1562 std::env::remove_var(TEMPO_HOME_ENV);
1563 std::env::remove_var(TEMPO_SESSION_ID_ENV);
1564 }
1565 }
1566 }
1567
1568 fn session_root() -> Address {
1569 SESSION_ROOT_ADDRESS.parse().unwrap()
1570 }
1571
1572 #[test]
1573 fn can_parse_sig() {
1574 let sig = "0x522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266";
1575 let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--sig", sig]);
1576 assert_eq!(args.sig, sig);
1577 }
1578
1579 #[test]
1580 fn parses_confirmations() {
1581 let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol"]);
1582 assert_eq!(args.confirmations, DEFAULT_CONFIRMATIONS);
1583
1584 let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--confirmations", "6"]);
1585 assert_eq!(args.confirmations, 6);
1586
1587 let err =
1588 ScriptArgs::try_parse_from(["foundry-cli", "Contract.sol", "--confirmations", "0"])
1589 .unwrap_err();
1590 assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
1591 }
1592
1593 #[test]
1594 fn verify_external_requires_verify() {
1595 let err = ScriptArgs::try_parse_from(["foundry-cli", "Contract.sol", "--verify-external"])
1596 .unwrap_err();
1597 assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
1598
1599 let args = ScriptArgs::try_parse_from([
1600 "foundry-cli",
1601 "Contract.sol",
1602 "--broadcast",
1603 "--verify",
1604 "--verify-external",
1605 ])
1606 .unwrap();
1607 assert!(args.verify_external);
1608 }
1609
1610 #[test]
1611 fn verify_external_requires_simulation_and_online_mode() {
1612 for conflicting_arg in ["--skip-simulation", "--offline"] {
1613 let err = ScriptArgs::try_parse_from([
1614 "foundry-cli",
1615 "Contract.sol",
1616 "--broadcast",
1617 "--verify",
1618 "--verify-external",
1619 conflicting_arg,
1620 ])
1621 .unwrap_err();
1622 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
1623 }
1624 }
1625
1626 #[test]
1627 fn rejects_max_sender_nonce() {
1628 let max_valid_nonce = (u64::MAX - 1).to_string();
1629 let args = ScriptArgs::try_parse_from([
1630 "foundry-cli",
1631 "Contract.sol",
1632 "--sender-nonce",
1633 &max_valid_nonce,
1634 ])
1635 .unwrap();
1636 assert_eq!(args.sender_nonce, Some(u64::MAX - 1));
1637
1638 let max_nonce = u64::MAX.to_string();
1639 let err = ScriptArgs::try_parse_from([
1640 "foundry-cli",
1641 "Contract.sol",
1642 "--sender-nonce",
1643 &max_nonce,
1644 ])
1645 .unwrap_err();
1646 assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
1647 }
1648
1649 #[tokio::test(flavor = "multi_thread")]
1650 async fn update_sender_fork_does_not_read_nonce_from_replacement_block() {
1651 let (_api, handle) = spawn(NodeConfig::test()).await;
1652 let accounts = handle.dev_wallets().collect::<Vec<_>>();
1653 let original_sender = accounts[0].address();
1654 let replacement_sender = accounts[1].address();
1655 let provider = handle.http_provider();
1656 let tx = TransactionRequest::default()
1657 .from(replacement_sender)
1658 .to(original_sender)
1659 .value(U256::from(1));
1660 provider.send_transaction(tx.into()).await.unwrap().get_receipt().await.unwrap();
1661
1662 let evm_opts = EvmOpts {
1663 fork_url: Some(handle.http_endpoint()),
1664 sender: original_sender,
1665 ..Default::default()
1666 };
1667 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1668 Config::default(),
1669 evm_opts,
1670 false,
1671 TempoOpts::default(),
1672 None,
1673 )
1674 .await
1675 .unwrap();
1676 let resolved = config.resolved_fork.clone().unwrap();
1677 assert_eq!(config.evm_opts.fork_block_number, None);
1678 assert_eq!(config.sender_nonce, 0);
1679
1680 provider
1681 .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
1682 .await
1683 .unwrap();
1684 assert_eq!(
1685 provider
1686 .get_transaction_count(replacement_sender)
1687 .block_id(BlockId::number(1))
1688 .await
1689 .unwrap(),
1690 0
1691 );
1692
1693 match config.update_sender(replacement_sender).await {
1694 Ok(()) => assert_eq!(config.sender_nonce, 1),
1695 Err(error) => assert_missing_orphaned_block(&error),
1696 }
1697 assert_eq!(config.resolved_fork.as_ref(), Some(&resolved));
1698 }
1699
1700 #[tokio::test(flavor = "multi_thread")]
1701 async fn sender_nonce_override_does_not_refresh_fork() {
1702 let (_api, handle) = spawn(NodeConfig::test()).await;
1703 let sender = handle.dev_accounts().next().unwrap();
1704 let evm_opts =
1705 EvmOpts { fork_url: Some(handle.http_endpoint()), sender, ..Default::default() };
1706 let mut config = ScriptConfig::<EthEvmNetwork>::new(
1707 Config::default(),
1708 evm_opts,
1709 false,
1710 TempoOpts::default(),
1711 Some(7),
1712 )
1713 .await
1714 .unwrap();
1715
1716 config.set_fork_url("http://127.0.0.1:1".to_string());
1717 config.update_sender(Address::with_last_byte(0x42)).await.unwrap();
1718 assert_eq!(config.sender_nonce, 7);
1719 }
1720
1721 #[test]
1722 fn can_parse_shared_tempo_opts() {
1723 let args = ScriptArgs::parse_from([
1724 "foundry-cli",
1725 "Contract.sol",
1726 "--tempo.fee-token",
1727 "1",
1728 "--tempo.expires",
1729 "10",
1730 ]);
1731
1732 assert_eq!(
1733 args.tempo.fee_token,
1734 Some(address!("0x20C0000000000000000000000000000000000001"))
1735 );
1736 assert_eq!(args.tempo.expires, Some(10));
1737 }
1738
1739 #[test]
1740 fn can_parse_sponsor_tempo_opts() {
1741 let args = ScriptArgs::parse_from([
1742 "foundry-cli",
1743 "Contract.sol",
1744 "--tempo.sponsor",
1745 SESSION_ROOT_ADDRESS,
1746 "--tempo.sponsor-signer",
1747 "env://TEMPO_SPONSOR_PK",
1748 ]);
1749
1750 assert_eq!(args.tempo.sponsor, Some(session_root()));
1751 assert_eq!(args.tempo.sponsor_signer.as_deref(), Some("env://TEMPO_SPONSOR_PK"));
1752 }
1753
1754 #[test]
1755 fn can_parse_full_tempo_opts() {
1756 let args =
1757 ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--tempo.nonce-key", "1"]);
1758
1759 assert_eq!(args.tempo.nonce_key, Some(U256::from(1)));
1760 }
1761
1762 #[test]
1763 fn can_parse_tempo_session_opt() {
1764 let args = ScriptArgs::parse_from([
1765 "foundry-cli",
1766 "Contract.sol",
1767 "--tempo.session",
1768 SESSION_ID_HEX,
1769 ]);
1770
1771 assert_eq!(args.tempo.session, Some(B256::from([0x11; 32])),);
1772 }
1773
1774 #[tokio::test]
1775 async fn tempo_session_sets_script_sender_to_root_account() {
1776 let temp = tempdir().unwrap();
1777 let session_id = B256::from([0x22; 32]);
1778 let root = session_root();
1779 let chain_id = foundry_common::DEV_CHAIN_ID;
1780
1781 let _guard = TempoHomeGuard::set(temp.path()).await;
1782 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1783
1784 let args = ScriptArgs::parse_from([
1785 "foundry-cli",
1786 "Contract.sol",
1787 "--tempo.session",
1788 &format!("{session_id:?}"),
1789 ]);
1790 let evm_opts = EvmOpts {
1791 networks: NetworkConfigs::with_tempo(),
1792 env: foundry_evm::opts::Env { chain_id: Some(chain_id), ..Default::default() },
1793 ..Default::default()
1794 };
1795
1796 let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1797 assert_eq!(state.script_config.evm_opts.sender, root);
1798 }
1799
1800 #[tokio::test]
1801 async fn tempo_session_resume_multi_defers_session_sender_until_reexecution() {
1802 let temp = tempdir().unwrap();
1803 let session_id = B256::from([0x55; 32]);
1804 let root = session_root();
1805 let chain_id = 4217;
1806
1807 let _guard = TempoHomeGuard::set(temp.path()).await;
1808 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1809
1810 let args = ScriptArgs::parse_from([
1811 "foundry-cli",
1812 "Contract.sol",
1813 "--resume",
1814 "--multi",
1815 "--tempo.session",
1816 &format!("{session_id:?}"),
1817 ]);
1818 let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
1819
1820 let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1821 assert_ne!(state.script_config.evm_opts.sender, root);
1822 }
1823
1824 #[tokio::test]
1825 async fn tempo_session_resume_defers_session_sender_until_reexecution() {
1826 let temp = tempdir().unwrap();
1827 let session_id = B256::from([0x77; 32]);
1828 let root = session_root();
1829 let chain_id = 4217;
1830
1831 let _guard = TempoHomeGuard::set(temp.path()).await;
1832 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1833
1834 let args = ScriptArgs::parse_from([
1835 "foundry-cli",
1836 "Contract.sol",
1837 "--resume",
1838 "--tempo.session",
1839 &format!("{session_id:?}"),
1840 ]);
1841 let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
1842
1843 let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1844 assert_ne!(state.script_config.evm_opts.sender, root);
1845 }
1846
1847 #[tokio::test]
1848 async fn tempo_session_non_resume_multi_sets_sender_without_chain_validation() {
1849 let temp = tempdir().unwrap();
1850 let session_id = B256::from([0x66; 32]);
1851 let root = session_root();
1852 let chain_id = 4217;
1853
1854 let _guard = TempoHomeGuard::set(temp.path()).await;
1855 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1856
1857 let args = ScriptArgs::parse_from([
1858 "foundry-cli",
1859 "Contract.sol",
1860 "--multi",
1861 "--tempo.session",
1862 &format!("{session_id:?}"),
1863 ]);
1864 let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
1865
1866 let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1867 assert_eq!(state.script_config.evm_opts.sender, root);
1868 }
1869
1870 #[tokio::test]
1871 async fn tempo_session_initial_broadcast_sets_sender_without_chain_validation() {
1872 let temp = tempdir().unwrap();
1873 let session_id = B256::from([0x88; 32]);
1874 let root = session_root();
1875 let chain_id = 4217;
1876
1877 let _guard = TempoHomeGuard::set(temp.path()).await;
1878 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1879
1880 let args = ScriptArgs::parse_from([
1881 "foundry-cli",
1882 "Contract.sol",
1883 "--broadcast",
1884 "--tempo.session",
1885 &format!("{session_id:?}"),
1886 ]);
1887 let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
1888
1889 let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1890 assert_eq!(state.script_config.evm_opts.sender, root);
1891 }
1892
1893 #[tokio::test]
1894 async fn tempo_session_env_selects_tempo_network() {
1895 let temp = tempdir().unwrap();
1896 let _guard = TempoHomeGuard::set(temp.path()).await;
1897 let session_id = B256::from([0x44; 32]);
1898 unsafe { std::env::set_var(TEMPO_SESSION_ID_ENV, format!("{session_id:?}")) };
1900
1901 let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol"]);
1902 let (_, evm_opts) = args.resolved_evm_opts().await.unwrap();
1903
1904 assert!(evm_opts.networks.is_tempo());
1905 }
1906
1907 #[tokio::test]
1908 async fn tempo_session_rejects_explicit_script_wallet_signer() {
1909 let temp = tempdir().unwrap();
1910 let session_id = B256::from([0x33; 32]);
1911 let root = session_root();
1912 let chain_id = foundry_common::DEV_CHAIN_ID;
1913
1914 let _guard = TempoHomeGuard::set(temp.path()).await;
1915 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1916
1917 let args = ScriptArgs::parse_from([
1918 "foundry-cli",
1919 "Contract.sol",
1920 "--tempo.session",
1921 &format!("{session_id:?}"),
1922 "--private-key",
1923 SESSION_PRIVATE_KEY,
1924 ]);
1925 let evm_opts = EvmOpts {
1926 networks: NetworkConfigs::with_tempo(),
1927 env: foundry_evm::opts::Env { chain_id: Some(chain_id), ..Default::default() },
1928 ..Default::default()
1929 };
1930
1931 let err = match args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await {
1932 Ok(_) => panic!("expected --tempo.session with --private-key to fail"),
1933 Err(err) => err,
1934 };
1935 assert!(err.to_string().contains("explicit wallet signer"), "{err}");
1936 }
1937
1938 #[test]
1939 fn can_parse_unlocked() {
1940 let args = ScriptArgs::parse_from([
1941 "foundry-cli",
1942 "Contract.sol",
1943 "--sender",
1944 "0x4e59b44847b379578588920ca78fbf26c0b4956c",
1945 "--unlocked",
1946 ]);
1947 assert!(args.unlocked);
1948
1949 let key = U256::ZERO;
1950 let args = ScriptArgs::try_parse_from([
1951 "foundry-cli",
1952 "Contract.sol",
1953 "--sender",
1954 "0x4e59b44847b379578588920ca78fbf26c0b4956c",
1955 "--unlocked",
1956 "--private-key",
1957 &key.to_string(),
1958 ]);
1959 assert!(args.is_err());
1960 }
1961
1962 #[test]
1963 fn can_merge_script_config() {
1964 let args = ScriptArgs::parse_from([
1965 "foundry-cli",
1966 "Contract.sol",
1967 "--etherscan-api-key",
1968 "goerli",
1969 ]);
1970 let config = args.load_config().unwrap();
1971 assert_eq!(config.etherscan_api_key, Some("goerli".to_string()));
1972 }
1973
1974 #[test]
1975 fn can_disable_code_size_limit() {
1976 let args =
1977 ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--disable-code-size-limit"]);
1978 assert!(args.disable_code_size_limit);
1979
1980 let result = ScriptResult::<Ethereum>::default();
1981 let contracts = ContractsByArtifact::default();
1982 let create = Address::ZERO;
1983 assert!(
1984 args.check_contract_sizes(ContractSizeLimits::default(), &result, &contracts, create)
1985 .is_ok()
1986 );
1987 }
1988
1989 #[test]
1990 fn can_parse_verifier_url() {
1991 let args = ScriptArgs::parse_from([
1992 "foundry-cli",
1993 "script",
1994 "script/Test.s.sol:TestScript",
1995 "--fork-url",
1996 "http://localhost:8545",
1997 "--verifier-url",
1998 "http://localhost:3000/api/verify",
1999 "--etherscan-api-key",
2000 "blacksmith",
2001 "--broadcast",
2002 "--verify",
2003 "-vvvvv",
2004 ]);
2005 assert_eq!(
2006 args.verifier.verifier_url,
2007 Some("http://localhost:3000/api/verify".to_string())
2008 );
2009 }
2010
2011 #[test]
2012 fn can_extract_code_size_limit() {
2013 let args = ScriptArgs::parse_from([
2014 "foundry-cli",
2015 "script",
2016 "script/Test.s.sol:TestScript",
2017 "--fork-url",
2018 "http://localhost:8545",
2019 "--broadcast",
2020 "--code-size-limit",
2021 "50000",
2022 ]);
2023 assert_eq!(args.evm.env.code_size_limit, Some(50000));
2024 }
2025
2026 #[test]
2029 fn cli_code_size_limit_is_honoured_by_check() {
2030 let args = ScriptArgs::parse_from([
2031 "foundry-cli",
2032 "script",
2033 "script/Test.s.sol:TestScript",
2034 "--code-size-limit",
2035 "2147483647",
2036 ]);
2037 assert_eq!(args.evm.env.code_size_limit, Some(2147483647));
2040 }
2041
2042 #[test]
2043 fn can_extract_script_etherscan_key() {
2044 let temp = tempdir().unwrap();
2045 let root = temp.path();
2046
2047 let config = r#"
2048 [profile.default]
2049 etherscan_api_key = "amoy"
2050
2051 [etherscan]
2052 amoy = { key = "https://etherscan-amoy.com/" }
2053 "#;
2054
2055 let toml_file = root.join(Config::FILE_NAME);
2056 fs::write(toml_file, config).unwrap();
2057 let args = ScriptArgs::parse_from([
2058 "foundry-cli",
2059 "Contract.sol",
2060 "--etherscan-api-key",
2061 "amoy",
2062 "--root",
2063 root.as_os_str().to_str().unwrap(),
2064 ]);
2065
2066 let config = args.load_config().unwrap();
2067 let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
2068 assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
2069 }
2070
2071 #[test]
2072 fn can_extract_script_rpc_alias() {
2073 let temp = tempdir().unwrap();
2074 let root = temp.path();
2075
2076 let config = r#"
2077 [profile.default]
2078
2079 [rpc_endpoints]
2080 polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_CAN_EXTRACT_RPC_ALIAS}"
2081 "#;
2082
2083 let toml_file = root.join(Config::FILE_NAME);
2084 fs::write(toml_file, config).unwrap();
2085 let args = ScriptArgs::parse_from([
2086 "foundry-cli",
2087 "DeployV1",
2088 "--rpc-url",
2089 "polygonAmoy",
2090 "--root",
2091 root.as_os_str().to_str().unwrap(),
2092 ]);
2093
2094 let err = args.load_config_and_evm_opts().unwrap_err();
2095
2096 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
2097
2098 unsafe {
2099 std::env::set_var("_CAN_EXTRACT_RPC_ALIAS", "123456");
2100 }
2101 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
2102 assert_eq!(config.eth_rpc_url, Some("polygonAmoy".to_string()));
2103 assert_eq!(
2104 evm_opts.fork_url,
2105 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
2106 );
2107 }
2108
2109 #[test]
2110 fn can_extract_script_rpc_and_etherscan_alias() {
2111 let temp = tempdir().unwrap();
2112 let root = temp.path();
2113
2114 let config = r#"
2115 [profile.default]
2116
2117 [rpc_endpoints]
2118 amoy = "https://polygon-amoy.g.alchemy.com/v2/${_EXTRACT_RPC_ALIAS}"
2119
2120 [etherscan]
2121 amoy = { key = "${_ETHERSCAN_API_KEY}", chain = 80002, url = "https://amoy.polygonscan.com/" }
2122 "#;
2123
2124 let toml_file = root.join(Config::FILE_NAME);
2125 fs::write(toml_file, config).unwrap();
2126 let args = ScriptArgs::parse_from([
2127 "foundry-cli",
2128 "DeployV1",
2129 "--rpc-url",
2130 "amoy",
2131 "--etherscan-api-key",
2132 "amoy",
2133 "--root",
2134 root.as_os_str().to_str().unwrap(),
2135 ]);
2136 let err = args.load_config_and_evm_opts().unwrap_err();
2137
2138 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
2139
2140 unsafe {
2141 std::env::set_var("_EXTRACT_RPC_ALIAS", "123456");
2142 }
2143 unsafe {
2144 std::env::set_var("_ETHERSCAN_API_KEY", "etherscan_api_key");
2145 }
2146 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
2147 assert_eq!(config.eth_rpc_url, Some("amoy".to_string()));
2148 assert_eq!(
2149 evm_opts.fork_url,
2150 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
2151 );
2152 let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
2153 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
2154 let etherscan = config.get_etherscan_api_key(None);
2155 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
2156 }
2157
2158 #[test]
2159 fn can_extract_script_rpc_and_sole_etherscan_alias() {
2160 let temp = tempdir().unwrap();
2161 let root = temp.path();
2162
2163 let config = r#"
2164 [profile.default]
2165
2166 [rpc_endpoints]
2167 amoy = "https://polygon-amoy.g.alchemy.com/v2/${_SOLE_EXTRACT_RPC_ALIAS}"
2168
2169 [etherscan]
2170 amoy = { key = "${_SOLE_ETHERSCAN_API_KEY}" }
2171 "#;
2172
2173 let toml_file = root.join(Config::FILE_NAME);
2174 fs::write(toml_file, config).unwrap();
2175 let args = ScriptArgs::parse_from([
2176 "foundry-cli",
2177 "DeployV1",
2178 "--rpc-url",
2179 "amoy",
2180 "--root",
2181 root.as_os_str().to_str().unwrap(),
2182 ]);
2183 let err = args.load_config_and_evm_opts().unwrap_err();
2184
2185 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
2186
2187 unsafe {
2188 std::env::set_var("_SOLE_EXTRACT_RPC_ALIAS", "123456");
2189 }
2190 unsafe {
2191 std::env::set_var("_SOLE_ETHERSCAN_API_KEY", "etherscan_api_key");
2192 }
2193 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
2194 assert_eq!(
2195 evm_opts.fork_url,
2196 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
2197 );
2198 let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
2199 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
2200 let etherscan = config.get_etherscan_api_key(None);
2201 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
2202 }
2203
2204 #[test]
2206 fn test_5923() {
2207 let args =
2208 ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--priority-gas-price", "100"]);
2209 assert!(args.priority_gas_price.is_some());
2210 }
2211
2212 #[test]
2213 fn test_eip1559_fee_estimate() {
2214 let args = ScriptArgs::parse_from(["foundry-cli", "DeployV1"]);
2216 assert!(args.eip1559_fee_estimate.is_none());
2217
2218 let args = ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--estimate", "aggressive"]);
2219 assert_eq!(args.eip1559_fee_estimate, Some(Eip1559FeeEstimatePreset::Aggressive));
2220 }
2221
2222 #[test]
2224 fn test_5910() {
2225 let args = ScriptArgs::parse_from([
2226 "foundry-cli",
2227 "--broadcast",
2228 "--with-gas-price",
2229 "0",
2230 "SolveTutorial",
2231 ]);
2232 assert!(args.with_gas_price.unwrap().is_zero());
2233 }
2234
2235 #[test]
2236 fn test_priority_gas_price_cannot_exceed_gas_price() {
2237 let args = ScriptArgs::parse_from([
2238 "foundry-cli",
2239 "--broadcast",
2240 "--with-gas-price",
2241 "100",
2242 "--priority-gas-price",
2243 "200",
2244 "Script",
2245 ]);
2246 assert!(args.priority_gas_price.unwrap() > args.with_gas_price.unwrap());
2248 }
2249}