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