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