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