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