Skip to main content

foundry_evm_core/
opts.rs

1use crate::{
2    EvmEnv, FoundryBlock, FoundryTransaction,
3    constants::DEFAULT_CREATE2_DEPLOYER,
4    fork::{CreateFork, ResolvedFork},
5    utils::{apply_chain_and_block_specific_env_changes_for_chain, block_env_from_header},
6};
7use alloy_consensus::BlockHeader;
8use alloy_eips::BlockNumHash;
9use alloy_network::{AnyNetwork, BlockResponse, Network, primitives::HeaderResponse};
10use alloy_primitives::{Address, B256, BlockNumber, ChainId, U256};
11use alloy_provider::{Provider, RootProvider};
12use alloy_rpc_types::{
13    BlockId, BlockNumberOrTag,
14    anvil::{Metadata, NodeInfo},
15};
16use eyre::{OptionExt, WrapErr};
17use foundry_common::{
18    ALCHEMY_FREE_TIER_CUPS, NON_ARCHIVE_NODE_WARNING,
19    provider::{ProviderBuilder, is_rpc_method_not_found},
20};
21use foundry_compilers::artifacts::EvmVersion;
22use foundry_config::{Chain, Config, ExecutionSpec, FoundryHardfork, GasLimit};
23use foundry_evm_networks::{NetworkConfigs, NetworkVariant};
24use revm::{context::CfgEnv, primitives::hardfork::SpecId};
25use serde::{Deserialize, Serialize};
26use std::fmt::Write;
27use url::Url;
28
29/// EVM execution options, including the configured remote fork.
30///
31/// Fork handling separates two responsibilities. Endpoint discovery identifies and caches the
32/// execution family without selecting a block. Exact resolution binds the configured source and
33/// selector to a block number, hash, and endpoint context. Forge normally performs discovery
34/// before resolution, while callers that already know the network can resolve directly.
35///
36/// An implicit `latest` selector remains unchanged in these options; only the returned
37/// [`ResolvedFork`] is pinned to the exact block observed during resolution.
38#[derive(Clone, Debug, Serialize, Deserialize)]
39pub struct EvmOpts {
40    /// The EVM environment configuration.
41    #[serde(flatten)]
42    pub env: Env,
43
44    /// Fetch state over a remote instead of starting from empty state.
45    #[serde(rename = "eth_rpc_url")]
46    pub fork_url: Option<String>,
47
48    /// Pins the block number for the state fork.
49    pub fork_block_number: Option<u64>,
50
51    /// The number of retries.
52    pub fork_retries: Option<u32>,
53
54    /// Initial retry backoff.
55    pub fork_retry_backoff: Option<u64>,
56
57    /// Headers to use with `fork_url`
58    pub fork_headers: Option<Vec<String>>,
59
60    /// JWT secret to use with the configured RPC endpoint.
61    #[serde(rename = "eth_rpc_jwt")]
62    pub rpc_jwt: Option<String>,
63
64    /// Headers to use with the configured RPC endpoint.
65    #[serde(rename = "eth_rpc_headers")]
66    pub rpc_headers: Option<Vec<String>>,
67
68    /// Request timeout to use with the configured RPC endpoint.
69    #[serde(rename = "eth_rpc_timeout")]
70    pub rpc_timeout: Option<u64>,
71
72    /// Whether to accept invalid certificates from the configured RPC endpoint.
73    #[serde(default, rename = "eth_rpc_accept_invalid_certs")]
74    pub rpc_accept_invalid_certs: bool,
75
76    /// Whether to disable automatic proxy detection for the configured RPC endpoint.
77    #[serde(default, rename = "eth_rpc_no_proxy")]
78    pub rpc_no_proxy: bool,
79
80    /// The available compute units per second.
81    ///
82    /// See also <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
83    pub compute_units_per_second: Option<u64>,
84
85    /// Disables RPC rate limiting entirely.
86    pub no_rpc_rate_limit: bool,
87
88    /// Disables storage caching entirely.
89    pub no_storage_caching: bool,
90
91    /// The initial balance of each deployed test contract.
92    pub initial_balance: U256,
93
94    /// The address which will be executing all tests.
95    pub sender: Address,
96
97    /// Enables the FFI cheatcode.
98    pub ffi: bool,
99
100    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
101    pub always_use_create_2_factory: bool,
102
103    /// Verbosity mode of EVM output as number of occurrences.
104    pub verbosity: u8,
105
106    /// The memory limit per EVM execution in bytes.
107    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
108    pub memory_limit: u64,
109
110    /// Whether to enable isolation of calls.
111    pub isolate: bool,
112
113    /// Whether to disable block gas limit checks.
114    pub disable_block_gas_limit: bool,
115
116    /// Whether to enable tx gas limit checks as imposed by Osaka (EIP-7825).
117    pub enable_tx_gas_limit: bool,
118
119    #[serde(flatten)]
120    /// Networks with enabled features.
121    pub networks: NetworkConfigs,
122
123    /// The CREATE2 deployer's address.
124    pub create2_deployer: Address,
125
126    /// Most recently discovered endpoint identity, cached for network dispatch and revalidation.
127    #[serde(skip)]
128    pub fork_endpoint: Option<ForkEndpointIdentity>,
129
130    /// Endpoint identity promoted to an invariant that later discovery must match.
131    #[serde(skip)]
132    pub expected_fork_endpoint: Option<ForkEndpointIdentity>,
133
134    /// Whether the active network selection was inferred from the fork endpoint.
135    #[serde(skip)]
136    pub fork_network_is_inferred: bool,
137
138    /// Whether `env.chain_id` was inferred from the fork endpoint.
139    #[serde(skip)]
140    pub fork_chain_id_is_inferred: bool,
141
142    /// Whether `fork_block_number` was pinned from the fork endpoint's latest block.
143    #[serde(skip)]
144    pub fork_block_number_is_inferred: bool,
145}
146
147/// A snapshot of the execution and upstream-source identity exposed by an RPC endpoint.
148///
149/// This identity is independent of the block selector Forge resolves. In particular,
150/// `source_fork_block_*` describes the upstream block from which an Anvil endpoint was itself
151/// forked; the target block selected for local execution belongs to [`ResolvedFork`]. Equality
152/// detects observable endpoint identity changes, including Anvil instance resets, and
153/// execution-profile changes between related RPC reads.
154#[derive(Clone, Debug, PartialEq, Eq)]
155pub struct ForkEndpointIdentity {
156    /// RPC URL from which this identity was discovered.
157    ///
158    /// Request headers and JWT credentials are bound separately by [`ResolvedFork`].
159    pub endpoint: String,
160    /// Chain ID exposed through `eth_chainId`.
161    pub execution_chain_id: ChainId,
162    /// Underlying source chain ID, when the endpoint is itself a fork.
163    pub source_chain_id: ChainId,
164    /// EVM family exposed by the endpoint.
165    pub network: NetworkVariant,
166    /// Complete execution profile exposed by the endpoint.
167    pub network_profile: NetworkConfigs,
168    /// Raw hardfork name reported by an Anvil endpoint.
169    pub reported_hardfork: Option<String>,
170    /// Parsed hardfork exposed by an Anvil endpoint, when recognized.
171    pub hardfork: Option<FoundryHardfork>,
172    /// Anvil instance identifier used to detect resets during multi-call discovery.
173    pub instance_id: Option<B256>,
174    /// Block number from which an Anvil endpoint was forked.
175    pub source_fork_block_number: Option<BlockNumber>,
176    /// Block hash from which an Anvil endpoint was forked.
177    pub source_fork_block_hash: Option<B256>,
178}
179
180#[derive(Clone, Copy, Debug, PartialEq, Eq)]
181enum EndpointHardforkPolicy {
182    /// Permit an unrecognized hardfork during early network-family discovery.
183    Optional,
184    /// Require a recognized hardfork before constructing an execution environment.
185    Required,
186}
187
188/// Best-effort Anvil detection that becomes strict after positive identification.
189///
190/// Before the first successful `anvil_nodeInfo` response, any probe failure means that the
191/// optional capability is unavailable. Mandatory standard RPC reads still expose endpoint-wide
192/// failures. Once a response or cached endpoint identity identifies Anvil, every later probe
193/// failure is returned so it cannot hide an endpoint reset or execution-profile change.
194#[derive(Clone, Copy, Debug, Default)]
195struct AnvilNodeInfoProbe {
196    identified: bool,
197}
198
199impl AnvilNodeInfoProbe {
200    const fn new(identified: bool) -> Self {
201        Self { identified }
202    }
203
204    async fn request<N: Network, P: Provider<N>>(
205        &mut self,
206        provider: &P,
207    ) -> eyre::Result<Option<NodeInfo>> {
208        match provider.raw_request::<_, NodeInfo>("anvil_nodeInfo".into(), ()).await {
209            Ok(node_info) => {
210                self.identified = true;
211                Ok(Some(node_info))
212            }
213            Err(_) if !self.identified => Ok(None),
214            Err(error) => Err(error).wrap_err("failed to determine network family from endpoint"),
215        }
216    }
217}
218
219#[derive(Clone, Copy)]
220enum ForkBlockTarget {
221    Configured(BlockNumberOrTag),
222    Resolved(BlockNumHash),
223}
224
225fn endpoint_hardfork(
226    network: NetworkVariant,
227    hardfork: &str,
228    policy: EndpointHardforkPolicy,
229) -> eyre::Result<Option<FoundryHardfork>> {
230    match network.parse_hardfork(hardfork) {
231        Ok(hardfork) => Ok(Some(hardfork)),
232        Err(_) if policy == EndpointHardforkPolicy::Optional => Ok(None),
233        Err(error) => Err(eyre::Report::msg(error)).wrap_err_with(|| {
234            format!("unsupported hardfork `{hardfork}` reported for `{network}`")
235        }),
236    }
237}
238
239/// Endpoint identity paired with the remote block number backing a fork.
240///
241/// [`ResolvedFork`] adds the exact block hash and configured RPC source. The remote
242/// `block_number` may differ from a remapped EVM block number on L2s, and the source chain ID
243/// remains distinct from a configured `CHAINID` opcode override.
244#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize)]
245pub struct ForkContext {
246    /// Chain ID exposed through `eth_chainId`.
247    pub execution_chain_id: ChainId,
248    /// Underlying source chain ID.
249    pub source_chain_id: ChainId,
250    /// Execution network exposed by the endpoint.
251    pub network: NetworkVariant,
252    /// Complete execution profile exposed by the endpoint.
253    pub network_profile: NetworkConfigs,
254    /// Actual block number fetched from the fork endpoint.
255    pub block_number: BlockNumber,
256    /// Exact hardfork reported by the fork endpoint, when available.
257    pub hardfork: Option<FoundryHardfork>,
258    /// Anvil instance identifier, when exposed by the endpoint.
259    pub instance_id: Option<B256>,
260    /// Block number from which the endpoint itself was forked.
261    pub source_fork_block_number: Option<BlockNumber>,
262    /// Block hash from which the endpoint itself was forked.
263    pub source_fork_block_hash: Option<B256>,
264}
265
266impl ForkContext {
267    /// Returns whether this block context belongs to `identity`.
268    pub fn matches_identity(self, identity: &ForkEndpointIdentity) -> bool {
269        self.execution_chain_id == identity.execution_chain_id
270            && self.source_chain_id == identity.source_chain_id
271            && self.network == identity.network
272            && self.network_profile == identity.network_profile
273            && self.hardfork == identity.hardfork
274            && self.instance_id == identity.instance_id
275            && self.source_fork_block_number == identity.source_fork_block_number
276            && self.source_fork_block_hash == identity.source_fork_block_hash
277    }
278
279    /// Returns whether two block contexts belong to the same endpoint identity.
280    pub(crate) fn has_same_endpoint_identity(self, other: Self) -> bool {
281        self.execution_chain_id == other.execution_chain_id
282            && self.source_chain_id == other.source_chain_id
283            && self.network == other.network
284            && self.network_profile == other.network_profile
285            && self.hardfork == other.hardfork
286            && self.instance_id == other.instance_id
287            && self.source_fork_block_number == other.source_fork_block_number
288            && self.source_fork_block_hash == other.source_fork_block_hash
289    }
290
291    /// Returns whether two contexts can reuse a backend pinned by block number.
292    pub fn has_same_backend_target(self, other: Self) -> bool {
293        self.has_same_endpoint_identity(other) && self.block_number == other.block_number
294    }
295}
296
297impl Default for EvmOpts {
298    fn default() -> Self {
299        Self {
300            env: Env::default(),
301            fork_url: None,
302            fork_block_number: None,
303            fork_retries: None,
304            fork_retry_backoff: None,
305            fork_headers: None,
306            rpc_jwt: None,
307            rpc_headers: None,
308            rpc_timeout: None,
309            rpc_accept_invalid_certs: false,
310            rpc_no_proxy: false,
311            compute_units_per_second: None,
312            no_rpc_rate_limit: false,
313            no_storage_caching: false,
314            initial_balance: U256::default(),
315            sender: Address::default(),
316            ffi: false,
317            always_use_create_2_factory: false,
318            verbosity: 0,
319            memory_limit: 0,
320            isolate: false,
321            disable_block_gas_limit: false,
322            enable_tx_gas_limit: false,
323            networks: NetworkConfigs::default(),
324            create2_deployer: DEFAULT_CREATE2_DEPLOYER,
325            fork_endpoint: None,
326            expected_fork_endpoint: None,
327            fork_network_is_inferred: false,
328            fork_chain_id_is_inferred: false,
329            fork_block_number_is_inferred: false,
330        }
331    }
332}
333
334impl EvmOpts {
335    /// Clears state derived from a fork endpoint while preserving explicit configuration.
336    fn clear_endpoint_derived_fork_state(&mut self) {
337        if self.fork_block_number_is_inferred {
338            self.fork_block_number = None;
339            self.fork_block_number_is_inferred = false;
340        }
341        self.fork_endpoint = None;
342        self.expected_fork_endpoint = None;
343        if self.fork_network_is_inferred {
344            self.networks = self.networks.with_rpc_profile(NetworkConfigs::default());
345            self.fork_network_is_inferred = false;
346        }
347        if self.fork_chain_id_is_inferred {
348            self.env.chain_id = None;
349            self.fork_chain_id_is_inferred = false;
350        }
351    }
352
353    /// Selects a new fork URL and clears endpoint-derived state from the previous source.
354    pub fn set_fork_url(&mut self, fork_url: String) {
355        if self.fork_url.as_ref() == Some(&fork_url) {
356            return;
357        }
358        self.fork_url = Some(fork_url);
359        self.clear_endpoint_derived_fork_state();
360    }
361
362    /// Invalidates endpoint-derived state when the effective RPC source changed since `fork` was
363    /// resolved.
364    ///
365    /// The effective source consists of the URL, active headers, and JWT. Explicit block, network,
366    /// and chain-ID selections are retained so the new source is resolved under the same user
367    /// configuration.
368    pub fn invalidate_fork_endpoint_if_source_changed(&mut self, fork: &ResolvedFork) -> bool {
369        let matches = self.fork_url.as_deref().is_some_and(|fork_url| {
370            fork.matches_source(fork_url, self.fork_source_headers(), self.rpc_jwt.as_deref())
371        });
372        if matches {
373            return false;
374        }
375        self.clear_endpoint_derived_fork_state();
376        true
377    }
378
379    /// Applies an explicit network override, replacing any endpoint-inferred selection.
380    pub fn set_explicit_network(&mut self, network: NetworkVariant) {
381        self.networks = network.into();
382        self.fork_network_is_inferred = false;
383    }
384
385    /// Requires subsequent local fork construction to use this exact endpoint identity.
386    pub fn expect_fork_endpoint(
387        &mut self,
388        identity: ForkEndpointIdentity,
389        network_is_inferred: bool,
390    ) {
391        self.fork_endpoint = Some(identity.clone());
392        self.expected_fork_endpoint = Some(identity);
393        self.fork_network_is_inferred = network_is_inferred;
394    }
395
396    fn ensure_expected_fork_endpoint(&self, identity: &ForkEndpointIdentity) -> eyre::Result<()> {
397        if let Some(expected) = &self.expected_fork_endpoint
398            && expected != identity
399        {
400            eyre::bail!(
401                "fork endpoint {} changed after its execution context was selected",
402                fork_endpoint_description(&identity.endpoint)
403            );
404        }
405        Ok(())
406    }
407
408    fn chain_id_override(&self) -> Option<ChainId> {
409        (!self.fork_chain_id_is_inferred).then_some(self.env.chain_id).flatten()
410    }
411
412    fn endpoint_network_fallback(&self) -> NetworkConfigs {
413        if self.networks.has_network_selection() && !self.fork_network_is_inferred {
414            self.networks
415        } else {
416            NetworkConfigs::default()
417        }
418    }
419
420    fn anvil_node_info_probe(&self) -> AnvilNodeInfoProbe {
421        let endpoint = self.fork_url.as_deref();
422        let identified = [self.fork_endpoint.as_ref(), self.expected_fork_endpoint.as_ref()]
423            .into_iter()
424            .flatten()
425            .any(|identity| {
426                Some(identity.endpoint.as_str()) == endpoint && identity.reported_hardfork.is_some()
427            });
428        AnvilNodeInfoProbe::new(identified)
429    }
430
431    fn fork_source_headers(&self) -> Option<&[String]> {
432        self.fork_headers.as_deref().or(self.rpc_headers.as_deref())
433    }
434
435    fn resolved_fork(
436        &self,
437        fork_url: &str,
438        block: BlockNumHash,
439        context: ForkContext,
440    ) -> ResolvedFork {
441        ResolvedFork::new(
442            fork_url,
443            self.fork_source_headers(),
444            self.rpc_jwt.as_deref(),
445            self.fork_block_number,
446            block,
447            context,
448        )
449    }
450
451    /// Converts an implicit `latest` selector into a block-number selector in place.
452    ///
453    /// This only reads the endpoint's current block number; it does not resolve a block hash or
454    /// endpoint identity and is therefore not a reorg-safe fork snapshot. Use
455    /// [`Self::resolve_fork`] when later reads and backend construction must share an exact
456    /// identity.
457    pub async fn pin_fork_block(&mut self) -> eyre::Result<Option<BlockNumber>> {
458        if self.fork_block_number.is_none()
459            && let Some(fork_url) = &self.fork_url
460        {
461            self.fork_block_number = Some(
462                self.fork_provider_with_url::<AnyNetwork>(fork_url)?.get_block_number().await?,
463            );
464            self.fork_block_number_is_inferred = true;
465        }
466        Ok(self.fork_block_number)
467    }
468
469    /// Resolves the configured fork source and selector to an exact snapshot without changing
470    /// `self`.
471    ///
472    /// Endpoint identity is read before and after the target block and the operation is retried if
473    /// it changes. The result binds the configured RPC source, configured selector, exact block
474    /// number and hash, and [`ForkContext`]. When the selector is implicit `latest`, only the
475    /// returned [`ResolvedFork`] is pinned.
476    pub async fn resolve_fork(&self) -> eyre::Result<Option<ResolvedFork>> {
477        let Some(fork_url) = &self.fork_url else { return Ok(None) };
478        let provider = self.fork_provider_with_url::<AnyNetwork>(fork_url)?;
479        let target = ForkBlockTarget::Configured(match self.fork_block_number {
480            Some(block_number) => BlockNumberOrTag::Number(block_number),
481            None => BlockNumberOrTag::Latest,
482        });
483        let mut node_info_probe = self.anvil_node_info_probe();
484        let (_, block, context) =
485            self.resolve_fork_block_with_context(&provider, target, &mut node_info_probe).await?;
486        Ok(Some(self.resolved_fork(fork_url, block, context)))
487    }
488
489    /// Returns whether the configured CREATE2 deployer can be used for library linking.
490    ///
491    /// Locally Foundry can only install its canonical deployer. On forks, any deployer with code
492    /// is usable because the call executes against the forked state.
493    pub async fn can_use_create2_deployer(
494        &self,
495        fork_block: Option<BlockNumber>,
496    ) -> eyre::Result<bool> {
497        let Some(fork_url) = &self.fork_url else {
498            return Ok(self.create2_deployer == DEFAULT_CREATE2_DEPLOYER);
499        };
500        let block = fork_block.ok_or_else(|| eyre::eyre!("fork block must be resolved"))?;
501        let provider = self.fork_provider_with_url::<AnyNetwork>(fork_url)?;
502        Ok(!provider
503            .get_code_at(self.create2_deployer)
504            .block_id(BlockId::number(block))
505            .await?
506            .is_empty())
507    }
508
509    /// Returns whether the configured CREATE2 deployer existed at the resolved fork block.
510    pub async fn can_use_create2_deployer_resolved(
511        &self,
512        fork: Option<&ResolvedFork>,
513    ) -> eyre::Result<bool> {
514        let Some(_) = &self.fork_url else {
515            eyre::ensure!(fork.is_none(), "resolved fork provided without a configured fork");
516            return Ok(self.create2_deployer == DEFAULT_CREATE2_DEPLOYER);
517        };
518        let fork = fork.ok_or_else(|| eyre::eyre!("fork must be resolved"))?;
519        let provider = self.provider_for_resolved_fork::<AnyNetwork>(fork)?;
520        self.ensure_resolved_fork_endpoint(&provider, fork).await?;
521        let available = !provider
522            .get_code_at(self.create2_deployer)
523            .block_id(fork.exact_block_id())
524            .await?
525            .is_empty();
526        self.ensure_resolved_fork_endpoint(&provider, fork).await?;
527        Ok(available)
528    }
529
530    /// Returns whether `fork` was resolved from the currently configured source and selector.
531    pub fn resolved_fork_matches(&self, fork: &ResolvedFork) -> bool {
532        self.fork_url.as_deref().is_some_and(|fork_url| {
533            fork.matches(
534                fork_url,
535                self.fork_source_headers(),
536                self.rpc_jwt.as_deref(),
537                self.fork_block_number,
538            )
539        })
540    }
541
542    /// Returns a provider configured for an already resolved fork.
543    pub fn provider_for_resolved_fork<N: Network>(
544        &self,
545        fork: &ResolvedFork,
546    ) -> eyre::Result<RootProvider<N>> {
547        eyre::ensure!(
548            self.resolved_fork_matches(fork),
549            "resolved fork does not match the configured source and selector"
550        );
551        self.fork_provider_with_url(
552            self.fork_url.as_deref().expect("a matching resolved fork requires a fork URL"),
553        )
554    }
555
556    async fn ensure_resolved_fork_endpoint<N: Network, P: Provider<N>>(
557        &self,
558        provider: &P,
559        fork: &ResolvedFork,
560    ) -> eyre::Result<()> {
561        let mut node_info_probe = self.anvil_node_info_probe();
562        node_info_probe.identified |= fork.context().hardfork.is_some();
563        let identity = self
564            .resolve_fork_endpoint_once(
565                provider,
566                &mut node_info_probe,
567                EndpointHardforkPolicy::Required,
568            )
569            .await?;
570        self.ensure_expected_fork_endpoint(&identity)?;
571        eyre::ensure!(
572            fork.context().matches_identity(&identity),
573            "fork endpoint {} changed after its block and execution context were resolved",
574            fork_endpoint_description(self.fork_url.as_deref().unwrap_or_default())
575        );
576        Ok(())
577    }
578
579    /// Returns an account nonce at the exact block and endpoint identity of `fork`.
580    pub async fn transaction_count_at_resolved_fork(
581        &self,
582        account: Address,
583        fork: &ResolvedFork,
584    ) -> eyre::Result<u64> {
585        let provider = self.provider_for_resolved_fork::<AnyNetwork>(fork)?;
586        self.ensure_resolved_fork_endpoint(&provider, fork).await?;
587        let nonce = provider.get_transaction_count(account).block_id(fork.exact_block_id()).await?;
588        self.ensure_resolved_fork_endpoint(&provider, fork).await?;
589        Ok(nonce)
590    }
591
592    /// Returns a `RootProvider` for the given fork URL configured with options in `self` and
593    /// annotated `Network` type.
594    pub fn fork_provider_with_url<N: Network>(
595        &self,
596        fork_url: &str,
597    ) -> eyre::Result<RootProvider<N>> {
598        let mut builder = ProviderBuilder::new(fork_url)
599            .maybe_max_retry(self.fork_retries)
600            .maybe_initial_backoff(self.fork_retry_backoff)
601            .maybe_headers(self.fork_headers.clone().or_else(|| self.rpc_headers.clone()))
602            .compute_units_per_second(self.get_compute_units_per_second())
603            .accept_invalid_certs(self.rpc_accept_invalid_certs)
604            .no_proxy(self.rpc_no_proxy);
605        if let Some(jwt) = &self.rpc_jwt {
606            builder = builder.jwt(jwt);
607        }
608        if let Some(timeout) = self.rpc_timeout {
609            builder = builder.timeout(std::time::Duration::from_secs(timeout));
610        }
611        builder.build()
612    }
613
614    /// Discovers and caches the network configuration and endpoint identity for a fork.
615    ///
616    /// This is the dispatch phase of fork setup: explicit network selections are preserved, while
617    /// inferred chain and network settings are applied when no override exists. The method does not
618    /// resolve or pin a fork block; block resolution happens later through [`Self::resolve_fork`]
619    /// or [`Self::env_resolved`].
620    pub async fn infer_network_from_fork(&mut self) -> eyre::Result<()> {
621        let previous_identity = self.fork_endpoint.clone();
622        let Some(fork_url) = self.fork_url.clone() else {
623            self.fork_endpoint = None;
624            self.expected_fork_endpoint = None;
625            if self.fork_network_is_inferred {
626                self.networks = self.networks.with_rpc_profile(NetworkConfigs::default());
627                self.fork_network_is_inferred = false;
628            }
629            if self.fork_chain_id_is_inferred {
630                self.env.chain_id = None;
631                self.fork_chain_id_is_inferred = false;
632            }
633            if self.fork_block_number_is_inferred {
634                self.fork_block_number = None;
635                self.fork_block_number_is_inferred = false;
636            }
637            return Ok(());
638        };
639        let explicit_network =
640            self.networks.has_network_selection() && !self.fork_network_is_inferred;
641        let identity = self.discover_fork_endpoint().await?;
642        self.ensure_expected_fork_endpoint(&identity)?;
643        if self.fork_network_is_inferred
644            && previous_identity.as_ref().is_some_and(|previous| {
645                previous.endpoint == fork_url
646                    && previous.network_profile != identity.network_profile
647            })
648        {
649            eyre::bail!(
650                "fork endpoint {} changed execution profile from `{}` to `{}`; rebuild \
651                 the EVM for the new network",
652                fork_endpoint_description(&fork_url),
653                self.networks.execution_profile_name(),
654                identity.network_profile.execution_profile_name()
655            );
656        }
657
658        if self.env.chain_id.is_none() || self.fork_chain_id_is_inferred {
659            self.env.chain_id = Some(identity.execution_chain_id);
660            self.fork_chain_id_is_inferred = true;
661        }
662        if !explicit_network {
663            self.networks = self.networks.with_rpc_profile(identity.network_profile);
664            self.fork_network_is_inferred = true;
665        }
666        debug_assert_eq!(identity.endpoint, fork_url);
667        self.fork_endpoint = Some(identity);
668        Ok(())
669    }
670
671    /// Discovers a stable identity snapshot without selecting a fork block.
672    ///
673    /// Each attempt reads `eth_chainId` and the optional Anvil identity methods before and after,
674    /// and returns only when both snapshots agree. Before Anvil is positively identified, a failed
675    /// `anvil_nodeInfo` probe is treated as absence of optional Anvil identity information for that
676    /// snapshot. Later probe failures are strict. This method does not mutate or cache the returned
677    /// identity in `self`.
678    pub async fn discover_fork_endpoint(&self) -> eyre::Result<ForkEndpointIdentity> {
679        let fork_url = self.fork_url.as_deref().ok_or_eyre("fork URL is not configured")?;
680        let provider = self.fork_provider_with_url::<AnyNetwork>(fork_url)?;
681        let unknown_fallback = self.endpoint_network_fallback();
682        let mut node_info_probe = self.anvil_node_info_probe();
683        for _ in 0..3 {
684            let before_chain_id = provider
685                .get_chain_id()
686                .await
687                .wrap_err("failed to retrieve chain ID from fork endpoint")?;
688            let before_node_info = node_info_probe.request(&provider).await?;
689            let before = Self::resolve_fork_endpoint_identity(
690                &provider,
691                fork_url,
692                before_chain_id,
693                before_node_info,
694                Some(unknown_fallback),
695                EndpointHardforkPolicy::Optional,
696            )
697            .await?;
698            let after_chain_id = provider
699                .get_chain_id()
700                .await
701                .wrap_err("failed to confirm chain ID from fork endpoint")?;
702            let after_node_info = node_info_probe.request(&provider).await?;
703            let after = Self::resolve_fork_endpoint_identity(
704                &provider,
705                fork_url,
706                after_chain_id,
707                after_node_info,
708                Some(unknown_fallback),
709                EndpointHardforkPolicy::Optional,
710            )
711            .await?;
712            if before_chain_id == after_chain_id && before == after {
713                return Ok(before);
714            }
715        }
716        eyre::bail!(
717            "fork endpoint {} changed while its identity was being resolved",
718            fork_endpoint_description(fork_url)
719        );
720    }
721
722    /// Reads one endpoint identity snapshot without performing a stability check.
723    ///
724    /// The shared node-info probe starts permissive for endpoints that do not expose Anvil methods
725    /// and becomes strict after Anvil is identified. Callers either bracket mutable remote reads
726    /// with two snapshots or use this helper to revalidate an already resolved fork.
727    async fn resolve_fork_endpoint_once<N: Network, P: Provider<N>>(
728        &self,
729        provider: &P,
730        node_info_probe: &mut AnvilNodeInfoProbe,
731        hardfork_policy: EndpointHardforkPolicy,
732    ) -> eyre::Result<ForkEndpointIdentity> {
733        let fork_url = self.fork_url.as_deref().ok_or_eyre("fork URL is not configured")?;
734        let cached_anvil = self.fork_endpoint.as_ref().is_some_and(|identity| {
735            identity.endpoint == fork_url && identity.reported_hardfork.is_some()
736        });
737        let node_info = node_info_probe.request(provider).await?;
738        let execution_chain_id = if cached_anvil && let Some(node_info) = &node_info {
739            node_info.environment.chain_id
740        } else {
741            provider
742                .get_chain_id()
743                .await
744                .wrap_err("failed to retrieve chain ID from fork endpoint")?
745        };
746        Self::resolve_fork_endpoint_identity(
747            provider,
748            fork_url,
749            execution_chain_id,
750            node_info,
751            Some(self.endpoint_network_fallback()),
752            hardfork_policy,
753        )
754        .await
755    }
756
757    /// Resolves the chain ID and network family exposed by the configured fork endpoint.
758    ///
759    /// Unlike [`Self::infer_network_from_fork`], this always inspects the endpoint. Endpoints whose
760    /// custom chain ID does not identify a supported network family retain Foundry's historical
761    /// Ethereum fallback when `anvil_nodeInfo` is unavailable.
762    pub async fn fork_network(&self) -> eyre::Result<(ChainId, NetworkVariant)> {
763        let identity = self.discover_fork_endpoint().await?;
764
765        Ok((identity.execution_chain_id, identity.network))
766    }
767
768    /// Builds an identity from one observed chain ID and optional Anvil node-info response.
769    ///
770    /// Anvil metadata is consulted only after node info has positively identified the endpoint.
771    async fn resolve_fork_endpoint_identity<N: Network, P: Provider<N>>(
772        provider: &P,
773        endpoint: &str,
774        execution_chain_id: ChainId,
775        node_info: Option<NodeInfo>,
776        unknown_fallback: Option<NetworkConfigs>,
777        hardfork_policy: EndpointHardforkPolicy,
778    ) -> eyre::Result<ForkEndpointIdentity> {
779        match node_info {
780            Some(node_info) => {
781                let (
782                    source_chain_id,
783                    instance_id,
784                    source_fork_block_number,
785                    source_fork_block_hash,
786                ) = match provider.raw_request::<_, Metadata>("anvil_metadata".into(), ()).await {
787                    Ok(metadata) => {
788                        let forked_network = metadata.forked_network;
789                        (
790                            forked_network.map(|fork| fork.chain_id).unwrap_or(execution_chain_id),
791                            Some(metadata.instance_id),
792                            forked_network.map(|fork| fork.fork_block_number),
793                            forked_network.map(|fork| fork.fork_block_hash),
794                        )
795                    }
796                    Err(error) if is_rpc_method_not_found(&error) => {
797                        (execution_chain_id, None, None, None)
798                    }
799                    Err(error) => {
800                        return Err(error)
801                            .wrap_err("failed to retrieve Anvil fork source identity");
802                    }
803                };
804                let identity_chain_id =
805                    if node_info.network.is_some() { execution_chain_id } else { source_chain_id };
806                let network_profile = NetworkConfigs::from_rpc_identity_profile_with_fallback(
807                    identity_chain_id,
808                    Some(node_info.network.as_deref()),
809                    unknown_fallback,
810                )
811                .map_err(eyre::Report::msg)?
812                .ok_or_else(|| {
813                    eyre::eyre!("Anvil metadata did not identify an execution profile")
814                })?;
815                let network = network_profile.execution_network();
816                let hardfork = endpoint_hardfork(network, &node_info.hard_fork, hardfork_policy)?;
817
818                Ok(ForkEndpointIdentity {
819                    endpoint: endpoint.to_string(),
820                    execution_chain_id,
821                    source_chain_id,
822                    network,
823                    network_profile,
824                    reported_hardfork: Some(node_info.hard_fork),
825                    hardfork,
826                    instance_id,
827                    source_fork_block_number,
828                    source_fork_block_hash,
829                })
830            }
831            None => {
832                let network_profile = NetworkConfigs::from_rpc_identity_profile_with_fallback(
833                    execution_chain_id,
834                    None,
835                    unknown_fallback,
836                )
837                .map_err(eyre::Report::msg)?;
838                if let Some(network_profile) = network_profile {
839                    let network = network_profile.execution_network();
840                    return Ok(ForkEndpointIdentity {
841                        endpoint: endpoint.to_string(),
842                        execution_chain_id,
843                        source_chain_id: execution_chain_id,
844                        network,
845                        network_profile,
846                        reported_hardfork: None,
847                        hardfork: None,
848                        instance_id: None,
849                        source_fork_block_number: None,
850                        source_fork_block_hash: None,
851                    });
852                }
853                Err(eyre::eyre!(
854                    "cannot determine network family for unknown chain ID \
855                     {execution_chain_id}: the fork endpoint does not expose `anvil_nodeInfo`"
856                ))
857            }
858        }
859    }
860
861    /// Returns a tuple with [`EvmEnv`], `TxEnv`, and the actual fork block number.
862    ///
863    /// If a `fork_url` is set, creates a provider and passes it to both `EvmOpts::fork_evm_env`
864    /// and `EvmOpts::fork_tx_env`. Falls back to local settings when no fork URL is configured.
865    ///
866    /// The fork block number is returned separately because on some L2s (e.g., Arbitrum) the
867    /// `block_env.number` may be remapped (to the L1 block number) and therefore cannot be used
868    /// to pin the fork.
869    pub async fn env<
870        SPEC: Into<SpecId> + Default + Copy,
871        BLOCK: FoundryBlock + Default,
872        TX: FoundryTransaction + Default,
873    >(
874        &self,
875    ) -> eyre::Result<(EvmEnv<SPEC, BLOCK>, TX, Option<BlockNumber>)> {
876        let (evm_env, tx, fork) = self.env_resolved().await?;
877        Ok((evm_env, tx, fork.as_ref().map(ResolvedFork::number)))
878    }
879
880    /// Returns the EVM and transaction environments with an exact resolved fork snapshot.
881    ///
882    /// Resolution brackets block and gas-price reads with endpoint identity checks and retries if
883    /// the endpoint changes. Downstream preflight reads and backend construction should reuse the
884    /// returned [`ResolvedFork`] instead of carrying only its block number.
885    pub async fn env_resolved<
886        SPEC: Into<SpecId> + Default + Copy,
887        BLOCK: FoundryBlock + Default,
888        TX: FoundryTransaction + Default,
889    >(
890        &self,
891    ) -> eyre::Result<(EvmEnv<SPEC, BLOCK>, TX, Option<ResolvedFork>)> {
892        let Some(fork_url) = &self.fork_url else {
893            return Ok((self.local_evm_env(), self.local_tx_env(), None));
894        };
895        let provider = self.fork_provider_with_url::<AnyNetwork>(fork_url)?;
896        let mut node_info_probe = self.anvil_node_info_probe();
897        for _ in 0..3 {
898            let (evm_env, block, context) =
899                self.fork_evm_env_resolved_with_context(&provider, &mut node_info_probe).await?;
900            let gas_price =
901                option_try_or_else(self.env.gas_price.map(|value| value as u128), async || {
902                    provider.get_gas_price().await
903                })
904                .await?;
905            let identity = self
906                .resolve_fork_endpoint_once(
907                    &provider,
908                    &mut node_info_probe,
909                    EndpointHardforkPolicy::Required,
910                )
911                .await?;
912            self.ensure_expected_fork_endpoint(&identity)?;
913            if context.matches_identity(&identity) {
914                let chain_id = self.chain_id_override().unwrap_or(context.execution_chain_id);
915                let tx = self.fork_tx_env(gas_price, chain_id);
916                return Ok((evm_env, tx, Some(self.resolved_fork(fork_url, block, context))));
917            }
918        }
919        eyre::bail!(
920            "fork endpoint {} changed while its EVM and transaction environments were being \
921             resolved",
922            fork_endpoint_description(fork_url)
923        );
924    }
925
926    /// Returns the execution environment and the source identity of its remote fork, if any.
927    ///
928    /// The source chain ID is always fetched from the fork endpoint, even when the execution
929    /// environment applies a configured `CHAINID` opcode override.
930    pub async fn env_with_fork_context<
931        SPEC: Into<SpecId> + Default + Copy,
932        BLOCK: FoundryBlock + Default,
933        TX: FoundryTransaction + Default,
934    >(
935        &self,
936    ) -> eyre::Result<(EvmEnv<SPEC, BLOCK>, TX, Option<ForkContext>)> {
937        let (evm_env, tx, fork) = self.env_resolved().await?;
938        Ok((evm_env, tx, fork.as_ref().map(ResolvedFork::context)))
939    }
940
941    /// Returns the EVM and transaction environments at an already resolved fork.
942    pub async fn env_with_resolved_fork<
943        SPEC: Into<SpecId> + Default + Copy,
944        BLOCK: FoundryBlock + Default,
945        TX: FoundryTransaction + Default,
946    >(
947        &self,
948        fork: Option<&ResolvedFork>,
949    ) -> eyre::Result<(EvmEnv<SPEC, BLOCK>, TX)> {
950        let Some(_) = &self.fork_url else {
951            eyre::ensure!(fork.is_none(), "resolved fork provided without a configured fork");
952            return Ok((self.local_evm_env(), self.local_tx_env()));
953        };
954        let fork = fork.ok_or_else(|| eyre::eyre!("fork must be resolved"))?;
955        let provider = self.provider_for_resolved_fork::<AnyNetwork>(fork)?;
956        let mut node_info_probe = self.anvil_node_info_probe();
957        node_info_probe.identified |= fork.context().hardfork.is_some();
958        for _ in 0..3 {
959            let (evm_env, endpoint) = self
960                .fork_evm_env_at_resolved_with_context(&provider, fork, &mut node_info_probe)
961                .await?;
962            let gas_price =
963                option_try_or_else(self.env.gas_price.map(|value| value as u128), async || {
964                    provider.get_gas_price().await
965                })
966                .await?;
967            let identity = self
968                .resolve_fork_endpoint_once(
969                    &provider,
970                    &mut node_info_probe,
971                    EndpointHardforkPolicy::Required,
972                )
973                .await?;
974            self.ensure_expected_fork_endpoint(&identity)?;
975            if endpoint == fork.context() && endpoint.matches_identity(&identity) {
976                let chain_id = self.chain_id_override().unwrap_or(endpoint.execution_chain_id);
977                return Ok((evm_env, self.fork_tx_env(gas_price, chain_id)));
978            }
979        }
980        eyre::bail!(
981            "fork endpoint {} changed while its resolved environment was being reconstructed",
982            fork_endpoint_description(self.fork_url.as_deref().unwrap_or_default())
983        );
984    }
985
986    /// Returns the [`EvmEnv`] (cfg + block) and [`BlockNumber`] fetched from the fork endpoint via
987    /// provider
988    pub async fn fork_evm_env<
989        SPEC: Into<SpecId> + Default + Copy,
990        BLOCK: FoundryBlock + Default,
991        N: Network,
992        P: Provider<N>,
993    >(
994        &self,
995        provider: &P,
996    ) -> eyre::Result<(EvmEnv<SPEC, BLOCK>, BlockNumber)> {
997        let mut node_info_probe = self.anvil_node_info_probe();
998        let (evm_env, block, _) =
999            self.fork_evm_env_resolved_with_context(provider, &mut node_info_probe).await?;
1000        Ok((evm_env, block.number))
1001    }
1002
1003    /// Returns the EVM environment and block identity fetched from the fork endpoint.
1004    pub(crate) async fn fork_evm_env_resolved<
1005        SPEC: Into<SpecId> + Default + Copy,
1006        BLOCK: FoundryBlock + Default,
1007        N: Network,
1008        P: Provider<N>,
1009    >(
1010        &self,
1011        provider: &P,
1012    ) -> eyre::Result<(EvmEnv<SPEC, BLOCK>, ResolvedFork)> {
1013        let mut node_info_probe = self.anvil_node_info_probe();
1014        let (evm_env, block, context) =
1015            self.fork_evm_env_resolved_with_context(provider, &mut node_info_probe).await?;
1016        let fork_url = self.fork_url.as_deref().unwrap_or_default();
1017        Ok((evm_env, self.resolved_fork(fork_url, block, context)))
1018    }
1019
1020    /// Returns the fork environment, exact block, and endpoint identity resolved together.
1021    async fn fork_evm_env_resolved_with_context<
1022        SPEC: Into<SpecId> + Default + Copy,
1023        BLOCK: FoundryBlock + Default,
1024        N: Network,
1025        P: Provider<N>,
1026    >(
1027        &self,
1028        provider: &P,
1029        node_info_probe: &mut AnvilNodeInfoProbe,
1030    ) -> eyre::Result<(EvmEnv<SPEC, BLOCK>, BlockNumHash, ForkContext)> {
1031        trace!(
1032            memory_limit = %self.memory_limit,
1033            override_chain_id = ?self.env.chain_id,
1034            pin_block = ?self.fork_block_number,
1035            origin = %self.sender,
1036            disable_block_gas_limit = %self.disable_block_gas_limit,
1037            enable_tx_gas_limit = %self.enable_tx_gas_limit,
1038            configs = ?self.networks,
1039            "creating fork environment"
1040        );
1041
1042        let target = ForkBlockTarget::Configured(match self.fork_block_number {
1043            Some(block_number) => BlockNumberOrTag::Number(block_number),
1044            None => BlockNumberOrTag::Latest,
1045        });
1046        let (block, fork_block, context) =
1047            self.resolve_fork_block_with_context(provider, target, node_info_probe).await?;
1048        let chain_id = self.chain_id_override().unwrap_or(context.execution_chain_id);
1049        let evm_env =
1050            self.fork_env_from_block::<SPEC, BLOCK, N>(chain_id, context.source_chain_id, &block);
1051        Ok((evm_env, fork_block, context))
1052    }
1053
1054    fn fork_env_error_context(&self) -> String {
1055        let mut message = "could not instantiate forked environment".to_string();
1056        if let Some(fork_url) = self.fork_url.as_deref()
1057            && let Ok(url) = Url::parse(fork_url)
1058            && let Some(host) = url.host()
1059        {
1060            write!(message, " with provider {host}").unwrap();
1061        }
1062        message
1063    }
1064
1065    async fn resolve_fork_block_with_context<N: Network, P: Provider<N>>(
1066        &self,
1067        provider: &P,
1068        target: ForkBlockTarget,
1069        node_info_probe: &mut AnvilNodeInfoProbe,
1070    ) -> eyre::Result<(N::BlockResponse, BlockNumHash, ForkContext)> {
1071        let endpoint = self.fork_url.as_deref().unwrap_or_default();
1072        let mut stable = None;
1073        for _ in 0..3 {
1074            let before = self
1075                .resolve_fork_endpoint_once(
1076                    provider,
1077                    node_info_probe,
1078                    EndpointHardforkPolicy::Required,
1079                )
1080                .await
1081                .wrap_err_with(|| self.fork_env_error_context())?;
1082            let block = match target {
1083                ForkBlockTarget::Configured(block_number) => provider
1084                    .get_block_by_number(block_number)
1085                    .await
1086                    .wrap_err_with(|| self.fork_env_error_context())?,
1087                ForkBlockTarget::Resolved(block) => provider
1088                    .get_block_by_hash(block.hash)
1089                    .await
1090                    .wrap_err("could not instantiate resolved fork environment")?,
1091            };
1092            let after = self
1093                .resolve_fork_endpoint_once(
1094                    provider,
1095                    node_info_probe,
1096                    EndpointHardforkPolicy::Required,
1097                )
1098                .await
1099                .wrap_err_with(|| self.fork_env_error_context())?;
1100            if before == after {
1101                stable = Some((before, block));
1102                break;
1103            }
1104        }
1105        let (identity, block) = stable.ok_or_else(|| {
1106            eyre::eyre!(
1107                "fork endpoint {} changed while its block and execution context were being \
1108                 resolved",
1109                fork_endpoint_description(endpoint)
1110            )
1111        })?;
1112        self.ensure_expected_fork_endpoint(&identity)?;
1113        if self.fork_network_is_inferred
1114            && !self.networks.has_same_execution_profile(&identity.network_profile)
1115        {
1116            eyre::bail!(
1117                "fork endpoint {} changed execution profile from `{}` to `{}`; rebuild the EVM \
1118                 for the new network",
1119                fork_endpoint_description(endpoint),
1120                self.networks.execution_profile_name(),
1121                identity.network_profile.execution_profile_name()
1122            );
1123        }
1124
1125        let block = match block {
1126            Some(block) => block,
1127            None => match target {
1128                ForkBlockTarget::Configured(block_number) => {
1129                    let block_number_message = match block_number {
1130                        BlockNumberOrTag::Number(block_number) => {
1131                            format!("block number: {block_number}")
1132                        }
1133                        block_number => format!("{block_number} block"),
1134                    };
1135                    let latest_message = if let Ok(latest_block) = provider.get_block_number().await
1136                    {
1137                        if let Some(block_number) = self.fork_block_number
1138                            && block_number <= latest_block
1139                        {
1140                            error!("{NON_ARCHIVE_NODE_WARNING}");
1141                        }
1142                        format!("; latest block number: {latest_block}")
1143                    } else {
1144                        Default::default()
1145                    };
1146                    eyre::bail!("failed to get {block_number_message}{latest_message}");
1147                }
1148                ForkBlockTarget::Resolved(block) => {
1149                    eyre::bail!("failed to get block hash: {}", block.hash);
1150                }
1151            },
1152        };
1153        let actual = BlockNumHash::new(block.header().number(), block.header().hash());
1154        if let ForkBlockTarget::Resolved(expected) = target {
1155            eyre::ensure!(
1156                actual == expected,
1157                "resolved fork block changed: expected {expected:?}, got {actual:?}"
1158            );
1159        }
1160
1161        let context = ForkContext {
1162            execution_chain_id: identity.execution_chain_id,
1163            source_chain_id: identity.source_chain_id,
1164            network: identity.network,
1165            network_profile: identity.network_profile,
1166            block_number: actual.number,
1167            hardfork: identity.hardfork,
1168            instance_id: identity.instance_id,
1169            source_fork_block_number: identity.source_fork_block_number,
1170            source_fork_block_hash: identity.source_fork_block_hash,
1171        };
1172        Ok((block, actual, context))
1173    }
1174
1175    async fn fork_evm_env_at_resolved_with_context<
1176        SPEC: Into<SpecId> + Default + Copy,
1177        BLOCK: FoundryBlock + Default,
1178        N: Network,
1179        P: Provider<N>,
1180    >(
1181        &self,
1182        provider: &P,
1183        expected: &ResolvedFork,
1184        node_info_probe: &mut AnvilNodeInfoProbe,
1185    ) -> eyre::Result<(EvmEnv<SPEC, BLOCK>, ForkContext)> {
1186        let (block, _, context) = self
1187            .resolve_fork_block_with_context(
1188                provider,
1189                ForkBlockTarget::Resolved(expected.block()),
1190                node_info_probe,
1191            )
1192            .await?;
1193        eyre::ensure!(
1194            context == expected.context(),
1195            "fork endpoint {} changed after its block and execution context were resolved",
1196            fork_endpoint_description(self.fork_url.as_deref().unwrap_or_default())
1197        );
1198        let chain_id = self.chain_id_override().unwrap_or(context.execution_chain_id);
1199        let evm_env =
1200            self.fork_env_from_block::<SPEC, BLOCK, N>(chain_id, context.source_chain_id, &block);
1201        Ok((evm_env, context))
1202    }
1203
1204    /// Reconstructs the fork environment at an already resolved exact block.
1205    pub(crate) async fn fork_evm_env_at_resolved<
1206        SPEC: Into<SpecId> + Default + Copy,
1207        BLOCK: FoundryBlock + Default,
1208        N: Network,
1209        P: Provider<N>,
1210    >(
1211        &self,
1212        provider: &P,
1213        expected: &ResolvedFork,
1214    ) -> eyre::Result<EvmEnv<SPEC, BLOCK>> {
1215        let mut node_info_probe = self.anvil_node_info_probe();
1216        node_info_probe.identified |= expected.context().hardfork.is_some();
1217        let (evm_env, _) = self
1218            .fork_evm_env_at_resolved_with_context(provider, expected, &mut node_info_probe)
1219            .await?;
1220        Ok(evm_env)
1221    }
1222
1223    fn fork_env_from_block<
1224        SPEC: Into<SpecId> + Default + Copy,
1225        BLOCK: FoundryBlock + Default,
1226        N: Network,
1227    >(
1228        &self,
1229        chain_id: ChainId,
1230        source_chain_id: ChainId,
1231        block: &N::BlockResponse,
1232    ) -> EvmEnv<SPEC, BLOCK> {
1233        let mut evm_env = EvmEnv {
1234            cfg_env: self.cfg_env(chain_id),
1235            block_env: block_env_from_header(block.header()),
1236        };
1237
1238        apply_chain_and_block_specific_env_changes_for_chain::<N, _, _>(
1239            &mut evm_env,
1240            block,
1241            source_chain_id,
1242            self.networks,
1243        );
1244
1245        evm_env
1246    }
1247
1248    /// Returns the [`EvmEnv`] configured with only local settings.
1249    fn local_evm_env<SPEC: Into<SpecId> + Default + Clone, BLOCK: FoundryBlock + Default>(
1250        &self,
1251    ) -> EvmEnv<SPEC, BLOCK> {
1252        let cfg_env = self.cfg_env(self.env.chain_id.unwrap_or(foundry_common::DEV_CHAIN_ID));
1253        let mut block_env = BLOCK::default();
1254        block_env.set_number(self.env.block_number);
1255        block_env.set_beneficiary(self.env.block_coinbase);
1256        block_env.set_timestamp(self.env.block_timestamp);
1257        block_env.set_difficulty(U256::from(self.env.block_difficulty));
1258        block_env.set_prevrandao(Some(self.env.block_prevrandao));
1259        block_env.set_basefee(self.env.block_base_fee_per_gas);
1260        block_env.set_gas_limit(self.gas_limit());
1261        EvmEnv::new(cfg_env, block_env)
1262    }
1263
1264    /// Returns the `TxEnv` with gas price and chain ID from a stable fork snapshot.
1265    fn fork_tx_env<TX: FoundryTransaction + Default>(
1266        &self,
1267        gas_price: u128,
1268        chain_id: ChainId,
1269    ) -> TX {
1270        let mut tx_env = TX::default();
1271        tx_env.set_caller(self.sender);
1272        tx_env.set_chain_id(Some(chain_id));
1273        tx_env.set_gas_price(gas_price);
1274        tx_env.set_gas_limit(self.gas_limit());
1275        tx_env
1276    }
1277
1278    /// Returns the `TxEnv` configured from local settings only.
1279    fn local_tx_env<TX: FoundryTransaction + Default>(&self) -> TX {
1280        let mut tx_env = TX::default();
1281        tx_env.set_caller(self.sender);
1282        tx_env.set_gas_price(self.env.gas_price.unwrap_or_default().into());
1283        tx_env.set_gas_limit(self.gas_limit());
1284        tx_env
1285    }
1286
1287    /// Builds a [`CfgEnv`] from the options, using the provided [`ChainId`].
1288    fn cfg_env<SPEC: Into<SpecId> + Default + Clone>(&self, chain_id: ChainId) -> CfgEnv<SPEC> {
1289        let mut cfg = CfgEnv::default();
1290        cfg.chain_id = chain_id;
1291        cfg.memory_limit = self.memory_limit;
1292        cfg.limit_contract_code_size = self.env.code_size_limit.or(Some(usize::MAX));
1293        // EIP-3607 rejects transactions from senders with deployed code.
1294        // If EIP-3607 is enabled it can cause issues during fuzz/invariant tests if the caller
1295        // is a contract. So we disable the check by default.
1296        cfg.disable_eip3607 = true;
1297        cfg.disable_block_gas_limit = self.disable_block_gas_limit;
1298        cfg.disable_nonce_check = true;
1299        // By default do not enforce transaction gas limits imposed by Osaka (EIP-7825).
1300        // Users can opt-in to enable these limits by setting `enable_tx_gas_limit` to true.
1301        if !self.enable_tx_gas_limit {
1302            cfg.tx_gas_limit_cap = Some(u64::MAX);
1303        }
1304        cfg
1305    }
1306
1307    /// Helper function that returns the [CreateFork] to use, if any.
1308    ///
1309    /// storage caching for the [CreateFork] will be enabled if
1310    ///   - `fork_url` is present
1311    ///   - `fork_block_number` is present
1312    ///   - `StorageCachingConfig` allows the `fork_url` + source chain ID pair
1313    ///   - storage is allowed (`no_storage_caching = false`)
1314    ///
1315    /// If all these criteria are met, then storage caching is enabled and storage info will be
1316    /// written to `<Config::foundry_cache_dir()>/<str(chainid)>/<block>/storage.json`.
1317    ///
1318    /// for `mainnet` and `--fork-block-number 14435000` on mac the corresponding storage cache will
1319    /// be at `~/.foundry/cache/mainnet/14435000/storage.json`.
1320    /// `fork_block_number` is the actual block number to pin the fork to. This must be the
1321    /// real chain block number, not a remapped value. On some L2s (e.g., Arbitrum)
1322    /// `block_env.number` is remapped to the L1 block number, so callers must pass the
1323    /// original block number returned by [`EvmOpts::env`] instead.
1324    pub fn get_fork(
1325        &self,
1326        config: &Config,
1327        chain_id: u64,
1328        fork_block_number: Option<BlockNumber>,
1329    ) -> Option<CreateFork> {
1330        self.get_fork_with_identity(config, chain_id, fork_block_number, None)
1331    }
1332
1333    /// Returns a fork configuration pinned to an already resolved block identity.
1334    pub fn get_fork_resolved(
1335        &self,
1336        config: &Config,
1337        chain_id: u64,
1338        fork: Option<&ResolvedFork>,
1339    ) -> Option<CreateFork> {
1340        let fork_block_number = fork.map(ResolvedFork::number);
1341        let source_chain_id = fork.map(|fork| fork.context().source_chain_id).unwrap_or(chain_id);
1342        self.get_fork_with_identity(config, source_chain_id, fork_block_number, fork.cloned())
1343    }
1344
1345    fn get_fork_with_identity(
1346        &self,
1347        config: &Config,
1348        chain_id: u64,
1349        fork_block_number: Option<BlockNumber>,
1350        resolved: Option<ResolvedFork>,
1351    ) -> Option<CreateFork> {
1352        let url = self.fork_url.clone()?;
1353        let enable_caching = config.enable_caching(&url, chain_id);
1354
1355        // Pin fork_block_number to the block that was already fetched in env, so subsequent
1356        // fork operations use the same block. This prevents inconsistencies when forking at
1357        // "latest" where the chain could advance between calls.
1358        let mut evm_opts = self.clone();
1359        if evm_opts.fork_block_number.is_none() {
1360            evm_opts.fork_block_number = fork_block_number;
1361            evm_opts.fork_block_number_is_inferred = fork_block_number.is_some();
1362        }
1363
1364        Some(CreateFork { url, enable_caching, evm_opts, resolved })
1365    }
1366
1367    /// Returns the gas limit to use
1368    pub fn gas_limit(&self) -> u64 {
1369        self.env.block_gas_limit.unwrap_or(self.env.gas_limit).0
1370    }
1371
1372    /// Returns the available compute units per second, which will be
1373    /// - u64::MAX, if `no_rpc_rate_limit` if set (as rate limiting is disabled)
1374    /// - the assigned compute units, if `compute_units_per_second` is set
1375    /// - ALCHEMY_FREE_TIER_CUPS (330) otherwise
1376    const fn get_compute_units_per_second(&self) -> u64 {
1377        if self.no_rpc_rate_limit {
1378            u64::MAX
1379        } else if let Some(cups) = self.compute_units_per_second {
1380            cups
1381        } else {
1382            ALCHEMY_FREE_TIER_CUPS
1383        }
1384    }
1385
1386    /// Returns the chain ID from the RPC, if any.
1387    pub async fn get_remote_chain_id(&self) -> Option<Chain> {
1388        if let Some(url) = &self.fork_url
1389            && let Ok(provider) = self.fork_provider_with_url::<AnyNetwork>(url)
1390        {
1391            trace!(?url, "retrieving chain via eth_chainId");
1392
1393            if let Ok(id) = provider.get_chain_id().await {
1394                return Some(Chain::from(id));
1395            }
1396
1397            // Provider URLs could be of the format `{CHAIN_IDENTIFIER}-mainnet`
1398            // (e.g. Alchemy `opt-mainnet`, `arb-mainnet`), fallback to this method only
1399            // if we're not able to retrieve chain id from `RetryProvider`.
1400            if url.contains("mainnet") {
1401                trace!(?url, "auto detected mainnet chain");
1402                return Some(Chain::mainnet());
1403            }
1404        }
1405
1406        None
1407    }
1408}
1409
1410/// Describes how an execution spec should use a source chain's hardfork schedule.
1411#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1412pub enum ExecutionSpecContext {
1413    /// Local execution without a fork.
1414    Local,
1415    /// Local execution backed by a fork.
1416    Fork {
1417        /// Underlying source chain ID.
1418        source_chain_id: ChainId,
1419        /// Exact hardfork reported by the endpoint.
1420        endpoint_hardfork: Option<FoundryHardfork>,
1421    },
1422    /// Historical execution whose semantics must match the source block.
1423    Historical {
1424        /// Underlying source chain ID.
1425        source_chain_id: ChainId,
1426        /// Exact hardfork reported by the endpoint.
1427        endpoint_hardfork: Option<FoundryHardfork>,
1428    },
1429}
1430
1431impl ExecutionSpecContext {
1432    /// Returns a local fork execution context.
1433    pub const fn fork(
1434        source_chain_id: ChainId,
1435        endpoint_hardfork: Option<FoundryHardfork>,
1436    ) -> Self {
1437        Self::Fork { source_chain_id, endpoint_hardfork }
1438    }
1439
1440    /// Returns the context for an optional local fork source.
1441    pub const fn local_or_fork(
1442        source_chain_id: Option<ChainId>,
1443        endpoint_hardfork: Option<FoundryHardfork>,
1444    ) -> Self {
1445        if let Some(source_chain_id) = source_chain_id {
1446            Self::Fork { source_chain_id, endpoint_hardfork }
1447        } else {
1448            Self::Local
1449        }
1450    }
1451
1452    /// Returns a historical execution context.
1453    pub const fn historical(
1454        source_chain_id: ChainId,
1455        endpoint_hardfork: Option<FoundryHardfork>,
1456    ) -> Self {
1457        Self::Historical { source_chain_id, endpoint_hardfork }
1458    }
1459
1460    fn hardfork<SPEC: ExecutionSpec>(self, timestamp: u64) -> Option<FoundryHardfork> {
1461        match self {
1462            Self::Local => None,
1463            Self::Fork { source_chain_id, endpoint_hardfork } => {
1464                SPEC::fork_hardfork(source_chain_id, timestamp, endpoint_hardfork)
1465            }
1466            Self::Historical { source_chain_id, endpoint_hardfork } => endpoint_hardfork
1467                .filter(|&hardfork| SPEC::from_foundry_hardfork(hardfork).is_some())
1468                .or_else(|| SPEC::historical_hardfork(source_chain_id, timestamp)),
1469        }
1470    }
1471}
1472
1473/// Resolves and applies the execution spec for an EVM environment.
1474///
1475/// A direct caller override takes precedence over a configured namespaced hardfork, followed by
1476/// exact endpoint metadata and the hardfork selected from the source schedule. Local Ethereum
1477/// forks retain Foundry's configured EVM version, namespaced local networks follow their source
1478/// identity, and historical execution always follows the source block's identity.
1479///
1480/// Returns the exact namespaced hardfork, when applicable, so execution and trace decoding can use
1481/// the same hardfork.
1482pub fn resolve_execution_spec<SPEC, BLOCK>(
1483    evm_version: EvmVersion,
1484    configured_hardfork: Option<FoundryHardfork>,
1485    evm_env: &mut EvmEnv<SPEC, BLOCK>,
1486    context: ExecutionSpecContext,
1487    explicit_spec: Option<SPEC>,
1488) -> Option<FoundryHardfork>
1489where
1490    SPEC: ExecutionSpec + Into<SpecId> + Copy,
1491    BLOCK: FoundryBlock,
1492{
1493    let (spec, hardfork) = if let Some(spec) = explicit_spec {
1494        (spec, spec.reported_hardfork())
1495    } else if let Some((hardfork, spec)) = configured_hardfork
1496        .filter(|&hardfork| SPEC::from_foundry_hardfork(hardfork).is_some())
1497        .or_else(|| context.hardfork::<SPEC>(evm_env.block_env.timestamp().saturating_to()))
1498        .and_then(|hardfork| SPEC::from_foundry_hardfork(hardfork).map(|spec| (hardfork, spec)))
1499    {
1500        (spec, Some(hardfork))
1501    } else {
1502        let spec = SPEC::from_evm_version(evm_version);
1503        (spec, spec.reported_hardfork())
1504    };
1505    evm_env.cfg_env.set_spec_and_mainnet_gas_params(spec);
1506    hardfork
1507}
1508
1509#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1510pub struct Env {
1511    /// The block gas limit.
1512    pub gas_limit: GasLimit,
1513
1514    /// The `CHAINID` opcode value.
1515    pub chain_id: Option<u64>,
1516
1517    /// the tx.gasprice value during EVM execution
1518    ///
1519    /// This is an Option, so we can determine in fork mode whether to use the config's gas price
1520    /// (if set by user) or the remote client's gas price.
1521    #[serde(default, skip_serializing_if = "Option::is_none")]
1522    pub gas_price: Option<u64>,
1523
1524    /// the base fee in a block
1525    pub block_base_fee_per_gas: u64,
1526
1527    /// the tx.origin value during EVM execution
1528    pub tx_origin: Address,
1529
1530    /// the block.coinbase value during EVM execution
1531    pub block_coinbase: Address,
1532
1533    /// the block.timestamp value during EVM execution
1534    #[serde(
1535        deserialize_with = "foundry_config::deserialize_u64_to_u256",
1536        serialize_with = "foundry_config::serialize_u64_or_u256"
1537    )]
1538    pub block_timestamp: U256,
1539
1540    /// the block.number value during EVM execution"
1541    #[serde(
1542        deserialize_with = "foundry_config::deserialize_u64_to_u256",
1543        serialize_with = "foundry_config::serialize_u64_or_u256"
1544    )]
1545    pub block_number: U256,
1546
1547    /// the block.difficulty value during EVM execution
1548    pub block_difficulty: u64,
1549
1550    /// Previous block beacon chain random value. Before merge this field is used for mix_hash
1551    pub block_prevrandao: B256,
1552
1553    /// the block.gaslimit value during EVM execution
1554    #[serde(default, skip_serializing_if = "Option::is_none")]
1555    pub block_gas_limit: Option<GasLimit>,
1556
1557    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests.
1558    #[serde(default, skip_serializing_if = "Option::is_none")]
1559    pub code_size_limit: Option<usize>,
1560}
1561
1562fn fork_endpoint_description(endpoint: &str) -> String {
1563    Url::parse(endpoint)
1564        .ok()
1565        .and_then(|url| url.host_str().map(|host| format!("provider {host}")))
1566        .unwrap_or_else(|| "configured provider".to_string())
1567}
1568
1569async fn option_try_or_else<T, E>(
1570    option: Option<T>,
1571    f: impl AsyncFnOnce() -> Result<T, E>,
1572) -> Result<T, E> {
1573    if let Some(value) = option { Ok(value) } else { f().await }
1574}
1575
1576#[cfg(test)]
1577mod tests {
1578    use super::*;
1579    use alloy_chains::NamedChain;
1580    use alloy_network::TransactionBuilder;
1581    use alloy_primitives::bytes;
1582    use alloy_rpc_types::TransactionRequest;
1583    use alloy_serde::WithOtherFields;
1584    use foundry_test_utils::rpc::{
1585        spawn_rpc_proxy_internal_error_after, spawn_rpc_proxy_method_not_found_before,
1586        spawn_rpc_proxy_rejecting_method_after,
1587    };
1588    use revm::context::{BlockEnv, TxEnv};
1589
1590    #[cfg(feature = "base")]
1591    use foundry_evm_hardforks::BaseUpgrade;
1592
1593    #[cfg(feature = "optimism")]
1594    use op_revm::OpSpecId;
1595
1596    fn resolved_context(block_number: BlockNumber) -> ForkContext {
1597        ForkContext {
1598            execution_chain_id: 1,
1599            source_chain_id: 1,
1600            network: NetworkVariant::Ethereum,
1601            network_profile: NetworkConfigs::default(),
1602            block_number,
1603            hardfork: None,
1604            instance_id: None,
1605            source_fork_block_number: None,
1606            source_fork_block_hash: None,
1607        }
1608    }
1609
1610    #[test]
1611    fn fork_endpoint_description_redacts_credentials_and_paths() {
1612        let endpoint = "https://user:secret@example.com/v2/private-api-key?token=another-secret";
1613        let description = fork_endpoint_description(endpoint);
1614
1615        assert_eq!(description, "provider example.com");
1616        assert!(!description.contains("secret"));
1617        assert!(!description.contains("private-api-key"));
1618    }
1619
1620    #[test]
1621    fn endpoint_network_fallback_preserves_explicit_selection() {
1622        let default = EvmOpts::default().endpoint_network_fallback();
1623        assert!(!default.has_network_selection());
1624
1625        let explicit = EvmOpts { networks: NetworkConfigs::with_ethereum(), ..Default::default() }
1626            .endpoint_network_fallback();
1627        assert!(explicit.has_network_selection());
1628        assert_eq!(explicit.execution_network(), NetworkVariant::Ethereum);
1629    }
1630
1631    #[test]
1632    fn backend_target_matches_number_pinned_fork_identity() {
1633        let context = ForkContext {
1634            execution_chain_id: 1,
1635            source_chain_id: 1,
1636            network: NetworkVariant::Ethereum,
1637            network_profile: NetworkConfigs::default(),
1638            block_number: 10,
1639            hardfork: None,
1640            instance_id: None,
1641            source_fork_block_number: None,
1642            source_fork_block_hash: None,
1643        };
1644
1645        let mut different_number = context;
1646        different_number.block_number += 1;
1647        assert!(!context.has_same_backend_target(different_number));
1648
1649        let mut different_profile = context;
1650        different_profile.network_profile = NetworkConfigs::with_celo();
1651        assert!(!context.has_same_backend_target(different_profile));
1652
1653        let mut different_hardfork = context;
1654        different_hardfork.hardfork =
1655            Some(FoundryHardfork::Ethereum(foundry_evm_hardforks::EthereumHardfork::Prague));
1656        assert!(!context.has_same_backend_target(different_hardfork));
1657
1658        let mut different_instance = context;
1659        different_instance.instance_id = Some(B256::with_last_byte(3));
1660        assert!(!context.has_same_backend_target(different_instance));
1661    }
1662
1663    #[tokio::test]
1664    async fn expected_fork_endpoint_requires_exact_identity() {
1665        let identity = ForkEndpointIdentity {
1666            endpoint: "http://localhost:8545".to_string(),
1667            execution_chain_id: 1,
1668            source_chain_id: 1,
1669            network: NetworkVariant::Ethereum,
1670            network_profile: NetworkConfigs::default(),
1671            reported_hardfork: Some("Prague".to_string()),
1672            hardfork: Some(FoundryHardfork::Ethereum(
1673                foundry_evm_hardforks::EthereumHardfork::Prague,
1674            )),
1675            instance_id: Some(B256::with_last_byte(1)),
1676            source_fork_block_number: None,
1677            source_fork_block_hash: None,
1678        };
1679        let mut evm_opts = EvmOpts::default();
1680        evm_opts.expect_fork_endpoint(identity.clone(), true);
1681
1682        assert!(evm_opts.ensure_expected_fork_endpoint(&identity).is_ok());
1683        assert_eq!(evm_opts.fork_endpoint, Some(identity.clone()));
1684        assert_eq!(evm_opts.expected_fork_endpoint, Some(identity.clone()));
1685        assert!(evm_opts.fork_network_is_inferred);
1686
1687        let mut changed_instance = identity.clone();
1688        changed_instance.instance_id = Some(B256::with_last_byte(2));
1689        assert!(evm_opts.ensure_expected_fork_endpoint(&changed_instance).is_err());
1690
1691        let mut changed_unknown_hardfork = identity;
1692        changed_unknown_hardfork.reported_hardfork = Some("Future".to_string());
1693        changed_unknown_hardfork.hardfork = None;
1694        assert!(evm_opts.ensure_expected_fork_endpoint(&changed_unknown_hardfork).is_err());
1695
1696        evm_opts.infer_network_from_fork().await.unwrap();
1697        assert_eq!(evm_opts.fork_endpoint, None);
1698        assert_eq!(evm_opts.expected_fork_endpoint, None);
1699    }
1700
1701    #[test]
1702    fn fork_expected_anvil_identity_seeds_strict_node_info_probe() {
1703        let authoritative = ForkEndpointIdentity {
1704            endpoint: "http://localhost:8545".to_string(),
1705            execution_chain_id: 1,
1706            source_chain_id: 1,
1707            network: NetworkVariant::Ethereum,
1708            network_profile: NetworkConfigs::default(),
1709            reported_hardfork: Some("Prague".to_string()),
1710            hardfork: Some(FoundryHardfork::Ethereum(
1711                foundry_evm_hardforks::EthereumHardfork::Prague,
1712            )),
1713            instance_id: Some(B256::with_last_byte(1)),
1714            source_fork_block_number: None,
1715            source_fork_block_hash: None,
1716        };
1717        let mut anonymous = authoritative.clone();
1718        anonymous.reported_hardfork = None;
1719        anonymous.hardfork = None;
1720        anonymous.instance_id = None;
1721        let mut evm_opts = EvmOpts {
1722            fork_url: Some(authoritative.endpoint.clone()),
1723            fork_endpoint: Some(anonymous),
1724            expected_fork_endpoint: Some(authoritative),
1725            ..Default::default()
1726        };
1727
1728        assert!(evm_opts.anvil_node_info_probe().identified);
1729
1730        evm_opts.fork_url = Some("http://localhost:9545".to_string());
1731        assert!(!evm_opts.anvil_node_info_probe().identified);
1732    }
1733
1734    #[test]
1735    fn selecting_new_fork_url_clears_only_endpoint_derived_state() {
1736        let identity = ForkEndpointIdentity {
1737            endpoint: "http://localhost:8545".to_string(),
1738            execution_chain_id: 42,
1739            source_chain_id: 42,
1740            network: NetworkVariant::Ethereum,
1741            network_profile: NetworkConfigs::with_celo(),
1742            reported_hardfork: None,
1743            hardfork: None,
1744            instance_id: None,
1745            source_fork_block_number: None,
1746            source_fork_block_hash: None,
1747        };
1748        let mut inferred = EvmOpts {
1749            fork_url: Some(identity.endpoint.clone()),
1750            fork_block_number: Some(123),
1751            fork_endpoint: Some(identity.clone()),
1752            expected_fork_endpoint: Some(identity.clone()),
1753            networks: identity.network_profile,
1754            fork_network_is_inferred: true,
1755            fork_chain_id_is_inferred: true,
1756            fork_block_number_is_inferred: true,
1757            ..Default::default()
1758        };
1759        inferred.env.chain_id = Some(identity.execution_chain_id);
1760
1761        inferred.set_fork_url("http://localhost:9545".to_string());
1762
1763        assert_eq!(inferred.fork_url.as_deref(), Some("http://localhost:9545"));
1764        assert_eq!(inferred.fork_block_number, None);
1765        assert_eq!(inferred.fork_endpoint, None);
1766        assert_eq!(inferred.expected_fork_endpoint, None);
1767        assert_eq!(inferred.networks, NetworkConfigs::default());
1768        assert_eq!(inferred.env.chain_id, None);
1769        assert!(!inferred.fork_network_is_inferred);
1770        assert!(!inferred.fork_chain_id_is_inferred);
1771        assert!(!inferred.fork_block_number_is_inferred);
1772
1773        let mut explicit = EvmOpts {
1774            fork_url: Some(identity.endpoint.clone()),
1775            fork_block_number: Some(123),
1776            fork_endpoint: Some(identity.clone()),
1777            expected_fork_endpoint: Some(identity),
1778            networks: NetworkConfigs::with_celo(),
1779            ..Default::default()
1780        };
1781        explicit.env.chain_id = Some(42);
1782
1783        explicit.set_fork_url("http://localhost:9545".to_string());
1784
1785        assert_eq!(explicit.networks, NetworkConfigs::with_celo());
1786        assert_eq!(explicit.env.chain_id, Some(42));
1787        assert_eq!(explicit.fork_block_number, Some(123));
1788        assert_eq!(explicit.fork_endpoint, None);
1789        assert_eq!(explicit.expected_fork_endpoint, None);
1790        assert!(!explicit.fork_block_number_is_inferred);
1791    }
1792
1793    #[test]
1794    fn changed_fork_auth_clears_only_endpoint_derived_state() {
1795        let endpoint = "http://localhost:8545";
1796        let identity = ForkEndpointIdentity {
1797            endpoint: endpoint.to_string(),
1798            execution_chain_id: 42,
1799            source_chain_id: 42,
1800            network: NetworkVariant::Ethereum,
1801            network_profile: NetworkConfigs::with_celo(),
1802            reported_hardfork: None,
1803            hardfork: None,
1804            instance_id: Some(B256::with_last_byte(1)),
1805            source_fork_block_number: None,
1806            source_fork_block_hash: None,
1807        };
1808        let context = ForkContext {
1809            execution_chain_id: identity.execution_chain_id,
1810            source_chain_id: identity.source_chain_id,
1811            network: identity.network,
1812            network_profile: identity.network_profile,
1813            block_number: 123,
1814            hardfork: identity.hardfork,
1815            instance_id: identity.instance_id,
1816            source_fork_block_number: identity.source_fork_block_number,
1817            source_fork_block_hash: identity.source_fork_block_hash,
1818        };
1819        let mut evm_opts = EvmOpts {
1820            fork_url: Some(endpoint.to_string()),
1821            fork_block_number: Some(123),
1822            fork_headers: Some(vec!["Authorization: first".to_string()]),
1823            rpc_jwt: Some("first-jwt".to_string()),
1824            fork_endpoint: Some(identity.clone()),
1825            expected_fork_endpoint: Some(identity),
1826            networks: NetworkConfigs::with_celo(),
1827            ..Default::default()
1828        };
1829        evm_opts.env.chain_id = Some(42);
1830        let fork = evm_opts.resolved_fork(
1831            endpoint,
1832            BlockNumHash::new(context.block_number, B256::with_last_byte(2)),
1833            context,
1834        );
1835
1836        evm_opts.fork_headers = Some(vec!["Authorization: second".to_string()]);
1837        evm_opts.rpc_jwt = Some("second-jwt".to_string());
1838
1839        assert!(evm_opts.invalidate_fork_endpoint_if_source_changed(&fork));
1840        assert_eq!(evm_opts.fork_endpoint, None);
1841        assert_eq!(evm_opts.expected_fork_endpoint, None);
1842        assert_eq!(evm_opts.fork_block_number, Some(123));
1843        assert_eq!(evm_opts.networks, NetworkConfigs::with_celo());
1844        assert_eq!(evm_opts.env.chain_id, Some(42));
1845    }
1846
1847    #[test]
1848    fn changed_fork_selector_preserves_endpoint_identity() {
1849        let endpoint = "http://localhost:8545";
1850        let identity = ForkEndpointIdentity {
1851            endpoint: endpoint.to_string(),
1852            execution_chain_id: 1,
1853            source_chain_id: 1,
1854            network: NetworkVariant::Ethereum,
1855            network_profile: NetworkConfigs::default(),
1856            reported_hardfork: None,
1857            hardfork: None,
1858            instance_id: Some(B256::with_last_byte(1)),
1859            source_fork_block_number: None,
1860            source_fork_block_hash: None,
1861        };
1862        let mut evm_opts = EvmOpts {
1863            fork_url: Some(endpoint.to_string()),
1864            fork_headers: Some(vec!["x-source: stable".to_string()]),
1865            rpc_jwt: Some("stable-jwt".to_string()),
1866            fork_endpoint: Some(identity.clone()),
1867            expected_fork_endpoint: Some(identity.clone()),
1868            ..Default::default()
1869        };
1870        let fork = evm_opts.resolved_fork(
1871            endpoint,
1872            BlockNumHash::new(1, B256::with_last_byte(2)),
1873            resolved_context(1),
1874        );
1875
1876        evm_opts.fork_block_number = Some(1);
1877
1878        assert!(!evm_opts.invalidate_fork_endpoint_if_source_changed(&fork));
1879        assert_eq!(evm_opts.fork_endpoint, Some(identity.clone()));
1880        assert_eq!(evm_opts.expected_fork_endpoint, Some(identity));
1881    }
1882
1883    #[test]
1884    fn selecting_same_fork_url_preserves_pinned_state() {
1885        let mut evm_opts = EvmOpts {
1886            fork_url: Some("http://localhost:8545".to_string()),
1887            fork_block_number: Some(123),
1888            networks: NetworkConfigs::with_celo(),
1889            fork_network_is_inferred: true,
1890            fork_chain_id_is_inferred: true,
1891            fork_block_number_is_inferred: true,
1892            ..Default::default()
1893        };
1894        evm_opts.env.chain_id = Some(42);
1895
1896        evm_opts.set_fork_url("http://localhost:8545".to_string());
1897
1898        assert_eq!(evm_opts.fork_block_number, Some(123));
1899        assert_eq!(evm_opts.networks, NetworkConfigs::with_celo());
1900        assert_eq!(evm_opts.env.chain_id, Some(42));
1901        assert!(evm_opts.fork_network_is_inferred);
1902        assert!(evm_opts.fork_chain_id_is_inferred);
1903        assert!(evm_opts.fork_block_number_is_inferred);
1904    }
1905
1906    #[cfg(feature = "monad")]
1907    fn monad_env(timestamp: u64) -> EvmEnv<foundry_evm_hardforks::MonadHardfork, BlockEnv> {
1908        let mut block = BlockEnv::default();
1909        block.set_timestamp(U256::from(timestamp));
1910        let mut cfg = CfgEnv::new_with_spec(foundry_evm_hardforks::MonadHardfork::default());
1911        cfg.chain_id = NamedChain::Monad as u64;
1912        EvmEnv::new(cfg, block)
1913    }
1914
1915    #[test]
1916    #[cfg(feature = "monad")]
1917    fn resolve_execution_spec_uses_monad_ten_activation_timestamp() {
1918        let config = Config::default();
1919        let activation =
1920            foundry_evm_hardforks::MonadHardfork::MonadTen.mainnet_activation_timestamp().unwrap();
1921
1922        let mut before = monad_env(activation - 1);
1923        assert_eq!(
1924            resolve_execution_spec(
1925                config.evm_version,
1926                config.hardfork,
1927                &mut before,
1928                ExecutionSpecContext::fork(NamedChain::Monad as u64, None),
1929                None,
1930            ),
1931            Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadNine))
1932        );
1933        assert_eq!(before.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadNine);
1934
1935        let mut after = monad_env(activation);
1936        assert_eq!(
1937            resolve_execution_spec(
1938                config.evm_version,
1939                config.hardfork,
1940                &mut after,
1941                ExecutionSpecContext::fork(NamedChain::Monad as u64, None),
1942                None,
1943            ),
1944            Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadTen))
1945        );
1946        assert_eq!(after.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadTen);
1947    }
1948
1949    #[test]
1950    #[cfg(feature = "monad")]
1951    fn resolve_execution_spec_prefers_exact_endpoint_hardfork() {
1952        let config = Config::default();
1953        let activation =
1954            foundry_evm_hardforks::MonadHardfork::MonadNine.mainnet_activation_timestamp().unwrap();
1955        let mut env = monad_env(activation);
1956        let endpoint_hardfork =
1957            FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadEight);
1958
1959        assert_eq!(
1960            resolve_execution_spec(
1961                config.evm_version,
1962                config.hardfork,
1963                &mut env,
1964                ExecutionSpecContext::fork(NamedChain::Monad as u64, Some(endpoint_hardfork),),
1965                None,
1966            ),
1967            Some(endpoint_hardfork)
1968        );
1969        assert_eq!(env.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadEight);
1970    }
1971
1972    #[test]
1973    #[cfg(feature = "monad")]
1974    fn resolve_execution_spec_ignores_schedule_for_local_env() {
1975        let config = Config::default();
1976        let activation =
1977            foundry_evm_hardforks::MonadHardfork::MonadTen.mainnet_activation_timestamp().unwrap();
1978        let mut env = monad_env(activation - 1);
1979
1980        assert_eq!(
1981            resolve_execution_spec(
1982                config.evm_version,
1983                config.hardfork,
1984                &mut env,
1985                ExecutionSpecContext::Local,
1986                None,
1987            ),
1988            Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadTen))
1989        );
1990        assert_eq!(env.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadTen);
1991    }
1992
1993    #[test]
1994    fn resolve_execution_spec_preserves_ethereum_config_for_local_forks() {
1995        let config = Config::default();
1996        let mut block = BlockEnv::default();
1997        block.set_timestamp(U256::from(1_500_000_000u64));
1998        let mut env = EvmEnv::new(CfgEnv::new_with_spec(SpecId::LONDON), block);
1999
2000        assert_eq!(
2001            resolve_execution_spec(
2002                config.evm_version,
2003                config.hardfork,
2004                &mut env,
2005                ExecutionSpecContext::fork(NamedChain::Mainnet as u64, None),
2006                None,
2007            ),
2008            None
2009        );
2010        assert_eq!(env.cfg_env.spec, config.evm_spec_id::<SpecId>());
2011
2012        let endpoint_hardfork =
2013            FoundryHardfork::Ethereum(foundry_evm_hardforks::EthereumHardfork::Frontier);
2014        assert_eq!(
2015            resolve_execution_spec(
2016                config.evm_version,
2017                config.hardfork,
2018                &mut env,
2019                ExecutionSpecContext::fork(NamedChain::Mainnet as u64, Some(endpoint_hardfork),),
2020                None,
2021            ),
2022            None
2023        );
2024        assert_eq!(env.cfg_env.spec, config.evm_spec_id::<SpecId>());
2025    }
2026
2027    #[test]
2028    fn resolve_execution_spec_uses_ethereum_schedule_for_historical_execution() {
2029        let config = Config::default();
2030        let timestamp = 1_500_000_000u64;
2031        let expected =
2032            FoundryHardfork::from_chain_and_timestamp(NamedChain::Mainnet as u64, timestamp)
2033                .unwrap();
2034        let mut block = BlockEnv::default();
2035        block.set_timestamp(U256::from(timestamp));
2036        let mut env = EvmEnv::new(CfgEnv::new_with_spec(SpecId::LONDON), block);
2037
2038        assert_eq!(
2039            resolve_execution_spec(
2040                config.evm_version,
2041                config.hardfork,
2042                &mut env,
2043                ExecutionSpecContext::historical(NamedChain::Mainnet as u64, None),
2044                None,
2045            ),
2046            Some(expected)
2047        );
2048        assert_eq!(env.cfg_env.spec, SpecId::from(expected));
2049    }
2050
2051    #[test]
2052    #[cfg(feature = "optimism")]
2053    fn resolve_execution_spec_uses_optimism_schedule_for_local_forks() {
2054        let config =
2055            Config { hardfork: Some("ethereum:london".parse().unwrap()), ..Default::default() };
2056        let chain_id = NamedChain::Optimism as u64;
2057        let timestamp = u64::MAX;
2058        let expected = FoundryHardfork::from_chain_and_timestamp(chain_id, timestamp).unwrap();
2059        let mut block = BlockEnv::default();
2060        block.set_timestamp(U256::from(timestamp));
2061        let mut env = EvmEnv::new(CfgEnv::new_with_spec(OpSpecId::default()), block);
2062
2063        assert_eq!(
2064            resolve_execution_spec(
2065                config.evm_version,
2066                config.hardfork,
2067                &mut env,
2068                ExecutionSpecContext::fork(chain_id, Some("tempo:T3".parse().unwrap())),
2069                None,
2070            ),
2071            Some(expected)
2072        );
2073        assert_eq!(env.cfg_env.spec, OpSpecId::from_foundry_hardfork(expected).unwrap());
2074    }
2075
2076    #[test]
2077    #[cfg(feature = "monad")]
2078    fn resolve_execution_spec_honors_explicit_precedence() {
2079        let activation =
2080            foundry_evm_hardforks::MonadHardfork::MonadNine.mainnet_activation_timestamp().unwrap();
2081        let mut configured = Config {
2082            hardfork: Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadNine)),
2083            ..Default::default()
2084        };
2085        let mut env = monad_env(activation - 1);
2086
2087        assert_eq!(
2088            resolve_execution_spec(
2089                configured.evm_version,
2090                configured.hardfork,
2091                &mut env,
2092                ExecutionSpecContext::fork(NamedChain::Monad as u64, None),
2093                None,
2094            ),
2095            configured.hardfork
2096        );
2097        assert_eq!(env.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadNine);
2098
2099        configured.hardfork = None;
2100        assert_eq!(
2101            resolve_execution_spec(
2102                configured.evm_version,
2103                configured.hardfork,
2104                &mut env,
2105                ExecutionSpecContext::fork(NamedChain::Monad as u64, None),
2106                Some(foundry_evm_hardforks::MonadHardfork::MonadEight),
2107            ),
2108            Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadEight))
2109        );
2110        assert_eq!(env.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadEight);
2111    }
2112
2113    #[tokio::test(flavor = "multi_thread")]
2114    #[cfg(feature = "monad")]
2115    async fn fork_context_preserves_source_chain_with_execution_override() {
2116        let activation =
2117            foundry_evm_hardforks::MonadHardfork::MonadNine.mainnet_activation_timestamp().unwrap();
2118        let (_api, handle) = anvil::spawn(
2119            anvil::NodeConfig::test()
2120                .with_networks(NetworkConfigs::with_ethereum())
2121                .with_chain_id(Some(NamedChain::Monad as u64))
2122                .with_genesis_timestamp(Some(activation - 1)),
2123        )
2124        .await;
2125        let mut inferred = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2126        inferred.infer_network_from_fork().await.unwrap();
2127        assert_eq!(inferred.networks, NetworkConfigs::default());
2128
2129        let mut evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2130        evm_opts.env.chain_id = Some(NamedChain::Mainnet as u64);
2131        evm_opts.networks = NetworkConfigs::with_monad();
2132
2133        let (mut evm_env, tx_env, fork_context) = evm_opts
2134            .env_with_fork_context::<foundry_evm_hardforks::MonadHardfork, BlockEnv, TxEnv>()
2135            .await
2136            .unwrap();
2137        let fork_context = fork_context.unwrap();
2138
2139        assert_eq!(fork_context.source_chain_id, NamedChain::Monad as u64);
2140        assert_eq!(fork_context.network, NetworkVariant::Ethereum);
2141        assert!(matches!(fork_context.hardfork, Some(FoundryHardfork::Ethereum(_))));
2142        assert_eq!(evm_env.cfg_env.chain_id, NamedChain::Mainnet as u64);
2143        assert_eq!(tx_env.chain_id, Some(NamedChain::Mainnet as u64));
2144        assert_eq!(
2145            resolve_execution_spec(
2146                Config::default().evm_version,
2147                Config::default().hardfork,
2148                &mut evm_env,
2149                ExecutionSpecContext::fork(fork_context.source_chain_id, fork_context.hardfork,),
2150                None,
2151            ),
2152            Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadEight))
2153        );
2154    }
2155
2156    #[tokio::test(flavor = "multi_thread")]
2157    async fn infer_network_default_and_custom_anvil_selects_ethereum() {
2158        for chain_id in [NamedChain::Mainnet as u64, NamedChain::AnvilHardhat as u64, 98_765_432] {
2159            let (_api, handle) =
2160                anvil::spawn(anvil::NodeConfig::test().with_chain_id(Some(chain_id))).await;
2161
2162            let config = Config::figment();
2163            let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2164            evm_opts.fork_url = Some(handle.http_endpoint());
2165            assert_eq!(evm_opts.networks, NetworkConfigs::default());
2166
2167            evm_opts.infer_network_from_fork().await.unwrap();
2168
2169            assert_eq!(evm_opts.env.chain_id, Some(chain_id));
2170            assert!(!evm_opts.networks.is_tempo());
2171            #[cfg(feature = "optimism")]
2172            assert!(!evm_opts.networks.is_optimism());
2173            #[cfg(feature = "base")]
2174            assert!(!evm_opts.networks.is_base());
2175            assert!(!evm_opts.networks.is_celo());
2176            assert_eq!(evm_opts.networks, NetworkConfigs::default());
2177        }
2178    }
2179
2180    #[tokio::test(flavor = "multi_thread")]
2181    async fn infer_network_preserves_explicit_ethereum() {
2182        let networks = NetworkConfigs::with_ethereum();
2183        let mut evm_opts = EvmOpts {
2184            fork_url: Some("http://127.0.0.1:1".to_string()),
2185            networks,
2186            ..Default::default()
2187        };
2188
2189        let error = evm_opts.infer_network_from_fork().await.unwrap_err();
2190
2191        assert!(error.to_string().contains("failed to retrieve chain ID"));
2192        assert_eq!(evm_opts.networks, networks);
2193        assert_eq!(evm_opts.env.chain_id, None);
2194    }
2195
2196    #[tokio::test]
2197    async fn clearing_fork_restores_inferred_network_defaults() {
2198        let mut profiles = vec![NetworkConfigs::with_celo(), NetworkConfigs::with_tempo()];
2199        #[cfg(feature = "optimism")]
2200        profiles.push(NetworkConfigs::with_optimism());
2201        #[cfg(feature = "monad")]
2202        profiles.push(NetworkConfigs::with_monad());
2203        #[cfg(feature = "base")]
2204        profiles.push(NetworkConfigs::with_base());
2205
2206        for networks in profiles {
2207            let mut evm_opts = EvmOpts {
2208                networks,
2209                fork_endpoint: Some(ForkEndpointIdentity {
2210                    endpoint: "http://localhost:8545".to_string(),
2211                    execution_chain_id: 1,
2212                    source_chain_id: 1,
2213                    network: networks.execution_network(),
2214                    network_profile: networks,
2215                    reported_hardfork: None,
2216                    hardfork: None,
2217                    instance_id: None,
2218                    source_fork_block_number: None,
2219                    source_fork_block_hash: None,
2220                }),
2221                fork_block_number: Some(123),
2222                fork_network_is_inferred: true,
2223                fork_chain_id_is_inferred: true,
2224                fork_block_number_is_inferred: true,
2225                ..Default::default()
2226            };
2227            evm_opts.env.chain_id = Some(1);
2228
2229            evm_opts.infer_network_from_fork().await.unwrap();
2230
2231            assert_eq!(evm_opts.networks, NetworkConfigs::default());
2232            assert_eq!(evm_opts.fork_endpoint, None);
2233            assert!(!evm_opts.fork_network_is_inferred);
2234            assert_eq!(evm_opts.env.chain_id, None);
2235            assert!(!evm_opts.fork_chain_id_is_inferred);
2236            assert_eq!(evm_opts.fork_block_number, None);
2237            assert!(!evm_opts.fork_block_number_is_inferred);
2238        }
2239    }
2240
2241    #[tokio::test(flavor = "multi_thread")]
2242    async fn fork_network_detects_ethereum_anvil() {
2243        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2244        let evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2245
2246        assert_eq!(
2247            evm_opts.fork_network().await.unwrap(),
2248            (NamedChain::AnvilHardhat as u64, NetworkVariant::Ethereum)
2249        );
2250    }
2251
2252    #[tokio::test(flavor = "multi_thread")]
2253    async fn fork_network_uses_anvil_metadata_for_unknown_chain() {
2254        let chain_id = 98_765_432;
2255        let (_api, handle) =
2256            anvil::spawn(anvil::NodeConfig::test().with_chain_id(Some(chain_id))).await;
2257        let evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2258
2259        assert_eq!(evm_opts.fork_network().await.unwrap(), (chain_id, NetworkVariant::Ethereum));
2260    }
2261
2262    #[tokio::test]
2263    #[cfg(feature = "base")]
2264    async fn base_endpoint_identity_uses_generic_chain_profile() {
2265        let endpoint = "http://127.0.0.1:1";
2266        let provider = EvmOpts::default().fork_provider_with_url::<AnyNetwork>(endpoint).unwrap();
2267
2268        let identity = EvmOpts::resolve_fork_endpoint_identity(
2269            &provider,
2270            endpoint,
2271            NamedChain::Base as u64,
2272            None,
2273            None,
2274            EndpointHardforkPolicy::Optional,
2275        )
2276        .await
2277        .unwrap();
2278
2279        assert_eq!(identity.execution_chain_id, NamedChain::Base as u64);
2280        assert_eq!(identity.source_chain_id, NamedChain::Base as u64);
2281        assert_eq!(identity.network, NetworkVariant::Base);
2282        assert!(identity.network_profile.is_base());
2283        assert_eq!(identity.reported_hardfork, None);
2284        assert_eq!(identity.hardfork, None);
2285    }
2286
2287    #[tokio::test(flavor = "multi_thread")]
2288    async fn fork_non_anvil_node_info_rpc_error_is_optional() {
2289        let (_api, handle) =
2290            anvil::spawn(anvil::NodeConfig::test().with_chain_id(Some(NamedChain::Mainnet as u64)))
2291                .await;
2292        let fork_url =
2293            spawn_rpc_proxy_internal_error_after(handle.http_endpoint(), "anvil_nodeInfo", 0).await;
2294        let mut evm_opts = EvmOpts { fork_url: Some(fork_url), ..Default::default() };
2295
2296        evm_opts.infer_network_from_fork().await.unwrap();
2297
2298        assert_eq!(evm_opts.networks, NetworkConfigs::default());
2299        assert_eq!(evm_opts.env.chain_id, Some(NamedChain::Mainnet as u64));
2300    }
2301
2302    #[tokio::test(flavor = "multi_thread")]
2303    async fn fork_node_info_failure_is_strict_after_anvil_is_identified() {
2304        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2305        let fork_url =
2306            spawn_rpc_proxy_rejecting_method_after(handle.http_endpoint(), "anvil_nodeInfo", 1)
2307                .await;
2308        let evm_opts = EvmOpts { fork_url: Some(fork_url), ..Default::default() };
2309
2310        let error = evm_opts.discover_fork_endpoint().await.unwrap_err();
2311
2312        assert!(
2313            error.to_string().contains("failed to determine network family from endpoint"),
2314            "{error}"
2315        );
2316    }
2317
2318    #[tokio::test(flavor = "multi_thread")]
2319    async fn fork_node_info_probe_becomes_strict_after_initial_failure() {
2320        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2321        let fork_url =
2322            spawn_rpc_proxy_method_not_found_before(handle.http_endpoint(), "anvil_nodeInfo", 1)
2323                .await;
2324        let evm_opts = EvmOpts { fork_url: Some(fork_url), ..Default::default() };
2325
2326        let identity = evm_opts.discover_fork_endpoint().await.unwrap();
2327
2328        assert!(identity.reported_hardfork.is_some());
2329        assert!(identity.instance_id.is_some());
2330    }
2331
2332    #[tokio::test(flavor = "multi_thread")]
2333    async fn fork_cached_anvil_identity_keeps_node_info_probe_strict() {
2334        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2335        let fork_url =
2336            spawn_rpc_proxy_rejecting_method_after(handle.http_endpoint(), "anvil_nodeInfo", 2)
2337                .await;
2338        let mut evm_opts = EvmOpts { fork_url: Some(fork_url), ..Default::default() };
2339        evm_opts.infer_network_from_fork().await.unwrap();
2340        assert!(evm_opts.fork_endpoint.as_ref().unwrap().reported_hardfork.is_some());
2341
2342        let error = evm_opts.fork_network().await.unwrap_err();
2343
2344        assert!(
2345            error.to_string().contains("failed to determine network family from endpoint"),
2346            "{error}"
2347        );
2348    }
2349
2350    #[test]
2351    fn known_network_variant_does_not_guess_unknown_chain() {
2352        assert_eq!(NetworkVariant::from_known_chain_id(98_765_432).unwrap(), None);
2353    }
2354
2355    #[test]
2356    #[cfg(feature = "monad")]
2357    fn known_network_variant_classifies_monad() {
2358        assert_eq!(
2359            NetworkVariant::from_known_chain_id(NamedChain::Monad as u64).unwrap(),
2360            Some(NetworkVariant::Monad)
2361        );
2362    }
2363
2364    #[test]
2365    #[cfg(not(feature = "monad"))]
2366    fn known_network_variant_rejects_disabled_monad() {
2367        assert_eq!(
2368            NetworkVariant::from_known_chain_id(NamedChain::Monad as u64).unwrap_err(),
2369            "network family `monad` is not enabled in this build"
2370        );
2371    }
2372
2373    #[tokio::test(flavor = "multi_thread")]
2374    async fn infer_network_tempo_anvil_via_node_info() {
2375        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test_tempo()).await;
2376
2377        let config = Config::figment();
2378        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2379        evm_opts.fork_url = Some(handle.http_endpoint());
2380        // Networks not set -> should query anvil_nodeInfo to discover tempo.
2381        assert_eq!(evm_opts.networks, NetworkConfigs::default());
2382
2383        evm_opts.infer_network_from_fork().await.unwrap();
2384
2385        assert!(evm_opts.networks.is_tempo(), "should detect tempo via anvil_nodeInfo");
2386    }
2387
2388    #[tokio::test(flavor = "multi_thread")]
2389    async fn infer_network_tempo_propagates_unavailable_rpc() {
2390        let config = Config::figment();
2391        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2392        evm_opts.fork_url = Some("http://127.0.0.1:1".to_string());
2393        evm_opts.networks = NetworkConfigs::with_tempo();
2394
2395        let error = evm_opts.infer_network_from_fork().await.unwrap_err();
2396
2397        assert!(error.to_string().contains("failed to retrieve chain ID"));
2398        assert!(evm_opts.networks.is_tempo());
2399    }
2400
2401    #[tokio::test]
2402    async fn create2_deployer_availability_requires_resolved_fork_block() {
2403        let evm_opts =
2404            EvmOpts { fork_url: Some("http://127.0.0.1:1".to_string()), ..Default::default() };
2405
2406        let err = evm_opts.can_use_create2_deployer(None).await.unwrap_err();
2407        assert!(err.to_string().contains("fork block must be resolved"));
2408    }
2409
2410    #[test]
2411    fn resolved_fork_matches_source_headers_and_selector() {
2412        let headers = vec!["Authorization: one".to_string()];
2413        let fork = ResolvedFork::new(
2414            "http://127.0.0.1:1",
2415            Some(&headers),
2416            None,
2417            None,
2418            BlockNumHash::new(1, B256::ZERO),
2419            resolved_context(1),
2420        );
2421        let mut evm_opts = EvmOpts {
2422            fork_url: Some("http://127.0.0.1:1".to_string()),
2423            fork_headers: Some(headers),
2424            rpc_headers: Some(vec!["x-fallback: ignored".to_string()]),
2425            ..Default::default()
2426        };
2427
2428        assert!(evm_opts.resolved_fork_matches(&fork));
2429        let debug = format!("{fork:?}");
2430        assert!(!debug.contains("127.0.0.1"));
2431        assert!(!debug.contains("Authorization"));
2432        assert!(!debug.contains("one"));
2433
2434        evm_opts.fork_headers = Some(vec!["Authorization: two".to_string()]);
2435        assert!(!evm_opts.resolved_fork_matches(&fork));
2436
2437        evm_opts.fork_headers = Some(vec!["Authorization: one".to_string()]);
2438        evm_opts.fork_block_number = Some(1);
2439        assert!(!evm_opts.resolved_fork_matches(&fork));
2440
2441        evm_opts.fork_block_number = None;
2442        evm_opts.fork_url = Some("http://127.0.0.1:2".to_string());
2443        assert!(!evm_opts.resolved_fork_matches(&fork));
2444
2445        let fork = ResolvedFork::new(
2446            "http://127.0.0.1:1",
2447            None,
2448            None,
2449            None,
2450            BlockNumHash::new(1, B256::ZERO),
2451            resolved_context(1),
2452        );
2453        evm_opts.fork_url = Some("http://127.0.0.1:1".to_string());
2454        evm_opts.fork_headers = Some(Vec::new());
2455        evm_opts.rpc_headers = None;
2456        assert!(evm_opts.resolved_fork_matches(&fork));
2457
2458        let rpc_headers = vec!["Authorization: fallback".to_string()];
2459        let fork = ResolvedFork::new(
2460            "http://127.0.0.1:1",
2461            Some(&rpc_headers),
2462            Some("secret-jwt"),
2463            None,
2464            BlockNumHash::new(1, B256::ZERO),
2465            resolved_context(1),
2466        );
2467        evm_opts.fork_headers = None;
2468        evm_opts.rpc_headers = Some(rpc_headers);
2469        evm_opts.rpc_jwt = Some("secret-jwt".to_string());
2470        assert!(evm_opts.resolved_fork_matches(&fork));
2471        assert!(!format!("{fork:?}").contains("secret-jwt"));
2472
2473        evm_opts.rpc_jwt = Some("different-jwt".to_string());
2474        assert!(!evm_opts.resolved_fork_matches(&fork));
2475    }
2476
2477    #[tokio::test(flavor = "multi_thread")]
2478    async fn create2_deployer_preflight_uses_exact_fork_hash() {
2479        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2480        let provider = handle.http_provider();
2481        let sender = handle.dev_accounts().next().unwrap();
2482
2483        let init_code = bytes!("6001600c60003960016000f300");
2484        let receipt = provider
2485            .send_transaction(WithOtherFields::new(
2486                TransactionRequest::default().from(sender).with_deploy_code(init_code),
2487            ))
2488            .await
2489            .unwrap()
2490            .get_receipt()
2491            .await
2492            .unwrap();
2493        let deployer = receipt.contract_address.unwrap();
2494        let block_number = receipt.block_number.unwrap();
2495
2496        let evm_opts = EvmOpts {
2497            fork_url: Some(handle.http_endpoint()),
2498            fork_block_number: Some(block_number),
2499            create2_deployer: deployer,
2500            ..Default::default()
2501        };
2502        let (_, _, fork) = evm_opts.env_resolved::<SpecId, BlockEnv, TxEnv>().await.unwrap();
2503        let fork = fork.unwrap();
2504        assert!(evm_opts.can_use_create2_deployer_resolved(Some(&fork)).await.unwrap());
2505
2506        provider
2507            .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
2508            .await
2509            .unwrap();
2510        let replacement = provider.get_block_by_number(block_number.into()).await.unwrap().unwrap();
2511        assert_ne!(replacement.header().hash(), fork.hash());
2512        assert!(
2513            !evm_opts.can_use_create2_deployer(Some(block_number)).await.unwrap(),
2514            "the replacement block must not contain the deployer"
2515        );
2516
2517        assert!(
2518            !matches!(evm_opts.can_use_create2_deployer_resolved(Some(&fork)).await, Ok(false)),
2519            "the exact lookup fell back to the replacement block"
2520        );
2521    }
2522
2523    #[tokio::test(flavor = "multi_thread")]
2524    #[cfg(feature = "monad")]
2525    async fn infer_network_monad_anvil_via_node_info() {
2526        for chain_id in [NamedChain::Mainnet as u64, NamedChain::AnvilHardhat as u64, 98_765_432] {
2527            let (_api, handle) =
2528                anvil::spawn(anvil::NodeConfig::test_monad().with_chain_id(Some(chain_id))).await;
2529
2530            let config = Config::figment();
2531            let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2532            evm_opts.fork_url = Some(handle.http_endpoint());
2533            assert_eq!(evm_opts.networks, NetworkConfigs::default());
2534
2535            evm_opts.infer_network_from_fork().await.unwrap();
2536
2537            assert_eq!(evm_opts.env.chain_id, Some(chain_id));
2538            assert!(
2539                evm_opts.networks.is_monad(),
2540                "should detect Monad via anvil_nodeInfo for chain {chain_id}"
2541            );
2542        }
2543    }
2544
2545    #[tokio::test(flavor = "multi_thread")]
2546    #[cfg(feature = "monad")]
2547    async fn fork_context_carries_exact_monad_anvil_hardfork() {
2548        let activation =
2549            foundry_evm_hardforks::MonadHardfork::MonadNine.mainnet_activation_timestamp().unwrap();
2550        let (_api, handle) = anvil::spawn(
2551            anvil::NodeConfig::test_monad()
2552                .with_chain_id(Some(NamedChain::Monad as u64))
2553                .with_genesis_timestamp(Some(activation))
2554                .with_hardfork(Some(foundry_evm_hardforks::MonadHardfork::MonadEight.into())),
2555        )
2556        .await;
2557        let mut evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2558
2559        evm_opts.infer_network_from_fork().await.unwrap();
2560        let (mut evm_env, _, context) = evm_opts
2561            .env_with_fork_context::<foundry_evm_hardforks::MonadHardfork, BlockEnv, TxEnv>()
2562            .await
2563            .unwrap();
2564        let context = context.unwrap();
2565
2566        assert_eq!(
2567            context.hardfork,
2568            Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadEight))
2569        );
2570        assert_eq!(
2571            resolve_execution_spec(
2572                Config::default().evm_version,
2573                Config::default().hardfork,
2574                &mut evm_env,
2575                ExecutionSpecContext::fork(context.source_chain_id, context.hardfork),
2576                None,
2577            ),
2578            context.hardfork
2579        );
2580        assert_eq!(evm_env.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadEight);
2581    }
2582
2583    #[tokio::test(flavor = "multi_thread")]
2584    #[cfg(feature = "monad")]
2585    async fn fork_context_refreshes_identity_after_same_url_reset() {
2586        let (_api, monad_eight) = anvil::spawn(
2587            anvil::NodeConfig::test_monad()
2588                .with_chain_id(Some(NamedChain::MonadTestnet as u64))
2589                .with_hardfork(Some(foundry_evm_hardforks::MonadHardfork::MonadEight.into())),
2590        )
2591        .await;
2592        let (_api, monad_nine) = anvil::spawn(
2593            anvil::NodeConfig::test_monad()
2594                .with_chain_id(Some(NamedChain::Monad as u64))
2595                .with_hardfork(Some(foundry_evm_hardforks::MonadHardfork::MonadNine.into())),
2596        )
2597        .await;
2598        let (fork_api, fork_handle) = anvil::spawn(
2599            anvil::NodeConfig::test()
2600                .with_chain_id(Some(NamedChain::Mainnet as u64))
2601                .with_no_storage_caching(true)
2602                .with_eth_rpc_url(Some(monad_eight.http_endpoint()))
2603                .with_fork_block_number(Some(0u64)),
2604        )
2605        .await;
2606
2607        let mut evm_opts =
2608            EvmOpts { fork_url: Some(fork_handle.http_endpoint()), ..Default::default() };
2609        evm_opts.infer_network_from_fork().await.unwrap();
2610        let cached = evm_opts.fork_endpoint.as_ref().unwrap();
2611        assert_eq!(cached.source_chain_id, NamedChain::MonadTestnet as u64);
2612        assert_eq!(
2613            cached.hardfork,
2614            Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadEight))
2615        );
2616
2617        let (_, _, first) = evm_opts
2618            .env_with_fork_context::<foundry_evm_hardforks::MonadHardfork, BlockEnv, TxEnv>()
2619            .await
2620            .unwrap();
2621        assert_eq!(first.unwrap().source_chain_id, NamedChain::MonadTestnet as u64);
2622
2623        fork_api
2624            .anvil_reset(Some(alloy_rpc_types::anvil::Forking {
2625                json_rpc_url: Some(monad_nine.http_endpoint()),
2626                block_number: Some(0),
2627            }))
2628            .await
2629            .unwrap();
2630
2631        let (_, _, second) = evm_opts
2632            .env_with_fork_context::<foundry_evm_hardforks::MonadHardfork, BlockEnv, TxEnv>()
2633            .await
2634            .unwrap();
2635        let second = second.unwrap();
2636        assert_eq!(second.source_chain_id, NamedChain::Monad as u64);
2637        assert_eq!(
2638            second.hardfork,
2639            Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadNine))
2640        );
2641    }
2642
2643    #[tokio::test(flavor = "multi_thread")]
2644    async fn expected_fork_endpoint_detects_same_url_reset() {
2645        let (api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2646        let mut evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2647        evm_opts.infer_network_from_fork().await.unwrap();
2648        let identity = evm_opts.fork_endpoint.clone().unwrap();
2649        let original_instance = identity.instance_id;
2650        let network_is_inferred = evm_opts.fork_network_is_inferred;
2651        evm_opts.expect_fork_endpoint(identity, network_is_inferred);
2652
2653        api.anvil_reset(None).await.unwrap();
2654
2655        assert_ne!(api.instance_id(), original_instance.unwrap());
2656        let error = evm_opts.env_with_fork_context::<SpecId, BlockEnv, TxEnv>().await.unwrap_err();
2657        assert!(
2658            error.to_string().contains("changed after its execution context was selected"),
2659            "{error}"
2660        );
2661    }
2662
2663    #[tokio::test(flavor = "multi_thread")]
2664    async fn resolved_fork_detects_same_url_reset_without_cached_expectation() {
2665        let (api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2666        let evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2667        let fork = evm_opts.resolve_fork().await.unwrap().unwrap();
2668        let original_instance = fork.context().instance_id;
2669
2670        api.anvil_reset(None).await.unwrap();
2671
2672        assert_ne!(api.instance_id(), original_instance.unwrap());
2673        let error = evm_opts
2674            .env_with_resolved_fork::<SpecId, BlockEnv, TxEnv>(Some(&fork))
2675            .await
2676            .unwrap_err();
2677        assert!(
2678            error
2679                .to_string()
2680                .contains("changed after its block and execution context were resolved"),
2681            "{error}"
2682        );
2683    }
2684
2685    #[test]
2686    #[cfg(feature = "monad")]
2687    fn unknown_endpoint_hardfork_is_optional_only_for_remote_execution() {
2688        assert_eq!(NetworkVariant::from_node_info_name("monad").unwrap(), NetworkVariant::Monad);
2689        assert_eq!(
2690            endpoint_hardfork(
2691                NetworkVariant::Monad,
2692                "MonadFuture",
2693                EndpointHardforkPolicy::Optional
2694            )
2695            .unwrap(),
2696            None
2697        );
2698
2699        let error = endpoint_hardfork(
2700            NetworkVariant::Monad,
2701            "MonadFuture",
2702            EndpointHardforkPolicy::Required,
2703        )
2704        .unwrap_err();
2705        assert!(
2706            error.to_string().contains("unsupported hardfork `MonadFuture` reported for `monad`")
2707        );
2708        assert_eq!(
2709            endpoint_hardfork(NetworkVariant::Monad, "MonadNine", EndpointHardforkPolicy::Required)
2710                .unwrap(),
2711            Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadNine))
2712        );
2713    }
2714
2715    #[test]
2716    #[cfg(feature = "base")]
2717    fn unknown_base_endpoint_hardfork_is_optional_only_for_remote_execution() {
2718        assert_eq!(NetworkVariant::from_node_info_name("base").unwrap(), NetworkVariant::Base);
2719        assert_eq!(
2720            endpoint_hardfork(NetworkVariant::Base, "BaseFuture", EndpointHardforkPolicy::Optional)
2721                .unwrap(),
2722            None
2723        );
2724
2725        let error =
2726            endpoint_hardfork(NetworkVariant::Base, "BaseFuture", EndpointHardforkPolicy::Required)
2727                .unwrap_err();
2728        assert!(
2729            error.to_string().contains("unsupported hardfork `BaseFuture` reported for `base`")
2730        );
2731        assert_eq!(
2732            endpoint_hardfork(NetworkVariant::Base, "Beryl", EndpointHardforkPolicy::Required)
2733                .unwrap(),
2734            Some(FoundryHardfork::Base(BaseUpgrade::Beryl))
2735        );
2736    }
2737
2738    #[tokio::test(flavor = "multi_thread")]
2739    #[cfg(feature = "monad")]
2740    async fn fork_network_detects_monad_anvil() {
2741        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test_monad()).await;
2742        let evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2743
2744        assert_eq!(
2745            evm_opts.fork_network().await.unwrap(),
2746            (NamedChain::AnvilHardhat as u64, NetworkVariant::Monad)
2747        );
2748    }
2749
2750    #[tokio::test(flavor = "multi_thread")]
2751    #[cfg(feature = "monad")]
2752    async fn infer_network_monad_propagates_unavailable_rpc() {
2753        let config = Config::figment();
2754        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2755        evm_opts.fork_url = Some("http://127.0.0.1:1".to_string());
2756        evm_opts.networks = NetworkConfigs::with_monad();
2757
2758        let error = evm_opts.infer_network_from_fork().await.unwrap_err();
2759
2760        assert!(error.to_string().contains("failed to retrieve chain ID"));
2761        assert!(evm_opts.networks.is_monad());
2762    }
2763
2764    #[tokio::test(flavor = "multi_thread")]
2765    #[cfg(feature = "base")]
2766    async fn infer_network_base_propagates_unavailable_rpc() {
2767        let mut evm_opts = EvmOpts {
2768            fork_url: Some("http://127.0.0.1:1".to_string()),
2769            networks: NetworkConfigs::with_base(),
2770            ..Default::default()
2771        };
2772
2773        let error = evm_opts.infer_network_from_fork().await.unwrap_err();
2774
2775        assert!(error.to_string().contains("failed to retrieve chain ID"));
2776        assert!(evm_opts.networks.is_base());
2777        assert_eq!(evm_opts.fork_endpoint, None);
2778    }
2779
2780    #[tokio::test(flavor = "multi_thread")]
2781    async fn flaky_infer_network_tempo_moderato_rpc() {
2782        let config = Config::figment();
2783        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2784        evm_opts.fork_url = Some("https://rpc.moderato.tempo.xyz".to_string());
2785        assert_eq!(evm_opts.networks, NetworkConfigs::default());
2786
2787        evm_opts.infer_network_from_fork().await.unwrap();
2788
2789        // Tempo Moderato has a known Tempo chain ID -> should be inferred via with_chain_id.
2790        assert!(evm_opts.networks.is_tempo(), "should detect tempo from Moderato chain ID");
2791    }
2792
2793    #[tokio::test(flavor = "multi_thread")]
2794    async fn get_fork_pins_block_number_from_env() {
2795        let endpoint = foundry_test_utils::rpc::next_http_rpc_endpoint();
2796
2797        let config = Config::figment();
2798        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2799        evm_opts.fork_url = Some(endpoint.clone());
2800        // Explicitly leave fork_block_number as None to simulate --fork-url without --block-number
2801        assert!(evm_opts.fork_block_number.is_none());
2802
2803        // Fetch the environment (this resolves "latest" to an actual block number)
2804        let (evm_env, _, fork_block) = evm_opts.env::<SpecId, BlockEnv, TxEnv>().await.unwrap();
2805        assert!(fork_block.is_some(), "should have resolved a fork block number");
2806        let resolved_block = fork_block.unwrap();
2807        assert!(resolved_block > 0, "should have resolved to a real block number");
2808
2809        // Create the fork - this should pin the block number
2810        let fork =
2811            evm_opts.get_fork(&Config::default(), evm_env.cfg_env.chain_id, fork_block).unwrap();
2812
2813        // The fork's evm_opts should now have fork_block_number set to the resolved block
2814        assert_eq!(
2815            fork.evm_opts.fork_block_number,
2816            Some(resolved_block),
2817            "get_fork should pin fork_block_number to the block from env"
2818        );
2819        assert!(fork.evm_opts.fork_block_number_is_inferred);
2820    }
2821
2822    #[tokio::test(flavor = "multi_thread")]
2823    async fn fork_backend_rejects_changed_resolved_context() {
2824        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2825        let mut evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2826        evm_opts.infer_network_from_fork().await.unwrap();
2827        let (_, _, resolved) = evm_opts.env_resolved::<SpecId, BlockEnv, TxEnv>().await.unwrap();
2828        let resolved = resolved.unwrap();
2829        let mut context = resolved.context();
2830        let mut invalid_instance = context.instance_id.unwrap_or_default();
2831        invalid_instance[31] ^= 1;
2832        context.instance_id = Some(invalid_instance);
2833        let invalid = ResolvedFork::new(
2834            evm_opts.fork_url.as_deref().unwrap(),
2835            evm_opts.fork_source_headers(),
2836            evm_opts.rpc_jwt.as_deref(),
2837            evm_opts.fork_block_number,
2838            resolved.block(),
2839            context,
2840        );
2841        let fork = evm_opts
2842            .get_fork_resolved(&Config::default(), context.execution_chain_id, Some(&invalid))
2843            .unwrap();
2844
2845        let error =
2846            crate::backend::Backend::<crate::evm::EthEvmNetwork>::spawn(Some(fork)).unwrap_err();
2847        assert!(
2848            error
2849                .to_string()
2850                .contains("changed after its block and execution context were resolved"),
2851            "{error}"
2852        );
2853    }
2854
2855    // Regression test for https://github.com/foundry-rs/foundry/issues/13576
2856    // On Arbitrum, `block_env.number` is remapped to the L1 block number by
2857    // `apply_chain_and_block_specific_env_changes`. The fork block number returned
2858    // by `env()` must be the actual L2 block number, not the remapped L1 value.
2859    #[tokio::test(flavor = "multi_thread")]
2860    async fn flaky_get_fork_uses_l2_block_number_on_arbitrum() {
2861        let endpoint =
2862            foundry_test_utils::rpc::next_rpc_endpoint(foundry_config::NamedChain::Arbitrum);
2863
2864        let config = Config::figment();
2865        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2866        evm_opts.fork_url = Some(endpoint.clone());
2867        assert!(evm_opts.fork_block_number.is_none());
2868
2869        let (evm_env, _, fork_block) = evm_opts.env::<SpecId, BlockEnv, TxEnv>().await.unwrap();
2870        let fork_block = fork_block.expect("should have resolved a fork block number");
2871
2872        // On Arbitrum, block_env.number is the L1 block number (much smaller).
2873        // The fork_block should be the actual L2 block number (much larger).
2874        let block_env_number: u64 = evm_env.block_env.number.to();
2875        assert!(
2876            fork_block > block_env_number,
2877            "fork_block ({fork_block}) should be the L2 block, which is larger than \
2878             block_env.number ({block_env_number}) which is the L1 block on Arbitrum"
2879        );
2880
2881        // Verify get_fork pins to the correct L2 block number
2882        let fork = evm_opts
2883            .get_fork(&Config::default(), evm_env.cfg_env.chain_id, Some(fork_block))
2884            .unwrap();
2885        assert_eq!(
2886            fork.evm_opts.fork_block_number,
2887            Some(fork_block),
2888            "get_fork should pin to the L2 block number, not the L1 block number"
2889        );
2890    }
2891
2892    #[tokio::test(flavor = "multi_thread")]
2893    async fn get_fork_preserves_explicit_block_number() {
2894        let endpoint = foundry_test_utils::rpc::next_http_rpc_endpoint();
2895
2896        let config = Config::figment();
2897        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2898        evm_opts.fork_url = Some(endpoint.clone());
2899        // Set an explicit block number
2900        evm_opts.fork_block_number = Some(12345678);
2901
2902        let (evm_env, _, fork_block) = evm_opts.env::<SpecId, BlockEnv, TxEnv>().await.unwrap();
2903
2904        let fork =
2905            evm_opts.get_fork(&Config::default(), evm_env.cfg_env.chain_id, fork_block).unwrap();
2906
2907        // Should preserve the explicit block number, not override it
2908        assert_eq!(
2909            fork.evm_opts.fork_block_number,
2910            Some(12345678),
2911            "get_fork should preserve explicitly set fork_block_number"
2912        );
2913        assert!(!fork.evm_opts.fork_block_number_is_inferred);
2914    }
2915}