forge_script/
lib.rs

1//! # foundry-script
2//!
3//! Smart contract scripting.
4
5#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
7
8#[macro_use]
9extern crate foundry_common;
10
11#[macro_use]
12extern crate tracing;
13
14use crate::runner::ScriptRunner;
15use alloy_json_abi::{Function, JsonAbi};
16use alloy_primitives::{
17    hex,
18    map::{AddressHashMap, HashMap},
19    Address, Bytes, Log, TxKind, U256,
20};
21use alloy_signer::Signer;
22use broadcast::next_nonce;
23use build::PreprocessedState;
24use clap::{Parser, ValueHint};
25use dialoguer::Confirm;
26use eyre::{ContextCompat, Result};
27use forge_script_sequence::{AdditionalContract, NestedValue};
28use forge_verify::{RetryArgs, VerifierArgs};
29use foundry_cli::{
30    opts::{BuildOpts, GlobalArgs},
31    utils::LoadConfig,
32};
33use foundry_common::{
34    abi::{encode_function_args, get_func},
35    evm::{Breakpoints, EvmArgs},
36    shell, ContractsByArtifact, CONTRACT_MAX_SIZE, SELECTOR_LEN,
37};
38use foundry_compilers::ArtifactId;
39use foundry_config::{
40    figment,
41    figment::{
42        value::{Dict, Map},
43        Metadata, Profile, Provider,
44    },
45    Config,
46};
47use foundry_evm::{
48    backend::Backend,
49    executors::ExecutorBuilder,
50    inspectors::{
51        cheatcodes::{BroadcastableTransactions, Wallets},
52        CheatsConfig,
53    },
54    opts::EvmOpts,
55    traces::{TraceMode, Traces},
56};
57use foundry_wallets::MultiWalletOpts;
58use serde::Serialize;
59use std::path::PathBuf;
60
61mod broadcast;
62mod build;
63mod execute;
64mod multi_sequence;
65mod progress;
66mod providers;
67mod receipts;
68mod runner;
69mod sequence;
70mod simulate;
71mod transaction;
72mod verify;
73
74// Loads project's figment and merges the build cli arguments into it
75foundry_config::merge_impl_figment_convert!(ScriptArgs, build, evm);
76
77/// CLI arguments for `forge script`.
78#[derive(Clone, Debug, Default, Parser)]
79pub struct ScriptArgs {
80    // Include global options for users of this struct.
81    #[command(flatten)]
82    pub global: GlobalArgs,
83
84    /// The contract you want to run. Either the file path or contract name.
85    ///
86    /// If multiple contracts exist in the same file you must specify the target contract with
87    /// --target-contract.
88    #[arg(value_hint = ValueHint::FilePath)]
89    pub path: String,
90
91    /// Arguments to pass to the script function.
92    pub args: Vec<String>,
93
94    /// The name of the contract you want to run.
95    #[arg(long, visible_alias = "tc", value_name = "CONTRACT_NAME")]
96    pub target_contract: Option<String>,
97
98    /// The signature of the function you want to call in the contract, or raw calldata.
99    #[arg(long, short, default_value = "run()")]
100    pub sig: String,
101
102    /// Max priority fee per gas for EIP1559 transactions.
103    #[arg(
104        long,
105        env = "ETH_PRIORITY_GAS_PRICE",
106        value_parser = foundry_cli::utils::parse_ether_value,
107        value_name = "PRICE"
108    )]
109    pub priority_gas_price: Option<U256>,
110
111    /// Use legacy transactions instead of EIP1559 ones.
112    ///
113    /// This is auto-enabled for common networks without EIP1559.
114    #[arg(long)]
115    pub legacy: bool,
116
117    /// Broadcasts the transactions.
118    #[arg(long)]
119    pub broadcast: bool,
120
121    /// Batch size of transactions.
122    ///
123    /// This is ignored and set to 1 if batching is not available or `--slow` is enabled.
124    #[arg(long, default_value = "100")]
125    pub batch_size: usize,
126
127    /// Skips on-chain simulation.
128    #[arg(long)]
129    pub skip_simulation: bool,
130
131    /// Relative percentage to multiply gas estimates by.
132    #[arg(long, short, default_value = "130")]
133    pub gas_estimate_multiplier: u64,
134
135    /// Send via `eth_sendTransaction` using the `--from` argument or `$ETH_FROM` as sender
136    #[arg(
137        long,
138        conflicts_with_all = &["private_key", "private_keys", "froms", "ledger", "trezor", "aws"],
139    )]
140    pub unlocked: bool,
141
142    /// Resumes submitting transactions that failed or timed-out previously.
143    ///
144    /// It DOES NOT simulate the script again and it expects nonces to have remained the same.
145    ///
146    /// Example: If transaction N has a nonce of 22, then the account should have a nonce of 22,
147    /// otherwise it fails.
148    #[arg(long)]
149    pub resume: bool,
150
151    /// If present, --resume or --verify will be assumed to be a multi chain deployment.
152    #[arg(long)]
153    pub multi: bool,
154
155    /// Open the script in the debugger.
156    ///
157    /// Takes precedence over broadcast.
158    #[arg(long)]
159    pub debug: bool,
160
161    /// Dumps all debugger steps to file.
162    #[arg(
163        long,
164        requires = "debug",
165        value_hint = ValueHint::FilePath,
166        value_name = "PATH"
167    )]
168    pub dump: Option<PathBuf>,
169
170    /// Makes sure a transaction is sent,
171    /// only after its previous one has been confirmed and succeeded.
172    #[arg(long)]
173    pub slow: bool,
174
175    /// Disables interactive prompts that might appear when deploying big contracts.
176    ///
177    /// For more info on the contract size limit, see EIP-170: <https://eips.ethereum.org/EIPS/eip-170>
178    #[arg(long)]
179    pub non_interactive: bool,
180
181    /// The Etherscan (or equivalent) API key
182    #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
183    pub etherscan_api_key: Option<String>,
184
185    /// Verifies all the contracts found in the receipts of a script, if any.
186    #[arg(long)]
187    pub verify: bool,
188
189    /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions, either
190    /// specified in wei, or as a string with a unit type.
191    ///
192    /// Examples: 1ether, 10gwei, 0.01ether
193    #[arg(
194        long,
195        env = "ETH_GAS_PRICE",
196        value_parser = foundry_cli::utils::parse_ether_value,
197        value_name = "PRICE",
198    )]
199    pub with_gas_price: Option<U256>,
200
201    /// Timeout to use for broadcasting transactions.
202    #[arg(long, env = "ETH_TIMEOUT")]
203    pub timeout: Option<u64>,
204
205    #[command(flatten)]
206    pub build: BuildOpts,
207
208    #[command(flatten)]
209    pub wallets: MultiWalletOpts,
210
211    #[command(flatten)]
212    pub evm: EvmArgs,
213
214    #[command(flatten)]
215    pub verifier: VerifierArgs,
216
217    #[command(flatten)]
218    pub retry: RetryArgs,
219}
220
221impl ScriptArgs {
222    pub async fn preprocess(self) -> Result<PreprocessedState> {
223        let script_wallets = Wallets::new(self.wallets.get_multi_wallet().await?, self.evm.sender);
224
225        let (config, mut evm_opts) = self.load_config_and_evm_opts()?;
226
227        if let Some(sender) = self.maybe_load_private_key()? {
228            evm_opts.sender = sender;
229        }
230
231        let script_config = ScriptConfig::new(config, evm_opts).await?;
232
233        Ok(PreprocessedState { args: self, script_config, script_wallets })
234    }
235
236    /// Executes the script
237    pub async fn run_script(self) -> Result<()> {
238        trace!(target: "script", "executing script command");
239
240        let state = self.preprocess().await?;
241        let create2_deployer = state.script_config.evm_opts.create2_deployer;
242        let compiled = state.compile()?;
243
244        // Move from `CompiledState` to `BundledState` either by resuming or executing and
245        // simulating script.
246        let bundled = if compiled.args.resume || (compiled.args.verify && !compiled.args.broadcast)
247        {
248            compiled.resume().await?
249        } else {
250            // Drive state machine to point at which we have everything needed for simulation.
251            let pre_simulation = compiled
252                .link()
253                .await?
254                .prepare_execution()
255                .await?
256                .execute()
257                .await?
258                .prepare_simulation()
259                .await?;
260
261            if pre_simulation.args.debug {
262                return match pre_simulation.args.dump.clone() {
263                    Some(path) => pre_simulation.dump_debugger(&path),
264                    None => pre_simulation.run_debugger(),
265                };
266            }
267
268            if shell::is_json() {
269                pre_simulation.show_json()?;
270            } else {
271                pre_simulation.show_traces().await?;
272            }
273
274            // Ensure that we have transactions to simulate/broadcast, otherwise exit early to avoid
275            // hard error.
276            if pre_simulation
277                .execution_result
278                .transactions
279                .as_ref()
280                .is_none_or(|txs| txs.is_empty())
281            {
282                return Ok(());
283            }
284
285            // Check if there are any missing RPCs and exit early to avoid hard error.
286            if pre_simulation.execution_artifacts.rpc_data.missing_rpc {
287                if !shell::is_json() {
288                    sh_println!("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?;
289                }
290
291                return Ok(());
292            }
293
294            pre_simulation.args.check_contract_sizes(
295                &pre_simulation.execution_result,
296                &pre_simulation.build_data.known_contracts,
297                create2_deployer,
298            )?;
299
300            pre_simulation.fill_metadata().await?.bundle().await?
301        };
302
303        // Exit early in case user didn't provide any broadcast/verify related flags.
304        if !bundled.args.should_broadcast() {
305            if !shell::is_json() {
306                if shell::verbosity() >= 4 {
307                    sh_println!("\n=== Transactions that will be broadcast ===\n")?;
308                    bundled.sequence.show_transactions()?;
309                }
310
311                sh_println!("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?;
312            }
313            return Ok(());
314        }
315
316        // Exit early if something is wrong with verification options.
317        if bundled.args.verify {
318            bundled.verify_preflight_check()?;
319        }
320
321        // Wait for pending txes and broadcast others.
322        let broadcasted = bundled.wait_for_pending().await?.broadcast().await?;
323
324        if broadcasted.args.verify {
325            broadcasted.verify().await?;
326        }
327
328        Ok(())
329    }
330
331    /// In case the user has loaded *only* one private-key, we can assume that he's using it as the
332    /// `--sender`
333    fn maybe_load_private_key(&self) -> Result<Option<Address>> {
334        let maybe_sender = self
335            .wallets
336            .private_keys()?
337            .filter(|pks| pks.len() == 1)
338            .map(|pks| pks.first().unwrap().address());
339        Ok(maybe_sender)
340    }
341
342    /// Returns the Function and calldata based on the signature
343    ///
344    /// If the `sig` is a valid human-readable function we find the corresponding function in the
345    /// `abi` If the `sig` is valid hex, we assume it's calldata and try to find the
346    /// corresponding function by matching the selector, first 4 bytes in the calldata.
347    ///
348    /// Note: We assume that the `sig` is already stripped of its prefix, See [`ScriptArgs`]
349    fn get_method_and_calldata(&self, abi: &JsonAbi) -> Result<(Function, Bytes)> {
350        if let Ok(decoded) = hex::decode(&self.sig) {
351            let selector = &decoded[..SELECTOR_LEN];
352            let func =
353                abi.functions().find(|func| selector == &func.selector()[..]).ok_or_else(|| {
354                    eyre::eyre!(
355                        "Function selector `{}` not found in the ABI",
356                        hex::encode(selector)
357                    )
358                })?;
359            return Ok((func.clone(), decoded.into()));
360        }
361
362        let func = if self.sig.contains('(') {
363            let func = get_func(&self.sig)?;
364            abi.functions()
365                .find(|&abi_func| abi_func.selector() == func.selector())
366                .wrap_err(format!("Function `{}` is not implemented in your script.", self.sig))?
367        } else {
368            let matching_functions =
369                abi.functions().filter(|func| func.name == self.sig).collect::<Vec<_>>();
370            match matching_functions.len() {
371                0 => eyre::bail!("Function `{}` not found in the ABI", self.sig),
372                1 => matching_functions[0],
373                2.. => eyre::bail!(
374                    "Multiple functions with the same name `{}` found in the ABI",
375                    self.sig
376                ),
377            }
378        };
379        let data = encode_function_args(func, &self.args)?;
380
381        Ok((func.clone(), data.into()))
382    }
383
384    /// Checks if the transaction is a deployment with either a size above the `CONTRACT_MAX_SIZE`
385    /// or specified `code_size_limit`.
386    ///
387    /// If `self.broadcast` is enabled, it asks confirmation of the user. Otherwise, it just warns
388    /// the user.
389    fn check_contract_sizes(
390        &self,
391        result: &ScriptResult,
392        known_contracts: &ContractsByArtifact,
393        create2_deployer: Address,
394    ) -> Result<()> {
395        // (name, &init, &deployed)[]
396        let mut bytecodes: Vec<(String, &[u8], &[u8])> = vec![];
397
398        // From artifacts
399        for (artifact, contract) in known_contracts.iter() {
400            let Some(bytecode) = contract.bytecode() else { continue };
401            let Some(deployed_bytecode) = contract.deployed_bytecode() else { continue };
402            bytecodes.push((artifact.name.clone(), bytecode, deployed_bytecode));
403        }
404
405        // From traces
406        let create_nodes = result.traces.iter().flat_map(|(_, traces)| {
407            traces.nodes().iter().filter(|node| node.trace.kind.is_any_create())
408        });
409        let mut unknown_c = 0usize;
410        for node in create_nodes {
411            let init_code = &node.trace.data;
412            let deployed_code = &node.trace.output;
413            if !bytecodes.iter().any(|(_, b, _)| *b == init_code.as_ref()) {
414                bytecodes.push((format!("Unknown{unknown_c}"), init_code, deployed_code));
415                unknown_c += 1;
416            }
417            continue;
418        }
419
420        let mut prompt_user = false;
421        let max_size = match self.evm.env.code_size_limit {
422            Some(size) => size,
423            None => CONTRACT_MAX_SIZE,
424        };
425
426        for (data, to) in result.transactions.iter().flat_map(|txes| {
427            txes.iter().filter_map(|tx| {
428                tx.transaction
429                    .input()
430                    .filter(|data| data.len() > max_size)
431                    .map(|data| (data, tx.transaction.to()))
432            })
433        }) {
434            let mut offset = 0;
435
436            // Find if it's a CREATE or CREATE2. Otherwise, skip transaction.
437            if let Some(TxKind::Call(to)) = to {
438                if to == create2_deployer {
439                    // Size of the salt prefix.
440                    offset = 32;
441                } else {
442                    continue;
443                }
444            } else if let Some(TxKind::Create) = to {
445                // Pass
446            }
447
448            // Find artifact with a deployment code same as the data.
449            if let Some((name, _, deployed_code)) =
450                bytecodes.iter().find(|(_, init_code, _)| *init_code == &data[offset..])
451            {
452                let deployment_size = deployed_code.len();
453
454                if deployment_size > max_size {
455                    prompt_user = self.should_broadcast();
456                    sh_err!(
457                        "`{name}` is above the contract size limit ({deployment_size} > {max_size})."
458                    )?;
459                }
460            }
461        }
462
463        // Only prompt if we're broadcasting and we've not disabled interactivity.
464        if prompt_user &&
465            !self.non_interactive &&
466            !Confirm::new().with_prompt("Do you wish to continue?".to_string()).interact()?
467        {
468            eyre::bail!("User canceled the script.");
469        }
470
471        Ok(())
472    }
473
474    /// We only broadcast transactions if --broadcast or --resume was passed.
475    fn should_broadcast(&self) -> bool {
476        self.broadcast || self.resume
477    }
478}
479
480impl Provider for ScriptArgs {
481    fn metadata(&self) -> Metadata {
482        Metadata::named("Script Args Provider")
483    }
484
485    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
486        let mut dict = Dict::default();
487        if let Some(ref etherscan_api_key) =
488            self.etherscan_api_key.as_ref().filter(|s| !s.trim().is_empty())
489        {
490            dict.insert(
491                "etherscan_api_key".to_string(),
492                figment::value::Value::from(etherscan_api_key.to_string()),
493            );
494        }
495        if let Some(timeout) = self.timeout {
496            dict.insert("transaction_timeout".to_string(), timeout.into());
497        }
498        Ok(Map::from([(Config::selected_profile(), dict)]))
499    }
500}
501
502#[derive(Default, Serialize)]
503pub struct ScriptResult {
504    pub success: bool,
505    #[serde(rename = "raw_logs")]
506    pub logs: Vec<Log>,
507    pub traces: Traces,
508    pub gas_used: u64,
509    pub labeled_addresses: AddressHashMap<String>,
510    #[serde(skip)]
511    pub transactions: Option<BroadcastableTransactions>,
512    pub returned: Bytes,
513    pub address: Option<Address>,
514    #[serde(skip)]
515    pub breakpoints: Breakpoints,
516}
517
518impl ScriptResult {
519    pub fn get_created_contracts(&self) -> Vec<AdditionalContract> {
520        self.traces
521            .iter()
522            .flat_map(|(_, traces)| {
523                traces.nodes().iter().filter_map(|node| {
524                    if node.trace.kind.is_any_create() {
525                        return Some(AdditionalContract {
526                            opcode: node.trace.kind,
527                            address: node.trace.address,
528                            init_code: node.trace.data.clone(),
529                        });
530                    }
531                    None
532                })
533            })
534            .collect()
535    }
536}
537
538#[derive(Serialize)]
539struct JsonResult<'a> {
540    logs: Vec<String>,
541    returns: &'a HashMap<String, NestedValue>,
542    #[serde(flatten)]
543    result: &'a ScriptResult,
544}
545
546#[derive(Clone, Debug)]
547pub struct ScriptConfig {
548    pub config: Config,
549    pub evm_opts: EvmOpts,
550    pub sender_nonce: u64,
551    /// Maps a rpc url to a backend
552    pub backends: HashMap<String, Backend>,
553}
554
555impl ScriptConfig {
556    pub async fn new(config: Config, evm_opts: EvmOpts) -> Result<Self> {
557        let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() {
558            next_nonce(evm_opts.sender, fork_url, evm_opts.fork_block_number).await?
559        } else {
560            // dapptools compatibility
561            1
562        };
563
564        Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default() })
565    }
566
567    pub async fn update_sender(&mut self, sender: Address) -> Result<()> {
568        self.sender_nonce = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
569            next_nonce(sender, fork_url, None).await?
570        } else {
571            // dapptools compatibility
572            1
573        };
574        self.evm_opts.sender = sender;
575        Ok(())
576    }
577
578    async fn get_runner(&mut self) -> Result<ScriptRunner> {
579        self._get_runner(None, false).await
580    }
581
582    async fn get_runner_with_cheatcodes(
583        &mut self,
584        known_contracts: ContractsByArtifact,
585        script_wallets: Wallets,
586        debug: bool,
587        target: ArtifactId,
588    ) -> Result<ScriptRunner> {
589        self._get_runner(Some((known_contracts, script_wallets, target)), debug).await
590    }
591
592    async fn _get_runner(
593        &mut self,
594        cheats_data: Option<(ContractsByArtifact, Wallets, ArtifactId)>,
595        debug: bool,
596    ) -> Result<ScriptRunner> {
597        trace!("preparing script runner");
598        let env = self.evm_opts.evm_env().await?;
599
600        let db = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
601            match self.backends.get(fork_url) {
602                Some(db) => db.clone(),
603                None => {
604                    let fork = self.evm_opts.get_fork(&self.config, env.clone());
605                    let backend = Backend::spawn(fork)?;
606                    self.backends.insert(fork_url.clone(), backend.clone());
607                    backend
608                }
609            }
610        } else {
611            // It's only really `None`, when we don't pass any `--fork-url`. And if so, there is
612            // no need to cache it, since there won't be any onchain simulation that we'd need
613            // to cache the backend for.
614            Backend::spawn(None)?
615        };
616
617        // We need to enable tracing to decode contract names: local or external.
618        let mut builder = ExecutorBuilder::new()
619            .inspectors(|stack| {
620                stack
621                    .trace_mode(if debug { TraceMode::Debug } else { TraceMode::Call })
622                    .odyssey(self.evm_opts.odyssey)
623                    .create2_deployer(self.evm_opts.create2_deployer)
624            })
625            .spec_id(self.config.evm_spec_id())
626            .gas_limit(self.evm_opts.gas_limit())
627            .legacy_assertions(self.config.legacy_assertions);
628
629        if let Some((known_contracts, script_wallets, target)) = cheats_data {
630            builder = builder.inspectors(|stack| {
631                stack
632                    .cheatcodes(
633                        CheatsConfig::new(
634                            &self.config,
635                            self.evm_opts.clone(),
636                            Some(known_contracts),
637                            Some(target),
638                        )
639                        .into(),
640                    )
641                    .wallets(script_wallets)
642                    .enable_isolation(self.evm_opts.isolate)
643            });
644        }
645
646        Ok(ScriptRunner::new(builder.build(env, db), self.evm_opts.clone()))
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653    use foundry_config::{NamedChain, UnresolvedEnvVarError};
654    use std::fs;
655    use tempfile::tempdir;
656
657    #[test]
658    fn can_parse_sig() {
659        let sig = "0x522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266";
660        let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--sig", sig]);
661        assert_eq!(args.sig, sig);
662    }
663
664    #[test]
665    fn can_parse_unlocked() {
666        let args = ScriptArgs::parse_from([
667            "foundry-cli",
668            "Contract.sol",
669            "--sender",
670            "0x4e59b44847b379578588920ca78fbf26c0b4956c",
671            "--unlocked",
672        ]);
673        assert!(args.unlocked);
674
675        let key = U256::ZERO;
676        let args = ScriptArgs::try_parse_from([
677            "foundry-cli",
678            "Contract.sol",
679            "--sender",
680            "0x4e59b44847b379578588920ca78fbf26c0b4956c",
681            "--unlocked",
682            "--private-key",
683            key.to_string().as_str(),
684        ]);
685        assert!(args.is_err());
686    }
687
688    #[test]
689    fn can_merge_script_config() {
690        let args = ScriptArgs::parse_from([
691            "foundry-cli",
692            "Contract.sol",
693            "--etherscan-api-key",
694            "goerli",
695        ]);
696        let config = args.load_config().unwrap();
697        assert_eq!(config.etherscan_api_key, Some("goerli".to_string()));
698    }
699
700    #[test]
701    fn can_parse_verifier_url() {
702        let args = ScriptArgs::parse_from([
703            "foundry-cli",
704            "script",
705            "script/Test.s.sol:TestScript",
706            "--fork-url",
707            "http://localhost:8545",
708            "--verifier-url",
709            "http://localhost:3000/api/verify",
710            "--etherscan-api-key",
711            "blacksmith",
712            "--broadcast",
713            "--verify",
714            "-vvvvv",
715        ]);
716        assert_eq!(
717            args.verifier.verifier_url,
718            Some("http://localhost:3000/api/verify".to_string())
719        );
720    }
721
722    #[test]
723    fn can_extract_code_size_limit() {
724        let args = ScriptArgs::parse_from([
725            "foundry-cli",
726            "script",
727            "script/Test.s.sol:TestScript",
728            "--fork-url",
729            "http://localhost:8545",
730            "--broadcast",
731            "--code-size-limit",
732            "50000",
733        ]);
734        assert_eq!(args.evm.env.code_size_limit, Some(50000));
735    }
736
737    #[test]
738    fn can_extract_script_etherscan_key() {
739        let temp = tempdir().unwrap();
740        let root = temp.path();
741
742        let config = r#"
743                [profile.default]
744                etherscan_api_key = "mumbai"
745
746                [etherscan]
747                mumbai = { key = "https://etherscan-mumbai.com/" }
748            "#;
749
750        let toml_file = root.join(Config::FILE_NAME);
751        fs::write(toml_file, config).unwrap();
752        let args = ScriptArgs::parse_from([
753            "foundry-cli",
754            "Contract.sol",
755            "--etherscan-api-key",
756            "mumbai",
757            "--root",
758            root.as_os_str().to_str().unwrap(),
759        ]);
760
761        let config = args.load_config().unwrap();
762        let mumbai = config.get_etherscan_api_key(Some(NamedChain::PolygonMumbai.into()));
763        assert_eq!(mumbai, Some("https://etherscan-mumbai.com/".to_string()));
764    }
765
766    #[test]
767    fn can_extract_script_rpc_alias() {
768        let temp = tempdir().unwrap();
769        let root = temp.path();
770
771        let config = r#"
772                [profile.default]
773
774                [rpc_endpoints]
775                polygonMumbai = "https://polygon-mumbai.g.alchemy.com/v2/${_CAN_EXTRACT_RPC_ALIAS}"
776            "#;
777
778        let toml_file = root.join(Config::FILE_NAME);
779        fs::write(toml_file, config).unwrap();
780        let args = ScriptArgs::parse_from([
781            "foundry-cli",
782            "DeployV1",
783            "--rpc-url",
784            "polygonMumbai",
785            "--root",
786            root.as_os_str().to_str().unwrap(),
787        ]);
788
789        let err = args.load_config_and_evm_opts().unwrap_err();
790
791        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
792
793        std::env::set_var("_CAN_EXTRACT_RPC_ALIAS", "123456");
794        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
795        assert_eq!(config.eth_rpc_url, Some("polygonMumbai".to_string()));
796        assert_eq!(
797            evm_opts.fork_url,
798            Some("https://polygon-mumbai.g.alchemy.com/v2/123456".to_string())
799        );
800    }
801
802    #[test]
803    fn can_extract_script_rpc_and_etherscan_alias() {
804        let temp = tempdir().unwrap();
805        let root = temp.path();
806
807        let config = r#"
808            [profile.default]
809
810            [rpc_endpoints]
811            mumbai = "https://polygon-mumbai.g.alchemy.com/v2/${_EXTRACT_RPC_ALIAS}"
812
813            [etherscan]
814            mumbai = { key = "${_POLYSCAN_API_KEY}", chain = 80001, url = "https://api-testnet.polygonscan.com/" }
815        "#;
816
817        let toml_file = root.join(Config::FILE_NAME);
818        fs::write(toml_file, config).unwrap();
819        let args = ScriptArgs::parse_from([
820            "foundry-cli",
821            "DeployV1",
822            "--rpc-url",
823            "mumbai",
824            "--etherscan-api-key",
825            "mumbai",
826            "--root",
827            root.as_os_str().to_str().unwrap(),
828        ]);
829        let err = args.load_config_and_evm_opts().unwrap_err();
830
831        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
832
833        std::env::set_var("_EXTRACT_RPC_ALIAS", "123456");
834        std::env::set_var("_POLYSCAN_API_KEY", "polygonkey");
835        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
836        assert_eq!(config.eth_rpc_url, Some("mumbai".to_string()));
837        assert_eq!(
838            evm_opts.fork_url,
839            Some("https://polygon-mumbai.g.alchemy.com/v2/123456".to_string())
840        );
841        let etherscan = config.get_etherscan_api_key(Some(80001u64.into()));
842        assert_eq!(etherscan, Some("polygonkey".to_string()));
843        let etherscan = config.get_etherscan_api_key(None);
844        assert_eq!(etherscan, Some("polygonkey".to_string()));
845    }
846
847    #[test]
848    fn can_extract_script_rpc_and_sole_etherscan_alias() {
849        let temp = tempdir().unwrap();
850        let root = temp.path();
851
852        let config = r#"
853                [profile.default]
854
855               [rpc_endpoints]
856                mumbai = "https://polygon-mumbai.g.alchemy.com/v2/${_SOLE_EXTRACT_RPC_ALIAS}"
857
858                [etherscan]
859                mumbai = { key = "${_SOLE_POLYSCAN_API_KEY}" }
860            "#;
861
862        let toml_file = root.join(Config::FILE_NAME);
863        fs::write(toml_file, config).unwrap();
864        let args = ScriptArgs::parse_from([
865            "foundry-cli",
866            "DeployV1",
867            "--rpc-url",
868            "mumbai",
869            "--root",
870            root.as_os_str().to_str().unwrap(),
871        ]);
872        let err = args.load_config_and_evm_opts().unwrap_err();
873
874        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
875
876        std::env::set_var("_SOLE_EXTRACT_RPC_ALIAS", "123456");
877        std::env::set_var("_SOLE_POLYSCAN_API_KEY", "polygonkey");
878        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
879        assert_eq!(
880            evm_opts.fork_url,
881            Some("https://polygon-mumbai.g.alchemy.com/v2/123456".to_string())
882        );
883        let etherscan = config.get_etherscan_api_key(Some(80001u64.into()));
884        assert_eq!(etherscan, Some("polygonkey".to_string()));
885        let etherscan = config.get_etherscan_api_key(None);
886        assert_eq!(etherscan, Some("polygonkey".to_string()));
887    }
888
889    // <https://github.com/foundry-rs/foundry/issues/5923>
890    #[test]
891    fn test_5923() {
892        let args =
893            ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--priority-gas-price", "100"]);
894        assert!(args.priority_gas_price.is_some());
895    }
896
897    // <https://github.com/foundry-rs/foundry/issues/5910>
898    #[test]
899    fn test_5910() {
900        let args = ScriptArgs::parse_from([
901            "foundry-cli",
902            "--broadcast",
903            "--with-gas-price",
904            "0",
905            "SolveTutorial",
906        ]);
907        assert!(args.with_gas_price.unwrap().is_zero());
908    }
909}