Skip to main content

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