Skip to main content

foundry_evm_core/
opts.rs

1use crate::{
2    EvmEnv, FoundryBlock, FoundryTransaction,
3    constants::DEFAULT_CREATE2_DEPLOYER,
4    fork::CreateFork,
5    utils::{apply_chain_and_block_specific_env_changes, block_env_from_header},
6};
7use alloy_chains::NamedChain;
8use alloy_consensus::BlockHeader;
9use alloy_network::{AnyNetwork, BlockResponse, Network};
10use alloy_primitives::{Address, B256, BlockNumber, ChainId, U256};
11use alloy_provider::{Provider, RootProvider};
12use alloy_rpc_types::{BlockNumberOrTag, anvil::NodeInfo};
13use eyre::WrapErr;
14use foundry_common::{ALCHEMY_FREE_TIER_CUPS, NON_ARCHIVE_NODE_WARNING, provider::ProviderBuilder};
15use foundry_config::{Chain, Config, GasLimit};
16use foundry_evm_networks::NetworkConfigs;
17use revm::{context::CfgEnv, primitives::hardfork::SpecId};
18use serde::{Deserialize, Serialize};
19use std::fmt::Write;
20use url::Url;
21
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct EvmOpts {
24    /// The EVM environment configuration.
25    #[serde(flatten)]
26    pub env: Env,
27
28    /// Fetch state over a remote instead of starting from empty state.
29    #[serde(rename = "eth_rpc_url")]
30    pub fork_url: Option<String>,
31
32    /// Pins the block number for the state fork.
33    pub fork_block_number: Option<u64>,
34
35    /// The number of retries.
36    pub fork_retries: Option<u32>,
37
38    /// Initial retry backoff.
39    pub fork_retry_backoff: Option<u64>,
40
41    /// Headers to use with `fork_url`
42    pub fork_headers: Option<Vec<String>>,
43
44    /// The available compute units per second.
45    ///
46    /// See also <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
47    pub compute_units_per_second: Option<u64>,
48
49    /// Disables RPC rate limiting entirely.
50    pub no_rpc_rate_limit: bool,
51
52    /// Disables storage caching entirely.
53    pub no_storage_caching: bool,
54
55    /// The initial balance of each deployed test contract.
56    pub initial_balance: U256,
57
58    /// The address which will be executing all tests.
59    pub sender: Address,
60
61    /// Enables the FFI cheatcode.
62    pub ffi: bool,
63
64    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
65    pub always_use_create_2_factory: bool,
66
67    /// Verbosity mode of EVM output as number of occurrences.
68    pub verbosity: u8,
69
70    /// The memory limit per EVM execution in bytes.
71    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
72    pub memory_limit: u64,
73
74    /// Whether to enable isolation of calls.
75    pub isolate: bool,
76
77    /// Whether to disable block gas limit checks.
78    pub disable_block_gas_limit: bool,
79
80    /// Whether to enable tx gas limit checks as imposed by Osaka (EIP-7825).
81    pub enable_tx_gas_limit: bool,
82
83    #[serde(flatten)]
84    /// Networks with enabled features.
85    pub networks: NetworkConfigs,
86
87    /// The CREATE2 deployer's address.
88    pub create2_deployer: Address,
89}
90
91impl Default for EvmOpts {
92    fn default() -> Self {
93        Self {
94            env: Env::default(),
95            fork_url: None,
96            fork_block_number: None,
97            fork_retries: None,
98            fork_retry_backoff: None,
99            fork_headers: None,
100            compute_units_per_second: None,
101            no_rpc_rate_limit: false,
102            no_storage_caching: false,
103            initial_balance: U256::default(),
104            sender: Address::default(),
105            ffi: false,
106            always_use_create_2_factory: false,
107            verbosity: 0,
108            memory_limit: 0,
109            isolate: false,
110            disable_block_gas_limit: false,
111            enable_tx_gas_limit: false,
112            networks: NetworkConfigs::default(),
113            create2_deployer: DEFAULT_CREATE2_DEPLOYER,
114        }
115    }
116}
117
118impl EvmOpts {
119    /// Returns a `RootProvider` for the given fork URL configured with options in `self` and
120    /// annotated `Network` type.
121    pub fn fork_provider_with_url<N: Network>(
122        &self,
123        fork_url: &str,
124    ) -> eyre::Result<RootProvider<N>> {
125        ProviderBuilder::new(fork_url)
126            .maybe_max_retry(self.fork_retries)
127            .maybe_initial_backoff(self.fork_retry_backoff)
128            .maybe_headers(self.fork_headers.clone())
129            .compute_units_per_second(self.get_compute_units_per_second())
130            .build()
131    }
132
133    /// Infers the network configuration from the fork chain ID if not already set.
134    ///
135    /// When a fork URL is configured and the network has not been explicitly set,
136    /// this fetches the chain ID from the remote endpoint, caches it for subsequent fork setup,
137    /// and calls [`NetworkConfigs::with_chain_id`] to auto-enable the correct network (e.g. Tempo,
138    /// OP Stack) based on the chain ID.
139    pub async fn infer_network_from_fork(&mut self) {
140        #[cfg(feature = "optimism")]
141        let already_op = self.networks.is_optimism();
142        #[cfg(not(feature = "optimism"))]
143        let already_op = false;
144        if !self.networks.is_tempo()
145            && !already_op
146            && let Some(ref fork_url) = self.fork_url
147            && let Ok(provider) = self.fork_provider_with_url::<AnyNetwork>(fork_url)
148            && let Ok(chain_id) = provider.get_chain_id().await
149        {
150            self.env.chain_id.get_or_insert(chain_id);
151
152            // If Anvil's chain, request anvil_nodeInfo to determine if the network is Tempo.
153            if chain_id == NamedChain::AnvilHardhat as u64 {
154                if let Ok(node_info) =
155                    provider.raw_request::<_, NodeInfo>("anvil_nodeInfo".into(), ()).await
156                    && node_info.network.is_some_and(|network| network == "tempo")
157                {
158                    self.networks = NetworkConfigs::with_tempo();
159                }
160            } else {
161                self.networks = self.networks.with_chain_id(chain_id);
162            }
163        }
164    }
165
166    /// Returns a tuple with [`EvmEnv`], `TxEnv`, and the actual fork block number.
167    ///
168    /// If a `fork_url` is set, creates a provider and passes it to both `EvmOpts::fork_evm_env`
169    /// and `EvmOpts::fork_tx_env`. Falls back to local settings when no fork URL is configured.
170    ///
171    /// The fork block number is returned separately because on some L2s (e.g., Arbitrum) the
172    /// `block_env.number` may be remapped (to the L1 block number) and therefore cannot be used
173    /// to pin the fork.
174    pub async fn env<
175        SPEC: Into<SpecId> + Default + Copy,
176        BLOCK: FoundryBlock + Default,
177        TX: FoundryTransaction + Default,
178    >(
179        &self,
180    ) -> eyre::Result<(EvmEnv<SPEC, BLOCK>, TX, Option<BlockNumber>)> {
181        if let Some(ref fork_url) = self.fork_url {
182            let provider = self.fork_provider_with_url::<AnyNetwork>(fork_url)?;
183            let ((evm_env, block_number), tx) =
184                tokio::try_join!(self.fork_evm_env(&provider), self.fork_tx_env(&provider))?;
185            Ok((evm_env, tx, Some(block_number)))
186        } else {
187            Ok((self.local_evm_env(), self.local_tx_env(), None))
188        }
189    }
190
191    /// Returns the [`EvmEnv`] (cfg + block) and [`BlockNumber`] fetched from the fork endpoint via
192    /// provider
193    pub async fn fork_evm_env<
194        SPEC: Into<SpecId> + Default + Copy,
195        BLOCK: FoundryBlock + Default,
196        N: Network,
197        P: Provider<N>,
198    >(
199        &self,
200        provider: &P,
201    ) -> eyre::Result<(EvmEnv<SPEC, BLOCK>, BlockNumber)> {
202        trace!(
203            memory_limit = %self.memory_limit,
204            override_chain_id = ?self.env.chain_id,
205            pin_block = ?self.fork_block_number,
206            origin = %self.sender,
207            disable_block_gas_limit = %self.disable_block_gas_limit,
208            enable_tx_gas_limit = %self.enable_tx_gas_limit,
209            configs = ?self.networks,
210            "creating fork environment"
211        );
212
213        let bn = match self.fork_block_number {
214            Some(bn) => BlockNumberOrTag::Number(bn),
215            None => BlockNumberOrTag::Latest,
216        };
217
218        let (chain_id, block) = tokio::try_join!(
219            option_try_or_else(self.env.chain_id, async || provider.get_chain_id().await),
220            provider.get_block_by_number(bn)
221        )
222        .wrap_err_with(|| {
223            let mut msg = "could not instantiate forked environment".to_string();
224            if let Some(fork_url) = self.fork_url.as_deref()
225                && let Ok(url) = Url::parse(fork_url)
226                && let Some(host) = url.host()
227            {
228                write!(msg, " with provider {host}").unwrap();
229            }
230            msg
231        })?;
232
233        let Some(block) = block else {
234            let bn_msg = match bn {
235                BlockNumberOrTag::Number(bn) => format!("block number: {bn}"),
236                bn => format!("{bn} block"),
237            };
238            let latest_msg = if let Ok(latest_block) = provider.get_block_number().await {
239                if let Some(block_number) = self.fork_block_number
240                    && block_number <= latest_block
241                {
242                    error!("{NON_ARCHIVE_NODE_WARNING}");
243                }
244                format!("; latest block number: {latest_block}")
245            } else {
246                Default::default()
247            };
248            eyre::bail!("failed to get {bn_msg}{latest_msg}");
249        };
250
251        let block_number = block.header().number();
252        let mut evm_env = EvmEnv {
253            cfg_env: self.cfg_env(chain_id),
254            block_env: block_env_from_header(block.header()),
255        };
256
257        apply_chain_and_block_specific_env_changes::<N, _, _>(&mut evm_env, &block, self.networks);
258
259        Ok((evm_env, block_number))
260    }
261
262    /// Returns the [`EvmEnv`] configured with only local settings.
263    fn local_evm_env<SPEC: Into<SpecId> + Default + Clone, BLOCK: FoundryBlock + Default>(
264        &self,
265    ) -> EvmEnv<SPEC, BLOCK> {
266        let cfg_env = self.cfg_env(self.env.chain_id.unwrap_or(foundry_common::DEV_CHAIN_ID));
267        let mut block_env = BLOCK::default();
268        block_env.set_number(self.env.block_number);
269        block_env.set_beneficiary(self.env.block_coinbase);
270        block_env.set_timestamp(self.env.block_timestamp);
271        block_env.set_difficulty(U256::from(self.env.block_difficulty));
272        block_env.set_prevrandao(Some(self.env.block_prevrandao));
273        block_env.set_basefee(self.env.block_base_fee_per_gas);
274        block_env.set_gas_limit(self.gas_limit());
275        EvmEnv::new(cfg_env, block_env)
276    }
277
278    /// Returns the `TxEnv` with gas price and chain id resolved from provider.
279    async fn fork_tx_env<TX: FoundryTransaction + Default, N: Network, P: Provider<N>>(
280        &self,
281        provider: &P,
282    ) -> eyre::Result<TX> {
283        let (gas_price, chain_id) = tokio::try_join!(
284            option_try_or_else(self.env.gas_price.map(|v| v as u128), async || {
285                provider.get_gas_price().await
286            }),
287            option_try_or_else(self.env.chain_id, async || provider.get_chain_id().await),
288        )?;
289        let mut tx_env = TX::default();
290        tx_env.set_caller(self.sender);
291        tx_env.set_chain_id(Some(chain_id));
292        tx_env.set_gas_price(gas_price);
293        tx_env.set_gas_limit(self.gas_limit());
294        Ok(tx_env)
295    }
296
297    /// Returns the `TxEnv` configured from local settings only.
298    fn local_tx_env<TX: FoundryTransaction + Default>(&self) -> TX {
299        let mut tx_env = TX::default();
300        tx_env.set_caller(self.sender);
301        tx_env.set_gas_price(self.env.gas_price.unwrap_or_default().into());
302        tx_env.set_gas_limit(self.gas_limit());
303        tx_env
304    }
305
306    /// Builds a [`CfgEnv`] from the options, using the provided [`ChainId`].
307    fn cfg_env<SPEC: Into<SpecId> + Default + Clone>(&self, chain_id: ChainId) -> CfgEnv<SPEC> {
308        let mut cfg = CfgEnv::default();
309        cfg.chain_id = chain_id;
310        cfg.memory_limit = self.memory_limit;
311        cfg.limit_contract_code_size = self.env.code_size_limit.or(Some(usize::MAX));
312        // EIP-3607 rejects transactions from senders with deployed code.
313        // If EIP-3607 is enabled it can cause issues during fuzz/invariant tests if the caller
314        // is a contract. So we disable the check by default.
315        cfg.disable_eip3607 = true;
316        cfg.disable_block_gas_limit = self.disable_block_gas_limit;
317        cfg.disable_nonce_check = true;
318        // By default do not enforce transaction gas limits imposed by Osaka (EIP-7825).
319        // Users can opt-in to enable these limits by setting `enable_tx_gas_limit` to true.
320        if !self.enable_tx_gas_limit {
321            cfg.tx_gas_limit_cap = Some(u64::MAX);
322        }
323        cfg
324    }
325
326    /// Helper function that returns the [CreateFork] to use, if any.
327    ///
328    /// storage caching for the [CreateFork] will be enabled if
329    ///   - `fork_url` is present
330    ///   - `fork_block_number` is present
331    ///   - `StorageCachingConfig` allows the `fork_url` + chain ID pair
332    ///   - storage is allowed (`no_storage_caching = false`)
333    ///
334    /// If all these criteria are met, then storage caching is enabled and storage info will be
335    /// written to `<Config::foundry_cache_dir()>/<str(chainid)>/<block>/storage.json`.
336    ///
337    /// for `mainnet` and `--fork-block-number 14435000` on mac the corresponding storage cache will
338    /// be at `~/.foundry/cache/mainnet/14435000/storage.json`.
339    /// `fork_block_number` is the actual block number to pin the fork to. This must be the
340    /// real chain block number, not a remapped value. On some L2s (e.g., Arbitrum)
341    /// `block_env.number` is remapped to the L1 block number, so callers must pass the
342    /// original block number returned by [`EvmOpts::env`] instead.
343    pub fn get_fork(
344        &self,
345        config: &Config,
346        chain_id: u64,
347        fork_block_number: Option<BlockNumber>,
348    ) -> Option<CreateFork> {
349        let url = self.fork_url.clone()?;
350        let enable_caching = config.enable_caching(&url, chain_id);
351
352        // Pin fork_block_number to the block that was already fetched in env, so subsequent
353        // fork operations use the same block. This prevents inconsistencies when forking at
354        // "latest" where the chain could advance between calls.
355        let mut evm_opts = self.clone();
356        evm_opts.fork_block_number = evm_opts.fork_block_number.or(fork_block_number);
357
358        Some(CreateFork { url, enable_caching, evm_opts })
359    }
360
361    /// Returns the gas limit to use
362    pub fn gas_limit(&self) -> u64 {
363        self.env.block_gas_limit.unwrap_or(self.env.gas_limit).0
364    }
365
366    /// Returns the available compute units per second, which will be
367    /// - u64::MAX, if `no_rpc_rate_limit` if set (as rate limiting is disabled)
368    /// - the assigned compute units, if `compute_units_per_second` is set
369    /// - ALCHEMY_FREE_TIER_CUPS (330) otherwise
370    const fn get_compute_units_per_second(&self) -> u64 {
371        if self.no_rpc_rate_limit {
372            u64::MAX
373        } else if let Some(cups) = self.compute_units_per_second {
374            cups
375        } else {
376            ALCHEMY_FREE_TIER_CUPS
377        }
378    }
379
380    /// Returns the chain ID from the RPC, if any.
381    pub async fn get_remote_chain_id(&self) -> Option<Chain> {
382        if let Some(url) = &self.fork_url
383            && let Ok(provider) = self.fork_provider_with_url::<AnyNetwork>(url)
384        {
385            trace!(?url, "retrieving chain via eth_chainId");
386
387            if let Ok(id) = provider.get_chain_id().await {
388                return Some(Chain::from(id));
389            }
390
391            // Provider URLs could be of the format `{CHAIN_IDENTIFIER}-mainnet`
392            // (e.g. Alchemy `opt-mainnet`, `arb-mainnet`), fallback to this method only
393            // if we're not able to retrieve chain id from `RetryProvider`.
394            if url.contains("mainnet") {
395                trace!(?url, "auto detected mainnet chain");
396                return Some(Chain::mainnet());
397            }
398        }
399
400        None
401    }
402}
403
404#[derive(Clone, Debug, Default, Serialize, Deserialize)]
405pub struct Env {
406    /// The block gas limit.
407    pub gas_limit: GasLimit,
408
409    /// The `CHAINID` opcode value.
410    pub chain_id: Option<u64>,
411
412    /// the tx.gasprice value during EVM execution
413    ///
414    /// This is an Option, so we can determine in fork mode whether to use the config's gas price
415    /// (if set by user) or the remote client's gas price.
416    #[serde(default, skip_serializing_if = "Option::is_none")]
417    pub gas_price: Option<u64>,
418
419    /// the base fee in a block
420    pub block_base_fee_per_gas: u64,
421
422    /// the tx.origin value during EVM execution
423    pub tx_origin: Address,
424
425    /// the block.coinbase value during EVM execution
426    pub block_coinbase: Address,
427
428    /// the block.timestamp value during EVM execution
429    #[serde(
430        deserialize_with = "foundry_config::deserialize_u64_to_u256",
431        serialize_with = "foundry_config::serialize_u64_or_u256"
432    )]
433    pub block_timestamp: U256,
434
435    /// the block.number value during EVM execution"
436    #[serde(
437        deserialize_with = "foundry_config::deserialize_u64_to_u256",
438        serialize_with = "foundry_config::serialize_u64_or_u256"
439    )]
440    pub block_number: U256,
441
442    /// the block.difficulty value during EVM execution
443    pub block_difficulty: u64,
444
445    /// Previous block beacon chain random value. Before merge this field is used for mix_hash
446    pub block_prevrandao: B256,
447
448    /// the block.gaslimit value during EVM execution
449    #[serde(default, skip_serializing_if = "Option::is_none")]
450    pub block_gas_limit: Option<GasLimit>,
451
452    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests.
453    #[serde(default, skip_serializing_if = "Option::is_none")]
454    pub code_size_limit: Option<usize>,
455}
456
457async fn option_try_or_else<T, E>(
458    option: Option<T>,
459    f: impl AsyncFnOnce() -> Result<T, E>,
460) -> Result<T, E> {
461    if let Some(value) = option { Ok(value) } else { f().await }
462}
463
464#[cfg(test)]
465mod tests {
466    use revm::context::{BlockEnv, TxEnv};
467
468    use super::*;
469
470    #[tokio::test(flavor = "multi_thread")]
471    async fn infer_network_default_anvil_selects_ethereum() {
472        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
473
474        let config = Config::figment();
475        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
476        evm_opts.fork_url = Some(handle.http_endpoint());
477        assert_eq!(evm_opts.networks, NetworkConfigs::default());
478
479        evm_opts.infer_network_from_fork().await;
480
481        // Plain anvil (chain id 31337) without tempo flag -> Ethereum (no network flags set).
482        assert_eq!(evm_opts.env.chain_id, Some(31337));
483        assert!(!evm_opts.networks.is_tempo());
484        #[cfg(feature = "optimism")]
485        assert!(!evm_opts.networks.is_optimism());
486        assert!(!evm_opts.networks.is_celo());
487        assert_eq!(evm_opts.networks, NetworkConfigs::default());
488    }
489
490    #[tokio::test(flavor = "multi_thread")]
491    async fn infer_network_tempo_anvil_via_node_info() {
492        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test_tempo()).await;
493
494        let config = Config::figment();
495        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
496        evm_opts.fork_url = Some(handle.http_endpoint());
497        // Networks not set -> should query anvil_nodeInfo to discover tempo.
498        assert_eq!(evm_opts.networks, NetworkConfigs::default());
499
500        evm_opts.infer_network_from_fork().await;
501
502        assert!(evm_opts.networks.is_tempo(), "should detect tempo via anvil_nodeInfo");
503    }
504
505    #[tokio::test(flavor = "multi_thread")]
506    async fn infer_network_tempo_anvil_skips_rpc_when_already_set() {
507        // Use a URL that would fail if any RPC call were attempted (connection refused).
508        // This proves the early-return guard prevents all network requests.
509        let config = Config::figment();
510        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
511        evm_opts.fork_url = Some("http://127.0.0.1:1".to_string());
512        // Explicitly set tempo before calling infer (simulates --tempo CLI flag).
513        evm_opts.networks = NetworkConfigs::with_tempo();
514
515        evm_opts.infer_network_from_fork().await;
516
517        // Should still be tempo, the early-return guard skips the RPC call.
518        assert!(evm_opts.networks.is_tempo());
519    }
520
521    #[tokio::test(flavor = "multi_thread")]
522    async fn flaky_infer_network_tempo_moderato_rpc() {
523        let config = Config::figment();
524        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
525        evm_opts.fork_url = Some("https://rpc.moderato.tempo.xyz".to_string());
526        assert_eq!(evm_opts.networks, NetworkConfigs::default());
527
528        evm_opts.infer_network_from_fork().await;
529
530        // Tempo Moderato has a known Tempo chain ID -> should be inferred via with_chain_id.
531        assert!(evm_opts.networks.is_tempo(), "should detect tempo from Moderato chain ID");
532    }
533
534    #[tokio::test(flavor = "multi_thread")]
535    async fn get_fork_pins_block_number_from_env() {
536        let endpoint = foundry_test_utils::rpc::next_http_rpc_endpoint();
537
538        let config = Config::figment();
539        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
540        evm_opts.fork_url = Some(endpoint.clone());
541        // Explicitly leave fork_block_number as None to simulate --fork-url without --block-number
542        assert!(evm_opts.fork_block_number.is_none());
543
544        // Fetch the environment (this resolves "latest" to an actual block number)
545        let (evm_env, _, fork_block) = evm_opts.env::<SpecId, BlockEnv, TxEnv>().await.unwrap();
546        assert!(fork_block.is_some(), "should have resolved a fork block number");
547        let resolved_block = fork_block.unwrap();
548        assert!(resolved_block > 0, "should have resolved to a real block number");
549
550        // Create the fork - this should pin the block number
551        let fork =
552            evm_opts.get_fork(&Config::default(), evm_env.cfg_env.chain_id, fork_block).unwrap();
553
554        // The fork's evm_opts should now have fork_block_number set to the resolved block
555        assert_eq!(
556            fork.evm_opts.fork_block_number,
557            Some(resolved_block),
558            "get_fork should pin fork_block_number to the block from env"
559        );
560    }
561
562    // Regression test for https://github.com/foundry-rs/foundry/issues/13576
563    // On Arbitrum, `block_env.number` is remapped to the L1 block number by
564    // `apply_chain_and_block_specific_env_changes`. The fork block number returned
565    // by `env()` must be the actual L2 block number, not the remapped L1 value.
566    #[tokio::test(flavor = "multi_thread")]
567    async fn flaky_get_fork_uses_l2_block_number_on_arbitrum() {
568        let endpoint =
569            foundry_test_utils::rpc::next_rpc_endpoint(foundry_config::NamedChain::Arbitrum);
570
571        let config = Config::figment();
572        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
573        evm_opts.fork_url = Some(endpoint.clone());
574        assert!(evm_opts.fork_block_number.is_none());
575
576        let (evm_env, _, fork_block) = evm_opts.env::<SpecId, BlockEnv, TxEnv>().await.unwrap();
577        let fork_block = fork_block.expect("should have resolved a fork block number");
578
579        // On Arbitrum, block_env.number is the L1 block number (much smaller).
580        // The fork_block should be the actual L2 block number (much larger).
581        let block_env_number: u64 = evm_env.block_env.number.to();
582        assert!(
583            fork_block > block_env_number,
584            "fork_block ({fork_block}) should be the L2 block, which is larger than \
585             block_env.number ({block_env_number}) which is the L1 block on Arbitrum"
586        );
587
588        // Verify get_fork pins to the correct L2 block number
589        let fork = evm_opts
590            .get_fork(&Config::default(), evm_env.cfg_env.chain_id, Some(fork_block))
591            .unwrap();
592        assert_eq!(
593            fork.evm_opts.fork_block_number,
594            Some(fork_block),
595            "get_fork should pin to the L2 block number, not the L1 block number"
596        );
597    }
598
599    #[tokio::test(flavor = "multi_thread")]
600    async fn get_fork_preserves_explicit_block_number() {
601        let endpoint = foundry_test_utils::rpc::next_http_rpc_endpoint();
602
603        let config = Config::figment();
604        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
605        evm_opts.fork_url = Some(endpoint.clone());
606        // Set an explicit block number
607        evm_opts.fork_block_number = Some(12345678);
608
609        let (evm_env, _, fork_block) = evm_opts.env::<SpecId, BlockEnv, TxEnv>().await.unwrap();
610
611        let fork =
612            evm_opts.get_fork(&Config::default(), evm_env.cfg_env.chain_id, fork_block).unwrap();
613
614        // Should preserve the explicit block number, not override it
615        assert_eq!(
616            fork.evm_opts.fork_block_number,
617            Some(12345678),
618            "get_fork should preserve explicitly set fork_block_number"
619        );
620    }
621}