Skip to main content

foundry_evm_core/
opts.rs

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