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