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    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_cli::{
29    opts::{BuildOpts, EvmArgs, GlobalArgs},
30    utils::LoadConfig,
31};
32use foundry_common::{
33    CONTRACT_MAX_SIZE, ContractsByArtifact, SELECTOR_LEN,
34    abi::{encode_function_args, get_func},
35    evm::Breakpoints,
36    shell,
37};
38use foundry_compilers::ArtifactId;
39use foundry_config::{
40    Config, figment,
41    figment::{
42        Metadata, Profile, Provider,
43        value::{Dict, Map},
44    },
45};
46use foundry_evm::{
47    backend::Backend,
48    executors::ExecutorBuilder,
49    inspectors::{
50        CheatsConfig,
51        cheatcodes::{BroadcastableTransactions, Wallets},
52    },
53    opts::EvmOpts,
54    traces::{TraceMode, Traces},
55};
56use foundry_wallets::MultiWalletOpts;
57use serde::Serialize;
58use std::path::PathBuf;
59
60mod broadcast;
61mod build;
62mod execute;
63mod multi_sequence;
64mod progress;
65mod providers;
66mod receipts;
67mod runner;
68mod sequence;
69mod simulate;
70mod transaction;
71mod verify;
72
73// Loads project's figment and merges the build cli arguments into it
74foundry_config::merge_impl_figment_convert!(ScriptArgs, build, evm);
75
76/// CLI arguments for `forge script`.
77#[derive(Clone, Debug, Default, Parser)]
78pub struct ScriptArgs {
79    // Include global options for users of this struct.
80    #[command(flatten)]
81    pub global: GlobalArgs,
82
83    /// The contract you want to run. Either the file path or contract name.
84    ///
85    /// If multiple contracts exist in the same file you must specify the target contract with
86    /// --target-contract.
87    #[arg(value_hint = ValueHint::FilePath)]
88    pub path: String,
89
90    /// Arguments to pass to the script function.
91    pub args: Vec<String>,
92
93    /// The name of the contract you want to run.
94    #[arg(long, visible_alias = "tc", value_name = "CONTRACT_NAME")]
95    pub target_contract: Option<String>,
96
97    /// The signature of the function you want to call in the contract, or raw calldata.
98    #[arg(long, short, default_value = "run")]
99    pub sig: String,
100
101    /// Max priority fee per gas for EIP1559 transactions.
102    #[arg(
103        long,
104        env = "ETH_PRIORITY_GAS_PRICE",
105        value_parser = foundry_cli::utils::parse_ether_value,
106        value_name = "PRICE"
107    )]
108    pub priority_gas_price: Option<U256>,
109
110    /// Use legacy transactions instead of EIP1559 ones.
111    ///
112    /// This is auto-enabled for common networks without EIP1559.
113    #[arg(long)]
114    pub legacy: bool,
115
116    /// Broadcasts the transactions.
117    #[arg(long)]
118    pub broadcast: bool,
119
120    /// Batch size of transactions.
121    ///
122    /// This is ignored and set to 1 if batching is not available or `--slow` is enabled.
123    #[arg(long, default_value = "100")]
124    pub batch_size: usize,
125
126    /// Skips on-chain simulation.
127    #[arg(long)]
128    pub skip_simulation: bool,
129
130    /// Relative percentage to multiply gas estimates by.
131    #[arg(long, short, default_value = "130")]
132    pub gas_estimate_multiplier: u64,
133
134    /// Send via `eth_sendTransaction` using the `--sender` argument as sender.
135    #[arg(
136        long,
137        conflicts_with_all = &["private_key", "private_keys", "ledger", "trezor", "aws"],
138    )]
139    pub unlocked: bool,
140
141    /// Resumes submitting transactions that failed or timed-out previously.
142    ///
143    /// It DOES NOT simulate the script again and it expects nonces to have remained the same.
144    ///
145    /// Example: If transaction N has a nonce of 22, then the account should have a nonce of 22,
146    /// otherwise it fails.
147    #[arg(long)]
148    pub resume: bool,
149
150    /// If present, --resume or --verify will be assumed to be a multi chain deployment.
151    #[arg(long)]
152    pub multi: bool,
153
154    /// Open the script in the debugger.
155    ///
156    /// Takes precedence over broadcast.
157    #[arg(long)]
158    pub debug: bool,
159
160    /// Dumps all debugger steps to file.
161    #[arg(
162        long,
163        requires = "debug",
164        value_hint = ValueHint::FilePath,
165        value_name = "PATH"
166    )]
167    pub dump: Option<PathBuf>,
168
169    /// Makes sure a transaction is sent,
170    /// only after its previous one has been confirmed and succeeded.
171    #[arg(long)]
172    pub slow: bool,
173
174    /// Disables interactive prompts that might appear when deploying big contracts.
175    ///
176    /// For more info on the contract size limit, see EIP-170: <https://eips.ethereum.org/EIPS/eip-170>
177    #[arg(long)]
178    pub non_interactive: bool,
179
180    /// Disables the contract size limit during script execution.
181    #[arg(long)]
182    pub disable_code_size_limit: bool,
183
184    /// Disables the labels in the traces.
185    #[arg(long)]
186    pub disable_labels: bool,
187
188    /// The Etherscan (or equivalent) API key
189    #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
190    pub etherscan_api_key: Option<String>,
191
192    /// Verifies all the contracts found in the receipts of a script, if any.
193    #[arg(long)]
194    pub verify: bool,
195
196    /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions, either
197    /// specified in wei, or as a string with a unit type.
198    ///
199    /// Examples: 1ether, 10gwei, 0.01ether
200    #[arg(
201        long,
202        env = "ETH_GAS_PRICE",
203        value_parser = foundry_cli::utils::parse_ether_value,
204        value_name = "PRICE",
205    )]
206    pub with_gas_price: Option<U256>,
207
208    /// Timeout to use for broadcasting transactions.
209    #[arg(long, env = "ETH_TIMEOUT")]
210    pub timeout: Option<u64>,
211
212    #[command(flatten)]
213    pub build: BuildOpts,
214
215    #[command(flatten)]
216    pub wallets: MultiWalletOpts,
217
218    #[command(flatten)]
219    pub evm: EvmArgs,
220
221    #[command(flatten)]
222    pub verifier: VerifierArgs,
223
224    #[command(flatten)]
225    pub retry: RetryArgs,
226}
227
228impl ScriptArgs {
229    pub async fn preprocess(self) -> Result<PreprocessedState> {
230        let script_wallets = Wallets::new(self.wallets.get_multi_wallet().await?, self.evm.sender);
231
232        let (config, mut evm_opts) = self.load_config_and_evm_opts()?;
233
234        if let Some(sender) = self.maybe_load_private_key()? {
235            evm_opts.sender = sender;
236        }
237
238        let script_config = ScriptConfig::new(config, evm_opts).await?;
239
240        Ok(PreprocessedState { args: self, script_config, script_wallets })
241    }
242
243    /// Executes the script
244    pub async fn run_script(self) -> Result<()> {
245        trace!(target: "script", "executing script command");
246
247        let state = self.preprocess().await?;
248        let create2_deployer = state.script_config.evm_opts.create2_deployer;
249        let compiled = state.compile()?;
250
251        // Move from `CompiledState` to `BundledState` either by resuming or executing and
252        // simulating script.
253        let bundled = if compiled.args.resume || (compiled.args.verify && !compiled.args.broadcast)
254        {
255            compiled.resume().await?
256        } else {
257            // Drive state machine to point at which we have everything needed for simulation.
258            let pre_simulation = compiled
259                .link()
260                .await?
261                .prepare_execution()
262                .await?
263                .execute()
264                .await?
265                .prepare_simulation()
266                .await?;
267
268            if pre_simulation.args.debug {
269                return match pre_simulation.args.dump.clone() {
270                    Some(path) => pre_simulation.dump_debugger(&path),
271                    None => pre_simulation.run_debugger(),
272                };
273            }
274
275            if shell::is_json() {
276                pre_simulation.show_json().await?;
277            } else {
278                pre_simulation.show_traces().await?;
279            }
280
281            // Ensure that we have transactions to simulate/broadcast, otherwise exit early to avoid
282            // hard error.
283            if pre_simulation
284                .execution_result
285                .transactions
286                .as_ref()
287                .is_none_or(|txs| txs.is_empty())
288            {
289                if pre_simulation.args.broadcast {
290                    sh_warn!("No transactions to broadcast.")?;
291                }
292
293                return Ok(());
294            }
295
296            // Check if there are any missing RPCs and exit early to avoid hard error.
297            if pre_simulation.execution_artifacts.rpc_data.missing_rpc {
298                if !shell::is_json() {
299                    sh_println!("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?;
300                }
301
302                return Ok(());
303            }
304
305            pre_simulation.args.check_contract_sizes(
306                &pre_simulation.execution_result,
307                &pre_simulation.build_data.known_contracts,
308                create2_deployer,
309            )?;
310
311            pre_simulation.fill_metadata().await?.bundle().await?
312        };
313
314        // Exit early in case user didn't provide any broadcast/verify related flags.
315        if !bundled.args.should_broadcast() {
316            if !shell::is_json() {
317                if shell::verbosity() >= 4 {
318                    sh_println!("\n=== Transactions that will be broadcast ===\n")?;
319                    bundled.sequence.show_transactions()?;
320                }
321
322                sh_println!(
323                    "\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more."
324                )?;
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
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
515        if let Some(timeout) = self.timeout {
516            dict.insert("transaction_timeout".to_string(), timeout.into());
517        }
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(
541        &self,
542        known_contracts: &ContractsByArtifact,
543    ) -> Vec<AdditionalContract> {
544        self.traces
545            .iter()
546            .flat_map(|(_, traces)| {
547                traces.nodes().iter().filter_map(|node| {
548                    if node.trace.kind.is_any_create() {
549                        let init_code = node.trace.data.clone();
550                        let contract_name = known_contracts
551                            .find_by_creation_code(init_code.as_ref())
552                            .map(|artifact| artifact.0.name.clone());
553                        return Some(AdditionalContract {
554                            opcode: node.trace.kind,
555                            address: node.trace.address,
556                            contract_name,
557                            init_code,
558                        });
559                    }
560                    None
561                })
562            })
563            .collect()
564    }
565}
566
567#[derive(Serialize)]
568struct JsonResult<'a> {
569    logs: Vec<String>,
570    returns: &'a HashMap<String, NestedValue>,
571    #[serde(flatten)]
572    result: &'a ScriptResult,
573}
574
575#[derive(Clone, Debug)]
576pub struct ScriptConfig {
577    pub config: Config,
578    pub evm_opts: EvmOpts,
579    pub sender_nonce: u64,
580    /// Maps a rpc url to a backend
581    pub backends: HashMap<String, Backend>,
582}
583
584impl ScriptConfig {
585    pub async fn new(config: Config, evm_opts: EvmOpts) -> Result<Self> {
586        let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() {
587            next_nonce(evm_opts.sender, fork_url, evm_opts.fork_block_number).await?
588        } else {
589            // dapptools compatibility
590            1
591        };
592
593        Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default() })
594    }
595
596    pub async fn update_sender(&mut self, sender: Address) -> Result<()> {
597        self.sender_nonce = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
598            next_nonce(sender, fork_url, None).await?
599        } else {
600            // dapptools compatibility
601            1
602        };
603        self.evm_opts.sender = sender;
604        Ok(())
605    }
606
607    async fn get_runner(&mut self) -> Result<ScriptRunner> {
608        self._get_runner(None, false).await
609    }
610
611    async fn get_runner_with_cheatcodes(
612        &mut self,
613        known_contracts: ContractsByArtifact,
614        script_wallets: Wallets,
615        debug: bool,
616        target: ArtifactId,
617    ) -> Result<ScriptRunner> {
618        self._get_runner(Some((known_contracts, script_wallets, target)), debug).await
619    }
620
621    async fn _get_runner(
622        &mut self,
623        cheats_data: Option<(ContractsByArtifact, Wallets, ArtifactId)>,
624        debug: bool,
625    ) -> Result<ScriptRunner> {
626        trace!("preparing script runner");
627        let env = self.evm_opts.evm_env().await?;
628
629        let db = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
630            match self.backends.get(fork_url) {
631                Some(db) => db.clone(),
632                None => {
633                    let fork = self.evm_opts.get_fork(&self.config, env.clone());
634                    let backend = Backend::spawn(fork)?;
635                    self.backends.insert(fork_url.clone(), backend.clone());
636                    backend
637                }
638            }
639        } else {
640            // It's only really `None`, when we don't pass any `--fork-url`. And if so, there is
641            // no need to cache it, since there won't be any onchain simulation that we'd need
642            // to cache the backend for.
643            Backend::spawn(None)?
644        };
645
646        // We need to enable tracing to decode contract names: local or external.
647        let mut builder = ExecutorBuilder::new()
648            .inspectors(|stack| {
649                stack
650                    .trace_mode(if debug { TraceMode::Debug } else { TraceMode::Call })
651                    .networks(self.evm_opts.networks)
652                    .create2_deployer(self.evm_opts.create2_deployer)
653            })
654            .spec_id(self.config.evm_spec_id())
655            .gas_limit(self.evm_opts.gas_limit())
656            .legacy_assertions(self.config.legacy_assertions);
657
658        if let Some((known_contracts, script_wallets, target)) = cheats_data {
659            builder = builder.inspectors(|stack| {
660                stack
661                    .cheatcodes(
662                        CheatsConfig::new(
663                            &self.config,
664                            self.evm_opts.clone(),
665                            Some(known_contracts),
666                            Some(target),
667                        )
668                        .into(),
669                    )
670                    .wallets(script_wallets)
671                    .enable_isolation(self.evm_opts.isolate)
672            });
673        }
674
675        Ok(ScriptRunner::new(builder.build(env, db), self.evm_opts.clone()))
676    }
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682    use foundry_config::{NamedChain, UnresolvedEnvVarError};
683    use std::fs;
684    use tempfile::tempdir;
685
686    #[test]
687    fn can_parse_sig() {
688        let sig = "0x522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266";
689        let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--sig", sig]);
690        assert_eq!(args.sig, sig);
691    }
692
693    #[test]
694    fn can_parse_unlocked() {
695        let args = ScriptArgs::parse_from([
696            "foundry-cli",
697            "Contract.sol",
698            "--sender",
699            "0x4e59b44847b379578588920ca78fbf26c0b4956c",
700            "--unlocked",
701        ]);
702        assert!(args.unlocked);
703
704        let key = U256::ZERO;
705        let args = ScriptArgs::try_parse_from([
706            "foundry-cli",
707            "Contract.sol",
708            "--sender",
709            "0x4e59b44847b379578588920ca78fbf26c0b4956c",
710            "--unlocked",
711            "--private-key",
712            key.to_string().as_str(),
713        ]);
714        assert!(args.is_err());
715    }
716
717    #[test]
718    fn can_merge_script_config() {
719        let args = ScriptArgs::parse_from([
720            "foundry-cli",
721            "Contract.sol",
722            "--etherscan-api-key",
723            "goerli",
724        ]);
725        let config = args.load_config().unwrap();
726        assert_eq!(config.etherscan_api_key, Some("goerli".to_string()));
727    }
728
729    #[test]
730    fn can_disable_code_size_limit() {
731        let args =
732            ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--disable-code-size-limit"]);
733        assert!(args.disable_code_size_limit);
734
735        let result = ScriptResult::default();
736        let contracts = ContractsByArtifact::default();
737        let create = Address::ZERO;
738        assert!(args.check_contract_sizes(&result, &contracts, create).is_ok());
739    }
740
741    #[test]
742    fn can_parse_verifier_url() {
743        let args = ScriptArgs::parse_from([
744            "foundry-cli",
745            "script",
746            "script/Test.s.sol:TestScript",
747            "--fork-url",
748            "http://localhost:8545",
749            "--verifier-url",
750            "http://localhost:3000/api/verify",
751            "--etherscan-api-key",
752            "blacksmith",
753            "--broadcast",
754            "--verify",
755            "-vvvvv",
756        ]);
757        assert_eq!(
758            args.verifier.verifier_url,
759            Some("http://localhost:3000/api/verify".to_string())
760        );
761    }
762
763    #[test]
764    fn can_extract_code_size_limit() {
765        let args = ScriptArgs::parse_from([
766            "foundry-cli",
767            "script",
768            "script/Test.s.sol:TestScript",
769            "--fork-url",
770            "http://localhost:8545",
771            "--broadcast",
772            "--code-size-limit",
773            "50000",
774        ]);
775        assert_eq!(args.evm.env.code_size_limit, Some(50000));
776    }
777
778    #[test]
779    fn can_extract_script_etherscan_key() {
780        let temp = tempdir().unwrap();
781        let root = temp.path();
782
783        let config = r#"
784                [profile.default]
785                etherscan_api_key = "amoy"
786
787                [etherscan]
788                amoy = { key = "https://etherscan-amoy.com/" }
789            "#;
790
791        let toml_file = root.join(Config::FILE_NAME);
792        fs::write(toml_file, config).unwrap();
793        let args = ScriptArgs::parse_from([
794            "foundry-cli",
795            "Contract.sol",
796            "--etherscan-api-key",
797            "amoy",
798            "--root",
799            root.as_os_str().to_str().unwrap(),
800        ]);
801
802        let config = args.load_config().unwrap();
803        let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
804        assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
805    }
806
807    #[test]
808    fn can_extract_script_rpc_alias() {
809        let temp = tempdir().unwrap();
810        let root = temp.path();
811
812        let config = r#"
813                [profile.default]
814
815                [rpc_endpoints]
816                polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_CAN_EXTRACT_RPC_ALIAS}"
817            "#;
818
819        let toml_file = root.join(Config::FILE_NAME);
820        fs::write(toml_file, config).unwrap();
821        let args = ScriptArgs::parse_from([
822            "foundry-cli",
823            "DeployV1",
824            "--rpc-url",
825            "polygonAmoy",
826            "--root",
827            root.as_os_str().to_str().unwrap(),
828        ]);
829
830        let err = args.load_config_and_evm_opts().unwrap_err();
831
832        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
833
834        unsafe {
835            std::env::set_var("_CAN_EXTRACT_RPC_ALIAS", "123456");
836        }
837        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
838        assert_eq!(config.eth_rpc_url, Some("polygonAmoy".to_string()));
839        assert_eq!(
840            evm_opts.fork_url,
841            Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
842        );
843    }
844
845    #[test]
846    fn can_extract_script_rpc_and_etherscan_alias() {
847        let temp = tempdir().unwrap();
848        let root = temp.path();
849
850        let config = r#"
851            [profile.default]
852
853            [rpc_endpoints]
854            amoy = "https://polygon-amoy.g.alchemy.com/v2/${_EXTRACT_RPC_ALIAS}"
855
856            [etherscan]
857            amoy = { key = "${_ETHERSCAN_API_KEY}", chain = 80002, url = "https://amoy.polygonscan.com/" }
858        "#;
859
860        let toml_file = root.join(Config::FILE_NAME);
861        fs::write(toml_file, config).unwrap();
862        let args = ScriptArgs::parse_from([
863            "foundry-cli",
864            "DeployV1",
865            "--rpc-url",
866            "amoy",
867            "--etherscan-api-key",
868            "amoy",
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        unsafe {
877            std::env::set_var("_EXTRACT_RPC_ALIAS", "123456");
878        }
879        unsafe {
880            std::env::set_var("_ETHERSCAN_API_KEY", "etherscan_api_key");
881        }
882        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
883        assert_eq!(config.eth_rpc_url, Some("amoy".to_string()));
884        assert_eq!(
885            evm_opts.fork_url,
886            Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
887        );
888        let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
889        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
890        let etherscan = config.get_etherscan_api_key(None);
891        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
892    }
893
894    #[test]
895    fn can_extract_script_rpc_and_sole_etherscan_alias() {
896        let temp = tempdir().unwrap();
897        let root = temp.path();
898
899        let config = r#"
900                [profile.default]
901
902               [rpc_endpoints]
903                amoy = "https://polygon-amoy.g.alchemy.com/v2/${_SOLE_EXTRACT_RPC_ALIAS}"
904
905                [etherscan]
906                amoy = { key = "${_SOLE_ETHERSCAN_API_KEY}" }
907            "#;
908
909        let toml_file = root.join(Config::FILE_NAME);
910        fs::write(toml_file, config).unwrap();
911        let args = ScriptArgs::parse_from([
912            "foundry-cli",
913            "DeployV1",
914            "--rpc-url",
915            "amoy",
916            "--root",
917            root.as_os_str().to_str().unwrap(),
918        ]);
919        let err = args.load_config_and_evm_opts().unwrap_err();
920
921        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
922
923        unsafe {
924            std::env::set_var("_SOLE_EXTRACT_RPC_ALIAS", "123456");
925        }
926        unsafe {
927            std::env::set_var("_SOLE_ETHERSCAN_API_KEY", "etherscan_api_key");
928        }
929        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
930        assert_eq!(
931            evm_opts.fork_url,
932            Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
933        );
934        let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
935        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
936        let etherscan = config.get_etherscan_api_key(None);
937        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
938    }
939
940    // <https://github.com/foundry-rs/foundry/issues/5923>
941    #[test]
942    fn test_5923() {
943        let args =
944            ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--priority-gas-price", "100"]);
945        assert!(args.priority_gas_price.is_some());
946    }
947
948    // <https://github.com/foundry-rs/foundry/issues/5910>
949    #[test]
950    fn test_5910() {
951        let args = ScriptArgs::parse_from([
952            "foundry-cli",
953            "--broadcast",
954            "--with-gas-price",
955            "0",
956            "SolveTutorial",
957        ]);
958        assert!(args.with_gas_price.unwrap().is_zero());
959    }
960}