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