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