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 #[serde(flatten)]
25 pub env: Env,
26
27 #[serde(rename = "eth_rpc_url")]
29 pub fork_url: Option<String>,
30
31 pub fork_block_number: Option<u64>,
33
34 pub fork_retries: Option<u32>,
36
37 pub fork_retry_backoff: Option<u64>,
39
40 pub fork_headers: Option<Vec<String>>,
42
43 pub compute_units_per_second: Option<u64>,
47
48 pub no_rpc_rate_limit: bool,
50
51 pub no_storage_caching: bool,
53
54 pub initial_balance: U256,
56
57 pub sender: Address,
59
60 pub ffi: bool,
62
63 pub always_use_create_2_factory: bool,
65
66 pub verbosity: u8,
68
69 pub memory_limit: u64,
72
73 pub isolate: bool,
75
76 pub disable_block_gas_limit: bool,
78
79 pub enable_tx_gas_limit: bool,
81
82 #[serde(flatten)]
83 pub networks: NetworkConfigs,
85
86 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 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 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 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 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 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 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 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 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 cfg.disable_eip3607 = true;
299 cfg.disable_block_gas_limit = self.disable_block_gas_limit;
300 cfg.disable_nonce_check = true;
301 if !self.enable_tx_gas_limit {
304 cfg.tx_gas_limit_cap = Some(u64::MAX);
305 }
306 cfg
307 }
308
309 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 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 pub fn gas_limit(&self) -> u64 {
346 self.env.block_gas_limit.unwrap_or(self.env.gas_limit).0
347 }
348
349 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 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 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 pub gas_limit: GasLimit,
391
392 pub chain_id: Option<u64>,
394
395 #[serde(default, skip_serializing_if = "Option::is_none")]
400 pub gas_price: Option<u64>,
401
402 pub block_base_fee_per_gas: u64,
404
405 pub tx_origin: Address,
407
408 pub block_coinbase: Address,
410
411 #[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 #[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 pub block_difficulty: u64,
427
428 pub block_prevrandao: B256,
430
431 #[serde(default, skip_serializing_if = "Option::is_none")]
433 pub block_gas_limit: Option<GasLimit>,
434
435 #[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 assert!(evm_opts.fork_block_number.is_none());
462
463 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 let fork =
471 evm_opts.get_fork(&Config::default(), evm_env.cfg_env.chain_id, fork_block).unwrap();
472
473 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 #[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 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 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 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 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}