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