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