Skip to main content

foundry_evm_core/
opts.rs

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