Skip to main content

forge_script/
lib.rs

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