Skip to main content

forge_script/
lib.rs

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