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