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, simulate::PreSimulationState};
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_resolved;
24use build::PreprocessedState;
25use clap::{Parser, ValueHint, builder::RangedU64ValueParser};
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, TracingArgs},
32    utils::LoadConfig,
33};
34use foundry_common::{
35    ContractsByArtifact, SELECTOR_LEN,
36    abi::{encode_function_args, get_func},
37    compile::ContractSizeLimits,
38    shell,
39};
40use foundry_compilers::{ArtifactId, artifacts::output_selection::ContractOutputSelection};
41use foundry_config::{
42    Config, Eip1559FeeEstimatePreset, FoundryHardfork, figment,
43    figment::{
44        Metadata, Profile, Provider,
45        value::{Dict, Map},
46    },
47};
48use foundry_debugger::DebuggerLayout;
49use foundry_evm::{
50    backend::Backend,
51    core::{
52        Breakpoints, FoundryTransaction,
53        evm::{EthEvmNetwork, EvmEnvFor, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor},
54        fork::ResolvedFork,
55    },
56    executors::ExecutorBuilder,
57    inspectors::{
58        CheatsConfig,
59        cheatcodes::{BroadcastableTransactions, Wallets},
60    },
61    opts::{EvmOpts, ExecutionSpecContext, resolve_execution_spec},
62    revm::interpreter::InstructionResult,
63    traces::{InternalTraceMode, TraceRequirements, Traces},
64};
65use foundry_evm_networks::NetworkConfigs;
66use foundry_wallets::MultiWalletOpts;
67use serde::Serialize;
68use std::path::PathBuf;
69
70#[cfg(feature = "base")]
71use foundry_evm::core::evm::BaseEvmNetwork;
72
73#[cfg(feature = "monad")]
74use foundry_evm::core::evm::MonadEvmNetwork;
75
76#[cfg(feature = "optimism")]
77use foundry_evm::core::evm::OpEvmNetwork;
78
79mod broadcast;
80mod build;
81mod execute;
82mod library_deployments;
83mod multi_sequence;
84mod progress;
85mod providers;
86mod receipts;
87mod runner;
88mod sequence;
89mod session;
90mod simulate;
91mod transaction;
92mod verify;
93mod wallet_session;
94
95pub use wallet_session::ScriptWalletSessionArgs;
96
97// Loads project's figment and merges the build cli arguments into it
98foundry_config::merge_impl_figment_convert!(ScriptArgs, build, evm);
99
100const DEFAULT_CONFIRMATIONS: u64 = 1;
101
102/// Shared preparation stops before execution-family-specific on-chain simulation.
103enum PreparedScript<FEN: FoundryEvmNetwork> {
104    Resume(Box<BundledState<FEN>>),
105    Simulate(Box<PreSimulationState<FEN>>),
106}
107
108/// CLI arguments for `forge script`.
109#[derive(Clone, Debug, Default, Parser)]
110pub struct ScriptArgs {
111    // Include global options for users of this struct.
112    #[command(flatten)]
113    pub global: GlobalArgs,
114
115    /// The contract you want to run. Either the file path or contract name.
116    ///
117    /// If multiple contracts exist in the same file you must specify the target contract with
118    /// --target-contract.
119    #[arg(value_hint = ValueHint::FilePath)]
120    pub path: String,
121
122    /// Arguments to pass to the script function.
123    pub args: Vec<String>,
124
125    /// The name of the contract you want to run.
126    #[arg(long, visible_alias = "tc", value_name = "CONTRACT_NAME")]
127    pub target_contract: Option<String>,
128
129    /// The signature of the function you want to call in the contract, or raw calldata.
130    #[arg(long, short, default_value = "run")]
131    pub sig: String,
132
133    /// Max priority fee per gas for EIP1559 transactions.
134    #[arg(
135        long,
136        env = "ETH_PRIORITY_GAS_PRICE",
137        value_parser = foundry_cli::utils::parse_ether_value,
138        value_name = "PRICE"
139    )]
140    pub priority_gas_price: Option<U256>,
141
142    /// Use legacy transactions instead of EIP1559 ones.
143    ///
144    /// This is auto-enabled for common networks without EIP1559.
145    #[arg(long)]
146    pub legacy: bool,
147
148    /// How to estimate EIP-1559 fees: `low`, `market` (default), or `aggressive`.
149    ///
150    /// The preset sets the priority-fee percentile and the `maxFeePerGas` buffer
151    /// (`low`: `base_fee * 1.5`, others: `* 2`); `low`'s tighter buffer is more
152    /// likely to stall if the base fee rises. `--with-gas-price` and
153    /// `--priority-gas-price` override only `maxFeePerGas` and
154    /// `maxPriorityFeePerGas` respectively. Ignored for `--legacy`.
155    #[arg(long = "estimate", value_name = "PRESET")]
156    pub eip1559_fee_estimate: Option<Eip1559FeeEstimatePreset>,
157
158    /// Broadcasts the transactions.
159    #[arg(long)]
160    pub broadcast: bool,
161
162    /// Batch all broadcast transactions into a single Tempo batch transaction.
163    ///
164    /// When enabled, all vm.broadcast() calls are collected and sent as a single
165    /// atomic type 0x76 transaction instead of individual transactions.
166    /// This provides atomicity (all-or-nothing execution) and gas savings.
167    #[arg(long)]
168    pub batch: bool,
169
170    /// Tempo transaction options.
171    #[command(flatten)]
172    pub tempo: TempoOpts,
173
174    /// Create a temporary Tempo wallet session, run this script with it, then revoke it.
175    #[command(flatten)]
176    pub wallet_session: ScriptWalletSessionArgs,
177
178    /// Skips on-chain simulation.
179    #[arg(long)]
180    pub skip_simulation: bool,
181
182    /// Relative percentage to multiply gas estimates by.
183    #[arg(long, short, default_value = "130")]
184    pub gas_estimate_multiplier: u64,
185
186    /// Override the sender's initial nonce for script execution and transaction generation.
187    #[arg(
188        long,
189        value_name = "NONCE",
190        value_parser = RangedU64ValueParser::<u64>::new().range(..u64::MAX),
191    )]
192    pub sender_nonce: Option<u64>,
193
194    /// Send via `eth_sendTransaction` using the `--sender` argument as sender.
195    #[arg(
196        long,
197        conflicts_with_all = &["private_key", "private_keys", "ledger", "trezor", "aws", "browser"],
198    )]
199    pub unlocked: bool,
200
201    /// Resumes submitting transactions that failed or timed-out previously.
202    ///
203    /// It DOES NOT simulate the script again and it expects nonces to have remained the same.
204    ///
205    /// Example: If transaction N has a nonce of 22, then the account should have a nonce of 22,
206    /// otherwise it fails.
207    #[arg(long)]
208    pub resume: bool,
209
210    /// If present, --resume or --verify will be assumed to be a multi chain deployment.
211    #[arg(long)]
212    pub multi: bool,
213
214    /// Open the script in the debugger.
215    ///
216    /// Takes precedence over broadcast.
217    #[arg(long)]
218    pub debug: bool,
219
220    /// Debugger layout to use.
221    #[arg(long = "debug-layout", requires = "debug", value_enum)]
222    pub debug_layout: Option<DebuggerLayout>,
223
224    /// Dumps all debugger steps to file.
225    #[arg(
226        long,
227        requires = "debug",
228        value_hint = ValueHint::FilePath,
229        value_name = "PATH"
230    )]
231    pub dump: Option<PathBuf>,
232
233    /// Makes sure a transaction is sent,
234    /// only after its previous one has been confirmed and succeeded.
235    #[arg(long)]
236    pub slow: bool,
237
238    /// The number of confirmations to wait for after broadcasting.
239    #[arg(
240        long,
241        default_value_t = DEFAULT_CONFIRMATIONS,
242        value_parser = RangedU64ValueParser::<u64>::new().range(1..),
243    )]
244    pub confirmations: u64,
245
246    /// Disables interactive prompts that might appear when deploying big contracts.
247    ///
248    /// For more info on the contract size limit, see EIP-170: <https://eips.ethereum.org/EIPS/eip-170>
249    #[arg(long)]
250    pub non_interactive: bool,
251
252    /// Disables the contract size limit during script execution.
253    #[arg(long)]
254    pub disable_code_size_limit: bool,
255
256    #[command(flatten)]
257    pub tracing: TracingArgs,
258
259    /// The Etherscan (or equivalent) API key
260    #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
261    pub etherscan_api_key: Option<String>,
262
263    /// Verifies all the contracts found in the receipts of a script, if any.
264    #[arg(long, requires = "broadcast")]
265    pub verify: bool,
266
267    /// Opt-in verification of contracts deployed by external factories using explorer source.
268    #[arg(long, requires = "verify", conflicts_with_all = ["skip_simulation", "offline"])]
269    pub verify_external: bool,
270
271    /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions, either
272    /// specified in wei, or as a string with a unit type.
273    ///
274    /// Examples: 1ether, 10gwei, 0.01ether
275    #[arg(
276        long,
277        env = "ETH_GAS_PRICE",
278        value_parser = foundry_cli::utils::parse_ether_value,
279        value_name = "PRICE",
280    )]
281    pub with_gas_price: Option<U256>,
282
283    /// Timeout to use for broadcasting transactions.
284    #[arg(long, env = "ETH_TIMEOUT")]
285    pub timeout: Option<u64>,
286
287    #[command(flatten)]
288    pub build: BuildOpts,
289
290    #[command(flatten)]
291    pub wallets: MultiWalletOpts,
292
293    #[command(flatten)]
294    pub evm: EvmArgs,
295
296    #[command(flatten)]
297    pub verifier: VerifierArgs,
298
299    #[command(flatten)]
300    pub retry: RetryArgs,
301}
302
303impl ScriptArgs {
304    fn has_tempo_session(&self) -> Result<bool> {
305        Ok(self.tempo.session_id()?.is_some())
306    }
307
308    /// Resolves evm_opts (including network inference from fork) using the loaded config.
309    async fn resolved_evm_opts(
310        &self,
311        mut config: Config,
312        mut evm_opts: EvmOpts,
313    ) -> Result<(Config, EvmOpts)> {
314        if self.tempo.is_tempo() || self.has_tempo_session()? {
315            if evm_opts.networks.has_network_selection() {
316                let network = evm_opts.networks.execution_network();
317                eyre::ensure!(
318                    network.is_tempo(),
319                    "Tempo transaction options conflict with configured network `{}`",
320                    evm_opts.networks.execution_profile_name()
321                );
322            }
323            // If Tempo tx options or a session are set, select the Tempo network.
324            evm_opts.networks = NetworkConfigs::with_tempo();
325        }
326        // Discover endpoint source identity and exact hardfork even when execution network is
327        // explicit. Without an explicit selection this also infers the execution network.
328        evm_opts.infer_network_from_fork().await?;
329        config.networks = evm_opts.networks;
330
331        Ok((config, evm_opts))
332    }
333
334    async fn preprocess<FEN: FoundryEvmNetwork>(
335        self,
336        mut config: Config,
337        mut evm_opts: EvmOpts,
338        executor_builder: ExecutorBuilder<FEN>,
339    ) -> Result<PreprocessedState<FEN>> {
340        let args = self;
341        let mut tempo = args.tempo.clone();
342
343        let session_sender = if args.resume {
344            None
345        } else {
346            // Initial scripts may only reveal multi-chain transactions during execution. Use the
347            // session root as the script sender here and validate chain scope during broadcast.
348            tempo.session_sender_for_multi_wallet(&args.wallets, args.evm.sender)?
349        };
350
351        let script_wallets = Wallets::new(args.wallets.get_multi_wallet().await?, args.evm.sender);
352        let browser_wallet = args.wallets.browser_signer::<FEN::Network>().await?;
353
354        if let Some(sender) = session_sender {
355            evm_opts.sender = sender;
356        } else if let Some(sender) = args.maybe_load_private_key()? {
357            evm_opts.sender = sender;
358        } else if args.evm.sender.is_none() {
359            // If no sender was explicitly set via --sender, auto-detect it from available signers:
360            // use the sole signer's address if there's exactly one, or fall back to the browser
361            // wallet address if present.
362            let addresses = script_wallets.addresses();
363            if addresses.len() == 1 {
364                evm_opts.sender = addresses[0];
365            } else if let Some(signer) = browser_wallet.as_ref().map(|b| b.address()) {
366                evm_opts.sender = signer
367            }
368        }
369
370        tempo.resolve_expires();
371        config.tracing = args.tracing.resolve(&config.tracing, evm_opts.verbosity);
372        if args.debug && !config.extra_output.contains(&ContractOutputSelection::StorageLayout) {
373            config.extra_output.push(ContractOutputSelection::StorageLayout);
374        }
375
376        let script_config = ScriptConfig::new(
377            config,
378            evm_opts,
379            executor_builder,
380            args.batch,
381            tempo,
382            args.sender_nonce,
383        )
384        .await?;
385        Ok(PreprocessedState { args, script_config, script_wallets, browser_wallet })
386    }
387
388    /// Executes the script
389    #[allow(clippy::large_stack_frames)]
390    pub async fn run_script(self) -> Result<()> {
391        trace!(target: "script", "executing script command");
392
393        if self.tempo.sponsor_url.is_some() {
394            eyre::bail!(
395                "--sponsor-url is not supported by forge script; use --tempo.sponsor with \
396                 --tempo.sponsor-signer or --tempo.sponsor-sig"
397            );
398        }
399
400        if self.wallet_session.enabled {
401            return self.run_wallet_session_wrapper();
402        }
403
404        let (mut config, evm_opts) = self.load_config_and_evm_opts()?;
405        self.install_missing_dependencies(&mut config)?;
406        let (config, evm_opts) = self.resolved_evm_opts(config, evm_opts).await?;
407
408        let is_tempo = evm_opts.networks.is_tempo();
409
410        if self.batch && !is_tempo {
411            eyre::bail!("--batch mode is only supported on Tempo networks");
412        }
413
414        if self.unlocked && self.has_tempo_session()? {
415            eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --unlocked");
416        }
417
418        // Box each branch's future to keep its large async state off `run_script`'s future;
419        // otherwise `run_command` trips `clippy::large_stack_frames` by a small margin.
420        if is_tempo {
421            let batch = self.batch;
422            return Box::pin(async move {
423                let bundled = match self
424                    .prepare_bundled::<TempoEvmNetwork>(
425                        config,
426                        evm_opts,
427                        ExecutorBuilder::<TempoEvmNetwork>::new(),
428                    )
429                    .await?
430                {
431                    Some(bundled) => bundled,
432                    None => return Ok(()),
433                };
434                // batch mode owns its own pending recovery inside broadcast_batch(); running the
435                // generic wait_for_pending() first would race with that and could double-process
436                // an already-confirmed batch hash.
437                let bundled = if batch { bundled } else { bundled.wait_for_pending().await? };
438                let broadcasted = if batch {
439                    bundled.broadcast_batch().await?
440                } else {
441                    bundled.broadcast().await?
442                };
443                if broadcasted.args.verify {
444                    broadcasted.verify().await?;
445                }
446                Ok(())
447            })
448            .await;
449        }
450
451        #[cfg(feature = "base")]
452        if evm_opts.networks.is_base() {
453            return Box::pin(self.run_generic_script::<BaseEvmNetwork>(
454                config,
455                evm_opts,
456                ExecutorBuilder::<BaseEvmNetwork>::new(),
457            ))
458            .await;
459        }
460
461        #[cfg(feature = "monad")]
462        if evm_opts.networks.is_monad() {
463            return Box::pin(async move {
464                let Some(prepared) = self
465                    .prepare_script(config, evm_opts, ExecutorBuilder::<MonadEvmNetwork>::new())
466                    .await?
467                else {
468                    return Ok(());
469                };
470                let bundled = match prepared {
471                    PreparedScript::Resume(bundled) => *bundled,
472                    PreparedScript::Simulate(state) => {
473                        state.fill_monad_metadata().await?.bundle().await?
474                    }
475                };
476                let Some(bundled) = Self::finish_bundle(bundled).await? else { return Ok(()) };
477                Self::broadcast_bundle(bundled).await
478            })
479            .await;
480        }
481
482        #[cfg(feature = "optimism")]
483        if evm_opts.networks.is_optimism() {
484            return Box::pin(self.run_generic_script::<OpEvmNetwork>(
485                config,
486                evm_opts,
487                ExecutorBuilder::<OpEvmNetwork>::new(),
488            ))
489            .await;
490        }
491
492        Box::pin(self.run_generic_script::<EthEvmNetwork>(
493            config,
494            evm_opts,
495            ExecutorBuilder::<EthEvmNetwork>::new(),
496        ))
497        .await
498    }
499
500    /// Prepares the bundled state (compile, simulate, bundle) and returns it
501    /// for broadcasting, or returns `None` if there's nothing to broadcast
502    /// (e.g., debug mode, no transactions, missing RPCs).
503    #[allow(clippy::large_stack_frames)]
504    async fn prepare_bundled<FEN: FoundryEvmNetwork>(
505        self,
506        config: Config,
507        evm_opts: EvmOpts,
508        executor_builder: ExecutorBuilder<FEN>,
509    ) -> Result<Option<BundledState<FEN>>> {
510        let Some(prepared) = self.prepare_script(config, evm_opts, executor_builder).await? else {
511            return Ok(None);
512        };
513        let bundled = match prepared {
514            PreparedScript::Resume(bundled) => *bundled,
515            PreparedScript::Simulate(state) => {
516                state.fill_ordinary_metadata().await?.bundle().await?
517            }
518        };
519        Self::finish_bundle(bundled).await
520    }
521
522    /// Compiles and executes the local script, leaving on-chain simulation to its concrete owner.
523    #[allow(clippy::large_stack_frames)]
524    async fn prepare_script<FEN: FoundryEvmNetwork>(
525        self,
526        config: Config,
527        evm_opts: EvmOpts,
528        executor_builder: ExecutorBuilder<FEN>,
529    ) -> Result<Option<PreparedScript<FEN>>> {
530        let state = self.preprocess::<FEN>(config, evm_opts, executor_builder).await?;
531        let create2_deployer = state.script_config.evm_opts.create2_deployer;
532        let compiled = state.compile()?;
533
534        // Move from `CompiledState` to `BundledState` either by resuming or executing and
535        // simulating script.
536        let prepared = if compiled.args.resume {
537            PreparedScript::Resume(Box::new(compiled.resume().await?))
538        } else {
539            // Drive state machine to point at which we have everything needed for simulation.
540            let pre_simulation = compiled
541                .link()
542                .await?
543                .prepare_execution()
544                .await?
545                .execute()
546                .await?
547                .prepare_simulation()
548                .await?
549                .optimize_library_deployments()
550                .await?;
551
552            if pre_simulation.args.debug {
553                return match pre_simulation.args.dump.clone() {
554                    Some(path) => pre_simulation.dump_debugger(&path).map(|_| None),
555                    None => pre_simulation.run_debugger().map(|_| None),
556                };
557            }
558
559            if shell::is_json() {
560                pre_simulation.show_json().await?;
561            } else {
562                pre_simulation.show_traces().await?;
563            }
564
565            // Ensure that we have transactions to simulate/broadcast, otherwise exit early to avoid
566            // hard error.
567            if pre_simulation
568                .execution_result
569                .transactions
570                .as_ref()
571                .is_none_or(|txs| txs.is_empty())
572            {
573                if pre_simulation.args.broadcast {
574                    sh_warn!("No transactions to broadcast.")?;
575                }
576
577                return Ok(None);
578            }
579
580            // Check if there are any missing RPCs and exit early to avoid hard error.
581            if pre_simulation.execution_artifacts.rpc_data.missing_rpc {
582                if !shell::is_json() {
583                    sh_println!("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?;
584                }
585
586                return Ok(None);
587            }
588
589            let size_limits = pre_simulation.args.contract_size_limits::<FEN>(
590                &pre_simulation.script_config.config,
591                &pre_simulation.script_config.evm_opts,
592            );
593            pre_simulation.args.check_contract_sizes(
594                size_limits,
595                &pre_simulation.execution_result,
596                &pre_simulation.build_data.known_contracts,
597                create2_deployer,
598            )?;
599
600            PreparedScript::Simulate(Box::new(pre_simulation))
601        };
602        Ok(Some(prepared))
603    }
604
605    /// Performs the shared post-simulation output and verification preflight checks.
606    async fn finish_bundle<FEN: FoundryEvmNetwork>(
607        bundled: BundledState<FEN>,
608    ) -> Result<Option<BundledState<FEN>>> {
609        // Exit early in case user didn't provide any broadcast/verify related flags.
610        if !bundled.args.should_broadcast() {
611            if !shell::is_json() {
612                if shell::verbosity() >= 4 {
613                    sh_println!("\n=== Transactions that will be broadcast ===\n")?;
614                    bundled.sequence.show_transactions()?;
615                }
616
617                sh_println!(
618                    "\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more."
619                )?;
620            }
621            return Ok(None);
622        }
623
624        // Exit early if something is wrong with verification options.
625        if bundled.args.verify {
626            bundled.verify_preflight_check().await?;
627        }
628
629        Ok(Some(bundled))
630    }
631
632    async fn run_generic_script<FEN: FoundryEvmNetwork>(
633        self,
634        config: Config,
635        evm_opts: EvmOpts,
636        executor_builder: ExecutorBuilder<FEN>,
637    ) -> Result<()> {
638        let bundled = match self.prepare_bundled::<FEN>(config, evm_opts, executor_builder).await? {
639            Some(bundled) => bundled,
640            None => return Ok(()),
641        };
642
643        // Wait for pending txes and broadcast others.
644        Self::broadcast_bundle(bundled).await
645    }
646
647    async fn broadcast_bundle<FEN: FoundryEvmNetwork>(bundled: BundledState<FEN>) -> Result<()> {
648        let broadcasted = bundled.wait_for_pending().await?.broadcast().await?;
649
650        if broadcasted.args.verify {
651            broadcasted.verify().await?;
652        }
653
654        Ok(())
655    }
656
657    /// In case the user has loaded *only* one private-key or a single remote signer (e.g.,
658    /// Turnkey), we can assume that they're using it as the `--sender`.
659    fn maybe_load_private_key(&self) -> Result<Option<Address>> {
660        if let Some(turnkey_address) = self.wallets.turnkey_address() {
661            return Ok(Some(turnkey_address));
662        }
663
664        let maybe_sender = self
665            .wallets
666            .private_keys()?
667            .filter(|pks| pks.len() == 1)
668            .map(|pks| pks.first().unwrap().address());
669        Ok(maybe_sender)
670    }
671
672    /// Returns the Function and calldata based on the signature
673    ///
674    /// If the `sig` is a valid human-readable function we find the corresponding function in the
675    /// `abi` If the `sig` is valid hex, we assume it's calldata and try to find the
676    /// corresponding function by matching the selector, first 4 bytes in the calldata.
677    ///
678    /// Note: We assume that the `sig` is already stripped of its prefix, See [`ScriptArgs`]
679    fn get_method_and_calldata(&self, abi: &JsonAbi) -> Result<(Function, Bytes)> {
680        if let Ok(decoded) = hex::decode(&self.sig) {
681            let selector = &decoded[..SELECTOR_LEN];
682            let func =
683                abi.functions().find(|func| selector == &func.selector()[..]).ok_or_else(|| {
684                    eyre::eyre!(
685                        "Function selector `{}` not found in the ABI",
686                        hex::encode(selector)
687                    )
688                })?;
689            return Ok((func.clone(), decoded.into()));
690        }
691
692        let func = if self.sig.contains('(') {
693            let func = get_func(&self.sig)?;
694            abi.functions()
695                .find(|&abi_func| abi_func.selector() == func.selector())
696                .wrap_err(format!("Function `{}` is not implemented in your script.", self.sig))?
697        } else {
698            let matching_functions =
699                abi.functions().filter(|func| func.name == self.sig).collect::<Vec<_>>();
700            match matching_functions.len() {
701                0 => {
702                    eyre::bail!("Function `{}` not found in the ABI", self.sig);
703                }
704                1 => matching_functions[0],
705                2.. => {
706                    eyre::bail!(
707                        "Multiple functions with the same name `{}` found in the ABI",
708                        self.sig
709                    );
710                }
711            }
712        };
713        let data = encode_function_args(func, &self.args)?;
714
715        Ok((func.clone(), data.into()))
716    }
717
718    /// Checks if the transaction is a deployment with a size above the active EVM specification's
719    /// contract size limit or the specified `code_size_limit`.
720    ///
721    /// If `self.broadcast` is enabled, it asks confirmation of the user. Otherwise, it just warns
722    /// the user.
723    fn check_contract_sizes<N: Network>(
724        &self,
725        size_limits: ContractSizeLimits,
726        result: &ScriptResult<N>,
727        known_contracts: &ContractsByArtifact,
728        create2_deployer: Address,
729    ) -> Result<()> {
730        // If disable-code-size-limit flag is enabled then skip the size check
731        if self.disable_code_size_limit {
732            return Ok(());
733        }
734
735        // (name, &init, &deployed)[]
736        let mut bytecodes: Vec<(String, &[u8], &[u8])> = vec![];
737
738        // From artifacts
739        for (artifact, contract) in known_contracts.iter() {
740            let Some(bytecode) = contract.bytecode() else { continue };
741            let Some(deployed_bytecode) = contract.deployed_bytecode() else { continue };
742            bytecodes.push((artifact.name.clone(), bytecode, deployed_bytecode));
743        }
744
745        // From traces
746        let create_nodes = result.traces.iter().flat_map(|(_, traces)| {
747            traces.nodes().iter().filter(|node| node.trace.kind.is_any_create())
748        });
749        let mut unknown_c = 0usize;
750        for node in create_nodes {
751            let init_code = &node.trace.data;
752            let deployed_code = &node.trace.output;
753            if !bytecodes.iter().any(|(_, b, _)| *b == init_code.as_ref()) {
754                bytecodes.push((format!("Unknown{unknown_c}"), init_code, deployed_code));
755                unknown_c += 1;
756            }
757        }
758
759        let mut prompt_user = false;
760        let max_size = size_limits.runtime;
761
762        for (data, to) in result.transactions.iter().flat_map(|txes| {
763            txes.iter().filter_map(|tx| {
764                tx.transaction
765                    .input()
766                    .filter(|data| data.len() > max_size)
767                    .map(|data| (data, tx.transaction.to()))
768            })
769        }) {
770            let mut offset = 0;
771
772            // Find if it's a CREATE or CREATE2. Otherwise, skip transaction.
773            if let Some(to) = to {
774                if to == create2_deployer {
775                    // Size of the salt prefix.
776                    offset = 32;
777                } else {
778                    continue;
779                }
780            }
781
782            // Find artifact with a deployment code same as the data.
783            if let Some((name, _, deployed_code)) =
784                bytecodes.iter().find(|(_, init_code, _)| *init_code == &data[offset..])
785            {
786                let deployment_size = deployed_code.len();
787
788                if deployment_size > max_size {
789                    prompt_user = self.should_broadcast();
790                    sh_err!(
791                        "`{name}` is above the contract size limit ({deployment_size} > {max_size})."
792                    )?;
793                }
794            }
795        }
796
797        // Only prompt if we're broadcasting and we've not disabled interactivity.
798        if prompt_user
799            && !self.non_interactive
800            && !Confirm::new().with_prompt("Do you wish to continue?".to_string()).interact()?
801        {
802            eyre::bail!("User canceled the script.");
803        }
804
805        Ok(())
806    }
807
808    fn contract_size_limits<FEN: FoundryEvmNetwork>(
809        &self,
810        config: &Config,
811        evm_opts: &EvmOpts,
812    ) -> ContractSizeLimits {
813        self.evm
814            .env
815            .code_size_limit
816            .or(evm_opts.env.code_size_limit)
817            .or(config.code_size_limit)
818            .map(ContractSizeLimits::with_runtime_limit)
819            .or_else(|| {
820                evm_opts
821                    .networks
822                    .contract_size_limits()
823                    .map(|limits| ContractSizeLimits::new(limits.runtime, limits.initcode))
824            })
825            .unwrap_or_else(|| {
826                let spec_id: SpecFor<FEN> = config.evm_spec_id();
827                ContractSizeLimits::for_spec_id(spec_id.into())
828            })
829    }
830
831    /// We only broadcast transactions if --broadcast, --resume, or --verify was passed.
832    const fn should_broadcast(&self) -> bool {
833        self.broadcast || self.resume || self.verify
834    }
835}
836
837impl Provider for ScriptArgs {
838    fn metadata(&self) -> Metadata {
839        Metadata::named("Script Args Provider")
840    }
841
842    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
843        let mut dict = Dict::default();
844
845        if let Some(etherscan_api_key) =
846            self.etherscan_api_key.as_ref().filter(|s| !s.trim().is_empty())
847        {
848            dict.insert(
849                "etherscan_api_key".to_string(),
850                figment::value::Value::from(etherscan_api_key.clone()),
851            );
852        }
853
854        if let Some(timeout) = self.timeout {
855            dict.insert("transaction_timeout".to_string(), timeout.into());
856        }
857
858        if let Some(preset) = self.eip1559_fee_estimate {
859            dict.insert(
860                "eip1559_fee_estimate".to_string(),
861                figment::value::Value::from(preset.to_string()),
862            );
863        }
864
865        Ok(Map::from([(Config::selected_profile(), dict)]))
866    }
867}
868
869#[derive(Serialize, Clone)]
870#[serde(bound = "")]
871pub struct ScriptResult<N: Network> {
872    pub success: bool,
873    #[serde(rename = "raw_logs")]
874    pub logs: Vec<Log>,
875    pub traces: Traces,
876    pub gas_used: u64,
877    pub labeled_addresses: AddressHashMap<String>,
878    #[serde(skip)]
879    pub debug_bytecodes: AddressHashMap<Bytes>,
880    #[serde(skip)]
881    pub transactions: Option<BroadcastableTransactions<N>>,
882    pub returned: Bytes,
883    #[serde(skip)]
884    pub exit_reason: Option<InstructionResult>,
885    pub address: Option<Address>,
886    #[serde(skip)]
887    pub breakpoints: Breakpoints,
888}
889
890impl<N: Network> Default for ScriptResult<N> {
891    fn default() -> Self {
892        Self {
893            success: Default::default(),
894            logs: Default::default(),
895            traces: Default::default(),
896            gas_used: Default::default(),
897            labeled_addresses: Default::default(),
898            debug_bytecodes: Default::default(),
899            transactions: Default::default(),
900            returned: Default::default(),
901            exit_reason: Default::default(),
902            address: Default::default(),
903            breakpoints: Default::default(),
904        }
905    }
906}
907
908impl<N: Network> ScriptResult<N> {
909    pub fn get_created_contracts(
910        &self,
911        known_contracts: &ContractsByArtifact,
912    ) -> Vec<AdditionalContract> {
913        self.traces
914            .iter()
915            .flat_map(|(_, traces)| {
916                let nodes = traces.nodes();
917                nodes.iter().filter_map(|node| {
918                    if !node.trace.kind.is_any_create() || !node.trace.success {
919                        return None;
920                    }
921
922                    let mut creator_code_addresses = Vec::new();
923                    let mut parent = node.parent;
924                    while let Some(parent_idx) = parent {
925                        let ancestor = &nodes[parent_idx];
926                        if !ancestor.trace.success {
927                            return None;
928                        }
929                        if !ancestor.trace.kind.is_any_create()
930                            && !creator_code_addresses.contains(&ancestor.trace.address)
931                        {
932                            creator_code_addresses.push(ancestor.trace.address);
933                        }
934                        parent = ancestor.parent;
935                    }
936
937                    let init_code = node.trace.data.clone();
938                    let contract_name = known_contracts
939                        .find_by_creation_code(init_code.as_ref())
940                        .map(|artifact| artifact.0.name.clone());
941                    Some(AdditionalContract {
942                        call_kind: node.trace.kind,
943                        address: node.trace.address,
944                        contract_name,
945                        init_code,
946                        creator_code_addresses,
947                    })
948                })
949            })
950            .collect()
951    }
952}
953
954#[derive(Serialize)]
955#[serde(bound = "")]
956struct JsonResult<'a, N: Network> {
957    logs: Vec<String>,
958    returns: &'a HashMap<String, NestedValue>,
959    #[serde(flatten)]
960    result: &'a ScriptResult<N>,
961}
962
963#[derive(Clone, Debug)]
964pub struct ScriptConfig<FEN: FoundryEvmNetwork> {
965    pub config: Config,
966    pub evm_opts: EvmOpts,
967    /// Executor construction selected by concrete network dispatch.
968    pub executor_builder: ExecutorBuilder<FEN>,
969    /// Exact network hardfork selected for script execution.
970    pub hardfork: Option<FoundryHardfork>,
971    /// Source chain used for trace decoding and external identifiers.
972    pub source_chain_id: Option<u64>,
973    pub sender_nonce: u64,
974    sender_nonce_override: Option<u64>,
975    resolved_fork: Option<ResolvedFork>,
976    /// Backends keyed by their complete resolved fork context.
977    backends: HashMap<ResolvedFork, Backend<FEN>>,
978    /// Whether to batch all broadcast transactions into a single Tempo batch transaction.
979    pub batch: bool,
980    /// Tempo transaction options applied to broadcast transactions.
981    pub tempo: TempoOpts,
982}
983
984async fn resolve_script_fork(
985    config: &mut Config,
986    evm_opts: &mut EvmOpts,
987    active_networks: Option<NetworkConfigs>,
988) -> Result<Option<ResolvedFork>> {
989    if evm_opts.fork_url.is_none() {
990        return Ok(None);
991    }
992    if evm_opts.fork_endpoint.is_none() {
993        evm_opts.infer_network_from_fork().await?;
994    }
995    if let Some(active_networks) = active_networks
996        && !active_networks.supports_fork_source(&evm_opts.networks)
997    {
998        eyre::bail!(
999            "fork network `{}` is incompatible with the active EVM",
1000            evm_opts.networks.execution_network()
1001        );
1002    }
1003    config.networks = evm_opts.networks;
1004    if let Some(identity) = evm_opts.fork_endpoint.clone() {
1005        let network_is_inferred = evm_opts.fork_network_is_inferred;
1006        evm_opts.expect_fork_endpoint(identity, network_is_inferred);
1007    }
1008    evm_opts.resolve_fork().await
1009}
1010
1011impl<FEN: FoundryEvmNetwork> ScriptConfig<FEN> {
1012    pub(crate) async fn new(
1013        mut config: Config,
1014        mut evm_opts: EvmOpts,
1015        executor_builder: ExecutorBuilder<FEN>,
1016        batch: bool,
1017        tempo: TempoOpts,
1018        sender_nonce_override: Option<u64>,
1019    ) -> Result<Self> {
1020        // Linking happens before runner construction, so resolve the fork context now and reuse it
1021        // for all preflight reads and environment construction.
1022        let resolved_fork = resolve_script_fork(&mut config, &mut evm_opts, None).await?;
1023        let sender_nonce = if let Some(sender_nonce) = sender_nonce_override {
1024            sender_nonce
1025        } else if evm_opts.fork_url.is_some() {
1026            let fork = resolved_fork.as_ref().context("fork must be resolved")?;
1027            next_nonce_resolved(evm_opts.sender, &evm_opts, fork).await?
1028        } else {
1029            // dapptools compatibility
1030            1
1031        };
1032
1033        Ok(Self {
1034            config,
1035            evm_opts,
1036            executor_builder,
1037            hardfork: None,
1038            source_chain_id: None,
1039            sender_nonce,
1040            sender_nonce_override,
1041            resolved_fork,
1042            backends: HashMap::default(),
1043            batch,
1044            tempo,
1045        })
1046    }
1047
1048    pub async fn update_sender(&mut self, sender: Address) -> Result<()> {
1049        self.sender_nonce = if let Some(sender_nonce) = self.sender_nonce_override {
1050            sender_nonce
1051        } else if self.evm_opts.fork_url.is_some() {
1052            let fork = self.ensure_resolved_fork().await?.context("fork must be resolved")?;
1053            next_nonce_resolved(sender, &self.evm_opts, &fork).await?
1054        } else {
1055            // dapptools compatibility
1056            1
1057        };
1058        self.evm_opts.sender = sender;
1059        Ok(())
1060    }
1061
1062    /// Returns the resolved fork when it still matches the configured source and selector.
1063    pub fn resolved_fork(&self) -> Result<Option<&ResolvedFork>> {
1064        match (&self.evm_opts.fork_url, &self.resolved_fork) {
1065            (None, None) => Ok(None),
1066            (None, Some(_)) => Err(eyre::eyre!("resolved fork exists without a configured fork")),
1067            (Some(_), Some(fork)) if self.evm_opts.resolved_fork_matches(fork) => Ok(Some(fork)),
1068            (Some(_), Some(_)) => {
1069                Err(eyre::eyre!("resolved fork does not match the configured source and selector"))
1070            }
1071            (Some(_), None) => Err(eyre::eyre!("fork must be resolved")),
1072        }
1073    }
1074
1075    fn set_fork_url(&mut self, fork_url: String) {
1076        self.evm_opts.set_fork_url(fork_url);
1077    }
1078
1079    async fn ensure_resolved_fork(&mut self) -> Result<Option<ResolvedFork>> {
1080        if self.evm_opts.fork_url.is_none() {
1081            if self.resolved_fork.take().is_some() {
1082                self.backends.clear();
1083            }
1084            return Ok(None);
1085        }
1086        if let Some(fork) = &self.resolved_fork
1087            && self.evm_opts.resolved_fork_matches(fork)
1088        {
1089            return Ok(Some(fork.clone()));
1090        }
1091        // Script execution was already dispatched to `FEN`; compare the new selection with that
1092        // execution profile, not with the previous RPC source profile.
1093        let active_networks = Some(self.config.networks);
1094        if let Some(fork) = &self.resolved_fork {
1095            self.evm_opts.invalidate_fork_endpoint_if_source_changed(fork);
1096        }
1097
1098        let fork =
1099            resolve_script_fork(&mut self.config, &mut self.evm_opts, active_networks).await?;
1100        self.resolved_fork = fork.clone();
1101        Ok(fork)
1102    }
1103
1104    pub(crate) async fn update_tempo_session_sender(
1105        &mut self,
1106        wallets: &MultiWalletOpts,
1107        expected_sender: Option<Address>,
1108    ) -> Result<()> {
1109        if let Some(sender) =
1110            self.tempo.session_sender_for_multi_wallet(wallets, expected_sender)?
1111        {
1112            self.update_sender(sender).await?;
1113        }
1114        Ok(())
1115    }
1116
1117    async fn get_runner_with_cheatcodes(
1118        &mut self,
1119        known_contracts: ContractsByArtifact,
1120        script_wallets: Wallets,
1121        debug: bool,
1122        target: ArtifactId,
1123        restricted: bool,
1124    ) -> Result<ScriptRunner<FEN>> {
1125        let mut runner = self
1126            ._get_runner(Some((known_contracts, script_wallets, target)), debug, restricted)
1127            .await?;
1128
1129        // Script execution is synthetic. Keep the Tempo transaction context, but do not charge
1130        // protocol fees for deploying or calling the local script contract.
1131        if self.evm_opts.networks.is_tempo() {
1132            runner.executor.evm_env_mut().cfg_env.disable_fee_charge = true;
1133        }
1134
1135        Ok(runner)
1136    }
1137
1138    async fn _get_runner(
1139        &mut self,
1140        cheats_data: Option<(ContractsByArtifact, Wallets, ArtifactId)>,
1141        debug: bool,
1142        restricted: bool,
1143    ) -> Result<ScriptRunner<FEN>> {
1144        trace!("preparing script runner");
1145        let (resolved, evm_env, mut tx_env) = self.resolve_execution_env().await?;
1146
1147        let db = if self.evm_opts.fork_url.is_some() {
1148            let resolved = resolved.context("fork must be resolved")?;
1149            if let Some(backend) = self.backends.get(&resolved) {
1150                backend.clone()
1151            } else {
1152                let fork = self.evm_opts.get_fork_resolved(
1153                    &self.config,
1154                    evm_env.cfg_env.chain_id,
1155                    Some(&resolved),
1156                );
1157                let backend = Backend::spawn(fork)?;
1158                self.backends.insert(resolved, backend.clone());
1159                backend
1160            }
1161        } else {
1162            // It's only really `None`, when we don't pass any `--fork-url`. And if so, there is
1163            // no need to cache it, since there won't be any onchain simulation that we'd need
1164            // to cache the backend for.
1165            Backend::spawn(None)?
1166        };
1167
1168        // We need to enable tracing to decode contract names: local or external.
1169        let mut builder = self
1170            .executor_builder
1171            .clone()
1172            .inspectors(|stack| {
1173                stack
1174                    .logs(self.config.live_logs)
1175                    .trace_requirements(script_trace_requirements(&self.config, debug))
1176                    .create2_deployer(self.evm_opts.create2_deployer)
1177            })
1178            .gas_limit(self.evm_opts.gas_limit())
1179            .legacy_assertions(self.config.legacy_assertions);
1180
1181        if let Some((known_contracts, script_wallets, target)) = cheats_data {
1182            builder = builder.inspectors(|stack| {
1183                let mut cheats_config = CheatsConfig::new(
1184                    &self.config,
1185                    self.evm_opts.clone(),
1186                    Some(known_contracts),
1187                    Some(target),
1188                    self.batch,
1189                );
1190                if restricted {
1191                    cheats_config.blocked_cheatcodes =
1192                        library_deployments::rerun_unsafe_cheatcode_selectors();
1193                }
1194                stack
1195                    .cheatcodes(cheats_config.into())
1196                    .wallets(script_wallets)
1197                    .enable_isolation(self.evm_opts.isolate)
1198            });
1199        }
1200
1201        // Propagate fee token to the transaction environment so that internal EVM calls
1202        // (e.g. script deployment, setUp) use the correct fee token for Tempo networks.
1203        tx_env.set_fee_token(self.tempo.fee_token);
1204
1205        let mut runner = ScriptRunner::new(
1206            builder.build(evm_env, tx_env, db, self.evm_opts.networks),
1207            self.evm_opts.clone(),
1208        )
1209        .with_debug_bytecodes(debug);
1210
1211        if self.sender_nonce_override.is_some() {
1212            runner.executor.set_nonce(self.evm_opts.sender, self.sender_nonce)?;
1213        }
1214
1215        Ok(runner)
1216    }
1217
1218    /// Resolves the configured fork and execution spec without constructing a database or runner.
1219    async fn resolve_execution_env(
1220        &mut self,
1221    ) -> Result<(Option<ResolvedFork>, EvmEnvFor<FEN>, TxEnvFor<FEN>)> {
1222        let resolved = self.ensure_resolved_fork().await?;
1223        let (mut evm_env, tx_env) =
1224            self.evm_opts.env_with_resolved_fork::<_, _, TxEnvFor<FEN>>(resolved.as_ref()).await?;
1225        let fork_context = resolved.as_ref().map(ResolvedFork::context);
1226        let fork_chain_id = fork_context.map(|context| context.source_chain_id);
1227        let fork_hardfork = fork_context.and_then(|context| context.hardfork);
1228        self.source_chain_id = fork_chain_id;
1229        self.hardfork = resolve_execution_spec(
1230            self.config.evm_version,
1231            self.config.hardfork,
1232            &mut evm_env,
1233            ExecutionSpecContext::local_or_fork(fork_chain_id, fork_hardfork),
1234            None,
1235        );
1236        Ok((resolved, evm_env, tx_env))
1237    }
1238}
1239
1240const fn script_trace_requirements(config: &Config, debug: bool) -> TraceRequirements {
1241    TraceRequirements::none()
1242        .with_calls(true)
1243        .with_debug(debug)
1244        .with_verbosity(config.tracing.verbosity)
1245        .with_decode_internal(if config.tracing.decode_internal {
1246            InternalTraceMode::Full
1247        } else {
1248            InternalTraceMode::None
1249        })
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254    use super::*;
1255    use alloy_chains::NamedChain;
1256    use alloy_eips::BlockId;
1257    use alloy_network::Ethereum;
1258    use alloy_primitives::{B256, address};
1259    use alloy_provider::Provider as _;
1260    use alloy_rpc_types::TransactionRequest;
1261    use alloy_signer::SignerSync as _;
1262    use anvil::{NodeConfig, spawn};
1263    use foundry_cli::opts::TEMPO_SESSION_ID_ENV;
1264    use foundry_common::tempo::{
1265        GeneratedSessionKey, SessionAuthorizationRequest, SessionEntry, TEMPO_HOME_ENV,
1266        upsert_session_entry,
1267    };
1268    use foundry_config::UnresolvedEnvVarError;
1269    use foundry_evm::{
1270        revm::context::Block as _,
1271        traces::{
1272            CallKind, CallTrace, CallTraceArena, CallTraceNode, SparsedTraceArena, TraceKind,
1273        },
1274    };
1275    use semver::Version;
1276    use std::{fs, num::NonZeroU64, sync::LazyLock};
1277    use tempfile::tempdir;
1278    use tokio::sync::{Mutex, MutexGuard};
1279
1280    const SESSION_PRIVATE_KEY: &str =
1281        "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
1282    const SESSION_ROOT_PRIVATE_KEY: &str =
1283        "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
1284    const SESSION_ID_HEX: &str =
1285        "0x1111111111111111111111111111111111111111111111111111111111111111";
1286    const SESSION_ROOT_ADDRESS: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
1287    static TEMPO_HOME_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
1288
1289    fn assert_missing_orphaned_block(error: &eyre::Report) {
1290        let message = format!("{error:#}").to_ascii_lowercase();
1291        assert!(
1292            message.contains("failed to get block hash")
1293                || message.contains("not found")
1294                || message.contains("unknown block"),
1295            "unexpected orphaned block lookup error: {error:#}"
1296        );
1297    }
1298
1299    #[tokio::test(flavor = "multi_thread")]
1300    async fn script_fork_context_is_re_resolved_for_a_new_rpc() {
1301        let (api_a, handle_a) = spawn(NodeConfig::test()).await;
1302        let (api_b, handle_b) = spawn(NodeConfig::test()).await;
1303        api_a.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1304        api_b.anvil_mine(Some(U256::from(3)), None).await.unwrap();
1305
1306        let evm_opts = EvmOpts {
1307            fork_url: Some(handle_a.http_endpoint()),
1308            sender: handle_a.dev_accounts().next().unwrap(),
1309            ..Default::default()
1310        };
1311        let mut config = ScriptConfig::<EthEvmNetwork>::new(
1312            Config::default(),
1313            evm_opts,
1314            ExecutorBuilder::<EthEvmNetwork>::new(),
1315            false,
1316            TempoOpts::default(),
1317            None,
1318        )
1319        .await
1320        .unwrap();
1321        let first = config.resolved_fork.clone().unwrap();
1322        assert_eq!(first.number(), 1);
1323        assert_eq!(config.evm_opts.fork_block_number, None);
1324        assert!(config.evm_opts.resolved_fork_matches(&first));
1325
1326        config._get_runner(None, false, false).await.unwrap();
1327
1328        config.set_fork_url(handle_b.http_endpoint());
1329        let second = config.ensure_resolved_fork().await.unwrap().unwrap();
1330
1331        assert_eq!(second.number(), 3);
1332        assert_eq!(config.evm_opts.fork_block_number, None);
1333        assert!(config.evm_opts.resolved_fork_matches(&second));
1334        assert_ne!(first, second);
1335    }
1336
1337    #[cfg(feature = "monad")]
1338    #[tokio::test(flavor = "multi_thread")]
1339    async fn script_explicit_network_is_preserved_for_a_new_rpc() {
1340        let (api_a, handle_a) = spawn(NodeConfig::test_monad()).await;
1341        let (api_b, handle_b) = spawn(NodeConfig::test_monad()).await;
1342        api_a.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1343        api_b.anvil_mine(Some(U256::from(3)), None).await.unwrap();
1344
1345        let evm_opts = EvmOpts {
1346            fork_url: Some(handle_a.http_endpoint()),
1347            sender: handle_a.dev_accounts().next().unwrap(),
1348            networks: NetworkConfigs::with_ethereum(),
1349            ..Default::default()
1350        };
1351        let mut config = ScriptConfig::<EthEvmNetwork>::new(
1352            Config::default(),
1353            evm_opts,
1354            ExecutorBuilder::<EthEvmNetwork>::new(),
1355            false,
1356            TempoOpts::default(),
1357            None,
1358        )
1359        .await
1360        .unwrap();
1361        assert_eq!(config.evm_opts.networks, NetworkConfigs::with_ethereum());
1362
1363        config.set_fork_url(handle_b.http_endpoint());
1364        let second = config.ensure_resolved_fork().await.unwrap().unwrap();
1365
1366        assert_eq!(second.number(), 3);
1367        assert_eq!(second.context().network_profile, NetworkConfigs::with_monad());
1368        assert_eq!(config.evm_opts.networks, NetworkConfigs::with_ethereum());
1369    }
1370
1371    #[tokio::test(flavor = "multi_thread")]
1372    async fn script_explicit_fork_block_is_preserved_for_a_new_rpc() {
1373        let (api_a, handle_a) = spawn(NodeConfig::test()).await;
1374        let (api_b, handle_b) = spawn(NodeConfig::test()).await;
1375        api_a.anvil_mine(Some(U256::from(2)), None).await.unwrap();
1376        api_b.anvil_mine(Some(U256::from(4)), None).await.unwrap();
1377
1378        let evm_opts = EvmOpts {
1379            fork_url: Some(handle_a.http_endpoint()),
1380            fork_block_number: Some(1),
1381            sender: handle_a.dev_accounts().next().unwrap(),
1382            ..Default::default()
1383        };
1384        let mut config = ScriptConfig::<EthEvmNetwork>::new(
1385            Config::default(),
1386            evm_opts,
1387            ExecutorBuilder::<EthEvmNetwork>::new(),
1388            false,
1389            TempoOpts::default(),
1390            None,
1391        )
1392        .await
1393        .unwrap();
1394        config._get_runner(None, false, false).await.unwrap();
1395
1396        config.set_fork_url(handle_b.http_endpoint());
1397        let context = config.ensure_resolved_fork().await.unwrap().unwrap();
1398
1399        assert_eq!(context.number(), 1);
1400        assert_eq!(config.evm_opts.fork_block_number, Some(1));
1401        assert!(config.evm_opts.resolved_fork_matches(&context));
1402    }
1403
1404    #[tokio::test(flavor = "multi_thread")]
1405    async fn script_explicit_fork_block_can_equal_previous_latest() {
1406        let (api_a, handle_a) = spawn(NodeConfig::test()).await;
1407        let (api_b, handle_b) = spawn(NodeConfig::test()).await;
1408        api_a.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1409        api_b.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1410
1411        let evm_opts = EvmOpts {
1412            fork_url: Some(handle_a.http_endpoint()),
1413            sender: handle_a.dev_accounts().next().unwrap(),
1414            ..Default::default()
1415        };
1416        let mut config = ScriptConfig::<EthEvmNetwork>::new(
1417            Config::default(),
1418            evm_opts,
1419            ExecutorBuilder::<EthEvmNetwork>::new(),
1420            false,
1421            TempoOpts::default(),
1422            None,
1423        )
1424        .await
1425        .unwrap();
1426        let latest = config.resolved_fork.clone().unwrap();
1427        assert_eq!(config.evm_opts.fork_block_number, None);
1428        config._get_runner(None, false, false).await.unwrap();
1429        assert!(config.backends.contains_key(&latest));
1430
1431        config.set_fork_url(handle_b.http_endpoint());
1432        config.evm_opts.fork_block_number = Some(1);
1433        let explicit = config.ensure_resolved_fork().await.unwrap().unwrap();
1434
1435        assert_eq!(explicit.number(), 1);
1436        assert_eq!(config.evm_opts.fork_block_number, Some(1));
1437        assert!(config.evm_opts.resolved_fork_matches(&explicit));
1438        assert_ne!(latest, explicit);
1439        assert!(config.backends.contains_key(&latest));
1440        assert!(!config.backends.contains_key(&explicit));
1441
1442        config._get_runner(None, false, false).await.unwrap();
1443        assert!(config.backends.contains_key(&latest));
1444        assert!(config.backends.contains_key(&explicit));
1445        assert_eq!(config.backends.len(), 2);
1446    }
1447
1448    #[tokio::test(flavor = "multi_thread")]
1449    async fn script_fork_cache_distinguishes_same_height_contexts() {
1450        let (api_a, handle_a) = spawn(NodeConfig::test()).await;
1451        let (api_b, handle_b) = spawn(NodeConfig::test()).await;
1452        let state_address = address!("0000000000000000000000000000000000001337");
1453        let balance_a = U256::from(1);
1454        let balance_b = U256::from(2);
1455        api_a.anvil_set_balance(state_address, balance_a).await.unwrap();
1456        api_b.anvil_set_balance(state_address, balance_b).await.unwrap();
1457        let original_prevrandao = B256::with_last_byte(0x42);
1458        api_a.anvil_set_next_block_prevrandao(original_prevrandao).await.unwrap();
1459        api_a.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1460        api_b.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1461        let url_a = handle_a.http_endpoint();
1462        let url_b = handle_b.http_endpoint();
1463        let sender = handle_a.dev_accounts().next().unwrap();
1464
1465        let evm_opts = EvmOpts { fork_url: Some(url_a.clone()), sender, ..Default::default() };
1466        let mut config = ScriptConfig::<EthEvmNetwork>::new(
1467            Config::default(),
1468            evm_opts,
1469            ExecutorBuilder::<EthEvmNetwork>::new(),
1470            false,
1471            TempoOpts::default(),
1472            None,
1473        )
1474        .await
1475        .unwrap();
1476        let runner_a = config._get_runner(None, false, false).await.unwrap();
1477        assert_eq!(runner_a.executor.get_balance(state_address).unwrap(), balance_a);
1478        let first_a = config.resolved_fork().unwrap().unwrap().clone();
1479        assert!(config.backends.contains_key(&first_a));
1480
1481        config.set_fork_url(url_b.clone());
1482        let runner_b = config._get_runner(None, false, false).await.unwrap();
1483        assert_eq!(runner_b.executor.get_balance(state_address).unwrap(), balance_b);
1484        let context_b = config.resolved_fork().unwrap().unwrap().clone();
1485        assert_eq!(context_b.number(), 1);
1486        assert!(config.backends.contains_key(&first_a));
1487        assert!(config.backends.contains_key(&context_b));
1488        assert_eq!(config.backends.len(), 2);
1489
1490        let provider_a = handle_a.http_provider();
1491        provider_a
1492            .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
1493            .await
1494            .unwrap();
1495        let replacement = provider_a.get_block_by_number(1.into()).await.unwrap().unwrap();
1496        assert_eq!(replacement.header.number, first_a.number());
1497        assert_ne!(replacement.header.hash, first_a.hash());
1498        assert_ne!(replacement.header.mix_hash, Some(original_prevrandao));
1499
1500        config.set_fork_url(url_a.clone());
1501        let second_a = config.ensure_resolved_fork().await.unwrap().unwrap();
1502        assert_eq!(second_a.number(), first_a.number());
1503        assert_eq!(second_a.hash(), replacement.header.hash);
1504        assert_ne!(second_a.hash(), first_a.hash());
1505        assert!(config.backends.contains_key(&first_a));
1506        assert!(config.backends.contains_key(&context_b));
1507        assert!(!config.backends.contains_key(&second_a));
1508        assert_eq!(config.backends.len(), 2);
1509
1510        let runner = config._get_runner(None, false, false).await.unwrap();
1511        assert!(config.backends.contains_key(&second_a));
1512        assert_eq!(config.backends.len(), 3);
1513        assert_eq!(runner.executor.get_balance(state_address).unwrap(), balance_a);
1514        assert_eq!(runner.executor.evm_env().block_env.prevrandao(), replacement.header.mix_hash);
1515    }
1516
1517    #[tokio::test(flavor = "multi_thread")]
1518    async fn script_same_endpoint_does_not_advance_resolved_fork() {
1519        let (api, handle) = spawn(NodeConfig::test()).await;
1520        api.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1521        let url = handle.http_endpoint();
1522        let evm_opts = EvmOpts {
1523            fork_url: Some(url.clone()),
1524            sender: handle.dev_accounts().next().unwrap(),
1525            ..Default::default()
1526        };
1527        let mut config = ScriptConfig::<EthEvmNetwork>::new(
1528            Config::default(),
1529            evm_opts,
1530            ExecutorBuilder::<EthEvmNetwork>::new(),
1531            false,
1532            TempoOpts::default(),
1533            None,
1534        )
1535        .await
1536        .unwrap();
1537        let resolved = config.resolved_fork.clone().unwrap();
1538        config._get_runner(None, false, false).await.unwrap();
1539
1540        api.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1541        config.set_fork_url(url.clone());
1542        let runner = config._get_runner(None, false, false).await.unwrap();
1543
1544        assert_eq!(config.resolved_fork.as_ref(), Some(&resolved));
1545        assert!(config.backends.contains_key(&resolved));
1546        assert_eq!(config.backends.len(), 1);
1547        assert_eq!(runner.executor.evm_env().block_env.number(), U256::from(1));
1548        assert_eq!(config.evm_opts.fork_block_number, None);
1549    }
1550
1551    #[tokio::test(flavor = "multi_thread")]
1552    async fn script_fork_changed_headers_replace_cached_backend() {
1553        let (api, handle) = spawn(NodeConfig::test()).await;
1554        api.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1555        let url = handle.http_endpoint();
1556        let evm_opts = EvmOpts {
1557            fork_url: Some(url.clone()),
1558            sender: handle.dev_accounts().next().unwrap(),
1559            ..Default::default()
1560        };
1561        let mut config = ScriptConfig::<EthEvmNetwork>::new(
1562            Config::default(),
1563            evm_opts,
1564            ExecutorBuilder::<EthEvmNetwork>::new(),
1565            false,
1566            TempoOpts::default(),
1567            None,
1568        )
1569        .await
1570        .unwrap();
1571        config._get_runner(None, false, false).await.unwrap();
1572        let without_headers = config.resolved_fork().unwrap().unwrap().clone();
1573        assert!(config.backends.contains_key(&without_headers));
1574
1575        config.evm_opts.fork_headers = Some(vec!["x-foundry-test: resolved-fork".to_string()]);
1576        let with_headers = config.ensure_resolved_fork().await.unwrap().unwrap();
1577        assert!(config.backends.contains_key(&without_headers));
1578        assert!(!config.backends.contains_key(&with_headers));
1579        assert_eq!(config.backends.len(), 1);
1580        config._get_runner(None, false, false).await.unwrap();
1581
1582        assert_ne!(without_headers, with_headers);
1583        assert!(config.evm_opts.resolved_fork_matches(&with_headers));
1584        assert!(config.backends.contains_key(&without_headers));
1585        assert!(config.backends.contains_key(&with_headers));
1586        assert_eq!(config.backends.len(), 2);
1587    }
1588
1589    async fn assert_same_url_fork_auth_change_re_resolves(
1590        configure_initial: impl FnOnce(&mut EvmOpts),
1591        configure_changed: impl FnOnce(&mut EvmOpts),
1592    ) {
1593        let (api, handle) = spawn(NodeConfig::test()).await;
1594        let mut evm_opts = EvmOpts {
1595            fork_url: Some(handle.http_endpoint()),
1596            fork_block_number: Some(0),
1597            networks: NetworkConfigs::with_ethereum(),
1598            ..Default::default()
1599        };
1600        evm_opts.env.chain_id = Some(42);
1601        configure_initial(&mut evm_opts);
1602
1603        let mut config = ScriptConfig::<EthEvmNetwork>::new(
1604            Config::default(),
1605            evm_opts,
1606            ExecutorBuilder::<EthEvmNetwork>::new(),
1607            false,
1608            TempoOpts::default(),
1609            None,
1610        )
1611        .await
1612        .unwrap();
1613        config._get_runner(None, false, false).await.unwrap();
1614        let first = config.resolved_fork().unwrap().unwrap().clone();
1615        let first_instance = first.context().instance_id.unwrap();
1616        assert_eq!(config.backends.len(), 1);
1617
1618        api.anvil_reset(None).await.unwrap();
1619        assert_ne!(api.instance_id(), first_instance);
1620        configure_changed(&mut config.evm_opts);
1621
1622        let second = config.ensure_resolved_fork().await.unwrap().unwrap();
1623        assert_ne!(second.context().instance_id, Some(first_instance));
1624        assert_ne!(second, first);
1625        assert_eq!(config.evm_opts.fork_block_number, Some(0));
1626        assert_eq!(config.evm_opts.networks, NetworkConfigs::with_ethereum());
1627        assert_eq!(config.evm_opts.env.chain_id, Some(42));
1628        assert!(config.backends.contains_key(&first));
1629        assert!(!config.backends.contains_key(&second));
1630
1631        config._get_runner(None, false, false).await.unwrap();
1632        assert!(config.backends.contains_key(&first));
1633        assert!(config.backends.contains_key(&second));
1634        assert_eq!(config.backends.len(), 2);
1635    }
1636
1637    #[tokio::test(flavor = "multi_thread")]
1638    async fn script_same_url_rpc_headers_change_adopts_new_fork_identity() {
1639        assert_same_url_fork_auth_change_re_resolves(
1640            |evm_opts| evm_opts.rpc_headers = Some(vec!["x-fork-source: first".to_string()]),
1641            |evm_opts| evm_opts.rpc_headers = Some(vec!["x-fork-source: second".to_string()]),
1642        )
1643        .await;
1644    }
1645
1646    #[tokio::test(flavor = "multi_thread")]
1647    async fn script_same_url_jwt_change_adopts_new_fork_identity() {
1648        const FIRST_JWT: &str = "5c43996d0d150a81f06ae452fce38120d97a4156650aec7487b3384bfe32edae";
1649        const SECOND_JWT: &str = "cabee703106087906e50f3e75a6ddbab60809f980511d1d1548d449d52220795";
1650
1651        assert_same_url_fork_auth_change_re_resolves(
1652            |evm_opts| evm_opts.rpc_jwt = Some(FIRST_JWT.to_string()),
1653            |evm_opts| evm_opts.rpc_jwt = Some(SECOND_JWT.to_string()),
1654        )
1655        .await;
1656    }
1657
1658    #[tokio::test(flavor = "multi_thread")]
1659    async fn script_runner_env_revalidates_resolved_fork_hash() {
1660        let (api, handle) = spawn(NodeConfig::test()).await;
1661        let prevrandao = B256::with_last_byte(0x42);
1662        api.anvil_set_next_block_prevrandao(prevrandao).await.unwrap();
1663        api.anvil_mine(Some(U256::from(1)), None).await.unwrap();
1664
1665        let evm_opts = EvmOpts {
1666            fork_url: Some(handle.http_endpoint()),
1667            sender: handle.dev_accounts().next().unwrap(),
1668            ..Default::default()
1669        };
1670        let mut config = ScriptConfig::<EthEvmNetwork>::new(
1671            Config::default(),
1672            evm_opts,
1673            ExecutorBuilder::<EthEvmNetwork>::new(),
1674            false,
1675            TempoOpts::default(),
1676            None,
1677        )
1678        .await
1679        .unwrap();
1680        assert!(
1681            config
1682                .evm_opts
1683                .can_use_create2_deployer_resolved(config.resolved_fork().unwrap())
1684                .await
1685                .unwrap()
1686        );
1687        config._get_runner(None, false, false).await.unwrap();
1688        assert_eq!(config.backends.len(), 1);
1689
1690        let provider = handle.http_provider();
1691        let pinned = config.resolved_fork.clone().unwrap();
1692        provider
1693            .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
1694            .await
1695            .unwrap();
1696        let replacement = provider.get_block_by_number(1.into()).await.unwrap().unwrap();
1697        assert_ne!(replacement.header.hash, pinned.hash());
1698        assert_ne!(replacement.header.mix_hash, Some(prevrandao));
1699
1700        // The environment is hash-revalidated here. The fork database remains number-pinned;
1701        // full state and ancestry exactness is tracked in #15897.
1702        config.set_fork_url(handle.http_endpoint());
1703        match config._get_runner(None, false, false).await {
1704            Ok(runner) => {
1705                assert_eq!(runner.executor.evm_env().block_env.prevrandao(), Some(prevrandao));
1706            }
1707            Err(error) => assert_missing_orphaned_block(&error),
1708        }
1709        assert_eq!(config.resolved_fork.as_ref(), Some(&pinned));
1710    }
1711
1712    #[test]
1713    fn script_trace_requirements_honor_tracing_verbosity() {
1714        let mut config = Config::default();
1715        config.tracing.verbosity = 5;
1716
1717        let tracing = script_trace_requirements(&config, false).into_config().unwrap();
1718        assert!(tracing.record_state_diff);
1719    }
1720
1721    #[test]
1722    fn created_contracts_include_successful_creator_code_ancestry() {
1723        let root = Address::repeat_byte(0x11);
1724        let implementation = Address::repeat_byte(0x22);
1725        let helper = Address::repeat_byte(0x33);
1726        let created = Address::repeat_byte(0x44);
1727        let init_code = Bytes::from_static(&[0x60, 0x00]);
1728        let mut arena = CallTraceArena::default();
1729        arena.nodes_mut()[0].trace =
1730            CallTrace { success: true, address: root, kind: CallKind::Call, ..Default::default() };
1731        arena.nodes_mut().extend([
1732            CallTraceNode {
1733                parent: Some(0),
1734                idx: 1,
1735                trace: CallTrace {
1736                    success: true,
1737                    address: implementation,
1738                    kind: CallKind::DelegateCall,
1739                    ..Default::default()
1740                },
1741                ..Default::default()
1742            },
1743            CallTraceNode {
1744                parent: Some(1),
1745                idx: 2,
1746                trace: CallTrace {
1747                    success: true,
1748                    address: helper,
1749                    kind: CallKind::Call,
1750                    ..Default::default()
1751                },
1752                ..Default::default()
1753            },
1754            CallTraceNode {
1755                parent: Some(2),
1756                idx: 3,
1757                trace: CallTrace {
1758                    success: true,
1759                    address: created,
1760                    kind: CallKind::Create2,
1761                    data: init_code.clone(),
1762                    ..Default::default()
1763                },
1764                ..Default::default()
1765            },
1766        ]);
1767        let result = ScriptResult::<Ethereum> {
1768            traces: vec![(
1769                TraceKind::Execution,
1770                SparsedTraceArena {
1771                    arena,
1772                    ignored: Default::default(),
1773                    diagnostics: Default::default(),
1774                },
1775            )],
1776            ..Default::default()
1777        };
1778
1779        let contracts = result.get_created_contracts(&ContractsByArtifact::default());
1780        assert_eq!(contracts.len(), 1);
1781        assert_eq!(contracts[0].address, created);
1782        assert_eq!(contracts[0].init_code, init_code);
1783        assert_eq!(contracts[0].creator_code_addresses, [helper, implementation, root]);
1784    }
1785
1786    #[test]
1787    fn created_contracts_exclude_creations_rolled_back_by_an_ancestor() {
1788        let mut arena = CallTraceArena::default();
1789        arena.nodes_mut()[0].trace = CallTrace { success: true, ..Default::default() };
1790        arena.nodes_mut().extend([
1791            CallTraceNode {
1792                parent: Some(0),
1793                idx: 1,
1794                trace: CallTrace { success: false, kind: CallKind::Call, ..Default::default() },
1795                ..Default::default()
1796            },
1797            CallTraceNode {
1798                parent: Some(1),
1799                idx: 2,
1800                trace: CallTrace {
1801                    success: true,
1802                    kind: CallKind::Create,
1803                    data: Bytes::from_static(&[0x60, 0x00]),
1804                    ..Default::default()
1805                },
1806                ..Default::default()
1807            },
1808        ]);
1809        let result = ScriptResult::<Ethereum> {
1810            traces: vec![(
1811                TraceKind::Execution,
1812                SparsedTraceArena {
1813                    arena,
1814                    ignored: Default::default(),
1815                    diagnostics: Default::default(),
1816                },
1817            )],
1818            ..Default::default()
1819        };
1820
1821        assert!(result.get_created_contracts(&ContractsByArtifact::default()).is_empty());
1822    }
1823
1824    fn active_session_entry(
1825        session_id: B256,
1826        root_account: Address,
1827        chain_id: u64,
1828    ) -> SessionEntry {
1829        let foundry_wallets::WalletSigner::Local(root) =
1830            foundry_wallets::utils::create_private_key_signer(SESSION_ROOT_PRIVATE_KEY).unwrap()
1831        else {
1832            unreachable!("a raw private key always creates a local signer")
1833        };
1834        assert_eq!(root.address(), root_account);
1835        let key = GeneratedSessionKey::from_private_key(SESSION_PRIVATE_KEY).unwrap();
1836        let prepared = SessionAuthorizationRequest {
1837            session_id,
1838            root_account,
1839            chain_id,
1840            key_address: key.address(),
1841            expiry: NonZeroU64::new(u64::MAX).unwrap(),
1842            scope: vec![tempo_primitives::transaction::CallScope {
1843                target: Address::repeat_byte(0xaa),
1844                selector_rules: vec![],
1845            }],
1846            spend_limits: vec![],
1847        }
1848        .prepare(0)
1849        .unwrap();
1850        let signature = root.sign_hash_sync(&prepared.authorization.signature_hash()).unwrap();
1851        let authorization = prepared
1852            .authorization
1853            .clone()
1854            .into_signed(tempo_primitives::transaction::PrimitiveSignature::Secp256k1(signature));
1855        prepared.into_active_entry(key, &authorization).unwrap()
1856    }
1857
1858    struct TempoHomeGuard {
1859        _guard: MutexGuard<'static, ()>,
1860    }
1861
1862    impl TempoHomeGuard {
1863        async fn set(path: &std::path::Path) -> Self {
1864            let guard = TEMPO_HOME_LOCK.lock().await;
1865            // SAFETY: test-only environment override for Tempo local state.
1866            unsafe {
1867                std::env::remove_var(TEMPO_SESSION_ID_ENV);
1868                std::env::set_var(TEMPO_HOME_ENV, path);
1869            }
1870            Self { _guard: guard }
1871        }
1872    }
1873
1874    impl Drop for TempoHomeGuard {
1875        fn drop(&mut self) {
1876            // SAFETY: restore process environment after the critical section.
1877            unsafe {
1878                std::env::remove_var(TEMPO_HOME_ENV);
1879                std::env::remove_var(TEMPO_SESSION_ID_ENV);
1880            }
1881        }
1882    }
1883
1884    fn session_root() -> Address {
1885        SESSION_ROOT_ADDRESS.parse().unwrap()
1886    }
1887
1888    #[test]
1889    fn can_parse_sig() {
1890        let sig = "0x522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266";
1891        let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--sig", sig]);
1892        assert_eq!(args.sig, sig);
1893    }
1894
1895    #[test]
1896    fn parses_confirmations() {
1897        let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol"]);
1898        assert_eq!(args.confirmations, DEFAULT_CONFIRMATIONS);
1899
1900        let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--confirmations", "6"]);
1901        assert_eq!(args.confirmations, 6);
1902
1903        let err =
1904            ScriptArgs::try_parse_from(["foundry-cli", "Contract.sol", "--confirmations", "0"])
1905                .unwrap_err();
1906        assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
1907    }
1908
1909    #[test]
1910    fn verify_external_requires_verify() {
1911        let err = ScriptArgs::try_parse_from(["foundry-cli", "Contract.sol", "--verify-external"])
1912            .unwrap_err();
1913        assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
1914
1915        let args = ScriptArgs::try_parse_from([
1916            "foundry-cli",
1917            "Contract.sol",
1918            "--broadcast",
1919            "--verify",
1920            "--verify-external",
1921        ])
1922        .unwrap();
1923        assert!(args.verify_external);
1924    }
1925
1926    #[test]
1927    fn verify_external_requires_simulation_and_online_mode() {
1928        for conflicting_arg in ["--skip-simulation", "--offline"] {
1929            let err = ScriptArgs::try_parse_from([
1930                "foundry-cli",
1931                "Contract.sol",
1932                "--broadcast",
1933                "--verify",
1934                "--verify-external",
1935                conflicting_arg,
1936            ])
1937            .unwrap_err();
1938            assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
1939        }
1940    }
1941
1942    #[test]
1943    fn rejects_max_sender_nonce() {
1944        let max_valid_nonce = (u64::MAX - 1).to_string();
1945        let args = ScriptArgs::try_parse_from([
1946            "foundry-cli",
1947            "Contract.sol",
1948            "--sender-nonce",
1949            &max_valid_nonce,
1950        ])
1951        .unwrap();
1952        assert_eq!(args.sender_nonce, Some(u64::MAX - 1));
1953
1954        let max_nonce = u64::MAX.to_string();
1955        let err = ScriptArgs::try_parse_from([
1956            "foundry-cli",
1957            "Contract.sol",
1958            "--sender-nonce",
1959            &max_nonce,
1960        ])
1961        .unwrap_err();
1962        assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
1963    }
1964
1965    #[tokio::test(flavor = "multi_thread")]
1966    async fn update_sender_fork_does_not_read_nonce_from_replacement_block() {
1967        let (_api, handle) = spawn(NodeConfig::test()).await;
1968        let accounts = handle.dev_wallets().collect::<Vec<_>>();
1969        let original_sender = accounts[0].address();
1970        let replacement_sender = accounts[1].address();
1971        let provider = handle.http_provider();
1972        let tx = TransactionRequest::default()
1973            .from(replacement_sender)
1974            .to(original_sender)
1975            .value(U256::from(1));
1976        provider.send_transaction(tx.into()).await.unwrap().get_receipt().await.unwrap();
1977
1978        let evm_opts = EvmOpts {
1979            fork_url: Some(handle.http_endpoint()),
1980            sender: original_sender,
1981            ..Default::default()
1982        };
1983        let mut config = ScriptConfig::<EthEvmNetwork>::new(
1984            Config::default(),
1985            evm_opts,
1986            ExecutorBuilder::<EthEvmNetwork>::new(),
1987            false,
1988            TempoOpts::default(),
1989            None,
1990        )
1991        .await
1992        .unwrap();
1993        let resolved = config.resolved_fork.clone().unwrap();
1994        assert_eq!(config.evm_opts.fork_block_number, None);
1995        assert_eq!(config.sender_nonce, 0);
1996
1997        provider
1998            .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
1999            .await
2000            .unwrap();
2001        assert_eq!(
2002            provider
2003                .get_transaction_count(replacement_sender)
2004                .block_id(BlockId::number(1))
2005                .await
2006                .unwrap(),
2007            0
2008        );
2009
2010        match config.update_sender(replacement_sender).await {
2011            Ok(()) => assert_eq!(config.sender_nonce, 1),
2012            Err(error) => assert_missing_orphaned_block(&error),
2013        }
2014        assert_eq!(config.resolved_fork.as_ref(), Some(&resolved));
2015    }
2016
2017    #[tokio::test(flavor = "multi_thread")]
2018    async fn sender_nonce_override_does_not_refresh_fork() {
2019        let (_api, handle) = spawn(NodeConfig::test()).await;
2020        let sender = handle.dev_accounts().next().unwrap();
2021        let evm_opts =
2022            EvmOpts { fork_url: Some(handle.http_endpoint()), sender, ..Default::default() };
2023        let mut config = ScriptConfig::<EthEvmNetwork>::new(
2024            Config::default(),
2025            evm_opts,
2026            ExecutorBuilder::<EthEvmNetwork>::new(),
2027            false,
2028            TempoOpts::default(),
2029            Some(7),
2030        )
2031        .await
2032        .unwrap();
2033
2034        config.set_fork_url("http://127.0.0.1:1".to_string());
2035        config.update_sender(Address::with_last_byte(0x42)).await.unwrap();
2036        assert_eq!(config.sender_nonce, 7);
2037    }
2038
2039    #[tokio::test(flavor = "multi_thread")]
2040    #[cfg(feature = "monad")]
2041    async fn script_runner_preserves_nested_fork_source_chain() {
2042        let (_origin_api, origin_handle) = spawn(
2043            NodeConfig::test_monad()
2044                .with_chain_id(Some(NamedChain::MonadTestnet as u64))
2045                .with_hardfork(Some(foundry_evm::hardforks::MonadHardfork::MonadNine.into())),
2046        )
2047        .await;
2048        let (_fork_api, fork_handle) = spawn(
2049            NodeConfig::test_monad()
2050                .with_chain_id(Some(NamedChain::Mainnet as u64))
2051                .with_no_storage_caching(true)
2052                .with_eth_rpc_url(Some(origin_handle.http_endpoint()))
2053                .with_fork_block_number(Some(0u64)),
2054        )
2055        .await;
2056        let mut evm_opts =
2057            EvmOpts { fork_url: Some(fork_handle.http_endpoint()), ..Default::default() };
2058        evm_opts.infer_network_from_fork().await.unwrap();
2059        let config = Config { networks: evm_opts.networks, ..Default::default() };
2060        let mut script = ScriptConfig::<MonadEvmNetwork>::new(
2061            config,
2062            evm_opts,
2063            ExecutorBuilder::<MonadEvmNetwork>::new(),
2064            false,
2065            TempoOpts::default(),
2066            None,
2067        )
2068        .await
2069        .unwrap();
2070
2071        let _runner = script._get_runner(None, false, false).await.unwrap();
2072
2073        assert_eq!(script.source_chain_id, Some(NamedChain::MonadTestnet as u64));
2074        assert_eq!(
2075            script.hardfork,
2076            Some(FoundryHardfork::Monad(foundry_evm::hardforks::MonadHardfork::MonadNine))
2077        );
2078    }
2079
2080    #[tokio::test(flavor = "multi_thread")]
2081    async fn tempo_runner_fee_charge_matches_execution_context() {
2082        let (_api, handle) = spawn(NodeConfig::test_tempo()).await;
2083        let networks = NetworkConfigs::with_tempo();
2084        let evm_opts = EvmOpts {
2085            fork_url: Some(handle.http_endpoint()),
2086            sender: handle.dev_accounts().next().unwrap(),
2087            networks,
2088            ..Default::default()
2089        };
2090        let config = Config { networks, ..Default::default() };
2091        let mut script = ScriptConfig::<TempoEvmNetwork>::new(
2092            config,
2093            evm_opts,
2094            ExecutorBuilder::<TempoEvmNetwork>::new(),
2095            false,
2096            TempoOpts::default(),
2097            None,
2098        )
2099        .await
2100        .unwrap();
2101
2102        let rpc_runner = script._get_runner(None, false, false).await.unwrap();
2103        assert!(!rpc_runner.executor.evm_env().cfg_env.disable_fee_charge);
2104
2105        let target = ArtifactId {
2106            path: PathBuf::from("Script.json"),
2107            name: "Script".to_string(),
2108            source: PathBuf::from("Script.sol"),
2109            version: Version::new(0, 8, 30),
2110            build_id: String::new(),
2111            profile: "default".to_string(),
2112        };
2113        let synthetic_runner = script
2114            .get_runner_with_cheatcodes(
2115                ContractsByArtifact::default(),
2116                Wallets::new(Default::default(), None),
2117                false,
2118                target,
2119                false,
2120            )
2121            .await
2122            .unwrap();
2123        assert!(synthetic_runner.executor.evm_env().cfg_env.disable_fee_charge);
2124    }
2125
2126    #[test]
2127    fn can_parse_shared_tempo_opts() {
2128        let args = ScriptArgs::parse_from([
2129            "foundry-cli",
2130            "Contract.sol",
2131            "--tempo.fee-token",
2132            "1",
2133            "--tempo.expires",
2134            "10",
2135        ]);
2136
2137        assert_eq!(
2138            args.tempo.fee_token,
2139            Some(address!("0x20C0000000000000000000000000000000000001"))
2140        );
2141        assert_eq!(args.tempo.expires, Some(10));
2142    }
2143
2144    #[test]
2145    fn can_parse_sponsor_tempo_opts() {
2146        let args = ScriptArgs::parse_from([
2147            "foundry-cli",
2148            "Contract.sol",
2149            "--tempo.sponsor",
2150            SESSION_ROOT_ADDRESS,
2151            "--tempo.sponsor-signer",
2152            "env://TEMPO_SPONSOR_PK",
2153        ]);
2154
2155        assert_eq!(args.tempo.sponsor, Some(session_root()));
2156        assert_eq!(args.tempo.sponsor_signer.as_deref(), Some("env://TEMPO_SPONSOR_PK"));
2157    }
2158
2159    #[test]
2160    fn can_parse_full_tempo_opts() {
2161        let args =
2162            ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--tempo.nonce-key", "1"]);
2163
2164        assert_eq!(args.tempo.nonce_key, Some(U256::from(1)));
2165    }
2166
2167    #[test]
2168    fn can_parse_tempo_session_opt() {
2169        let args = ScriptArgs::parse_from([
2170            "foundry-cli",
2171            "Contract.sol",
2172            "--tempo.session",
2173            SESSION_ID_HEX,
2174        ]);
2175
2176        assert_eq!(args.tempo.session, Some(B256::from([0x11; 32])),);
2177    }
2178
2179    #[tokio::test]
2180    async fn tempo_session_sets_script_sender_to_root_account() {
2181        let temp = tempdir().unwrap();
2182        let session_id = B256::from([0x22; 32]);
2183        let root = session_root();
2184        let chain_id = foundry_common::DEV_CHAIN_ID;
2185
2186        let _guard = TempoHomeGuard::set(temp.path()).await;
2187        upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
2188
2189        let args = ScriptArgs::parse_from([
2190            "foundry-cli",
2191            "Contract.sol",
2192            "--tempo.session",
2193            &format!("{session_id:?}"),
2194        ]);
2195        let evm_opts = EvmOpts {
2196            networks: NetworkConfigs::with_tempo(),
2197            env: foundry_evm::opts::Env { chain_id: Some(chain_id), ..Default::default() },
2198            ..Default::default()
2199        };
2200
2201        let state = args
2202            .preprocess::<TempoEvmNetwork>(
2203                Config::default(),
2204                evm_opts,
2205                ExecutorBuilder::<TempoEvmNetwork>::new(),
2206            )
2207            .await
2208            .unwrap();
2209        assert_eq!(state.script_config.evm_opts.sender, root);
2210    }
2211
2212    #[tokio::test]
2213    async fn tempo_session_resume_multi_defers_session_sender_until_reexecution() {
2214        let temp = tempdir().unwrap();
2215        let session_id = B256::from([0x55; 32]);
2216        let root = session_root();
2217        let chain_id = 4217;
2218
2219        let _guard = TempoHomeGuard::set(temp.path()).await;
2220        upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
2221
2222        let args = ScriptArgs::parse_from([
2223            "foundry-cli",
2224            "Contract.sol",
2225            "--resume",
2226            "--multi",
2227            "--tempo.session",
2228            &format!("{session_id:?}"),
2229        ]);
2230        let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
2231
2232        let state = args
2233            .preprocess::<TempoEvmNetwork>(
2234                Config::default(),
2235                evm_opts,
2236                ExecutorBuilder::<TempoEvmNetwork>::new(),
2237            )
2238            .await
2239            .unwrap();
2240        assert_ne!(state.script_config.evm_opts.sender, root);
2241    }
2242
2243    #[tokio::test]
2244    async fn tempo_session_resume_defers_session_sender_until_reexecution() {
2245        let temp = tempdir().unwrap();
2246        let session_id = B256::from([0x77; 32]);
2247        let root = session_root();
2248        let chain_id = 4217;
2249
2250        let _guard = TempoHomeGuard::set(temp.path()).await;
2251        upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
2252
2253        let args = ScriptArgs::parse_from([
2254            "foundry-cli",
2255            "Contract.sol",
2256            "--resume",
2257            "--tempo.session",
2258            &format!("{session_id:?}"),
2259        ]);
2260        let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
2261
2262        let state = args
2263            .preprocess::<TempoEvmNetwork>(
2264                Config::default(),
2265                evm_opts,
2266                ExecutorBuilder::<TempoEvmNetwork>::new(),
2267            )
2268            .await
2269            .unwrap();
2270        assert_ne!(state.script_config.evm_opts.sender, root);
2271    }
2272
2273    #[tokio::test]
2274    async fn tempo_session_non_resume_multi_sets_sender_without_chain_validation() {
2275        let temp = tempdir().unwrap();
2276        let session_id = B256::from([0x66; 32]);
2277        let root = session_root();
2278        let chain_id = 4217;
2279
2280        let _guard = TempoHomeGuard::set(temp.path()).await;
2281        upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
2282
2283        let args = ScriptArgs::parse_from([
2284            "foundry-cli",
2285            "Contract.sol",
2286            "--multi",
2287            "--tempo.session",
2288            &format!("{session_id:?}"),
2289        ]);
2290        let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
2291
2292        let state = args
2293            .preprocess::<TempoEvmNetwork>(
2294                Config::default(),
2295                evm_opts,
2296                ExecutorBuilder::<TempoEvmNetwork>::new(),
2297            )
2298            .await
2299            .unwrap();
2300        assert_eq!(state.script_config.evm_opts.sender, root);
2301    }
2302
2303    #[tokio::test]
2304    async fn tempo_session_initial_broadcast_sets_sender_without_chain_validation() {
2305        let temp = tempdir().unwrap();
2306        let session_id = B256::from([0x88; 32]);
2307        let root = session_root();
2308        let chain_id = 4217;
2309
2310        let _guard = TempoHomeGuard::set(temp.path()).await;
2311        upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
2312
2313        let args = ScriptArgs::parse_from([
2314            "foundry-cli",
2315            "Contract.sol",
2316            "--broadcast",
2317            "--tempo.session",
2318            &format!("{session_id:?}"),
2319        ]);
2320        let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
2321
2322        let state = args
2323            .preprocess::<TempoEvmNetwork>(
2324                Config::default(),
2325                evm_opts,
2326                ExecutorBuilder::<TempoEvmNetwork>::new(),
2327            )
2328            .await
2329            .unwrap();
2330        assert_eq!(state.script_config.evm_opts.sender, root);
2331    }
2332
2333    #[tokio::test]
2334    async fn tempo_options_preserve_explicit_script_network() {
2335        let temp = tempdir().unwrap();
2336        let _guard = TempoHomeGuard::set(temp.path()).await;
2337        for options in [
2338            vec!["--tempo.fee-token", "0x20c0000000000000000000000000000000000000"],
2339            vec![
2340                "--tempo.session",
2341                "0x4444444444444444444444444444444444444444444444444444444444444444",
2342            ],
2343        ] {
2344            let args =
2345                ScriptArgs::parse_from(["foundry-cli", "Contract.sol"].into_iter().chain(options));
2346            for (networks, name) in [
2347                (NetworkConfigs::with_ethereum(), "ethereum"),
2348                (NetworkConfigs::with_celo(), "celo"),
2349                #[cfg(feature = "base")]
2350                (NetworkConfigs::with_base(), "base"),
2351            ] {
2352                let evm_opts = EvmOpts { networks, ..Default::default() };
2353                let err = args.resolved_evm_opts(Config::default(), evm_opts).await.unwrap_err();
2354                assert_eq!(
2355                    err.to_string(),
2356                    format!("Tempo transaction options conflict with configured network `{name}`")
2357                );
2358            }
2359        }
2360    }
2361
2362    #[tokio::test]
2363    async fn tempo_session_env_selects_tempo_network() {
2364        let temp = tempdir().unwrap();
2365        let _guard = TempoHomeGuard::set(temp.path()).await;
2366        let session_id = B256::from([0x44; 32]);
2367        // SAFETY: serialized by TempoHomeGuard.
2368        unsafe { std::env::set_var(TEMPO_SESSION_ID_ENV, format!("{session_id:?}")) };
2369
2370        let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol"]);
2371        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
2372        let (_, evm_opts) = args.resolved_evm_opts(config, evm_opts).await.unwrap();
2373
2374        assert!(evm_opts.networks.is_tempo());
2375    }
2376
2377    #[tokio::test]
2378    async fn tempo_session_rejects_explicit_script_wallet_signer() {
2379        let temp = tempdir().unwrap();
2380        let session_id = B256::from([0x33; 32]);
2381        let root = session_root();
2382        let chain_id = foundry_common::DEV_CHAIN_ID;
2383
2384        let _guard = TempoHomeGuard::set(temp.path()).await;
2385        upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
2386
2387        let args = ScriptArgs::parse_from([
2388            "foundry-cli",
2389            "Contract.sol",
2390            "--tempo.session",
2391            &format!("{session_id:?}"),
2392            "--private-key",
2393            SESSION_PRIVATE_KEY,
2394        ]);
2395        let evm_opts = EvmOpts {
2396            networks: NetworkConfigs::with_tempo(),
2397            env: foundry_evm::opts::Env { chain_id: Some(chain_id), ..Default::default() },
2398            ..Default::default()
2399        };
2400
2401        let err = match args
2402            .preprocess::<TempoEvmNetwork>(
2403                Config::default(),
2404                evm_opts,
2405                ExecutorBuilder::<TempoEvmNetwork>::new(),
2406            )
2407            .await
2408        {
2409            Ok(_) => panic!("expected --tempo.session with --private-key to fail"),
2410            Err(err) => err,
2411        };
2412        assert!(err.to_string().contains("explicit wallet signer"), "{err}");
2413    }
2414
2415    #[test]
2416    fn can_parse_unlocked() {
2417        let args = ScriptArgs::parse_from([
2418            "foundry-cli",
2419            "Contract.sol",
2420            "--sender",
2421            "0x4e59b44847b379578588920ca78fbf26c0b4956c",
2422            "--unlocked",
2423        ]);
2424        assert!(args.unlocked);
2425
2426        let key = U256::ZERO;
2427        let args = ScriptArgs::try_parse_from([
2428            "foundry-cli",
2429            "Contract.sol",
2430            "--sender",
2431            "0x4e59b44847b379578588920ca78fbf26c0b4956c",
2432            "--unlocked",
2433            "--private-key",
2434            &key.to_string(),
2435        ]);
2436        assert!(args.is_err());
2437    }
2438
2439    #[test]
2440    fn can_merge_script_config() {
2441        let args = ScriptArgs::parse_from([
2442            "foundry-cli",
2443            "Contract.sol",
2444            "--etherscan-api-key",
2445            "goerli",
2446        ]);
2447        let config = args.load_config().unwrap();
2448        assert_eq!(config.etherscan_api_key, Some("goerli".to_string()));
2449    }
2450
2451    #[test]
2452    fn can_disable_code_size_limit() {
2453        let args =
2454            ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--disable-code-size-limit"]);
2455        assert!(args.disable_code_size_limit);
2456
2457        let result = ScriptResult::<Ethereum>::default();
2458        let contracts = ContractsByArtifact::default();
2459        let create = Address::ZERO;
2460        assert!(
2461            args.check_contract_sizes(ContractSizeLimits::default(), &result, &contracts, create)
2462                .is_ok()
2463        );
2464    }
2465
2466    #[test]
2467    fn can_parse_verifier_url() {
2468        let args = ScriptArgs::parse_from([
2469            "foundry-cli",
2470            "script",
2471            "script/Test.s.sol:TestScript",
2472            "--fork-url",
2473            "http://localhost:8545",
2474            "--verifier-url",
2475            "http://localhost:3000/api/verify",
2476            "--etherscan-api-key",
2477            "blacksmith",
2478            "--broadcast",
2479            "--verify",
2480            "-vvvvv",
2481        ]);
2482        assert_eq!(
2483            args.verifier.verifier_url,
2484            Some("http://localhost:3000/api/verify".to_string())
2485        );
2486    }
2487
2488    #[test]
2489    fn can_extract_code_size_limit() {
2490        let args = ScriptArgs::parse_from([
2491            "foundry-cli",
2492            "script",
2493            "script/Test.s.sol:TestScript",
2494            "--fork-url",
2495            "http://localhost:8545",
2496            "--broadcast",
2497            "--code-size-limit",
2498            "50000",
2499        ]);
2500        assert_eq!(args.evm.env.code_size_limit, Some(50000));
2501    }
2502
2503    /// `--code-size-limit` on the CLI should be used by `check_contract_sizes`, not silently
2504    /// ignored in favour of the foundry.toml value (which defaults to None → EIP-170's 24576).
2505    #[test]
2506    fn cli_code_size_limit_is_honoured_by_check() {
2507        let args = ScriptArgs::parse_from([
2508            "foundry-cli",
2509            "script",
2510            "script/Test.s.sol:TestScript",
2511            "--code-size-limit",
2512            "2147483647",
2513        ]);
2514        // The CLI flag must land in evm_opts so that the size_limits computation in run() picks
2515        // it up via `.evm_opts.env.code_size_limit.or(config.code_size_limit)`.
2516        assert_eq!(args.evm.env.code_size_limit, Some(2147483647));
2517    }
2518
2519    #[test]
2520    #[cfg(feature = "monad")]
2521    fn contract_size_limits_use_resolved_monad_network() {
2522        let args =
2523            ScriptArgs::parse_from(["foundry-cli", "script", "script/Test.s.sol:TestScript"]);
2524        let evm_opts = EvmOpts { networks: NetworkConfigs::with_monad(), ..Default::default() };
2525
2526        let limits = args.contract_size_limits::<MonadEvmNetwork>(&Config::default(), &evm_opts);
2527
2528        assert!(limits.runtime > ContractSizeLimits::default().runtime);
2529        assert!(limits.initcode > ContractSizeLimits::default().initcode);
2530    }
2531
2532    #[test]
2533    #[cfg(feature = "monad")]
2534    fn contract_size_limits_prefer_cli_then_config_over_network() {
2535        let args = ScriptArgs::parse_from([
2536            "foundry-cli",
2537            "script",
2538            "script/Test.s.sol:TestScript",
2539            "--code-size-limit",
2540            "64",
2541        ]);
2542        let mut config = Config { code_size_limit: Some(128), ..Default::default() };
2543        let evm_opts = EvmOpts { networks: NetworkConfigs::with_monad(), ..Default::default() };
2544        assert_eq!(
2545            args.contract_size_limits::<MonadEvmNetwork>(&config, &evm_opts,),
2546            ContractSizeLimits::with_runtime_limit(64)
2547        );
2548
2549        let args =
2550            ScriptArgs::parse_from(["foundry-cli", "script", "script/Test.s.sol:TestScript"]);
2551        config.code_size_limit = Some(128);
2552        assert_eq!(
2553            args.contract_size_limits::<MonadEvmNetwork>(&config, &evm_opts,),
2554            ContractSizeLimits::with_runtime_limit(128)
2555        );
2556    }
2557
2558    #[test]
2559    fn can_extract_script_etherscan_key() {
2560        let temp = tempdir().unwrap();
2561        let root = temp.path();
2562
2563        let config = r#"
2564                [profile.default]
2565                etherscan_api_key = "amoy"
2566
2567                [etherscan]
2568                amoy = { key = "https://etherscan-amoy.com/" }
2569            "#;
2570
2571        let toml_file = root.join(Config::FILE_NAME);
2572        fs::write(toml_file, config).unwrap();
2573        let args = ScriptArgs::parse_from([
2574            "foundry-cli",
2575            "Contract.sol",
2576            "--etherscan-api-key",
2577            "amoy",
2578            "--root",
2579            root.as_os_str().to_str().unwrap(),
2580        ]);
2581
2582        let config = args.load_config().unwrap();
2583        let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
2584        assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
2585    }
2586
2587    #[test]
2588    fn can_extract_script_rpc_alias() {
2589        let temp = tempdir().unwrap();
2590        let root = temp.path();
2591
2592        let config = r#"
2593                [profile.default]
2594
2595                [rpc_endpoints]
2596                polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_CAN_EXTRACT_RPC_ALIAS}"
2597            "#;
2598
2599        let toml_file = root.join(Config::FILE_NAME);
2600        fs::write(toml_file, config).unwrap();
2601        let args = ScriptArgs::parse_from([
2602            "foundry-cli",
2603            "DeployV1",
2604            "--rpc-url",
2605            "polygonAmoy",
2606            "--root",
2607            root.as_os_str().to_str().unwrap(),
2608        ]);
2609
2610        let err = args.load_config_and_evm_opts().unwrap_err();
2611
2612        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
2613
2614        unsafe {
2615            std::env::set_var("_CAN_EXTRACT_RPC_ALIAS", "123456");
2616        }
2617        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
2618        assert_eq!(config.eth_rpc_url, Some("polygonAmoy".to_string()));
2619        assert_eq!(
2620            evm_opts.fork_url,
2621            Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
2622        );
2623    }
2624
2625    #[test]
2626    fn can_extract_script_rpc_and_etherscan_alias() {
2627        let temp = tempdir().unwrap();
2628        let root = temp.path();
2629
2630        let config = r#"
2631            [profile.default]
2632
2633            [rpc_endpoints]
2634            amoy = "https://polygon-amoy.g.alchemy.com/v2/${_EXTRACT_RPC_ALIAS}"
2635
2636            [etherscan]
2637            amoy = { key = "${_ETHERSCAN_API_KEY}", chain = 80002, url = "https://amoy.polygonscan.com/" }
2638        "#;
2639
2640        let toml_file = root.join(Config::FILE_NAME);
2641        fs::write(toml_file, config).unwrap();
2642        let args = ScriptArgs::parse_from([
2643            "foundry-cli",
2644            "DeployV1",
2645            "--rpc-url",
2646            "amoy",
2647            "--etherscan-api-key",
2648            "amoy",
2649            "--root",
2650            root.as_os_str().to_str().unwrap(),
2651        ]);
2652        let err = args.load_config_and_evm_opts().unwrap_err();
2653
2654        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
2655
2656        unsafe {
2657            std::env::set_var("_EXTRACT_RPC_ALIAS", "123456");
2658        }
2659        unsafe {
2660            std::env::set_var("_ETHERSCAN_API_KEY", "etherscan_api_key");
2661        }
2662        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
2663        assert_eq!(config.eth_rpc_url, Some("amoy".to_string()));
2664        assert_eq!(
2665            evm_opts.fork_url,
2666            Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
2667        );
2668        let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
2669        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
2670        let etherscan = config.get_etherscan_api_key(None);
2671        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
2672    }
2673
2674    #[test]
2675    fn can_extract_script_rpc_and_sole_etherscan_alias() {
2676        let temp = tempdir().unwrap();
2677        let root = temp.path();
2678
2679        let config = r#"
2680                [profile.default]
2681
2682               [rpc_endpoints]
2683                amoy = "https://polygon-amoy.g.alchemy.com/v2/${_SOLE_EXTRACT_RPC_ALIAS}"
2684
2685                [etherscan]
2686                amoy = { key = "${_SOLE_ETHERSCAN_API_KEY}" }
2687            "#;
2688
2689        let toml_file = root.join(Config::FILE_NAME);
2690        fs::write(toml_file, config).unwrap();
2691        let args = ScriptArgs::parse_from([
2692            "foundry-cli",
2693            "DeployV1",
2694            "--rpc-url",
2695            "amoy",
2696            "--root",
2697            root.as_os_str().to_str().unwrap(),
2698        ]);
2699        let err = args.load_config_and_evm_opts().unwrap_err();
2700
2701        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
2702
2703        unsafe {
2704            std::env::set_var("_SOLE_EXTRACT_RPC_ALIAS", "123456");
2705        }
2706        unsafe {
2707            std::env::set_var("_SOLE_ETHERSCAN_API_KEY", "etherscan_api_key");
2708        }
2709        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
2710        assert_eq!(
2711            evm_opts.fork_url,
2712            Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
2713        );
2714        let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
2715        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
2716        let etherscan = config.get_etherscan_api_key(None);
2717        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
2718    }
2719
2720    // <https://github.com/foundry-rs/foundry/issues/5923>
2721    #[test]
2722    fn test_5923() {
2723        let args =
2724            ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--priority-gas-price", "100"]);
2725        assert!(args.priority_gas_price.is_some());
2726    }
2727
2728    #[test]
2729    fn test_eip1559_fee_estimate() {
2730        // Defaults to unset (config provides `market`).
2731        let args = ScriptArgs::parse_from(["foundry-cli", "DeployV1"]);
2732        assert!(args.eip1559_fee_estimate.is_none());
2733
2734        let args = ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--estimate", "aggressive"]);
2735        assert_eq!(args.eip1559_fee_estimate, Some(Eip1559FeeEstimatePreset::Aggressive));
2736    }
2737
2738    // <https://github.com/foundry-rs/foundry/issues/5910>
2739    #[test]
2740    fn test_5910() {
2741        let args = ScriptArgs::parse_from([
2742            "foundry-cli",
2743            "--broadcast",
2744            "--with-gas-price",
2745            "0",
2746            "SolveTutorial",
2747        ]);
2748        assert!(args.with_gas_price.unwrap().is_zero());
2749    }
2750
2751    #[test]
2752    fn test_priority_gas_price_cannot_exceed_gas_price() {
2753        let args = ScriptArgs::parse_from([
2754            "foundry-cli",
2755            "--broadcast",
2756            "--with-gas-price",
2757            "100",
2758            "--priority-gas-price",
2759            "200",
2760            "Script",
2761        ]);
2762        // priority (200) > max_fee (100) — broadcast should reject this at runtime
2763        assert!(args.priority_gas_price.unwrap() > args.with_gas_price.unwrap());
2764    }
2765}