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_chains::{Chain, NamedChain};
17use alloy_json_abi::{Function, JsonAbi};
18use alloy_network::Network;
19use alloy_primitives::{
20 Address, Bytes, Log, U256, hex,
21 map::{AddressHashMap, HashMap},
22};
23use alloy_signer::Signer;
24use broadcast::next_nonce;
25use build::PreprocessedState;
26use clap::{Parser, ValueHint};
27use dialoguer::Confirm;
28use eyre::{ContextCompat, Result};
29use forge_script_sequence::{AdditionalContract, NestedValue};
30use forge_verify::{RetryArgs, VerifierArgs};
31use foundry_cli::{
32 opts::{BuildOpts, EvmArgs, GlobalArgs, TempoOpts},
33 utils::LoadConfig,
34};
35use foundry_common::{
36 ContractsByArtifact, SELECTOR_LEN,
37 abi::{encode_function_args, get_func},
38 compile::ContractSizeLimits,
39 shell,
40 tempo::resolve_fee_token,
41};
42use foundry_compilers::ArtifactId;
43use foundry_config::{
44 Config, figment,
45 figment::{
46 Metadata, Profile, Provider,
47 value::{Dict, Map},
48 },
49};
50#[cfg(feature = "optimism")]
51use foundry_evm::core::evm::OpEvmNetwork;
52use foundry_evm::{
53 backend::Backend,
54 core::{
55 Breakpoints, FoundryTransaction,
56 evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor},
57 },
58 executors::ExecutorBuilder,
59 inspectors::{
60 CheatsConfig,
61 cheatcodes::{BroadcastableTransactions, Wallets},
62 },
63 opts::EvmOpts,
64 revm::interpreter::InstructionResult,
65 traces::{TraceMode, 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 multi_sequence;
76mod progress;
77mod providers;
78mod receipts;
79mod runner;
80mod sequence;
81mod session;
82mod simulate;
83mod transaction;
84mod verify;
85mod wallet_session;
86
87pub use wallet_session::ScriptWalletSessionArgs;
88
89foundry_config::merge_impl_figment_convert!(ScriptArgs, build, evm);
91
92#[derive(Clone, Debug, Default, Parser)]
94pub struct ScriptArgs {
95 #[command(flatten)]
97 pub global: GlobalArgs,
98
99 #[arg(value_hint = ValueHint::FilePath)]
104 pub path: String,
105
106 pub args: Vec<String>,
108
109 #[arg(long, visible_alias = "tc", value_name = "CONTRACT_NAME")]
111 pub target_contract: Option<String>,
112
113 #[arg(long, short, default_value = "run")]
115 pub sig: String,
116
117 #[arg(
119 long,
120 env = "ETH_PRIORITY_GAS_PRICE",
121 value_parser = foundry_cli::utils::parse_ether_value,
122 value_name = "PRICE"
123 )]
124 pub priority_gas_price: Option<U256>,
125
126 #[arg(long)]
130 pub legacy: bool,
131
132 #[arg(long)]
134 pub broadcast: bool,
135
136 #[arg(long)]
142 pub batch: bool,
143
144 #[command(flatten)]
146 pub tempo: TempoOpts,
147
148 #[command(flatten)]
150 pub wallet_session: ScriptWalletSessionArgs,
151
152 #[arg(long)]
154 pub skip_simulation: bool,
155
156 #[arg(long, short, default_value = "130")]
158 pub gas_estimate_multiplier: u64,
159
160 #[arg(
162 long,
163 conflicts_with_all = &["private_key", "private_keys", "ledger", "trezor", "aws", "browser"],
164 )]
165 pub unlocked: bool,
166
167 #[arg(long)]
174 pub resume: bool,
175
176 #[arg(long)]
178 pub multi: bool,
179
180 #[arg(long)]
184 pub debug: bool,
185
186 #[arg(
188 long,
189 requires = "debug",
190 value_hint = ValueHint::FilePath,
191 value_name = "PATH"
192 )]
193 pub dump: Option<PathBuf>,
194
195 #[arg(long)]
198 pub slow: bool,
199
200 #[arg(long)]
204 pub non_interactive: bool,
205
206 #[arg(long)]
208 pub disable_code_size_limit: bool,
209
210 #[arg(long)]
212 pub disable_labels: bool,
213
214 #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
216 pub etherscan_api_key: Option<String>,
217
218 #[arg(long, requires = "broadcast")]
220 pub verify: bool,
221
222 #[arg(
227 long,
228 env = "ETH_GAS_PRICE",
229 value_parser = foundry_cli::utils::parse_ether_value,
230 value_name = "PRICE",
231 )]
232 pub with_gas_price: Option<U256>,
233
234 #[arg(long, env = "ETH_TIMEOUT")]
236 pub timeout: Option<u64>,
237
238 #[command(flatten)]
239 pub build: BuildOpts,
240
241 #[command(flatten)]
242 pub wallets: MultiWalletOpts,
243
244 #[command(flatten)]
245 pub evm: EvmArgs,
246
247 #[command(flatten)]
248 pub verifier: VerifierArgs,
249
250 #[command(flatten)]
251 pub retry: RetryArgs,
252}
253
254impl ScriptArgs {
255 fn has_tempo_session(&self) -> Result<bool> {
256 Ok(self.tempo.session_id()?.is_some())
257 }
258
259 async fn resolved_evm_opts(&self) -> Result<(Config, EvmOpts)> {
261 let (config, mut evm_opts) = self.load_config_and_evm_opts()?;
262
263 if self.tempo.is_tempo() || self.has_tempo_session()? {
264 evm_opts.networks = NetworkConfigs::with_tempo();
266 } else {
267 evm_opts.infer_network_from_fork().await;
269 }
270
271 Ok((config, evm_opts))
272 }
273
274 async fn preprocess<FEN: FoundryEvmNetwork>(
275 self,
276 config: Config,
277 mut evm_opts: EvmOpts,
278 ) -> Result<PreprocessedState<FEN>> {
279 let args = self;
280 let mut tempo = args.tempo.clone();
281
282 let session_sender = if args.resume {
283 None
284 } else {
285 tempo.session_sender_for_multi_wallet(&args.wallets, args.evm.sender)?
288 };
289
290 let script_wallets = Wallets::new(args.wallets.get_multi_wallet().await?, args.evm.sender);
291 let browser_wallet = args.wallets.browser_signer::<FEN::Network>().await?;
292
293 if let Some(sender) = session_sender {
294 evm_opts.sender = sender;
295 } else if let Some(sender) = args.maybe_load_private_key()? {
296 evm_opts.sender = sender;
297 } else if args.evm.sender.is_none() {
298 if let Ok(signers) = script_wallets.signers()
302 && signers.len() == 1
303 {
304 evm_opts.sender = signers[0];
305 } else if let Some(signer) = browser_wallet.as_ref().map(|b| b.address()) {
306 evm_opts.sender = signer
307 }
308 }
309
310 tempo.resolve_expires();
311
312 let chain = evm_opts.networks.is_tempo().then(|| Chain::from_named(NamedChain::Tempo));
314 tempo.fee_token = resolve_fee_token(chain, tempo.fee_token);
315
316 let script_config = ScriptConfig::new(config, evm_opts, args.batch, tempo).await?;
317 Ok(PreprocessedState { args, script_config, script_wallets, browser_wallet })
318 }
319
320 #[allow(clippy::large_stack_frames)]
322 pub async fn run_script(self) -> Result<()> {
323 trace!(target: "script", "executing script command");
324
325 if self.wallet_session.enabled {
326 return self.run_wallet_session_wrapper();
327 }
328
329 let (config, evm_opts) = self.resolved_evm_opts().await?;
330
331 let is_tempo = evm_opts.networks.is_tempo();
332
333 if self.batch && !is_tempo {
334 eyre::bail!("--batch mode is only supported on Tempo networks");
335 }
336
337 if self.unlocked && self.has_tempo_session()? {
338 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --unlocked");
339 }
340
341 if is_tempo {
342 let batch = self.batch;
343 let bundled = match self.prepare_bundled::<TempoEvmNetwork>(config, evm_opts).await? {
344 Some(bundled) => bundled,
345 None => return Ok(()),
346 };
347 let bundled = if batch { bundled } else { bundled.wait_for_pending().await? };
351 let broadcasted =
352 if batch { bundled.broadcast_batch().await? } else { bundled.broadcast().await? };
353 if broadcasted.args.verify {
354 broadcasted.verify().await?;
355 }
356 return Ok(());
357 }
358
359 #[cfg(feature = "optimism")]
360 if evm_opts.networks.is_optimism() {
361 return self.run_generic_script::<OpEvmNetwork>(config, evm_opts).await;
362 }
363
364 self.run_generic_script::<EthEvmNetwork>(config, evm_opts).await
365 }
366
367 #[allow(clippy::large_stack_frames)]
371 async fn prepare_bundled<FEN: FoundryEvmNetwork>(
372 self,
373 config: Config,
374 evm_opts: EvmOpts,
375 ) -> Result<Option<BundledState<FEN>>> {
376 let state = self.preprocess::<FEN>(config, evm_opts).await?;
377 let create2_deployer = state.script_config.evm_opts.create2_deployer;
378 let compiled = state.compile()?;
379
380 let bundled = if compiled.args.resume {
383 compiled.resume().await?
384 } else {
385 let pre_simulation = compiled
387 .link()
388 .await?
389 .prepare_execution()
390 .await?
391 .execute()
392 .await?
393 .prepare_simulation()
394 .await?;
395
396 if pre_simulation.args.debug {
397 return match pre_simulation.args.dump.clone() {
398 Some(path) => pre_simulation.dump_debugger(&path).map(|_| None),
399 None => pre_simulation.run_debugger().map(|_| None),
400 };
401 }
402
403 if shell::is_json() {
404 pre_simulation.show_json().await?;
405 } else {
406 pre_simulation.show_traces().await?;
407 }
408
409 if pre_simulation
412 .execution_result
413 .transactions
414 .as_ref()
415 .is_none_or(|txs| txs.is_empty())
416 {
417 if pre_simulation.args.broadcast {
418 sh_warn!("No transactions to broadcast.")?;
419 }
420
421 return Ok(None);
422 }
423
424 if pre_simulation.execution_artifacts.rpc_data.missing_rpc {
426 if !shell::is_json() {
427 sh_println!("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?;
428 }
429
430 return Ok(None);
431 }
432
433 let size_limits = pre_simulation
434 .script_config
435 .config
436 .code_size_limit
437 .map(ContractSizeLimits::with_runtime_limit)
438 .unwrap_or_default();
439 pre_simulation.args.check_contract_sizes(
440 size_limits,
441 &pre_simulation.execution_result,
442 &pre_simulation.build_data.known_contracts,
443 create2_deployer,
444 )?;
445
446 pre_simulation.fill_metadata().await?.bundle().await?
447 };
448
449 if !bundled.args.should_broadcast() {
451 if !shell::is_json() {
452 if shell::verbosity() >= 4 {
453 sh_println!("\n=== Transactions that will be broadcast ===\n")?;
454 bundled.sequence.show_transactions()?;
455 }
456
457 sh_println!(
458 "\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more."
459 )?;
460 }
461 return Ok(None);
462 }
463
464 if bundled.args.verify {
466 bundled.verify_preflight_check().await?;
467 }
468
469 Ok(Some(bundled))
470 }
471
472 async fn run_generic_script<FEN: FoundryEvmNetwork>(
473 self,
474 config: Config,
475 evm_opts: EvmOpts,
476 ) -> Result<()> {
477 let bundled = match self.prepare_bundled::<FEN>(config, evm_opts).await? {
478 Some(bundled) => bundled,
479 None => return Ok(()),
480 };
481
482 let broadcasted = bundled.wait_for_pending().await?.broadcast().await?;
484
485 if broadcasted.args.verify {
486 broadcasted.verify().await?;
487 }
488
489 Ok(())
490 }
491
492 fn maybe_load_private_key(&self) -> Result<Option<Address>> {
495 if let Some(turnkey_address) = self.wallets.turnkey_address() {
496 return Ok(Some(turnkey_address));
497 }
498
499 let maybe_sender = self
500 .wallets
501 .private_keys()?
502 .filter(|pks| pks.len() == 1)
503 .map(|pks| pks.first().unwrap().address());
504 Ok(maybe_sender)
505 }
506
507 fn get_method_and_calldata(&self, abi: &JsonAbi) -> Result<(Function, Bytes)> {
515 if let Ok(decoded) = hex::decode(&self.sig) {
516 let selector = &decoded[..SELECTOR_LEN];
517 let func =
518 abi.functions().find(|func| selector == &func.selector()[..]).ok_or_else(|| {
519 eyre::eyre!(
520 "Function selector `{}` not found in the ABI",
521 hex::encode(selector)
522 )
523 })?;
524 return Ok((func.clone(), decoded.into()));
525 }
526
527 let func = if self.sig.contains('(') {
528 let func = get_func(&self.sig)?;
529 abi.functions()
530 .find(|&abi_func| abi_func.selector() == func.selector())
531 .wrap_err(format!("Function `{}` is not implemented in your script.", self.sig))?
532 } else {
533 let matching_functions =
534 abi.functions().filter(|func| func.name == self.sig).collect::<Vec<_>>();
535 match matching_functions.len() {
536 0 => eyre::bail!("Function `{}` not found in the ABI", self.sig),
537 1 => matching_functions[0],
538 2.. => eyre::bail!(
539 "Multiple functions with the same name `{}` found in the ABI",
540 self.sig
541 ),
542 }
543 };
544 let data = encode_function_args(func, &self.args)?;
545
546 Ok((func.clone(), data.into()))
547 }
548
549 fn check_contract_sizes<N: Network>(
555 &self,
556 size_limits: ContractSizeLimits,
557 result: &ScriptResult<N>,
558 known_contracts: &ContractsByArtifact,
559 create2_deployer: Address,
560 ) -> Result<()> {
561 if self.disable_code_size_limit {
563 return Ok(());
564 }
565
566 let mut bytecodes: Vec<(String, &[u8], &[u8])> = vec![];
568
569 for (artifact, contract) in known_contracts.iter() {
571 let Some(bytecode) = contract.bytecode() else { continue };
572 let Some(deployed_bytecode) = contract.deployed_bytecode() else { continue };
573 bytecodes.push((artifact.name.clone(), bytecode, deployed_bytecode));
574 }
575
576 let create_nodes = result.traces.iter().flat_map(|(_, traces)| {
578 traces.nodes().iter().filter(|node| node.trace.kind.is_any_create())
579 });
580 let mut unknown_c = 0usize;
581 for node in create_nodes {
582 let init_code = &node.trace.data;
583 let deployed_code = &node.trace.output;
584 if !bytecodes.iter().any(|(_, b, _)| *b == init_code.as_ref()) {
585 bytecodes.push((format!("Unknown{unknown_c}"), init_code, deployed_code));
586 unknown_c += 1;
587 }
588 }
589
590 let mut prompt_user = false;
591 let max_size = size_limits.runtime;
592
593 for (data, to) in result.transactions.iter().flat_map(|txes| {
594 txes.iter().filter_map(|tx| {
595 tx.transaction
596 .input()
597 .filter(|data| data.len() > max_size)
598 .map(|data| (data, tx.transaction.to()))
599 })
600 }) {
601 let mut offset = 0;
602
603 if let Some(to) = to {
605 if to == create2_deployer {
606 offset = 32;
608 } else {
609 continue;
610 }
611 }
612
613 if let Some((name, _, deployed_code)) =
615 bytecodes.iter().find(|(_, init_code, _)| *init_code == &data[offset..])
616 {
617 let deployment_size = deployed_code.len();
618
619 if deployment_size > max_size {
620 prompt_user = self.should_broadcast();
621 sh_err!(
622 "`{name}` is above the contract size limit ({deployment_size} > {max_size})."
623 )?;
624 }
625 }
626 }
627
628 if prompt_user
630 && !self.non_interactive
631 && !Confirm::new().with_prompt("Do you wish to continue?".to_string()).interact()?
632 {
633 eyre::bail!("User canceled the script.");
634 }
635
636 Ok(())
637 }
638
639 const fn should_broadcast(&self) -> bool {
641 self.broadcast || self.resume || self.verify
642 }
643}
644
645impl Provider for ScriptArgs {
646 fn metadata(&self) -> Metadata {
647 Metadata::named("Script Args Provider")
648 }
649
650 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
651 let mut dict = Dict::default();
652
653 if let Some(etherscan_api_key) =
654 self.etherscan_api_key.as_ref().filter(|s| !s.trim().is_empty())
655 {
656 dict.insert(
657 "etherscan_api_key".to_string(),
658 figment::value::Value::from(etherscan_api_key.clone()),
659 );
660 }
661
662 if let Some(timeout) = self.timeout {
663 dict.insert("transaction_timeout".to_string(), timeout.into());
664 }
665
666 Ok(Map::from([(Config::selected_profile(), dict)]))
667 }
668}
669
670#[derive(Serialize, Clone)]
671#[serde(bound = "")]
672pub struct ScriptResult<N: Network> {
673 pub success: bool,
674 #[serde(rename = "raw_logs")]
675 pub logs: Vec<Log>,
676 pub traces: Traces,
677 pub gas_used: u64,
678 pub labeled_addresses: AddressHashMap<String>,
679 #[serde(skip)]
680 pub transactions: Option<BroadcastableTransactions<N>>,
681 pub returned: Bytes,
682 #[serde(skip)]
683 pub exit_reason: Option<InstructionResult>,
684 pub address: Option<Address>,
685 #[serde(skip)]
686 pub breakpoints: Breakpoints,
687}
688
689impl<N: Network> Default for ScriptResult<N> {
690 fn default() -> Self {
691 Self {
692 success: Default::default(),
693 logs: Default::default(),
694 traces: Default::default(),
695 gas_used: Default::default(),
696 labeled_addresses: Default::default(),
697 transactions: Default::default(),
698 returned: Default::default(),
699 exit_reason: Default::default(),
700 address: Default::default(),
701 breakpoints: Default::default(),
702 }
703 }
704}
705
706impl<N: Network> ScriptResult<N> {
707 pub fn get_created_contracts(
708 &self,
709 known_contracts: &ContractsByArtifact,
710 ) -> Vec<AdditionalContract> {
711 self.traces
712 .iter()
713 .flat_map(|(_, traces)| {
714 traces.nodes().iter().filter_map(|node| {
715 if node.trace.kind.is_any_create() {
716 let init_code = node.trace.data.clone();
717 let contract_name = known_contracts
718 .find_by_creation_code(init_code.as_ref())
719 .map(|artifact| artifact.0.name.clone());
720 return Some(AdditionalContract {
721 call_kind: node.trace.kind,
722 address: node.trace.address,
723 contract_name,
724 init_code,
725 });
726 }
727 None
728 })
729 })
730 .collect()
731 }
732}
733
734#[derive(Serialize)]
735#[serde(bound = "")]
736struct JsonResult<'a, N: Network> {
737 logs: Vec<String>,
738 returns: &'a HashMap<String, NestedValue>,
739 #[serde(flatten)]
740 result: &'a ScriptResult<N>,
741}
742
743#[derive(Clone, Debug)]
744pub struct ScriptConfig<FEN: FoundryEvmNetwork> {
745 pub config: Config,
746 pub evm_opts: EvmOpts,
747 pub sender_nonce: u64,
748 pub backends: HashMap<String, Backend<FEN>>,
750 pub batch: bool,
752 pub tempo: TempoOpts,
754}
755
756impl<FEN: FoundryEvmNetwork> ScriptConfig<FEN> {
757 pub async fn new(
758 config: Config,
759 evm_opts: EvmOpts,
760 batch: bool,
761 tempo: TempoOpts,
762 ) -> Result<Self> {
763 let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() {
764 next_nonce(evm_opts.sender, fork_url, evm_opts.fork_block_number).await?
765 } else {
766 1
768 };
769
770 Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default(), batch, tempo })
771 }
772
773 pub async fn update_sender(&mut self, sender: Address) -> Result<()> {
774 self.sender_nonce = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
775 next_nonce(sender, fork_url, None).await?
776 } else {
777 1
779 };
780 self.evm_opts.sender = sender;
781 Ok(())
782 }
783
784 pub(crate) async fn update_tempo_session_sender(
785 &mut self,
786 wallets: &MultiWalletOpts,
787 expected_sender: Option<Address>,
788 ) -> Result<()> {
789 if let Some(sender) =
790 self.tempo.session_sender_for_multi_wallet(wallets, expected_sender)?
791 {
792 self.update_sender(sender).await?;
793 }
794 Ok(())
795 }
796
797 async fn get_runner(&mut self) -> Result<ScriptRunner<FEN>> {
798 self._get_runner(None, false).await
799 }
800
801 async fn get_runner_with_cheatcodes(
802 &mut self,
803 known_contracts: ContractsByArtifact,
804 script_wallets: Wallets,
805 debug: bool,
806 target: ArtifactId,
807 ) -> Result<ScriptRunner<FEN>> {
808 self._get_runner(Some((known_contracts, script_wallets, target)), debug).await
809 }
810
811 async fn _get_runner(
812 &mut self,
813 cheats_data: Option<(ContractsByArtifact, Wallets, ArtifactId)>,
814 debug: bool,
815 ) -> Result<ScriptRunner<FEN>> {
816 trace!("preparing script runner");
817 let (evm_env, mut tx_env, fork_block) = self.evm_opts.env::<_, _, TxEnvFor<FEN>>().await?;
818
819 let db = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
820 match self.backends.get(fork_url) {
821 Some(db) => db.clone(),
822 None => {
823 let fork =
824 self.evm_opts.get_fork(&self.config, evm_env.cfg_env.chain_id, fork_block);
825 let backend = Backend::spawn(fork)?;
826 self.backends.insert(fork_url.clone(), backend.clone());
827 backend
828 }
829 }
830 } else {
831 Backend::spawn(None)?
835 };
836
837 let mut builder = ExecutorBuilder::default()
839 .inspectors(|stack| {
840 stack
841 .logs(self.config.live_logs)
842 .trace_mode(if debug { TraceMode::Debug } else { TraceMode::Call })
843 .networks(self.evm_opts.networks)
844 .create2_deployer(self.evm_opts.create2_deployer)
845 })
846 .spec_id(self.config.evm_spec_id())
847 .gas_limit(self.evm_opts.gas_limit())
848 .legacy_assertions(self.config.legacy_assertions);
849
850 if let Some((known_contracts, script_wallets, target)) = cheats_data {
851 builder = builder.inspectors(|stack| {
852 stack
853 .cheatcodes(
854 CheatsConfig::new(
855 &self.config,
856 self.evm_opts.clone(),
857 Some(known_contracts),
858 Some(target),
859 self.tempo.fee_token,
860 self.batch,
861 )
862 .into(),
863 )
864 .wallets(script_wallets)
865 .enable_isolation(self.evm_opts.isolate)
866 });
867 }
868
869 tx_env.set_fee_token(self.tempo.fee_token);
872
873 Ok(ScriptRunner::new(builder.build(evm_env, tx_env, db), self.evm_opts.clone()))
874 }
875}
876
877#[cfg(test)]
878mod tests {
879 use super::*;
880 use alloy_network::Ethereum;
881 use alloy_primitives::{B256, address};
882 use foundry_cli::opts::TEMPO_SESSION_ID_ENV;
883 use foundry_common::tempo::{
884 KeyType, SessionEntry, SessionKeyMaterial, SessionStatus, TEMPO_HOME_ENV,
885 upsert_session_entry,
886 };
887 use foundry_config::UnresolvedEnvVarError;
888 use std::{fs, sync::LazyLock};
889 use tempfile::tempdir;
890 use tokio::sync::{Mutex, MutexGuard};
891
892 const SESSION_PRIVATE_KEY: &str =
893 "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
894 const SESSION_ID_HEX: &str =
895 "0x1111111111111111111111111111111111111111111111111111111111111111";
896 const SESSION_ROOT_ADDRESS: &str = "0x1111111111111111111111111111111111111111";
897 static TEMPO_HOME_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
898
899 fn active_session_entry(
900 session_id: B256,
901 root_account: Address,
902 chain_id: u64,
903 ) -> SessionEntry {
904 let key = foundry_wallets::utils::create_private_key_signer(SESSION_PRIVATE_KEY).unwrap();
905 SessionEntry {
906 session_id,
907 root_account,
908 chain_id,
909 key_address: key.address(),
910 expiry: u64::MAX,
911 scope: None,
912 limits: None,
913 status: SessionStatus::Active,
914 key: Some(SessionKeyMaterial {
915 key_type: KeyType::Secp256k1,
916 key: SESSION_PRIVATE_KEY.to_string(),
917 key_authorization: None,
918 }),
919 }
920 }
921
922 struct TempoHomeGuard {
923 _guard: MutexGuard<'static, ()>,
924 }
925
926 impl TempoHomeGuard {
927 async fn set(path: &std::path::Path) -> Self {
928 let guard = TEMPO_HOME_LOCK.lock().await;
929 unsafe {
931 std::env::remove_var(TEMPO_SESSION_ID_ENV);
932 std::env::set_var(TEMPO_HOME_ENV, path);
933 }
934 Self { _guard: guard }
935 }
936 }
937
938 impl Drop for TempoHomeGuard {
939 fn drop(&mut self) {
940 unsafe {
942 std::env::remove_var(TEMPO_HOME_ENV);
943 std::env::remove_var(TEMPO_SESSION_ID_ENV);
944 }
945 }
946 }
947
948 fn session_root() -> Address {
949 SESSION_ROOT_ADDRESS.parse().unwrap()
950 }
951
952 #[test]
953 fn can_parse_sig() {
954 let sig = "0x522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266";
955 let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--sig", sig]);
956 assert_eq!(args.sig, sig);
957 }
958
959 #[test]
960 fn can_parse_shared_tempo_opts() {
961 let args = ScriptArgs::parse_from([
962 "foundry-cli",
963 "Contract.sol",
964 "--tempo.fee-token",
965 "1",
966 "--tempo.expires",
967 "10",
968 ]);
969
970 assert_eq!(
971 args.tempo.fee_token,
972 Some(address!("0x20C0000000000000000000000000000000000001"))
973 );
974 assert_eq!(args.tempo.expires, Some(10));
975 }
976
977 #[test]
978 fn can_parse_sponsor_tempo_opts() {
979 let args = ScriptArgs::parse_from([
980 "foundry-cli",
981 "Contract.sol",
982 "--tempo.sponsor",
983 SESSION_ROOT_ADDRESS,
984 "--tempo.sponsor-signer",
985 "env://TEMPO_SPONSOR_PK",
986 ]);
987
988 assert_eq!(args.tempo.sponsor, Some(session_root()));
989 assert_eq!(args.tempo.sponsor_signer.as_deref(), Some("env://TEMPO_SPONSOR_PK"));
990 }
991
992 #[test]
993 fn can_parse_full_tempo_opts() {
994 let args =
995 ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--tempo.nonce-key", "1"]);
996
997 assert_eq!(args.tempo.nonce_key, Some(U256::from(1)));
998 }
999
1000 #[test]
1001 fn can_parse_tempo_session_opt() {
1002 let args = ScriptArgs::parse_from([
1003 "foundry-cli",
1004 "Contract.sol",
1005 "--tempo.session",
1006 SESSION_ID_HEX,
1007 ]);
1008
1009 assert_eq!(args.tempo.session, Some(B256::from([0x11; 32])),);
1010 }
1011
1012 #[tokio::test]
1013 async fn tempo_session_sets_script_sender_to_root_account() {
1014 let temp = tempdir().unwrap();
1015 let session_id = B256::from([0x22; 32]);
1016 let root = session_root();
1017 let chain_id = foundry_common::DEV_CHAIN_ID;
1018
1019 let _guard = TempoHomeGuard::set(temp.path()).await;
1020 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1021
1022 let args = ScriptArgs::parse_from([
1023 "foundry-cli",
1024 "Contract.sol",
1025 "--tempo.session",
1026 &format!("{session_id:?}"),
1027 ]);
1028 let evm_opts = EvmOpts {
1029 networks: NetworkConfigs::with_tempo(),
1030 env: foundry_evm::opts::Env { chain_id: Some(chain_id), ..Default::default() },
1031 ..Default::default()
1032 };
1033
1034 let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1035 assert_eq!(state.script_config.evm_opts.sender, root);
1036 }
1037
1038 #[tokio::test]
1039 async fn tempo_session_resume_multi_defers_session_sender_until_reexecution() {
1040 let temp = tempdir().unwrap();
1041 let session_id = B256::from([0x55; 32]);
1042 let root = session_root();
1043 let chain_id = 4217;
1044
1045 let _guard = TempoHomeGuard::set(temp.path()).await;
1046 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1047
1048 let args = ScriptArgs::parse_from([
1049 "foundry-cli",
1050 "Contract.sol",
1051 "--resume",
1052 "--multi",
1053 "--tempo.session",
1054 &format!("{session_id:?}"),
1055 ]);
1056 let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
1057
1058 let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1059 assert_ne!(state.script_config.evm_opts.sender, root);
1060 }
1061
1062 #[tokio::test]
1063 async fn tempo_session_resume_defers_session_sender_until_reexecution() {
1064 let temp = tempdir().unwrap();
1065 let session_id = B256::from([0x77; 32]);
1066 let root = session_root();
1067 let chain_id = 4217;
1068
1069 let _guard = TempoHomeGuard::set(temp.path()).await;
1070 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1071
1072 let args = ScriptArgs::parse_from([
1073 "foundry-cli",
1074 "Contract.sol",
1075 "--resume",
1076 "--tempo.session",
1077 &format!("{session_id:?}"),
1078 ]);
1079 let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
1080
1081 let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1082 assert_ne!(state.script_config.evm_opts.sender, root);
1083 }
1084
1085 #[tokio::test]
1086 async fn tempo_session_non_resume_multi_sets_sender_without_chain_validation() {
1087 let temp = tempdir().unwrap();
1088 let session_id = B256::from([0x66; 32]);
1089 let root = session_root();
1090 let chain_id = 4217;
1091
1092 let _guard = TempoHomeGuard::set(temp.path()).await;
1093 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1094
1095 let args = ScriptArgs::parse_from([
1096 "foundry-cli",
1097 "Contract.sol",
1098 "--multi",
1099 "--tempo.session",
1100 &format!("{session_id:?}"),
1101 ]);
1102 let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
1103
1104 let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1105 assert_eq!(state.script_config.evm_opts.sender, root);
1106 }
1107
1108 #[tokio::test]
1109 async fn tempo_session_initial_broadcast_sets_sender_without_chain_validation() {
1110 let temp = tempdir().unwrap();
1111 let session_id = B256::from([0x88; 32]);
1112 let root = session_root();
1113 let chain_id = 4217;
1114
1115 let _guard = TempoHomeGuard::set(temp.path()).await;
1116 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1117
1118 let args = ScriptArgs::parse_from([
1119 "foundry-cli",
1120 "Contract.sol",
1121 "--broadcast",
1122 "--tempo.session",
1123 &format!("{session_id:?}"),
1124 ]);
1125 let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
1126
1127 let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1128 assert_eq!(state.script_config.evm_opts.sender, root);
1129 }
1130
1131 #[tokio::test]
1132 async fn tempo_session_env_selects_tempo_network() {
1133 let temp = tempdir().unwrap();
1134 let _guard = TempoHomeGuard::set(temp.path()).await;
1135 let session_id = B256::from([0x44; 32]);
1136 unsafe { std::env::set_var(TEMPO_SESSION_ID_ENV, format!("{session_id:?}")) };
1138
1139 let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol"]);
1140 let (_, evm_opts) = args.resolved_evm_opts().await.unwrap();
1141
1142 assert!(evm_opts.networks.is_tempo());
1143 }
1144
1145 #[tokio::test]
1146 async fn tempo_session_rejects_explicit_script_wallet_signer() {
1147 let temp = tempdir().unwrap();
1148 let session_id = B256::from([0x33; 32]);
1149 let root = session_root();
1150 let chain_id = foundry_common::DEV_CHAIN_ID;
1151
1152 let _guard = TempoHomeGuard::set(temp.path()).await;
1153 upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1154
1155 let args = ScriptArgs::parse_from([
1156 "foundry-cli",
1157 "Contract.sol",
1158 "--tempo.session",
1159 &format!("{session_id:?}"),
1160 "--private-key",
1161 SESSION_PRIVATE_KEY,
1162 ]);
1163 let evm_opts = EvmOpts {
1164 networks: NetworkConfigs::with_tempo(),
1165 env: foundry_evm::opts::Env { chain_id: Some(chain_id), ..Default::default() },
1166 ..Default::default()
1167 };
1168
1169 let err = match args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await {
1170 Ok(_) => panic!("expected --tempo.session with --private-key to fail"),
1171 Err(err) => err,
1172 };
1173 assert!(err.to_string().contains("explicit wallet signer"), "{err}");
1174 }
1175
1176 #[test]
1177 fn can_parse_unlocked() {
1178 let args = ScriptArgs::parse_from([
1179 "foundry-cli",
1180 "Contract.sol",
1181 "--sender",
1182 "0x4e59b44847b379578588920ca78fbf26c0b4956c",
1183 "--unlocked",
1184 ]);
1185 assert!(args.unlocked);
1186
1187 let key = U256::ZERO;
1188 let args = ScriptArgs::try_parse_from([
1189 "foundry-cli",
1190 "Contract.sol",
1191 "--sender",
1192 "0x4e59b44847b379578588920ca78fbf26c0b4956c",
1193 "--unlocked",
1194 "--private-key",
1195 &key.to_string(),
1196 ]);
1197 assert!(args.is_err());
1198 }
1199
1200 #[test]
1201 fn can_merge_script_config() {
1202 let args = ScriptArgs::parse_from([
1203 "foundry-cli",
1204 "Contract.sol",
1205 "--etherscan-api-key",
1206 "goerli",
1207 ]);
1208 let config = args.load_config().unwrap();
1209 assert_eq!(config.etherscan_api_key, Some("goerli".to_string()));
1210 }
1211
1212 #[test]
1213 fn can_disable_code_size_limit() {
1214 let args =
1215 ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--disable-code-size-limit"]);
1216 assert!(args.disable_code_size_limit);
1217
1218 let result = ScriptResult::<Ethereum>::default();
1219 let contracts = ContractsByArtifact::default();
1220 let create = Address::ZERO;
1221 assert!(
1222 args.check_contract_sizes(ContractSizeLimits::default(), &result, &contracts, create)
1223 .is_ok()
1224 );
1225 }
1226
1227 #[test]
1228 fn can_parse_verifier_url() {
1229 let args = ScriptArgs::parse_from([
1230 "foundry-cli",
1231 "script",
1232 "script/Test.s.sol:TestScript",
1233 "--fork-url",
1234 "http://localhost:8545",
1235 "--verifier-url",
1236 "http://localhost:3000/api/verify",
1237 "--etherscan-api-key",
1238 "blacksmith",
1239 "--broadcast",
1240 "--verify",
1241 "-vvvvv",
1242 ]);
1243 assert_eq!(
1244 args.verifier.verifier_url,
1245 Some("http://localhost:3000/api/verify".to_string())
1246 );
1247 }
1248
1249 #[test]
1250 fn can_extract_code_size_limit() {
1251 let args = ScriptArgs::parse_from([
1252 "foundry-cli",
1253 "script",
1254 "script/Test.s.sol:TestScript",
1255 "--fork-url",
1256 "http://localhost:8545",
1257 "--broadcast",
1258 "--code-size-limit",
1259 "50000",
1260 ]);
1261 assert_eq!(args.evm.env.code_size_limit, Some(50000));
1262 }
1263
1264 #[test]
1265 fn can_extract_script_etherscan_key() {
1266 let temp = tempdir().unwrap();
1267 let root = temp.path();
1268
1269 let config = r#"
1270 [profile.default]
1271 etherscan_api_key = "amoy"
1272
1273 [etherscan]
1274 amoy = { key = "https://etherscan-amoy.com/" }
1275 "#;
1276
1277 let toml_file = root.join(Config::FILE_NAME);
1278 fs::write(toml_file, config).unwrap();
1279 let args = ScriptArgs::parse_from([
1280 "foundry-cli",
1281 "Contract.sol",
1282 "--etherscan-api-key",
1283 "amoy",
1284 "--root",
1285 root.as_os_str().to_str().unwrap(),
1286 ]);
1287
1288 let config = args.load_config().unwrap();
1289 let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
1290 assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
1291 }
1292
1293 #[test]
1294 fn can_extract_script_rpc_alias() {
1295 let temp = tempdir().unwrap();
1296 let root = temp.path();
1297
1298 let config = r#"
1299 [profile.default]
1300
1301 [rpc_endpoints]
1302 polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_CAN_EXTRACT_RPC_ALIAS}"
1303 "#;
1304
1305 let toml_file = root.join(Config::FILE_NAME);
1306 fs::write(toml_file, config).unwrap();
1307 let args = ScriptArgs::parse_from([
1308 "foundry-cli",
1309 "DeployV1",
1310 "--rpc-url",
1311 "polygonAmoy",
1312 "--root",
1313 root.as_os_str().to_str().unwrap(),
1314 ]);
1315
1316 let err = args.load_config_and_evm_opts().unwrap_err();
1317
1318 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
1319
1320 unsafe {
1321 std::env::set_var("_CAN_EXTRACT_RPC_ALIAS", "123456");
1322 }
1323 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
1324 assert_eq!(config.eth_rpc_url, Some("polygonAmoy".to_string()));
1325 assert_eq!(
1326 evm_opts.fork_url,
1327 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
1328 );
1329 }
1330
1331 #[test]
1332 fn can_extract_script_rpc_and_etherscan_alias() {
1333 let temp = tempdir().unwrap();
1334 let root = temp.path();
1335
1336 let config = r#"
1337 [profile.default]
1338
1339 [rpc_endpoints]
1340 amoy = "https://polygon-amoy.g.alchemy.com/v2/${_EXTRACT_RPC_ALIAS}"
1341
1342 [etherscan]
1343 amoy = { key = "${_ETHERSCAN_API_KEY}", chain = 80002, url = "https://amoy.polygonscan.com/" }
1344 "#;
1345
1346 let toml_file = root.join(Config::FILE_NAME);
1347 fs::write(toml_file, config).unwrap();
1348 let args = ScriptArgs::parse_from([
1349 "foundry-cli",
1350 "DeployV1",
1351 "--rpc-url",
1352 "amoy",
1353 "--etherscan-api-key",
1354 "amoy",
1355 "--root",
1356 root.as_os_str().to_str().unwrap(),
1357 ]);
1358 let err = args.load_config_and_evm_opts().unwrap_err();
1359
1360 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
1361
1362 unsafe {
1363 std::env::set_var("_EXTRACT_RPC_ALIAS", "123456");
1364 }
1365 unsafe {
1366 std::env::set_var("_ETHERSCAN_API_KEY", "etherscan_api_key");
1367 }
1368 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
1369 assert_eq!(config.eth_rpc_url, Some("amoy".to_string()));
1370 assert_eq!(
1371 evm_opts.fork_url,
1372 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
1373 );
1374 let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
1375 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1376 let etherscan = config.get_etherscan_api_key(None);
1377 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1378 }
1379
1380 #[test]
1381 fn can_extract_script_rpc_and_sole_etherscan_alias() {
1382 let temp = tempdir().unwrap();
1383 let root = temp.path();
1384
1385 let config = r#"
1386 [profile.default]
1387
1388 [rpc_endpoints]
1389 amoy = "https://polygon-amoy.g.alchemy.com/v2/${_SOLE_EXTRACT_RPC_ALIAS}"
1390
1391 [etherscan]
1392 amoy = { key = "${_SOLE_ETHERSCAN_API_KEY}" }
1393 "#;
1394
1395 let toml_file = root.join(Config::FILE_NAME);
1396 fs::write(toml_file, config).unwrap();
1397 let args = ScriptArgs::parse_from([
1398 "foundry-cli",
1399 "DeployV1",
1400 "--rpc-url",
1401 "amoy",
1402 "--root",
1403 root.as_os_str().to_str().unwrap(),
1404 ]);
1405 let err = args.load_config_and_evm_opts().unwrap_err();
1406
1407 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
1408
1409 unsafe {
1410 std::env::set_var("_SOLE_EXTRACT_RPC_ALIAS", "123456");
1411 }
1412 unsafe {
1413 std::env::set_var("_SOLE_ETHERSCAN_API_KEY", "etherscan_api_key");
1414 }
1415 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
1416 assert_eq!(
1417 evm_opts.fork_url,
1418 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
1419 );
1420 let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
1421 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1422 let etherscan = config.get_etherscan_api_key(None);
1423 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1424 }
1425
1426 #[test]
1428 fn test_5923() {
1429 let args =
1430 ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--priority-gas-price", "100"]);
1431 assert!(args.priority_gas_price.is_some());
1432 }
1433
1434 #[test]
1436 fn test_5910() {
1437 let args = ScriptArgs::parse_from([
1438 "foundry-cli",
1439 "--broadcast",
1440 "--with-gas-price",
1441 "0",
1442 "SolveTutorial",
1443 ]);
1444 assert!(args.with_gas_price.unwrap().is_zero());
1445 }
1446
1447 #[test]
1448 fn test_priority_gas_price_cannot_exceed_gas_price() {
1449 let args = ScriptArgs::parse_from([
1450 "foundry-cli",
1451 "--broadcast",
1452 "--with-gas-price",
1453 "100",
1454 "--priority-gas-price",
1455 "200",
1456 "Script",
1457 ]);
1458 assert!(args.priority_gas_price.unwrap() > args.with_gas_price.unwrap());
1460 }
1461}