Skip to main content

foundry_evm_core/
opts.rs

1use crate::{
2    EvmEnv,
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::{BlockEnv, CfgEnv, TxEnv};
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    /// Returns a tuple with [`EvmEnv`] and [`TxEnv`]
133    ///
134    /// If a `fork_url` is set, creates a provider and passes it to both `EvmOpts::fork_evm_env`
135    /// and `EvmOpts::fork_tx_env`. Falls back to local settings when no fork URL is configured.
136    pub async fn env(&self) -> eyre::Result<(EvmEnv, TxEnv)> {
137        if let Some(ref fork_url) = self.fork_url {
138            let provider = self.fork_provider_with_url::<AnyNetwork>(fork_url)?;
139            let ((evm_env, _block), tx) =
140                tokio::try_join!(self.fork_evm_env(&provider), self.fork_tx_env(&provider))?;
141            Ok((evm_env, tx))
142        } else {
143            Ok((self.local_evm_env(), self.local_tx_env()))
144        }
145    }
146
147    /// Returns the [`EvmEnv`] (cfg + block) and [`BlockNumber`] fetched from the fork endpoint via
148    /// provider
149    pub async fn fork_evm_env<N: Network, P: Provider<N>>(
150        &self,
151        provider: &P,
152    ) -> eyre::Result<(EvmEnv, BlockNumber)> {
153        trace!(
154            memory_limit = %self.memory_limit,
155            override_chain_id = ?self.env.chain_id,
156            pin_block = ?self.fork_block_number,
157            origin = %self.sender,
158            disable_block_gas_limit = %self.disable_block_gas_limit,
159            enable_tx_gas_limit = %self.enable_tx_gas_limit,
160            configs = ?self.networks,
161            "creating fork environment"
162        );
163
164        let bn = match self.fork_block_number {
165            Some(bn) => BlockNumberOrTag::Number(bn),
166            None => BlockNumberOrTag::Latest,
167        };
168
169        let (chain_id, block) = tokio::try_join!(
170            option_try_or_else(self.env.chain_id, async || provider.get_chain_id().await),
171            provider.get_block_by_number(bn)
172        )
173        .wrap_err_with(|| {
174            let mut msg = "could not instantiate forked environment".to_string();
175            if let Some(fork_url) = self.fork_url.as_deref()
176                && let Ok(url) = Url::parse(fork_url)
177                && let Some(host) = url.host()
178            {
179                write!(msg, " with provider {host}").unwrap();
180            }
181            msg
182        })?;
183
184        let Some(block) = block else {
185            let bn_msg = match bn {
186                BlockNumberOrTag::Number(bn) => format!("block number: {bn}"),
187                bn => format!("{bn} block"),
188            };
189            let latest_msg = if let Ok(latest_block) = provider.get_block_number().await {
190                if let Some(block_number) = self.fork_block_number
191                    && block_number <= latest_block
192                {
193                    error!("{NON_ARCHIVE_NODE_WARNING}");
194                }
195                format!("; latest block number: {latest_block}")
196            } else {
197                Default::default()
198            };
199            eyre::bail!("failed to get {bn_msg}{latest_msg}");
200        };
201
202        let block_number = block.header().number();
203        let mut evm_env = EvmEnv {
204            cfg_env: self.cfg_env(chain_id),
205            block_env: block_env_from_header(block.header()),
206        };
207
208        apply_chain_and_block_specific_env_changes::<N>(&mut evm_env, &block, self.networks);
209
210        Ok((evm_env, block_number))
211    }
212
213    /// Returns the [`EvmEnv`] configured with only local settings.
214    fn local_evm_env(&self) -> EvmEnv {
215        let gas_limit = self.gas_limit();
216        EvmEnv {
217            cfg_env: self.cfg_env(self.env.chain_id.unwrap_or(foundry_common::DEV_CHAIN_ID)),
218            block_env: BlockEnv {
219                number: self.env.block_number,
220                beneficiary: self.env.block_coinbase,
221                timestamp: self.env.block_timestamp,
222                difficulty: U256::from(self.env.block_difficulty),
223                prevrandao: Some(self.env.block_prevrandao),
224                basefee: self.env.block_base_fee_per_gas,
225                gas_limit,
226                ..Default::default()
227            },
228        }
229    }
230
231    /// Returns the [`TxEnv`] with gas price and chain id resolved from provider.
232    async fn fork_tx_env<N: Network, P: Provider<N>>(&self, provider: &P) -> eyre::Result<TxEnv> {
233        let (gas_price, chain_id) = tokio::try_join!(
234            option_try_or_else(self.env.gas_price.map(|v| v as u128), async || {
235                provider.get_gas_price().await
236            }),
237            option_try_or_else(self.env.chain_id, async || provider.get_chain_id().await),
238        )?;
239        Ok(TxEnv {
240            caller: self.sender,
241            gas_price,
242            chain_id: Some(chain_id),
243            gas_limit: self.gas_limit(),
244            ..Default::default()
245        })
246    }
247
248    /// Returns the [`TxEnv`] configured from local settings only.
249    fn local_tx_env(&self) -> TxEnv {
250        TxEnv {
251            caller: self.sender,
252            gas_price: self.env.gas_price.unwrap_or_default().into(),
253            gas_limit: self.gas_limit(),
254            ..Default::default()
255        }
256    }
257
258    /// Builds a [`CfgEnv`] from the options, using the provided [`ChainId`].
259    fn cfg_env(&self, chain_id: ChainId) -> CfgEnv {
260        let mut cfg = CfgEnv::default();
261        cfg.chain_id = chain_id;
262        cfg.memory_limit = self.memory_limit;
263        cfg.limit_contract_code_size = Some(usize::MAX);
264        // EIP-3607 rejects transactions from senders with deployed code.
265        // If EIP-3607 is enabled it can cause issues during fuzz/invariant tests if the caller
266        // is a contract. So we disable the check by default.
267        cfg.disable_eip3607 = true;
268        cfg.disable_block_gas_limit = self.disable_block_gas_limit;
269        cfg.disable_nonce_check = true;
270        // By default do not enforce transaction gas limits imposed by Osaka (EIP-7825).
271        // Users can opt-in to enable these limits by setting `enable_tx_gas_limit` to true.
272        if !self.enable_tx_gas_limit {
273            cfg.tx_gas_limit_cap = Some(u64::MAX);
274        }
275        cfg
276    }
277
278    /// Helper function that returns the [CreateFork] to use, if any.
279    ///
280    /// storage caching for the [CreateFork] will be enabled if
281    ///   - `fork_url` is present
282    ///   - `fork_block_number` is present
283    ///   - `StorageCachingConfig` allows the `fork_url` + chain ID pair
284    ///   - storage is allowed (`no_storage_caching = false`)
285    ///
286    /// If all these criteria are met, then storage caching is enabled and storage info will be
287    /// written to `<Config::foundry_cache_dir()>/<str(chainid)>/<block>/storage.json`.
288    ///
289    /// for `mainnet` and `--fork-block-number 14435000` on mac the corresponding storage cache will
290    /// be at `~/.foundry/cache/mainnet/14435000/storage.json`.
291    pub fn get_fork(&self, config: &Config, evm_env: EvmEnv) -> Option<CreateFork> {
292        let url = self.fork_url.clone()?;
293        let enable_caching = config.enable_caching(&url, evm_env.cfg_env.chain_id);
294
295        // Pin fork_block_number to the block that was already fetched in env, so subsequent
296        // fork operations use the same block. This prevents inconsistencies when forking at
297        // "latest" where the chain could advance between calls.
298        let mut evm_opts = self.clone();
299        if evm_opts.fork_block_number.is_none() {
300            evm_opts.fork_block_number = Some(evm_env.block_env.number.to());
301        }
302
303        Some(CreateFork { url, enable_caching, evm_env, evm_opts })
304    }
305
306    /// Returns the gas limit to use
307    pub fn gas_limit(&self) -> u64 {
308        self.env.block_gas_limit.unwrap_or(self.env.gas_limit).0
309    }
310
311    /// Returns the available compute units per second, which will be
312    /// - u64::MAX, if `no_rpc_rate_limit` if set (as rate limiting is disabled)
313    /// - the assigned compute units, if `compute_units_per_second` is set
314    /// - ALCHEMY_FREE_TIER_CUPS (330) otherwise
315    fn get_compute_units_per_second(&self) -> u64 {
316        if self.no_rpc_rate_limit {
317            u64::MAX
318        } else if let Some(cups) = self.compute_units_per_second {
319            cups
320        } else {
321            ALCHEMY_FREE_TIER_CUPS
322        }
323    }
324
325    /// Returns the chain ID from the RPC, if any.
326    pub async fn get_remote_chain_id(&self) -> Option<Chain> {
327        if let Some(url) = &self.fork_url
328            && let Ok(provider) = self.fork_provider_with_url::<AnyNetwork>(url)
329        {
330            trace!(?url, "retrieving chain via eth_chainId");
331
332            if let Ok(id) = provider.get_chain_id().await {
333                return Some(Chain::from(id));
334            }
335
336            // Provider URLs could be of the format `{CHAIN_IDENTIFIER}-mainnet`
337            // (e.g. Alchemy `opt-mainnet`, `arb-mainnet`), fallback to this method only
338            // if we're not able to retrieve chain id from `RetryProvider`.
339            if url.contains("mainnet") {
340                trace!(?url, "auto detected mainnet chain");
341                return Some(Chain::mainnet());
342            }
343        }
344
345        None
346    }
347}
348
349#[derive(Clone, Debug, Default, Serialize, Deserialize)]
350pub struct Env {
351    /// The block gas limit.
352    pub gas_limit: GasLimit,
353
354    /// The `CHAINID` opcode value.
355    pub chain_id: Option<u64>,
356
357    /// the tx.gasprice value during EVM execution
358    ///
359    /// This is an Option, so we can determine in fork mode whether to use the config's gas price
360    /// (if set by user) or the remote client's gas price.
361    #[serde(default, skip_serializing_if = "Option::is_none")]
362    pub gas_price: Option<u64>,
363
364    /// the base fee in a block
365    pub block_base_fee_per_gas: u64,
366
367    /// the tx.origin value during EVM execution
368    pub tx_origin: Address,
369
370    /// the block.coinbase value during EVM execution
371    pub block_coinbase: Address,
372
373    /// the block.timestamp value during EVM execution
374    #[serde(
375        deserialize_with = "foundry_config::deserialize_u64_to_u256",
376        serialize_with = "foundry_config::serialize_u64_or_u256"
377    )]
378    pub block_timestamp: U256,
379
380    /// the block.number value during EVM execution"
381    #[serde(
382        deserialize_with = "foundry_config::deserialize_u64_to_u256",
383        serialize_with = "foundry_config::serialize_u64_or_u256"
384    )]
385    pub block_number: U256,
386
387    /// the block.difficulty value during EVM execution
388    pub block_difficulty: u64,
389
390    /// Previous block beacon chain random value. Before merge this field is used for mix_hash
391    pub block_prevrandao: B256,
392
393    /// the block.gaslimit value during EVM execution
394    #[serde(default, skip_serializing_if = "Option::is_none")]
395    pub block_gas_limit: Option<GasLimit>,
396
397    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests.
398    #[serde(default, skip_serializing_if = "Option::is_none")]
399    pub code_size_limit: Option<usize>,
400}
401
402async fn option_try_or_else<T, E>(
403    option: Option<T>,
404    f: impl AsyncFnOnce() -> Result<T, E>,
405) -> Result<T, E> {
406    if let Some(value) = option { Ok(value) } else { f().await }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    #[tokio::test(flavor = "multi_thread")]
414    async fn get_fork_pins_block_number_from_env() {
415        let endpoint = foundry_test_utils::rpc::next_http_rpc_endpoint();
416
417        let config = Config::figment();
418        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
419        evm_opts.fork_url = Some(endpoint.clone());
420        // Explicitly leave fork_block_number as None to simulate --fork-url without --block-number
421        assert!(evm_opts.fork_block_number.is_none());
422
423        // Fetch the environment (this resolves "latest" to an actual block number)
424        let (evm_env, _) = evm_opts.env().await.unwrap();
425        let resolved_block = evm_env.block_env.number;
426        assert!(resolved_block > U256::ZERO, "should have resolved to a real block number");
427
428        // Create the fork - this should pin the block number
429        let fork = evm_opts.get_fork(&Config::default(), evm_env).unwrap();
430
431        // The fork's evm_opts should now have fork_block_number set to the resolved block
432        assert_eq!(
433            fork.evm_opts.fork_block_number,
434            Some(resolved_block.to::<u64>()),
435            "get_fork should pin fork_block_number to the block from env"
436        );
437    }
438
439    #[tokio::test(flavor = "multi_thread")]
440    async fn get_fork_preserves_explicit_block_number() {
441        let endpoint = foundry_test_utils::rpc::next_http_rpc_endpoint();
442
443        let config = Config::figment();
444        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
445        evm_opts.fork_url = Some(endpoint.clone());
446        // Set an explicit block number
447        evm_opts.fork_block_number = Some(12345678);
448
449        let (evm_env, _) = evm_opts.env().await.unwrap();
450
451        let fork = evm_opts.get_fork(&Config::default(), evm_env).unwrap();
452
453        // Should preserve the explicit block number, not override it
454        assert_eq!(
455            fork.evm_opts.fork_block_number,
456            Some(12345678),
457            "get_fork should preserve explicitly set fork_block_number"
458        );
459    }
460}