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;
24use build::PreprocessedState;
25use clap::{Parser, ValueHint};
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},
32 utils::LoadConfig,
33};
34use foundry_common::{
35 CONTRACT_MAX_SIZE, ContractsByArtifact, SELECTOR_LEN,
36 abi::{encode_function_args, get_func},
37 shell,
38};
39use foundry_compilers::ArtifactId;
40use foundry_config::{
41 Config, figment,
42 figment::{
43 Metadata, Profile, Provider,
44 value::{Dict, Map},
45 },
46};
47#[cfg(feature = "optimism")]
48use foundry_evm::core::evm::OpEvmNetwork;
49use foundry_evm::{
50 backend::Backend,
51 core::{
52 Breakpoints, FoundryTransaction,
53 evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor},
54 tempo::PATH_USD_ADDRESS,
55 },
56 executors::ExecutorBuilder,
57 inspectors::{
58 CheatsConfig,
59 cheatcodes::{BroadcastableTransactions, Wallets},
60 },
61 opts::EvmOpts,
62 revm::interpreter::InstructionResult,
63 traces::{TraceMode, Traces},
64};
65use foundry_evm_networks::NetworkConfigs;
66use foundry_wallets::MultiWalletOpts;
67use serde::Serialize;
68use std::path::PathBuf;
69
70mod broadcast;
71mod build;
72mod execute;
73mod multi_sequence;
74mod progress;
75mod providers;
76mod receipts;
77mod runner;
78mod sequence;
79mod simulate;
80mod transaction;
81mod verify;
82
83foundry_config::merge_impl_figment_convert!(ScriptArgs, build, evm);
85
86#[derive(Clone, Debug, Default, Parser)]
88pub struct ScriptArgs {
89 #[command(flatten)]
91 pub global: GlobalArgs,
92
93 #[arg(value_hint = ValueHint::FilePath)]
98 pub path: String,
99
100 pub args: Vec<String>,
102
103 #[arg(long, visible_alias = "tc", value_name = "CONTRACT_NAME")]
105 pub target_contract: Option<String>,
106
107 #[arg(long, short, default_value = "run")]
109 pub sig: String,
110
111 #[arg(
113 long,
114 env = "ETH_PRIORITY_GAS_PRICE",
115 value_parser = foundry_cli::utils::parse_ether_value,
116 value_name = "PRICE"
117 )]
118 pub priority_gas_price: Option<U256>,
119
120 #[arg(long)]
124 pub legacy: bool,
125
126 #[arg(long)]
128 pub broadcast: bool,
129
130 #[arg(long)]
136 pub batch: bool,
137
138 #[arg(long, requires = "batch", default_value = "100")]
143 pub batch_size: usize,
144
145 #[command(flatten)]
147 pub tempo: TempoOpts,
148
149 #[arg(long)]
151 pub skip_simulation: bool,
152
153 #[arg(long, short, default_value = "130")]
155 pub gas_estimate_multiplier: u64,
156
157 #[arg(
159 long,
160 conflicts_with_all = &["private_key", "private_keys", "ledger", "trezor", "aws", "browser"],
161 )]
162 pub unlocked: bool,
163
164 #[arg(long)]
171 pub resume: bool,
172
173 #[arg(long)]
175 pub multi: bool,
176
177 #[arg(long)]
181 pub debug: bool,
182
183 #[arg(
185 long,
186 requires = "debug",
187 value_hint = ValueHint::FilePath,
188 value_name = "PATH"
189 )]
190 pub dump: Option<PathBuf>,
191
192 #[arg(long)]
195 pub slow: bool,
196
197 #[arg(long)]
201 pub non_interactive: bool,
202
203 #[arg(long)]
205 pub disable_code_size_limit: bool,
206
207 #[arg(long)]
209 pub disable_labels: bool,
210
211 #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
213 pub etherscan_api_key: Option<String>,
214
215 #[arg(long, requires = "broadcast")]
217 pub verify: bool,
218
219 #[arg(
224 long,
225 env = "ETH_GAS_PRICE",
226 value_parser = foundry_cli::utils::parse_ether_value,
227 value_name = "PRICE",
228 )]
229 pub with_gas_price: Option<U256>,
230
231 #[arg(long, env = "ETH_TIMEOUT")]
233 pub timeout: Option<u64>,
234
235 #[command(flatten)]
236 pub build: BuildOpts,
237
238 #[command(flatten)]
239 pub wallets: MultiWalletOpts,
240
241 #[command(flatten)]
242 pub evm: EvmArgs,
243
244 #[command(flatten)]
245 pub verifier: VerifierArgs,
246
247 #[command(flatten)]
248 pub retry: RetryArgs,
249}
250
251impl ScriptArgs {
252 async fn resolved_evm_opts(&self) -> Result<(Config, EvmOpts)> {
254 let (config, mut evm_opts) = self.load_config_and_evm_opts()?;
255
256 if self.tempo.is_tempo() {
257 evm_opts.networks = NetworkConfigs::with_tempo();
259 } else {
260 evm_opts.infer_network_from_fork().await;
262 }
263
264 Ok((config, evm_opts))
265 }
266
267 async fn preprocess<FEN: FoundryEvmNetwork>(
268 self,
269 config: Config,
270 mut evm_opts: EvmOpts,
271 ) -> Result<PreprocessedState<FEN>> {
272 let script_wallets = Wallets::new(self.wallets.get_multi_wallet().await?, self.evm.sender);
273 let browser_wallet = self.wallets.browser_signer::<FEN::Network>().await?;
274
275 if let Some(sender) = self.maybe_load_private_key()? {
276 evm_opts.sender = sender;
277 } else if self.evm.sender.is_none() {
278 if let Ok(signers) = script_wallets.signers()
282 && signers.len() == 1
283 {
284 evm_opts.sender = signers[0];
285 } else if let Some(signer) = browser_wallet.as_ref().map(|b| b.address()) {
286 evm_opts.sender = signer
287 }
288 }
289
290 let mut tempo = self.tempo.clone();
291 tempo.resolve_expires();
292
293 if evm_opts.networks.is_tempo() && tempo.fee_token.is_none() {
294 tempo.fee_token = Some(PATH_USD_ADDRESS);
295 }
296
297 let script_config = ScriptConfig::new(config, evm_opts, self.batch, tempo).await?;
298 Ok(PreprocessedState { args: self, script_config, script_wallets, browser_wallet })
299 }
300
301 #[allow(clippy::large_stack_frames)]
303 pub async fn run_script(self) -> Result<()> {
304 trace!(target: "script", "executing script command");
305
306 let (config, evm_opts) = self.resolved_evm_opts().await?;
307
308 let is_tempo = evm_opts.networks.is_tempo();
309
310 if self.batch && !is_tempo {
311 eyre::bail!("--batch mode is only supported on Tempo networks");
312 }
313
314 if is_tempo {
315 let batch = self.batch;
316 let bundled = match self.prepare_bundled::<TempoEvmNetwork>(config, evm_opts).await? {
317 Some(bundled) => bundled,
318 None => return Ok(()),
319 };
320 let bundled = bundled.wait_for_pending().await?;
321 let broadcasted =
322 if batch { bundled.broadcast_batch().await? } else { bundled.broadcast().await? };
323 if broadcasted.args.verify {
324 broadcasted.verify().await?;
325 }
326 return Ok(());
327 }
328
329 #[cfg(feature = "optimism")]
330 if evm_opts.networks.is_optimism() {
331 return self.run_generic_script::<OpEvmNetwork>(config, evm_opts).await;
332 }
333
334 self.run_generic_script::<EthEvmNetwork>(config, evm_opts).await
335 }
336
337 #[allow(clippy::large_stack_frames)]
341 async fn prepare_bundled<FEN: FoundryEvmNetwork>(
342 self,
343 config: Config,
344 evm_opts: EvmOpts,
345 ) -> Result<Option<BundledState<FEN>>> {
346 let state = self.preprocess::<FEN>(config, evm_opts).await?;
347 let create2_deployer = state.script_config.evm_opts.create2_deployer;
348 let compiled = state.compile()?;
349
350 let bundled = if compiled.args.resume {
353 compiled.resume().await?
354 } else {
355 let pre_simulation = compiled
357 .link()
358 .await?
359 .prepare_execution()
360 .await?
361 .execute()
362 .await?
363 .prepare_simulation()
364 .await?;
365
366 if pre_simulation.args.debug {
367 return match pre_simulation.args.dump.clone() {
368 Some(path) => pre_simulation.dump_debugger(&path).map(|_| None),
369 None => pre_simulation.run_debugger().map(|_| None),
370 };
371 }
372
373 if shell::is_json() {
374 pre_simulation.show_json().await?;
375 } else {
376 pre_simulation.show_traces().await?;
377 }
378
379 if pre_simulation
382 .execution_result
383 .transactions
384 .as_ref()
385 .is_none_or(|txs| txs.is_empty())
386 {
387 if pre_simulation.args.broadcast {
388 sh_warn!("No transactions to broadcast.")?;
389 }
390
391 return Ok(None);
392 }
393
394 if pre_simulation.execution_artifacts.rpc_data.missing_rpc {
396 if !shell::is_json() {
397 sh_println!("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?;
398 }
399
400 return Ok(None);
401 }
402
403 pre_simulation.args.check_contract_sizes(
404 &pre_simulation.execution_result,
405 &pre_simulation.build_data.known_contracts,
406 create2_deployer,
407 )?;
408
409 pre_simulation.fill_metadata().await?.bundle().await?
410 };
411
412 if !bundled.args.should_broadcast() {
414 if !shell::is_json() {
415 if shell::verbosity() >= 4 {
416 sh_println!("\n=== Transactions that will be broadcast ===\n")?;
417 bundled.sequence.show_transactions()?;
418 }
419
420 sh_println!(
421 "\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more."
422 )?;
423 }
424 return Ok(None);
425 }
426
427 if bundled.args.verify {
429 bundled.verify_preflight_check()?;
430 }
431
432 Ok(Some(bundled))
433 }
434
435 async fn run_generic_script<FEN: FoundryEvmNetwork>(
436 self,
437 config: Config,
438 evm_opts: EvmOpts,
439 ) -> Result<()> {
440 let bundled = match self.prepare_bundled::<FEN>(config, evm_opts).await? {
441 Some(bundled) => bundled,
442 None => return Ok(()),
443 };
444
445 let broadcasted = bundled.wait_for_pending().await?.broadcast().await?;
447
448 if broadcasted.args.verify {
449 broadcasted.verify().await?;
450 }
451
452 Ok(())
453 }
454
455 fn maybe_load_private_key(&self) -> Result<Option<Address>> {
458 if let Some(turnkey_address) = self.wallets.turnkey_address() {
459 return Ok(Some(turnkey_address));
460 }
461
462 let maybe_sender = self
463 .wallets
464 .private_keys()?
465 .filter(|pks| pks.len() == 1)
466 .map(|pks| pks.first().unwrap().address());
467 Ok(maybe_sender)
468 }
469
470 fn get_method_and_calldata(&self, abi: &JsonAbi) -> Result<(Function, Bytes)> {
478 if let Ok(decoded) = hex::decode(&self.sig) {
479 let selector = &decoded[..SELECTOR_LEN];
480 let func =
481 abi.functions().find(|func| selector == &func.selector()[..]).ok_or_else(|| {
482 eyre::eyre!(
483 "Function selector `{}` not found in the ABI",
484 hex::encode(selector)
485 )
486 })?;
487 return Ok((func.clone(), decoded.into()));
488 }
489
490 let func = if self.sig.contains('(') {
491 let func = get_func(&self.sig)?;
492 abi.functions()
493 .find(|&abi_func| abi_func.selector() == func.selector())
494 .wrap_err(format!("Function `{}` is not implemented in your script.", self.sig))?
495 } else {
496 let matching_functions =
497 abi.functions().filter(|func| func.name == self.sig).collect::<Vec<_>>();
498 match matching_functions.len() {
499 0 => eyre::bail!("Function `{}` not found in the ABI", self.sig),
500 1 => matching_functions[0],
501 2.. => eyre::bail!(
502 "Multiple functions with the same name `{}` found in the ABI",
503 self.sig
504 ),
505 }
506 };
507 let data = encode_function_args(func, &self.args)?;
508
509 Ok((func.clone(), data.into()))
510 }
511
512 fn check_contract_sizes<N: Network>(
518 &self,
519 result: &ScriptResult<N>,
520 known_contracts: &ContractsByArtifact,
521 create2_deployer: Address,
522 ) -> Result<()> {
523 if self.disable_code_size_limit {
525 return Ok(());
526 }
527
528 let mut bytecodes: Vec<(String, &[u8], &[u8])> = vec![];
530
531 for (artifact, contract) in known_contracts.iter() {
533 let Some(bytecode) = contract.bytecode() else { continue };
534 let Some(deployed_bytecode) = contract.deployed_bytecode() else { continue };
535 bytecodes.push((artifact.name.clone(), bytecode, deployed_bytecode));
536 }
537
538 let create_nodes = result.traces.iter().flat_map(|(_, traces)| {
540 traces.nodes().iter().filter(|node| node.trace.kind.is_any_create())
541 });
542 let mut unknown_c = 0usize;
543 for node in create_nodes {
544 let init_code = &node.trace.data;
545 let deployed_code = &node.trace.output;
546 if !bytecodes.iter().any(|(_, b, _)| *b == init_code.as_ref()) {
547 bytecodes.push((format!("Unknown{unknown_c}"), init_code, deployed_code));
548 unknown_c += 1;
549 }
550 }
551
552 let mut prompt_user = false;
553 let max_size = match self.evm.env.code_size_limit {
554 Some(size) => size,
555 None => CONTRACT_MAX_SIZE,
556 };
557
558 for (data, to) in result.transactions.iter().flat_map(|txes| {
559 txes.iter().filter_map(|tx| {
560 tx.transaction
561 .input()
562 .filter(|data| data.len() > max_size)
563 .map(|data| (data, tx.transaction.to()))
564 })
565 }) {
566 let mut offset = 0;
567
568 if let Some(to) = to {
570 if to == create2_deployer {
571 offset = 32;
573 } else {
574 continue;
575 }
576 }
577
578 if let Some((name, _, deployed_code)) =
580 bytecodes.iter().find(|(_, init_code, _)| *init_code == &data[offset..])
581 {
582 let deployment_size = deployed_code.len();
583
584 if deployment_size > max_size {
585 prompt_user = self.should_broadcast();
586 sh_err!(
587 "`{name}` is above the contract size limit ({deployment_size} > {max_size})."
588 )?;
589 }
590 }
591 }
592
593 if prompt_user
595 && !self.non_interactive
596 && !Confirm::new().with_prompt("Do you wish to continue?".to_string()).interact()?
597 {
598 eyre::bail!("User canceled the script.");
599 }
600
601 Ok(())
602 }
603
604 const fn should_broadcast(&self) -> bool {
606 self.broadcast || self.resume || self.verify
607 }
608}
609
610impl Provider for ScriptArgs {
611 fn metadata(&self) -> Metadata {
612 Metadata::named("Script Args Provider")
613 }
614
615 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
616 let mut dict = Dict::default();
617
618 if let Some(etherscan_api_key) =
619 self.etherscan_api_key.as_ref().filter(|s| !s.trim().is_empty())
620 {
621 dict.insert(
622 "etherscan_api_key".to_string(),
623 figment::value::Value::from(etherscan_api_key.clone()),
624 );
625 }
626
627 if let Some(timeout) = self.timeout {
628 dict.insert("transaction_timeout".to_string(), timeout.into());
629 }
630
631 Ok(Map::from([(Config::selected_profile(), dict)]))
632 }
633}
634
635#[derive(Serialize, Clone)]
636#[serde(bound = "")]
637pub struct ScriptResult<N: Network> {
638 pub success: bool,
639 #[serde(rename = "raw_logs")]
640 pub logs: Vec<Log>,
641 pub traces: Traces,
642 pub gas_used: u64,
643 pub labeled_addresses: AddressHashMap<String>,
644 #[serde(skip)]
645 pub transactions: Option<BroadcastableTransactions<N>>,
646 pub returned: Bytes,
647 #[serde(skip)]
648 pub exit_reason: Option<InstructionResult>,
649 pub address: Option<Address>,
650 #[serde(skip)]
651 pub breakpoints: Breakpoints,
652}
653
654impl<N: Network> Default for ScriptResult<N> {
655 fn default() -> Self {
656 Self {
657 success: Default::default(),
658 logs: Default::default(),
659 traces: Default::default(),
660 gas_used: Default::default(),
661 labeled_addresses: Default::default(),
662 transactions: Default::default(),
663 returned: Default::default(),
664 exit_reason: Default::default(),
665 address: Default::default(),
666 breakpoints: Default::default(),
667 }
668 }
669}
670
671impl<N: Network> ScriptResult<N> {
672 pub fn get_created_contracts(
673 &self,
674 known_contracts: &ContractsByArtifact,
675 ) -> Vec<AdditionalContract> {
676 self.traces
677 .iter()
678 .flat_map(|(_, traces)| {
679 traces.nodes().iter().filter_map(|node| {
680 if node.trace.kind.is_any_create() {
681 let init_code = node.trace.data.clone();
682 let contract_name = known_contracts
683 .find_by_creation_code(init_code.as_ref())
684 .map(|artifact| artifact.0.name.clone());
685 return Some(AdditionalContract {
686 call_kind: node.trace.kind,
687 address: node.trace.address,
688 contract_name,
689 init_code,
690 });
691 }
692 None
693 })
694 })
695 .collect()
696 }
697}
698
699#[derive(Serialize)]
700#[serde(bound = "")]
701struct JsonResult<'a, N: Network> {
702 logs: Vec<String>,
703 returns: &'a HashMap<String, NestedValue>,
704 #[serde(flatten)]
705 result: &'a ScriptResult<N>,
706}
707
708#[derive(Clone, Debug)]
709pub struct ScriptConfig<FEN: FoundryEvmNetwork> {
710 pub config: Config,
711 pub evm_opts: EvmOpts,
712 pub sender_nonce: u64,
713 pub backends: HashMap<String, Backend<FEN>>,
715 pub batch: bool,
717 pub tempo: TempoOpts,
719}
720
721impl<FEN: FoundryEvmNetwork> ScriptConfig<FEN> {
722 pub async fn new(
723 config: Config,
724 evm_opts: EvmOpts,
725 batch: bool,
726 tempo: TempoOpts,
727 ) -> Result<Self> {
728 let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() {
729 next_nonce(evm_opts.sender, fork_url, evm_opts.fork_block_number).await?
730 } else {
731 1
733 };
734
735 Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default(), batch, tempo })
736 }
737
738 pub async fn update_sender(&mut self, sender: Address) -> Result<()> {
739 self.sender_nonce = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
740 next_nonce(sender, fork_url, None).await?
741 } else {
742 1
744 };
745 self.evm_opts.sender = sender;
746 Ok(())
747 }
748
749 async fn get_runner(&mut self) -> Result<ScriptRunner<FEN>> {
750 self._get_runner(None, false).await
751 }
752
753 async fn get_runner_with_cheatcodes(
754 &mut self,
755 known_contracts: ContractsByArtifact,
756 script_wallets: Wallets,
757 debug: bool,
758 target: ArtifactId,
759 ) -> Result<ScriptRunner<FEN>> {
760 self._get_runner(Some((known_contracts, script_wallets, target)), debug).await
761 }
762
763 async fn _get_runner(
764 &mut self,
765 cheats_data: Option<(ContractsByArtifact, Wallets, ArtifactId)>,
766 debug: bool,
767 ) -> Result<ScriptRunner<FEN>> {
768 trace!("preparing script runner");
769 let (evm_env, mut tx_env, fork_block) = self.evm_opts.env::<_, _, TxEnvFor<FEN>>().await?;
770
771 let db = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
772 match self.backends.get(fork_url) {
773 Some(db) => db.clone(),
774 None => {
775 let fork =
776 self.evm_opts.get_fork(&self.config, evm_env.cfg_env.chain_id, fork_block);
777 let backend = Backend::spawn(fork)?;
778 self.backends.insert(fork_url.clone(), backend.clone());
779 backend
780 }
781 }
782 } else {
783 Backend::spawn(None)?
787 };
788
789 let mut builder = ExecutorBuilder::default()
791 .inspectors(|stack| {
792 stack
793 .logs(self.config.live_logs)
794 .trace_mode(if debug { TraceMode::Debug } else { TraceMode::Call })
795 .networks(self.evm_opts.networks)
796 .create2_deployer(self.evm_opts.create2_deployer)
797 })
798 .spec_id(self.config.evm_spec_id())
799 .gas_limit(self.evm_opts.gas_limit())
800 .legacy_assertions(self.config.legacy_assertions);
801
802 if let Some((known_contracts, script_wallets, target)) = cheats_data {
803 builder = builder.inspectors(|stack| {
804 stack
805 .cheatcodes(
806 CheatsConfig::new(
807 &self.config,
808 self.evm_opts.clone(),
809 Some(known_contracts),
810 Some(target),
811 self.tempo.fee_token,
812 )
813 .into(),
814 )
815 .wallets(script_wallets)
816 .enable_isolation(self.evm_opts.isolate)
817 });
818 }
819
820 tx_env.set_fee_token(self.tempo.fee_token);
823
824 Ok(ScriptRunner::new(builder.build(evm_env, tx_env, db), self.evm_opts.clone()))
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831 use alloy_network::Ethereum;
832 use alloy_primitives::address;
833 use foundry_config::{NamedChain, UnresolvedEnvVarError};
834 use std::fs;
835 use tempfile::tempdir;
836
837 #[test]
838 fn can_parse_sig() {
839 let sig = "0x522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266";
840 let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--sig", sig]);
841 assert_eq!(args.sig, sig);
842 }
843
844 #[test]
845 fn can_parse_shared_tempo_opts() {
846 let args = ScriptArgs::parse_from([
847 "foundry-cli",
848 "Contract.sol",
849 "--tempo.fee-token",
850 "1",
851 "--tempo.expires",
852 "10",
853 ]);
854
855 assert_eq!(
856 args.tempo.fee_token,
857 Some(address!("0x20C0000000000000000000000000000000000001"))
858 );
859 assert_eq!(args.tempo.expires, Some(10));
860 }
861
862 #[test]
863 fn can_parse_sponsor_tempo_opts() {
864 let args = ScriptArgs::parse_from([
865 "foundry-cli",
866 "Contract.sol",
867 "--tempo.sponsor",
868 "0x1111111111111111111111111111111111111111",
869 "--tempo.sponsor-signer",
870 "env://TEMPO_SPONSOR_PK",
871 ]);
872
873 assert_eq!(
874 args.tempo.sponsor,
875 Some(address!("0x1111111111111111111111111111111111111111"))
876 );
877 assert_eq!(args.tempo.sponsor_signer.as_deref(), Some("env://TEMPO_SPONSOR_PK"));
878 }
879
880 #[test]
881 fn can_parse_full_tempo_opts() {
882 let args =
883 ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--tempo.nonce-key", "1"]);
884
885 assert_eq!(args.tempo.nonce_key, Some(U256::from(1)));
886 }
887
888 #[test]
889 fn can_parse_unlocked() {
890 let args = ScriptArgs::parse_from([
891 "foundry-cli",
892 "Contract.sol",
893 "--sender",
894 "0x4e59b44847b379578588920ca78fbf26c0b4956c",
895 "--unlocked",
896 ]);
897 assert!(args.unlocked);
898
899 let key = U256::ZERO;
900 let args = ScriptArgs::try_parse_from([
901 "foundry-cli",
902 "Contract.sol",
903 "--sender",
904 "0x4e59b44847b379578588920ca78fbf26c0b4956c",
905 "--unlocked",
906 "--private-key",
907 &key.to_string(),
908 ]);
909 assert!(args.is_err());
910 }
911
912 #[test]
913 fn can_merge_script_config() {
914 let args = ScriptArgs::parse_from([
915 "foundry-cli",
916 "Contract.sol",
917 "--etherscan-api-key",
918 "goerli",
919 ]);
920 let config = args.load_config().unwrap();
921 assert_eq!(config.etherscan_api_key, Some("goerli".to_string()));
922 }
923
924 #[test]
925 fn can_disable_code_size_limit() {
926 let args =
927 ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--disable-code-size-limit"]);
928 assert!(args.disable_code_size_limit);
929
930 let result = ScriptResult::<Ethereum>::default();
931 let contracts = ContractsByArtifact::default();
932 let create = Address::ZERO;
933 assert!(args.check_contract_sizes(&result, &contracts, create).is_ok());
934 }
935
936 #[test]
937 fn can_parse_verifier_url() {
938 let args = ScriptArgs::parse_from([
939 "foundry-cli",
940 "script",
941 "script/Test.s.sol:TestScript",
942 "--fork-url",
943 "http://localhost:8545",
944 "--verifier-url",
945 "http://localhost:3000/api/verify",
946 "--etherscan-api-key",
947 "blacksmith",
948 "--broadcast",
949 "--verify",
950 "-vvvvv",
951 ]);
952 assert_eq!(
953 args.verifier.verifier_url,
954 Some("http://localhost:3000/api/verify".to_string())
955 );
956 }
957
958 #[test]
959 fn can_extract_code_size_limit() {
960 let args = ScriptArgs::parse_from([
961 "foundry-cli",
962 "script",
963 "script/Test.s.sol:TestScript",
964 "--fork-url",
965 "http://localhost:8545",
966 "--broadcast",
967 "--code-size-limit",
968 "50000",
969 ]);
970 assert_eq!(args.evm.env.code_size_limit, Some(50000));
971 }
972
973 #[test]
974 fn can_extract_script_etherscan_key() {
975 let temp = tempdir().unwrap();
976 let root = temp.path();
977
978 let config = r#"
979 [profile.default]
980 etherscan_api_key = "amoy"
981
982 [etherscan]
983 amoy = { key = "https://etherscan-amoy.com/" }
984 "#;
985
986 let toml_file = root.join(Config::FILE_NAME);
987 fs::write(toml_file, config).unwrap();
988 let args = ScriptArgs::parse_from([
989 "foundry-cli",
990 "Contract.sol",
991 "--etherscan-api-key",
992 "amoy",
993 "--root",
994 root.as_os_str().to_str().unwrap(),
995 ]);
996
997 let config = args.load_config().unwrap();
998 let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
999 assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
1000 }
1001
1002 #[test]
1003 fn can_extract_script_rpc_alias() {
1004 let temp = tempdir().unwrap();
1005 let root = temp.path();
1006
1007 let config = r#"
1008 [profile.default]
1009
1010 [rpc_endpoints]
1011 polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_CAN_EXTRACT_RPC_ALIAS}"
1012 "#;
1013
1014 let toml_file = root.join(Config::FILE_NAME);
1015 fs::write(toml_file, config).unwrap();
1016 let args = ScriptArgs::parse_from([
1017 "foundry-cli",
1018 "DeployV1",
1019 "--rpc-url",
1020 "polygonAmoy",
1021 "--root",
1022 root.as_os_str().to_str().unwrap(),
1023 ]);
1024
1025 let err = args.load_config_and_evm_opts().unwrap_err();
1026
1027 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
1028
1029 unsafe {
1030 std::env::set_var("_CAN_EXTRACT_RPC_ALIAS", "123456");
1031 }
1032 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
1033 assert_eq!(config.eth_rpc_url, Some("polygonAmoy".to_string()));
1034 assert_eq!(
1035 evm_opts.fork_url,
1036 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
1037 );
1038 }
1039
1040 #[test]
1041 fn can_extract_script_rpc_and_etherscan_alias() {
1042 let temp = tempdir().unwrap();
1043 let root = temp.path();
1044
1045 let config = r#"
1046 [profile.default]
1047
1048 [rpc_endpoints]
1049 amoy = "https://polygon-amoy.g.alchemy.com/v2/${_EXTRACT_RPC_ALIAS}"
1050
1051 [etherscan]
1052 amoy = { key = "${_ETHERSCAN_API_KEY}", chain = 80002, url = "https://amoy.polygonscan.com/" }
1053 "#;
1054
1055 let toml_file = root.join(Config::FILE_NAME);
1056 fs::write(toml_file, config).unwrap();
1057 let args = ScriptArgs::parse_from([
1058 "foundry-cli",
1059 "DeployV1",
1060 "--rpc-url",
1061 "amoy",
1062 "--etherscan-api-key",
1063 "amoy",
1064 "--root",
1065 root.as_os_str().to_str().unwrap(),
1066 ]);
1067 let err = args.load_config_and_evm_opts().unwrap_err();
1068
1069 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
1070
1071 unsafe {
1072 std::env::set_var("_EXTRACT_RPC_ALIAS", "123456");
1073 }
1074 unsafe {
1075 std::env::set_var("_ETHERSCAN_API_KEY", "etherscan_api_key");
1076 }
1077 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
1078 assert_eq!(config.eth_rpc_url, Some("amoy".to_string()));
1079 assert_eq!(
1080 evm_opts.fork_url,
1081 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
1082 );
1083 let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
1084 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1085 let etherscan = config.get_etherscan_api_key(None);
1086 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1087 }
1088
1089 #[test]
1090 fn can_extract_script_rpc_and_sole_etherscan_alias() {
1091 let temp = tempdir().unwrap();
1092 let root = temp.path();
1093
1094 let config = r#"
1095 [profile.default]
1096
1097 [rpc_endpoints]
1098 amoy = "https://polygon-amoy.g.alchemy.com/v2/${_SOLE_EXTRACT_RPC_ALIAS}"
1099
1100 [etherscan]
1101 amoy = { key = "${_SOLE_ETHERSCAN_API_KEY}" }
1102 "#;
1103
1104 let toml_file = root.join(Config::FILE_NAME);
1105 fs::write(toml_file, config).unwrap();
1106 let args = ScriptArgs::parse_from([
1107 "foundry-cli",
1108 "DeployV1",
1109 "--rpc-url",
1110 "amoy",
1111 "--root",
1112 root.as_os_str().to_str().unwrap(),
1113 ]);
1114 let err = args.load_config_and_evm_opts().unwrap_err();
1115
1116 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
1117
1118 unsafe {
1119 std::env::set_var("_SOLE_EXTRACT_RPC_ALIAS", "123456");
1120 }
1121 unsafe {
1122 std::env::set_var("_SOLE_ETHERSCAN_API_KEY", "etherscan_api_key");
1123 }
1124 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
1125 assert_eq!(
1126 evm_opts.fork_url,
1127 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
1128 );
1129 let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
1130 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1131 let etherscan = config.get_etherscan_api_key(None);
1132 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1133 }
1134
1135 #[test]
1137 fn test_5923() {
1138 let args =
1139 ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--priority-gas-price", "100"]);
1140 assert!(args.priority_gas_price.is_some());
1141 }
1142
1143 #[test]
1145 fn test_5910() {
1146 let args = ScriptArgs::parse_from([
1147 "foundry-cli",
1148 "--broadcast",
1149 "--with-gas-price",
1150 "0",
1151 "SolveTutorial",
1152 ]);
1153 assert!(args.with_gas_price.unwrap().is_zero());
1154 }
1155
1156 #[test]
1157 fn test_priority_gas_price_cannot_exceed_gas_price() {
1158 let args = ScriptArgs::parse_from([
1159 "foundry-cli",
1160 "--broadcast",
1161 "--with-gas-price",
1162 "100",
1163 "--priority-gas-price",
1164 "200",
1165 "Script",
1166 ]);
1167 assert!(args.priority_gas_price.unwrap() > args.with_gas_price.unwrap());
1169 }
1170}