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 #[serde(flatten)]
26 pub env: Env,
27
28 #[serde(rename = "eth_rpc_url")]
30 pub fork_url: Option<String>,
31
32 pub fork_block_number: Option<u64>,
34
35 pub fork_retries: Option<u32>,
37
38 pub fork_retry_backoff: Option<u64>,
40
41 pub fork_headers: Option<Vec<String>>,
43
44 pub compute_units_per_second: Option<u64>,
48
49 pub no_rpc_rate_limit: bool,
51
52 pub no_storage_caching: bool,
54
55 pub initial_balance: U256,
57
58 pub sender: Address,
60
61 pub ffi: bool,
63
64 pub always_use_create_2_factory: bool,
66
67 pub verbosity: u8,
69
70 pub memory_limit: u64,
73
74 pub isolate: bool,
76
77 pub disable_block_gas_limit: bool,
79
80 pub enable_tx_gas_limit: bool,
82
83 #[serde(flatten)]
84 pub networks: NetworkConfigs,
86
87 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 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 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 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 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 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 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 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 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 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 cfg.disable_eip3607 = true;
316 cfg.disable_block_gas_limit = self.disable_block_gas_limit;
317 cfg.disable_nonce_check = true;
318 if !self.enable_tx_gas_limit {
321 cfg.tx_gas_limit_cap = Some(u64::MAX);
322 }
323 cfg
324 }
325
326 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 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 pub fn gas_limit(&self) -> u64 {
363 self.env.block_gas_limit.unwrap_or(self.env.gas_limit).0
364 }
365
366 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 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 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 pub gas_limit: GasLimit,
408
409 pub chain_id: Option<u64>,
411
412 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub gas_price: Option<u64>,
418
419 pub block_base_fee_per_gas: u64,
421
422 pub tx_origin: Address,
424
425 pub block_coinbase: Address,
427
428 #[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 #[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 pub block_difficulty: u64,
444
445 pub block_prevrandao: B256,
447
448 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub block_gas_limit: Option<GasLimit>,
451
452 #[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 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 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 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 evm_opts.networks = NetworkConfigs::with_tempo();
514
515 evm_opts.infer_network_from_fork().await;
516
517 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 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 assert!(evm_opts.fork_block_number.is_none());
543
544 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 let fork =
552 evm_opts.get_fork(&Config::default(), evm_env.cfg_env.chain_id, fork_block).unwrap();
553
554 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 #[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 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 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 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 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}