Skip to main content

forge_script/
lib.rs

1//! # foundry-script
2//!
3//! Smart contract scripting.
4
5#![recursion_limit = "256"]
6#![cfg_attr(not(test), warn(unused_crate_dependencies))]
7#![cfg_attr(docsrs, feature(doc_cfg))]
8
9#[macro_use]
10extern crate foundry_common;
11
12#[macro_use]
13extern crate tracing;
14
15use crate::{broadcast::BundledState, runner::ScriptRunner};
16use alloy_json_abi::{Function, JsonAbi};
17use alloy_network::Network;
18use alloy_primitives::{
19    Address, Bytes, Log, U256, hex,
20    map::{AddressHashMap, HashMap},
21};
22use alloy_signer::Signer;
23use broadcast::next_nonce;
24use build::PreprocessedState;
25use clap::{Parser, ValueHint};
26use dialoguer::Confirm;
27use eyre::{ContextCompat, Result};
28use forge_script_sequence::{AdditionalContract, NestedValue};
29use forge_verify::{RetryArgs, VerifierArgs};
30use foundry_cli::{
31    opts::{BuildOpts, EvmArgs, GlobalArgs},
32    utils::{LoadConfig, parse_fee_token_address},
33};
34use foundry_common::{
35    CONTRACT_MAX_SIZE, ContractsByArtifact, SELECTOR_LEN,
36    abi::{encode_function_args, get_func},
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    core::{
50        Breakpoints, FoundryTransaction,
51        evm::{EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, TempoEvmNetwork, TxEnvFor},
52        tempo::PATH_USD_ADDRESS,
53    },
54    executors::ExecutorBuilder,
55    inspectors::{
56        CheatsConfig,
57        cheatcodes::{BroadcastableTransactions, Wallets},
58    },
59    opts::EvmOpts,
60    traces::{TraceMode, Traces},
61};
62use foundry_wallets::MultiWalletOpts;
63use serde::Serialize;
64use std::path::PathBuf;
65
66mod broadcast;
67mod build;
68mod execute;
69mod multi_sequence;
70mod progress;
71mod providers;
72mod receipts;
73mod runner;
74mod sequence;
75mod simulate;
76mod transaction;
77mod verify;
78
79// Loads project's figment and merges the build cli arguments into it
80foundry_config::merge_impl_figment_convert!(ScriptArgs, build, evm);
81
82/// CLI arguments for `forge script`.
83#[derive(Clone, Debug, Default, Parser)]
84pub struct ScriptArgs {
85    // Include global options for users of this struct.
86    #[command(flatten)]
87    pub global: GlobalArgs,
88
89    /// The contract you want to run. Either the file path or contract name.
90    ///
91    /// If multiple contracts exist in the same file you must specify the target contract with
92    /// --target-contract.
93    #[arg(value_hint = ValueHint::FilePath)]
94    pub path: String,
95
96    /// Arguments to pass to the script function.
97    pub args: Vec<String>,
98
99    /// The name of the contract you want to run.
100    #[arg(long, visible_alias = "tc", value_name = "CONTRACT_NAME")]
101    pub target_contract: Option<String>,
102
103    /// The signature of the function you want to call in the contract, or raw calldata.
104    #[arg(long, short, default_value = "run")]
105    pub sig: String,
106
107    /// Max priority fee per gas for EIP1559 transactions.
108    #[arg(
109        long,
110        env = "ETH_PRIORITY_GAS_PRICE",
111        value_parser = foundry_cli::utils::parse_ether_value,
112        value_name = "PRICE"
113    )]
114    pub priority_gas_price: Option<U256>,
115
116    /// Use legacy transactions instead of EIP1559 ones.
117    ///
118    /// This is auto-enabled for common networks without EIP1559.
119    #[arg(long)]
120    pub legacy: bool,
121
122    /// Broadcasts the transactions.
123    #[arg(long)]
124    pub broadcast: bool,
125
126    /// Batch all broadcast transactions into a single Tempo batch transaction.
127    ///
128    /// When enabled, all vm.broadcast() calls are collected and sent as a single
129    /// atomic type 0x76 transaction instead of individual transactions.
130    /// This provides atomicity (all-or-nothing execution) and gas savings.
131    #[arg(long)]
132    pub batch: bool,
133
134    /// Number of calls per Tempo batch transaction.
135    ///
136    /// When `--batch` is enabled, splits the collected calls into multiple batch
137    /// transactions of at most this many calls each.
138    #[arg(long, requires = "batch", default_value = "100")]
139    pub batch_size: usize,
140
141    /// Tempo fee token address for paying transaction fees.
142    #[arg(long = "tempo.fee-token", value_parser = parse_fee_token_address)]
143    pub fee_token: Option<Address>,
144
145    /// Skips on-chain simulation.
146    #[arg(long)]
147    pub skip_simulation: bool,
148
149    /// Relative percentage to multiply gas estimates by.
150    #[arg(long, short, default_value = "130")]
151    pub gas_estimate_multiplier: u64,
152
153    /// Send via `eth_sendTransaction` using the `--sender` argument as sender.
154    #[arg(
155        long,
156        conflicts_with_all = &["private_key", "private_keys", "ledger", "trezor", "aws", "browser"],
157    )]
158    pub unlocked: bool,
159
160    /// Resumes submitting transactions that failed or timed-out previously.
161    ///
162    /// It DOES NOT simulate the script again and it expects nonces to have remained the same.
163    ///
164    /// Example: If transaction N has a nonce of 22, then the account should have a nonce of 22,
165    /// otherwise it fails.
166    #[arg(long)]
167    pub resume: bool,
168
169    /// If present, --resume or --verify will be assumed to be a multi chain deployment.
170    #[arg(long)]
171    pub multi: bool,
172
173    /// Open the script in the debugger.
174    ///
175    /// Takes precedence over broadcast.
176    #[arg(long)]
177    pub debug: bool,
178
179    /// Dumps all debugger steps to file.
180    #[arg(
181        long,
182        requires = "debug",
183        value_hint = ValueHint::FilePath,
184        value_name = "PATH"
185    )]
186    pub dump: Option<PathBuf>,
187
188    /// Makes sure a transaction is sent,
189    /// only after its previous one has been confirmed and succeeded.
190    #[arg(long)]
191    pub slow: bool,
192
193    /// Disables interactive prompts that might appear when deploying big contracts.
194    ///
195    /// For more info on the contract size limit, see EIP-170: <https://eips.ethereum.org/EIPS/eip-170>
196    #[arg(long)]
197    pub non_interactive: bool,
198
199    /// Disables the contract size limit during script execution.
200    #[arg(long)]
201    pub disable_code_size_limit: bool,
202
203    /// Disables the labels in the traces.
204    #[arg(long)]
205    pub disable_labels: bool,
206
207    /// The Etherscan (or equivalent) API key
208    #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
209    pub etherscan_api_key: Option<String>,
210
211    /// Verifies all the contracts found in the receipts of a script, if any.
212    #[arg(long, requires = "broadcast")]
213    pub verify: bool,
214
215    /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions, either
216    /// specified in wei, or as a string with a unit type.
217    ///
218    /// Examples: 1ether, 10gwei, 0.01ether
219    #[arg(
220        long,
221        env = "ETH_GAS_PRICE",
222        value_parser = foundry_cli::utils::parse_ether_value,
223        value_name = "PRICE",
224    )]
225    pub with_gas_price: Option<U256>,
226
227    /// Timeout to use for broadcasting transactions.
228    #[arg(long, env = "ETH_TIMEOUT")]
229    pub timeout: Option<u64>,
230
231    #[command(flatten)]
232    pub build: BuildOpts,
233
234    #[command(flatten)]
235    pub wallets: MultiWalletOpts,
236
237    #[command(flatten)]
238    pub evm: EvmArgs,
239
240    #[command(flatten)]
241    pub verifier: VerifierArgs,
242
243    #[command(flatten)]
244    pub retry: RetryArgs,
245}
246
247impl ScriptArgs {
248    /// Loads config, resolves evm_opts (including network inference from fork), and returns them.
249    async fn resolved_evm_opts(&self) -> Result<(Config, EvmOpts)> {
250        let (config, mut evm_opts) = self.load_config_and_evm_opts()?;
251
252        // Auto-detect network from fork chain ID when not explicitly configured.
253        evm_opts.infer_network_from_fork().await;
254
255        Ok((config, evm_opts))
256    }
257
258    async fn preprocess<FEN: FoundryEvmNetwork>(
259        self,
260        config: Config,
261        mut evm_opts: EvmOpts,
262    ) -> Result<PreprocessedState<FEN>> {
263        let script_wallets = Wallets::new(self.wallets.get_multi_wallet().await?, self.evm.sender);
264        let browser_wallet = self.wallets.browser_signer::<FEN::Network>().await?;
265
266        if let Some(sender) = self.maybe_load_private_key()? {
267            evm_opts.sender = sender;
268        } else if self.evm.sender.is_none() {
269            // If no sender was explicitly set via --sender, auto-detect it from available signers:
270            // use the sole signer's address if there's exactly one, or fall back to the browser
271            // wallet address if present.
272            if let Ok(signers) = script_wallets.signers()
273                && signers.len() == 1
274            {
275                evm_opts.sender = signers[0];
276            } else if let Some(signer) = browser_wallet.as_ref().map(|b| b.address()) {
277                evm_opts.sender = signer
278            }
279        }
280
281        let fee_token = if evm_opts.networks.is_tempo() && self.fee_token.is_none() {
282            Some(PATH_USD_ADDRESS)
283        } else {
284            self.fee_token
285        };
286
287        let script_config = ScriptConfig::new(config, evm_opts, self.batch, fee_token).await?;
288        Ok(PreprocessedState { args: self, script_config, script_wallets, browser_wallet })
289    }
290
291    /// Executes the script
292    pub async fn run_script(self) -> Result<()> {
293        trace!(target: "script", "executing script command");
294
295        let (config, evm_opts) = self.resolved_evm_opts().await?;
296
297        let is_tempo = evm_opts.networks.is_tempo();
298
299        if self.batch && !is_tempo {
300            eyre::bail!("--batch mode is only supported on Tempo networks");
301        }
302
303        if is_tempo {
304            let batch = self.batch;
305            let bundled = match self.prepare_bundled::<TempoEvmNetwork>(config, evm_opts).await? {
306                Some(bundled) => bundled,
307                None => return Ok(()),
308            };
309            let bundled = bundled.wait_for_pending().await?;
310            let broadcasted =
311                if batch { bundled.broadcast_batch().await? } else { bundled.broadcast().await? };
312            if broadcasted.args.verify {
313                broadcasted.verify().await?;
314            }
315            Ok(())
316        } else if evm_opts.networks.is_optimism() {
317            self.run_generic_script::<OpEvmNetwork>(config, evm_opts).await
318        } else {
319            self.run_generic_script::<EthEvmNetwork>(config, evm_opts).await
320        }
321    }
322
323    /// Prepares the bundled state (compile, simulate, bundle) and returns it
324    /// for broadcasting, or returns `None` if there's nothing to broadcast
325    /// (e.g., debug mode, no transactions, missing RPCs).
326    async fn prepare_bundled<FEN: FoundryEvmNetwork>(
327        self,
328        config: Config,
329        evm_opts: EvmOpts,
330    ) -> Result<Option<BundledState<FEN>>> {
331        let state = self.preprocess::<FEN>(config, evm_opts).await?;
332        let create2_deployer = state.script_config.evm_opts.create2_deployer;
333        let compiled = state.compile()?;
334
335        // Move from `CompiledState` to `BundledState` either by resuming or executing and
336        // simulating script.
337        let bundled = if compiled.args.resume {
338            compiled.resume().await?
339        } else {
340            // Drive state machine to point at which we have everything needed for simulation.
341            let pre_simulation = compiled
342                .link()
343                .await?
344                .prepare_execution()
345                .await?
346                .execute()
347                .await?
348                .prepare_simulation()
349                .await?;
350
351            if pre_simulation.args.debug {
352                return match pre_simulation.args.dump.clone() {
353                    Some(path) => pre_simulation.dump_debugger(&path).map(|_| None),
354                    None => pre_simulation.run_debugger().map(|_| None),
355                };
356            }
357
358            if shell::is_json() {
359                pre_simulation.show_json().await?;
360            } else {
361                pre_simulation.show_traces().await?;
362            }
363
364            // Ensure that we have transactions to simulate/broadcast, otherwise exit early to avoid
365            // hard error.
366            if pre_simulation
367                .execution_result
368                .transactions
369                .as_ref()
370                .is_none_or(|txs| txs.is_empty())
371            {
372                if pre_simulation.args.broadcast {
373                    sh_warn!("No transactions to broadcast.")?;
374                }
375
376                return Ok(None);
377            }
378
379            // Check if there are any missing RPCs and exit early to avoid hard error.
380            if pre_simulation.execution_artifacts.rpc_data.missing_rpc {
381                if !shell::is_json() {
382                    sh_println!("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?;
383                }
384
385                return Ok(None);
386            }
387
388            pre_simulation.args.check_contract_sizes(
389                &pre_simulation.execution_result,
390                &pre_simulation.build_data.known_contracts,
391                create2_deployer,
392            )?;
393
394            pre_simulation.fill_metadata().await?.bundle().await?
395        };
396
397        // Exit early in case user didn't provide any broadcast/verify related flags.
398        if !bundled.args.should_broadcast() {
399            if !shell::is_json() {
400                if shell::verbosity() >= 4 {
401                    sh_println!("\n=== Transactions that will be broadcast ===\n")?;
402                    bundled.sequence.show_transactions()?;
403                }
404
405                sh_println!(
406                    "\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more."
407                )?;
408            }
409            return Ok(None);
410        }
411
412        // Exit early if something is wrong with verification options.
413        if bundled.args.verify {
414            bundled.verify_preflight_check()?;
415        }
416
417        Ok(Some(bundled))
418    }
419
420    async fn run_generic_script<FEN: FoundryEvmNetwork>(
421        self,
422        config: Config,
423        evm_opts: EvmOpts,
424    ) -> Result<()> {
425        let bundled = match self.prepare_bundled::<FEN>(config, evm_opts).await? {
426            Some(bundled) => bundled,
427            None => return Ok(()),
428        };
429
430        // Wait for pending txes and broadcast others.
431        let broadcasted = bundled.wait_for_pending().await?.broadcast().await?;
432
433        if broadcasted.args.verify {
434            broadcasted.verify().await?;
435        }
436
437        Ok(())
438    }
439
440    /// In case the user has loaded *only* one private-key or a single remote signer (e.g.,
441    /// Turnkey), we can assume that they're using it as the `--sender`.
442    fn maybe_load_private_key(&self) -> Result<Option<Address>> {
443        if let Some(turnkey_address) = self.wallets.turnkey_address() {
444            return Ok(Some(turnkey_address));
445        }
446
447        let maybe_sender = self
448            .wallets
449            .private_keys()?
450            .filter(|pks| pks.len() == 1)
451            .map(|pks| pks.first().unwrap().address());
452        Ok(maybe_sender)
453    }
454
455    /// Returns the Function and calldata based on the signature
456    ///
457    /// If the `sig` is a valid human-readable function we find the corresponding function in the
458    /// `abi` If the `sig` is valid hex, we assume it's calldata and try to find the
459    /// corresponding function by matching the selector, first 4 bytes in the calldata.
460    ///
461    /// Note: We assume that the `sig` is already stripped of its prefix, See [`ScriptArgs`]
462    fn get_method_and_calldata(&self, abi: &JsonAbi) -> Result<(Function, Bytes)> {
463        if let Ok(decoded) = hex::decode(&self.sig) {
464            let selector = &decoded[..SELECTOR_LEN];
465            let func =
466                abi.functions().find(|func| selector == &func.selector()[..]).ok_or_else(|| {
467                    eyre::eyre!(
468                        "Function selector `{}` not found in the ABI",
469                        hex::encode(selector)
470                    )
471                })?;
472            return Ok((func.clone(), decoded.into()));
473        }
474
475        let func = if self.sig.contains('(') {
476            let func = get_func(&self.sig)?;
477            abi.functions()
478                .find(|&abi_func| abi_func.selector() == func.selector())
479                .wrap_err(format!("Function `{}` is not implemented in your script.", self.sig))?
480        } else {
481            let matching_functions =
482                abi.functions().filter(|func| func.name == self.sig).collect::<Vec<_>>();
483            match matching_functions.len() {
484                0 => eyre::bail!("Function `{}` not found in the ABI", self.sig),
485                1 => matching_functions[0],
486                2.. => eyre::bail!(
487                    "Multiple functions with the same name `{}` found in the ABI",
488                    self.sig
489                ),
490            }
491        };
492        let data = encode_function_args(func, &self.args)?;
493
494        Ok((func.clone(), data.into()))
495    }
496
497    /// Checks if the transaction is a deployment with either a size above the `CONTRACT_MAX_SIZE`
498    /// or specified `code_size_limit`.
499    ///
500    /// If `self.broadcast` is enabled, it asks confirmation of the user. Otherwise, it just warns
501    /// the user.
502    fn check_contract_sizes<N: Network>(
503        &self,
504        result: &ScriptResult<N>,
505        known_contracts: &ContractsByArtifact,
506        create2_deployer: Address,
507    ) -> Result<()> {
508        // If disable-code-size-limit flag is enabled then skip the size check
509        if self.disable_code_size_limit {
510            return Ok(());
511        }
512
513        // (name, &init, &deployed)[]
514        let mut bytecodes: Vec<(String, &[u8], &[u8])> = vec![];
515
516        // From artifacts
517        for (artifact, contract) in known_contracts.iter() {
518            let Some(bytecode) = contract.bytecode() else { continue };
519            let Some(deployed_bytecode) = contract.deployed_bytecode() else { continue };
520            bytecodes.push((artifact.name.clone(), bytecode, deployed_bytecode));
521        }
522
523        // From traces
524        let create_nodes = result.traces.iter().flat_map(|(_, traces)| {
525            traces.nodes().iter().filter(|node| node.trace.kind.is_any_create())
526        });
527        let mut unknown_c = 0usize;
528        for node in create_nodes {
529            let init_code = &node.trace.data;
530            let deployed_code = &node.trace.output;
531            if !bytecodes.iter().any(|(_, b, _)| *b == init_code.as_ref()) {
532                bytecodes.push((format!("Unknown{unknown_c}"), init_code, deployed_code));
533                unknown_c += 1;
534            }
535            continue;
536        }
537
538        let mut prompt_user = false;
539        let max_size = match self.evm.env.code_size_limit {
540            Some(size) => size,
541            None => CONTRACT_MAX_SIZE,
542        };
543
544        for (data, to) in result.transactions.iter().flat_map(|txes| {
545            txes.iter().filter_map(|tx| {
546                tx.transaction
547                    .input()
548                    .filter(|data| data.len() > max_size)
549                    .map(|data| (data, tx.transaction.to()))
550            })
551        }) {
552            let mut offset = 0;
553
554            // Find if it's a CREATE or CREATE2. Otherwise, skip transaction.
555            if let Some(to) = to {
556                if to == create2_deployer {
557                    // Size of the salt prefix.
558                    offset = 32;
559                } else {
560                    continue;
561                }
562            }
563
564            // Find artifact with a deployment code same as the data.
565            if let Some((name, _, deployed_code)) =
566                bytecodes.iter().find(|(_, init_code, _)| *init_code == &data[offset..])
567            {
568                let deployment_size = deployed_code.len();
569
570                if deployment_size > max_size {
571                    prompt_user = self.should_broadcast();
572                    sh_err!(
573                        "`{name}` is above the contract size limit ({deployment_size} > {max_size})."
574                    )?;
575                }
576            }
577        }
578
579        // Only prompt if we're broadcasting and we've not disabled interactivity.
580        if prompt_user
581            && !self.non_interactive
582            && !Confirm::new().with_prompt("Do you wish to continue?".to_string()).interact()?
583        {
584            eyre::bail!("User canceled the script.");
585        }
586
587        Ok(())
588    }
589
590    /// We only broadcast transactions if --broadcast, --resume, or --verify was passed.
591    fn should_broadcast(&self) -> bool {
592        self.broadcast || self.resume || self.verify
593    }
594}
595
596impl Provider for ScriptArgs {
597    fn metadata(&self) -> Metadata {
598        Metadata::named("Script Args Provider")
599    }
600
601    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
602        let mut dict = Dict::default();
603
604        if let Some(etherscan_api_key) =
605            self.etherscan_api_key.as_ref().filter(|s| !s.trim().is_empty())
606        {
607            dict.insert(
608                "etherscan_api_key".to_string(),
609                figment::value::Value::from(etherscan_api_key.clone()),
610            );
611        }
612
613        if let Some(timeout) = self.timeout {
614            dict.insert("transaction_timeout".to_string(), timeout.into());
615        }
616
617        Ok(Map::from([(Config::selected_profile(), dict)]))
618    }
619}
620
621#[derive(Serialize, Clone)]
622#[serde(bound = "")]
623pub struct ScriptResult<N: Network> {
624    pub success: bool,
625    #[serde(rename = "raw_logs")]
626    pub logs: Vec<Log>,
627    pub traces: Traces,
628    pub gas_used: u64,
629    pub labeled_addresses: AddressHashMap<String>,
630    #[serde(skip)]
631    pub transactions: Option<BroadcastableTransactions<N>>,
632    pub returned: Bytes,
633    pub address: Option<Address>,
634    #[serde(skip)]
635    pub breakpoints: Breakpoints,
636}
637
638impl<N: Network> Default for ScriptResult<N> {
639    fn default() -> Self {
640        Self {
641            success: Default::default(),
642            logs: Default::default(),
643            traces: Default::default(),
644            gas_used: Default::default(),
645            labeled_addresses: Default::default(),
646            transactions: Default::default(),
647            returned: Default::default(),
648            address: Default::default(),
649            breakpoints: Default::default(),
650        }
651    }
652}
653
654impl<N: Network> ScriptResult<N> {
655    pub fn get_created_contracts(
656        &self,
657        known_contracts: &ContractsByArtifact,
658    ) -> Vec<AdditionalContract> {
659        self.traces
660            .iter()
661            .flat_map(|(_, traces)| {
662                traces.nodes().iter().filter_map(|node| {
663                    if node.trace.kind.is_any_create() {
664                        let init_code = node.trace.data.clone();
665                        let contract_name = known_contracts
666                            .find_by_creation_code(init_code.as_ref())
667                            .map(|artifact| artifact.0.name.clone());
668                        return Some(AdditionalContract {
669                            call_kind: node.trace.kind,
670                            address: node.trace.address,
671                            contract_name,
672                            init_code,
673                        });
674                    }
675                    None
676                })
677            })
678            .collect()
679    }
680}
681
682#[derive(Serialize)]
683#[serde(bound = "")]
684struct JsonResult<'a, N: Network> {
685    logs: Vec<String>,
686    returns: &'a HashMap<String, NestedValue>,
687    #[serde(flatten)]
688    result: &'a ScriptResult<N>,
689}
690
691#[derive(Clone, Debug)]
692pub struct ScriptConfig<FEN: FoundryEvmNetwork> {
693    pub config: Config,
694    pub evm_opts: EvmOpts,
695    pub sender_nonce: u64,
696    /// Maps a rpc url to a backend
697    pub backends: HashMap<String, Backend<FEN>>,
698    /// Whether to batch all broadcast transactions into a single Tempo batch transaction.
699    pub batch: bool,
700    /// Tempo fee token address for paying transaction fees.
701    pub fee_token: Option<Address>,
702}
703
704impl<FEN: FoundryEvmNetwork> ScriptConfig<FEN> {
705    pub async fn new(
706        config: Config,
707        evm_opts: EvmOpts,
708        batch: bool,
709        fee_token: Option<Address>,
710    ) -> Result<Self> {
711        let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() {
712            next_nonce(evm_opts.sender, fork_url, evm_opts.fork_block_number).await?
713        } else {
714            // dapptools compatibility
715            1
716        };
717
718        Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default(), batch, fee_token })
719    }
720
721    pub async fn update_sender(&mut self, sender: Address) -> Result<()> {
722        self.sender_nonce = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
723            next_nonce(sender, fork_url, None).await?
724        } else {
725            // dapptools compatibility
726            1
727        };
728        self.evm_opts.sender = sender;
729        Ok(())
730    }
731
732    async fn get_runner(&mut self) -> Result<ScriptRunner<FEN>> {
733        self._get_runner(None, false).await
734    }
735
736    async fn get_runner_with_cheatcodes(
737        &mut self,
738        known_contracts: ContractsByArtifact,
739        script_wallets: Wallets,
740        debug: bool,
741        target: ArtifactId,
742    ) -> Result<ScriptRunner<FEN>> {
743        self._get_runner(Some((known_contracts, script_wallets, target)), debug).await
744    }
745
746    async fn _get_runner(
747        &mut self,
748        cheats_data: Option<(ContractsByArtifact, Wallets, ArtifactId)>,
749        debug: bool,
750    ) -> Result<ScriptRunner<FEN>> {
751        trace!("preparing script runner");
752        let (evm_env, mut tx_env, fork_block) = self.evm_opts.env::<_, _, TxEnvFor<FEN>>().await?;
753
754        let db = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
755            match self.backends.get(fork_url) {
756                Some(db) => db.clone(),
757                None => {
758                    let fork =
759                        self.evm_opts.get_fork(&self.config, evm_env.cfg_env.chain_id, fork_block);
760                    let backend = Backend::spawn(fork)?;
761                    self.backends.insert(fork_url.clone(), backend.clone());
762                    backend
763                }
764            }
765        } else {
766            // It's only really `None`, when we don't pass any `--fork-url`. And if so, there is
767            // no need to cache it, since there won't be any onchain simulation that we'd need
768            // to cache the backend for.
769            Backend::spawn(None)?
770        };
771
772        // We need to enable tracing to decode contract names: local or external.
773        let mut builder = ExecutorBuilder::default()
774            .inspectors(|stack| {
775                stack
776                    .logs(self.config.live_logs)
777                    .trace_mode(if debug { TraceMode::Debug } else { TraceMode::Call })
778                    .networks(self.evm_opts.networks)
779                    .create2_deployer(self.evm_opts.create2_deployer)
780            })
781            .spec_id(self.config.evm_spec_id())
782            .gas_limit(self.evm_opts.gas_limit())
783            .legacy_assertions(self.config.legacy_assertions);
784
785        if let Some((known_contracts, script_wallets, target)) = cheats_data {
786            builder = builder.inspectors(|stack| {
787                stack
788                    .cheatcodes(
789                        CheatsConfig::new(
790                            &self.config,
791                            self.evm_opts.clone(),
792                            Some(known_contracts),
793                            Some(target),
794                            self.fee_token,
795                        )
796                        .into(),
797                    )
798                    .wallets(script_wallets)
799                    .enable_isolation(self.evm_opts.isolate)
800            });
801        }
802
803        // Propagate fee token to the transaction environment so that internal EVM calls
804        // (e.g. script deployment, setUp) use the correct fee token for Tempo networks.
805        tx_env.set_fee_token(self.fee_token);
806
807        Ok(ScriptRunner::new(builder.build(evm_env, tx_env, db), self.evm_opts.clone()))
808    }
809}
810
811#[cfg(test)]
812mod tests {
813    use super::*;
814    use alloy_network::Ethereum;
815    use foundry_config::{NamedChain, UnresolvedEnvVarError};
816    use std::fs;
817    use tempfile::tempdir;
818
819    #[test]
820    fn can_parse_sig() {
821        let sig = "0x522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266";
822        let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--sig", sig]);
823        assert_eq!(args.sig, sig);
824    }
825
826    #[test]
827    fn can_parse_unlocked() {
828        let args = ScriptArgs::parse_from([
829            "foundry-cli",
830            "Contract.sol",
831            "--sender",
832            "0x4e59b44847b379578588920ca78fbf26c0b4956c",
833            "--unlocked",
834        ]);
835        assert!(args.unlocked);
836
837        let key = U256::ZERO;
838        let args = ScriptArgs::try_parse_from([
839            "foundry-cli",
840            "Contract.sol",
841            "--sender",
842            "0x4e59b44847b379578588920ca78fbf26c0b4956c",
843            "--unlocked",
844            "--private-key",
845            &key.to_string(),
846        ]);
847        assert!(args.is_err());
848    }
849
850    #[test]
851    fn can_merge_script_config() {
852        let args = ScriptArgs::parse_from([
853            "foundry-cli",
854            "Contract.sol",
855            "--etherscan-api-key",
856            "goerli",
857        ]);
858        let config = args.load_config().unwrap();
859        assert_eq!(config.etherscan_api_key, Some("goerli".to_string()));
860    }
861
862    #[test]
863    fn can_disable_code_size_limit() {
864        let args =
865            ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--disable-code-size-limit"]);
866        assert!(args.disable_code_size_limit);
867
868        let result = ScriptResult::<Ethereum>::default();
869        let contracts = ContractsByArtifact::default();
870        let create = Address::ZERO;
871        assert!(args.check_contract_sizes(&result, &contracts, create).is_ok());
872    }
873
874    #[test]
875    fn can_parse_verifier_url() {
876        let args = ScriptArgs::parse_from([
877            "foundry-cli",
878            "script",
879            "script/Test.s.sol:TestScript",
880            "--fork-url",
881            "http://localhost:8545",
882            "--verifier-url",
883            "http://localhost:3000/api/verify",
884            "--etherscan-api-key",
885            "blacksmith",
886            "--broadcast",
887            "--verify",
888            "-vvvvv",
889        ]);
890        assert_eq!(
891            args.verifier.verifier_url,
892            Some("http://localhost:3000/api/verify".to_string())
893        );
894    }
895
896    #[test]
897    fn can_extract_code_size_limit() {
898        let args = ScriptArgs::parse_from([
899            "foundry-cli",
900            "script",
901            "script/Test.s.sol:TestScript",
902            "--fork-url",
903            "http://localhost:8545",
904            "--broadcast",
905            "--code-size-limit",
906            "50000",
907        ]);
908        assert_eq!(args.evm.env.code_size_limit, Some(50000));
909    }
910
911    #[test]
912    fn can_extract_script_etherscan_key() {
913        let temp = tempdir().unwrap();
914        let root = temp.path();
915
916        let config = r#"
917                [profile.default]
918                etherscan_api_key = "amoy"
919
920                [etherscan]
921                amoy = { key = "https://etherscan-amoy.com/" }
922            "#;
923
924        let toml_file = root.join(Config::FILE_NAME);
925        fs::write(toml_file, config).unwrap();
926        let args = ScriptArgs::parse_from([
927            "foundry-cli",
928            "Contract.sol",
929            "--etherscan-api-key",
930            "amoy",
931            "--root",
932            root.as_os_str().to_str().unwrap(),
933        ]);
934
935        let config = args.load_config().unwrap();
936        let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
937        assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
938    }
939
940    #[test]
941    fn can_extract_script_rpc_alias() {
942        let temp = tempdir().unwrap();
943        let root = temp.path();
944
945        let config = r#"
946                [profile.default]
947
948                [rpc_endpoints]
949                polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_CAN_EXTRACT_RPC_ALIAS}"
950            "#;
951
952        let toml_file = root.join(Config::FILE_NAME);
953        fs::write(toml_file, config).unwrap();
954        let args = ScriptArgs::parse_from([
955            "foundry-cli",
956            "DeployV1",
957            "--rpc-url",
958            "polygonAmoy",
959            "--root",
960            root.as_os_str().to_str().unwrap(),
961        ]);
962
963        let err = args.load_config_and_evm_opts().unwrap_err();
964
965        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
966
967        unsafe {
968            std::env::set_var("_CAN_EXTRACT_RPC_ALIAS", "123456");
969        }
970        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
971        assert_eq!(config.eth_rpc_url, Some("polygonAmoy".to_string()));
972        assert_eq!(
973            evm_opts.fork_url,
974            Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
975        );
976    }
977
978    #[test]
979    fn can_extract_script_rpc_and_etherscan_alias() {
980        let temp = tempdir().unwrap();
981        let root = temp.path();
982
983        let config = r#"
984            [profile.default]
985
986            [rpc_endpoints]
987            amoy = "https://polygon-amoy.g.alchemy.com/v2/${_EXTRACT_RPC_ALIAS}"
988
989            [etherscan]
990            amoy = { key = "${_ETHERSCAN_API_KEY}", chain = 80002, url = "https://amoy.polygonscan.com/" }
991        "#;
992
993        let toml_file = root.join(Config::FILE_NAME);
994        fs::write(toml_file, config).unwrap();
995        let args = ScriptArgs::parse_from([
996            "foundry-cli",
997            "DeployV1",
998            "--rpc-url",
999            "amoy",
1000            "--etherscan-api-key",
1001            "amoy",
1002            "--root",
1003            root.as_os_str().to_str().unwrap(),
1004        ]);
1005        let err = args.load_config_and_evm_opts().unwrap_err();
1006
1007        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
1008
1009        unsafe {
1010            std::env::set_var("_EXTRACT_RPC_ALIAS", "123456");
1011        }
1012        unsafe {
1013            std::env::set_var("_ETHERSCAN_API_KEY", "etherscan_api_key");
1014        }
1015        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
1016        assert_eq!(config.eth_rpc_url, Some("amoy".to_string()));
1017        assert_eq!(
1018            evm_opts.fork_url,
1019            Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
1020        );
1021        let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
1022        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1023        let etherscan = config.get_etherscan_api_key(None);
1024        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1025    }
1026
1027    #[test]
1028    fn can_extract_script_rpc_and_sole_etherscan_alias() {
1029        let temp = tempdir().unwrap();
1030        let root = temp.path();
1031
1032        let config = r#"
1033                [profile.default]
1034
1035               [rpc_endpoints]
1036                amoy = "https://polygon-amoy.g.alchemy.com/v2/${_SOLE_EXTRACT_RPC_ALIAS}"
1037
1038                [etherscan]
1039                amoy = { key = "${_SOLE_ETHERSCAN_API_KEY}" }
1040            "#;
1041
1042        let toml_file = root.join(Config::FILE_NAME);
1043        fs::write(toml_file, config).unwrap();
1044        let args = ScriptArgs::parse_from([
1045            "foundry-cli",
1046            "DeployV1",
1047            "--rpc-url",
1048            "amoy",
1049            "--root",
1050            root.as_os_str().to_str().unwrap(),
1051        ]);
1052        let err = args.load_config_and_evm_opts().unwrap_err();
1053
1054        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
1055
1056        unsafe {
1057            std::env::set_var("_SOLE_EXTRACT_RPC_ALIAS", "123456");
1058        }
1059        unsafe {
1060            std::env::set_var("_SOLE_ETHERSCAN_API_KEY", "etherscan_api_key");
1061        }
1062        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
1063        assert_eq!(
1064            evm_opts.fork_url,
1065            Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
1066        );
1067        let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
1068        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1069        let etherscan = config.get_etherscan_api_key(None);
1070        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1071    }
1072
1073    // <https://github.com/foundry-rs/foundry/issues/5923>
1074    #[test]
1075    fn test_5923() {
1076        let args =
1077            ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--priority-gas-price", "100"]);
1078        assert!(args.priority_gas_price.is_some());
1079    }
1080
1081    // <https://github.com/foundry-rs/foundry/issues/5910>
1082    #[test]
1083    fn test_5910() {
1084        let args = ScriptArgs::parse_from([
1085            "foundry-cli",
1086            "--broadcast",
1087            "--with-gas-price",
1088            "0",
1089            "SolveTutorial",
1090        ]);
1091        assert!(args.with_gas_price.unwrap().is_zero());
1092    }
1093
1094    #[test]
1095    fn test_priority_gas_price_cannot_exceed_gas_price() {
1096        let args = ScriptArgs::parse_from([
1097            "foundry-cli",
1098            "--broadcast",
1099            "--with-gas-price",
1100            "100",
1101            "--priority-gas-price",
1102            "200",
1103            "Script",
1104        ]);
1105        // priority (200) > max_fee (100) — broadcast should reject this at runtime
1106        assert!(args.priority_gas_price.unwrap() > args.with_gas_price.unwrap());
1107    }
1108}