Skip to main content

foundry_evm_core/
opts.rs

1use super::fork::environment;
2use crate::{
3    EvmEnv,
4    constants::DEFAULT_CREATE2_DEPLOYER,
5    fork::{CreateFork, configure_env},
6};
7use alloy_network::{AnyNetwork, Network};
8use alloy_primitives::{Address, B256, U256};
9use alloy_provider::{Provider, RootProvider, network::AnyRpcBlock};
10use eyre::WrapErr;
11use foundry_common::{ALCHEMY_FREE_TIER_CUPS, provider::ProviderBuilder};
12use foundry_config::{Chain, Config, GasLimit};
13use foundry_evm_networks::NetworkConfigs;
14use revm::context::{BlockEnv, TxEnv};
15use serde::{Deserialize, Serialize};
16use std::fmt::Write;
17use url::Url;
18
19#[derive(Clone, Debug, Serialize, Deserialize)]
20pub struct EvmOpts {
21    /// The EVM environment configuration.
22    #[serde(flatten)]
23    pub env: Env,
24
25    /// Fetch state over a remote instead of starting from empty state.
26    #[serde(rename = "eth_rpc_url")]
27    pub fork_url: Option<String>,
28
29    /// Pins the block number for the state fork.
30    pub fork_block_number: Option<u64>,
31
32    /// The number of retries.
33    pub fork_retries: Option<u32>,
34
35    /// Initial retry backoff.
36    pub fork_retry_backoff: Option<u64>,
37
38    /// Headers to use with `fork_url`
39    pub fork_headers: Option<Vec<String>>,
40
41    /// The available compute units per second.
42    ///
43    /// See also <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
44    pub compute_units_per_second: Option<u64>,
45
46    /// Disables RPC rate limiting entirely.
47    pub no_rpc_rate_limit: bool,
48
49    /// Disables storage caching entirely.
50    pub no_storage_caching: bool,
51
52    /// The initial balance of each deployed test contract.
53    pub initial_balance: U256,
54
55    /// The address which will be executing all tests.
56    pub sender: Address,
57
58    /// Enables the FFI cheatcode.
59    pub ffi: bool,
60
61    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
62    pub always_use_create_2_factory: bool,
63
64    /// Verbosity mode of EVM output as number of occurrences.
65    pub verbosity: u8,
66
67    /// The memory limit per EVM execution in bytes.
68    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
69    pub memory_limit: u64,
70
71    /// Whether to enable isolation of calls.
72    pub isolate: bool,
73
74    /// Whether to disable block gas limit checks.
75    pub disable_block_gas_limit: bool,
76
77    /// Whether to enable tx gas limit checks as imposed by Osaka (EIP-7825).
78    pub enable_tx_gas_limit: bool,
79
80    #[serde(flatten)]
81    /// Networks with enabled features.
82    pub networks: NetworkConfigs,
83
84    /// The CREATE2 deployer's address.
85    pub create2_deployer: Address,
86}
87
88impl Default for EvmOpts {
89    fn default() -> Self {
90        Self {
91            env: Env::default(),
92            fork_url: None,
93            fork_block_number: None,
94            fork_retries: None,
95            fork_retry_backoff: None,
96            fork_headers: None,
97            compute_units_per_second: None,
98            no_rpc_rate_limit: false,
99            no_storage_caching: false,
100            initial_balance: U256::default(),
101            sender: Address::default(),
102            ffi: false,
103            always_use_create_2_factory: false,
104            verbosity: 0,
105            memory_limit: 0,
106            isolate: false,
107            disable_block_gas_limit: false,
108            enable_tx_gas_limit: false,
109            networks: NetworkConfigs::default(),
110            create2_deployer: DEFAULT_CREATE2_DEPLOYER,
111        }
112    }
113}
114
115impl EvmOpts {
116    /// Returns a `RootProvider` for the given fork URL configured with options in `self` and
117    /// annotated `Network` type.
118    pub fn fork_provider_with_url<N: Network>(
119        &self,
120        fork_url: &str,
121    ) -> eyre::Result<RootProvider<N>> {
122        ProviderBuilder::new(fork_url)
123            .maybe_max_retry(self.fork_retries)
124            .maybe_initial_backoff(self.fork_retry_backoff)
125            .maybe_headers(self.fork_headers.clone())
126            .compute_units_per_second(self.get_compute_units_per_second())
127            .build()
128    }
129
130    /// Configures a new `revm::Env`
131    ///
132    /// If a `fork_url` is set, it gets configured with settings fetched from the endpoint (chain
133    /// id, )
134    pub async fn evm_env(&self) -> eyre::Result<crate::Env> {
135        if let Some(ref fork_url) = self.fork_url {
136            Ok(self.fork_evm_env(fork_url).await?.0)
137        } else {
138            Ok(self.local_evm_env())
139        }
140    }
141
142    /// Returns the `revm::Env` that is configured with settings retrieved from the endpoint,
143    /// and the block that was used to configure the environment.
144    pub async fn fork_evm_env(&self, fork_url: &str) -> eyre::Result<(crate::Env, AnyRpcBlock)> {
145        let provider = self.fork_provider_with_url::<AnyNetwork>(fork_url)?;
146        self.fork_evm_env_with_provider(fork_url, &provider).await
147    }
148
149    /// Returns the `revm::Env` that is configured with settings retrieved from the provider,
150    /// and the block that was used to configure the environment.
151    pub async fn fork_evm_env_with_provider<P: Provider<N>, N: Network>(
152        &self,
153        fork_url: &str,
154        provider: &P,
155    ) -> eyre::Result<(crate::Env, N::BlockResponse)> {
156        environment(
157            provider,
158            self.memory_limit,
159            self.env.gas_price.map(|v| v as u128),
160            self.env.chain_id,
161            self.fork_block_number,
162            self.sender,
163            self.disable_block_gas_limit,
164            self.enable_tx_gas_limit,
165            self.networks,
166        )
167        .await
168        .wrap_err_with(|| {
169            let mut msg = "could not instantiate forked environment".to_string();
170            if let Ok(url) = Url::parse(fork_url)
171                && let Some(provider) = url.host()
172            {
173                write!(msg, " with provider {provider}").unwrap();
174            }
175            msg
176        })
177    }
178
179    /// Returns the `revm::Env` configured with only local settings
180    fn local_evm_env(&self) -> crate::Env {
181        let cfg = configure_env(
182            self.env.chain_id.unwrap_or(foundry_common::DEV_CHAIN_ID),
183            self.memory_limit,
184            self.disable_block_gas_limit,
185            self.enable_tx_gas_limit,
186        );
187
188        crate::Env {
189            evm_env: EvmEnv {
190                cfg_env: cfg,
191                block_env: BlockEnv {
192                    number: self.env.block_number,
193                    beneficiary: self.env.block_coinbase,
194                    timestamp: self.env.block_timestamp,
195                    difficulty: U256::from(self.env.block_difficulty),
196                    prevrandao: Some(self.env.block_prevrandao),
197                    basefee: self.env.block_base_fee_per_gas,
198                    gas_limit: self.gas_limit(),
199                    ..Default::default()
200                },
201            },
202            tx: TxEnv {
203                gas_price: self.env.gas_price.unwrap_or_default().into(),
204                gas_limit: self.gas_limit(),
205                caller: self.sender,
206                ..Default::default()
207            },
208        }
209    }
210
211    /// Helper function that returns the [CreateFork] to use, if any.
212    ///
213    /// storage caching for the [CreateFork] will be enabled if
214    ///   - `fork_url` is present
215    ///   - `fork_block_number` is present
216    ///   - `StorageCachingConfig` allows the `fork_url` + chain ID pair
217    ///   - storage is allowed (`no_storage_caching = false`)
218    ///
219    /// If all these criteria are met, then storage caching is enabled and storage info will be
220    /// written to `<Config::foundry_cache_dir()>/<str(chainid)>/<block>/storage.json`.
221    ///
222    /// for `mainnet` and `--fork-block-number 14435000` on mac the corresponding storage cache will
223    /// be at `~/.foundry/cache/mainnet/14435000/storage.json`.
224    pub fn get_fork(&self, config: &Config, env: crate::Env) -> Option<CreateFork> {
225        let url = self.fork_url.clone()?;
226        let enable_caching = config.enable_caching(&url, env.evm_env.cfg_env.chain_id);
227
228        // Pin fork_block_number to the block that was already fetched in env, so subsequent
229        // fork operations use the same block. This prevents inconsistencies when forking at
230        // "latest" where the chain could advance between calls.
231        let mut evm_opts = self.clone();
232        if evm_opts.fork_block_number.is_none() {
233            evm_opts.fork_block_number = Some(env.evm_env.block_env.number.to());
234        }
235
236        Some(CreateFork { url, enable_caching, env, evm_opts })
237    }
238
239    /// Returns the gas limit to use
240    pub fn gas_limit(&self) -> u64 {
241        self.env.block_gas_limit.unwrap_or(self.env.gas_limit).0
242    }
243
244    /// Returns the available compute units per second, which will be
245    /// - u64::MAX, if `no_rpc_rate_limit` if set (as rate limiting is disabled)
246    /// - the assigned compute units, if `compute_units_per_second` is set
247    /// - ALCHEMY_FREE_TIER_CUPS (330) otherwise
248    fn get_compute_units_per_second(&self) -> u64 {
249        if self.no_rpc_rate_limit {
250            u64::MAX
251        } else if let Some(cups) = self.compute_units_per_second {
252            cups
253        } else {
254            ALCHEMY_FREE_TIER_CUPS
255        }
256    }
257
258    /// Returns the chain ID from the RPC, if any.
259    pub async fn get_remote_chain_id(&self) -> Option<Chain> {
260        if let Some(url) = &self.fork_url
261            && let Ok(provider) = self.fork_provider_with_url::<AnyNetwork>(url)
262        {
263            trace!(?url, "retrieving chain via eth_chainId");
264
265            if let Ok(id) = provider.get_chain_id().await {
266                return Some(Chain::from(id));
267            }
268
269            // Provider URLs could be of the format `{CHAIN_IDENTIFIER}-mainnet`
270            // (e.g. Alchemy `opt-mainnet`, `arb-mainnet`), fallback to this method only
271            // if we're not able to retrieve chain id from `RetryProvider`.
272            if url.contains("mainnet") {
273                trace!(?url, "auto detected mainnet chain");
274                return Some(Chain::mainnet());
275            }
276        }
277
278        None
279    }
280}
281
282#[derive(Clone, Debug, Default, Serialize, Deserialize)]
283pub struct Env {
284    /// The block gas limit.
285    pub gas_limit: GasLimit,
286
287    /// The `CHAINID` opcode value.
288    pub chain_id: Option<u64>,
289
290    /// the tx.gasprice value during EVM execution
291    ///
292    /// This is an Option, so we can determine in fork mode whether to use the config's gas price
293    /// (if set by user) or the remote client's gas price.
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub gas_price: Option<u64>,
296
297    /// the base fee in a block
298    pub block_base_fee_per_gas: u64,
299
300    /// the tx.origin value during EVM execution
301    pub tx_origin: Address,
302
303    /// the block.coinbase value during EVM execution
304    pub block_coinbase: Address,
305
306    /// the block.timestamp value during EVM execution
307    #[serde(
308        deserialize_with = "foundry_config::deserialize_u64_to_u256",
309        serialize_with = "foundry_config::serialize_u64_or_u256"
310    )]
311    pub block_timestamp: U256,
312
313    /// the block.number value during EVM execution"
314    #[serde(
315        deserialize_with = "foundry_config::deserialize_u64_to_u256",
316        serialize_with = "foundry_config::serialize_u64_or_u256"
317    )]
318    pub block_number: U256,
319
320    /// the block.difficulty value during EVM execution
321    pub block_difficulty: u64,
322
323    /// Previous block beacon chain random value. Before merge this field is used for mix_hash
324    pub block_prevrandao: B256,
325
326    /// the block.gaslimit value during EVM execution
327    #[serde(default, skip_serializing_if = "Option::is_none")]
328    pub block_gas_limit: Option<GasLimit>,
329
330    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests.
331    #[serde(default, skip_serializing_if = "Option::is_none")]
332    pub code_size_limit: Option<usize>,
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[tokio::test(flavor = "multi_thread")]
340    async fn get_fork_pins_block_number_from_env() {
341        let endpoint = foundry_test_utils::rpc::next_http_rpc_endpoint();
342
343        let config = Config::figment();
344        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
345        evm_opts.fork_url = Some(endpoint.clone());
346        // Explicitly leave fork_block_number as None to simulate --fork-url without --block-number
347        assert!(evm_opts.fork_block_number.is_none());
348
349        // Fetch the environment (this resolves "latest" to an actual block number)
350        let env = evm_opts.evm_env().await.unwrap();
351        let resolved_block = env.evm_env.block_env.number;
352        assert!(resolved_block > U256::ZERO, "should have resolved to a real block number");
353
354        // Create the fork - this should pin the block number
355        let fork = evm_opts.get_fork(&Config::default(), env).unwrap();
356
357        // The fork's evm_opts should now have fork_block_number set to the resolved block
358        assert_eq!(
359            fork.evm_opts.fork_block_number,
360            Some(resolved_block.to::<u64>()),
361            "get_fork should pin fork_block_number to the block from env"
362        );
363    }
364
365    #[tokio::test(flavor = "multi_thread")]
366    async fn get_fork_preserves_explicit_block_number() {
367        let endpoint = foundry_test_utils::rpc::next_http_rpc_endpoint();
368
369        let config = Config::figment();
370        let mut evm_opts = config.extract::<EvmOpts>().unwrap();
371        evm_opts.fork_url = Some(endpoint.clone());
372        // Set an explicit block number
373        evm_opts.fork_block_number = Some(12345678);
374
375        let env = evm_opts.evm_env().await.unwrap();
376
377        let fork = evm_opts.get_fork(&Config::default(), env).unwrap();
378
379        // Should preserve the explicit block number, not override it
380        assert_eq!(
381            fork.evm_opts.fork_block_number,
382            Some(12345678),
383            "get_fork should preserve explicitly set fork_block_number"
384        );
385    }
386}