1use crate::{
2 EvmEnv, FoundryBlock, FoundryTransaction,
3 constants::DEFAULT_CREATE2_DEPLOYER,
4 fork::{CreateFork, Fork},
5 utils::{apply_chain_and_block_specific_env_changes_for_chain, block_env_from_header},
6};
7use alloy_consensus::BlockHeader;
8use alloy_eips::BlockNumHash;
9use alloy_network::{AnyNetwork, BlockResponse, Network, primitives::HeaderResponse};
10use alloy_primitives::{Address, B256, BlockNumber, ChainId, U256};
11use alloy_provider::{Provider, RootProvider};
12use alloy_rpc_client::RpcClient;
13use alloy_rpc_types::{
14 BlockNumberOrTag,
15 anvil::{Metadata, NodeInfo},
16};
17use eyre::{OptionExt, WrapErr};
18use foundry_common::{
19 ALCHEMY_FREE_TIER_CUPS, NON_ARCHIVE_NODE_WARNING,
20 provider::{ProviderBuilder, is_rpc_method_not_found},
21};
22use foundry_compilers::artifacts::EvmVersion;
23use foundry_config::{Chain, Config, ExecutionSpec, FoundryHardfork, GasLimit, evm_spec_id};
24use foundry_evm_networks::{NetworkConfigs, NetworkVariant};
25use revm::{context::CfgEnv, primitives::hardfork::SpecId};
26use serde::{Deserialize, Serialize};
27use std::fmt::Write;
28use url::Url;
29
30#[derive(Clone, Debug, Serialize, Deserialize)]
44pub struct EvmOpts {
45 #[serde(flatten)]
47 pub env: Env,
48
49 #[serde(rename = "eth_rpc_url")]
51 pub fork_url: Option<String>,
52
53 pub fork_block_number: Option<u64>,
55
56 #[serde(default)]
58 pub fork_state_by_number: bool,
59
60 pub fork_retries: Option<u32>,
62
63 pub fork_retry_backoff: Option<u64>,
65
66 pub fork_headers: Option<Vec<String>>,
68
69 #[serde(rename = "eth_rpc_jwt")]
71 pub rpc_jwt: Option<String>,
72
73 #[serde(rename = "eth_rpc_headers")]
75 pub rpc_headers: Option<Vec<String>>,
76
77 #[serde(rename = "eth_rpc_timeout")]
79 pub rpc_timeout: Option<u64>,
80
81 #[serde(default, rename = "eth_rpc_accept_invalid_certs")]
83 pub rpc_accept_invalid_certs: bool,
84
85 #[serde(default, rename = "eth_rpc_no_proxy")]
87 pub rpc_no_proxy: bool,
88
89 pub compute_units_per_second: Option<u64>,
93
94 pub no_rpc_rate_limit: bool,
96
97 pub no_storage_caching: bool,
99
100 #[serde(default)]
102 pub no_fork_bal: bool,
103
104 #[serde(default)]
106 pub fork_prefetch: bool,
107
108 pub initial_balance: U256,
110
111 pub sender: Address,
113
114 pub ffi: bool,
116
117 #[serde(skip)]
120 pub no_network: bool,
121
122 pub always_use_create_2_factory: bool,
124
125 pub verbosity: u8,
127
128 pub memory_limit: u64,
131
132 pub isolate: bool,
134
135 pub disable_block_gas_limit: bool,
137
138 pub enable_tx_gas_limit: bool,
140
141 #[serde(flatten)]
142 pub networks: NetworkConfigs,
144
145 pub create2_deployer: Address,
147
148 #[serde(skip)]
153 pub fork_endpoint: Option<ForkEndpointIdentity>,
154
155 #[serde(skip)]
157 pub expected_fork_endpoint: Option<ForkEndpointIdentity>,
158
159 #[serde(skip)]
161 pub fork_network_is_inferred: bool,
162
163 #[serde(skip)]
165 pub fork_chain_id_is_inferred: bool,
166
167 #[serde(skip)]
169 pub fork_block_number_is_inferred: bool,
170}
171
172#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct ForkEndpointIdentity {
181 pub endpoint: String,
185 pub execution_chain_id: ChainId,
187 pub source_chain_id: ChainId,
189 pub network: NetworkVariant,
191 pub network_profile: NetworkConfigs,
193 pub reported_hardfork: Option<String>,
195 pub hardfork: Option<FoundryHardfork>,
197 pub instance_id: Option<B256>,
199 pub source_fork_block_number: Option<BlockNumber>,
201 pub source_fork_block_hash: Option<B256>,
203}
204
205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
206enum EndpointHardforkPolicy {
207 Optional,
209 Required,
211}
212
213#[derive(Clone, Copy, Debug, Default)]
223struct AnvilNodeInfoProbe {
224 identified: bool,
225}
226
227impl AnvilNodeInfoProbe {
228 const fn new(identified: bool) -> Self {
229 Self { identified }
230 }
231
232 async fn request<N: Network, P: Provider<N>>(
233 &mut self,
234 provider: &P,
235 ) -> eyre::Result<Option<NodeInfo>> {
236 match provider.raw_request::<_, NodeInfo>("anvil_nodeInfo".into(), ()).await {
237 Ok(node_info) => {
238 self.identified = true;
239 Ok(Some(node_info))
240 }
241 Err(_) if !self.identified => Ok(None),
242 Err(error) => Err(error).wrap_err("failed to determine network family from endpoint"),
243 }
244 }
245}
246
247fn endpoint_hardfork(
248 network: NetworkVariant,
249 hardfork: &str,
250 policy: EndpointHardforkPolicy,
251) -> eyre::Result<Option<FoundryHardfork>> {
252 match network.parse_hardfork(hardfork) {
253 Ok(hardfork) => Ok(Some(hardfork)),
254 Err(_) if policy == EndpointHardforkPolicy::Optional => Ok(None),
255 Err(error) => Err(eyre::Report::msg(error)).wrap_err_with(|| {
256 format!("unsupported hardfork `{hardfork}` reported for `{network}`")
257 }),
258 }
259}
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize)]
267pub struct ForkContext {
268 pub execution_chain_id: ChainId,
270 pub source_chain_id: ChainId,
272 pub network: NetworkVariant,
274 pub network_profile: NetworkConfigs,
276 pub block_number: BlockNumber,
278 pub hardfork: Option<FoundryHardfork>,
280 pub instance_id: Option<B256>,
282 pub source_fork_block_number: Option<BlockNumber>,
284 pub source_fork_block_hash: Option<B256>,
286}
287
288impl ForkContext {
289 pub fn matches_identity(self, identity: &ForkEndpointIdentity) -> bool {
291 self.execution_chain_id == identity.execution_chain_id
292 && self.source_chain_id == identity.source_chain_id
293 && self.network == identity.network
294 && self.network_profile == identity.network_profile
295 && self.hardfork == identity.hardfork
296 && self.instance_id == identity.instance_id
297 && self.source_fork_block_number == identity.source_fork_block_number
298 && self.source_fork_block_hash == identity.source_fork_block_hash
299 }
300
301 pub(crate) fn has_same_endpoint_identity(self, other: Self) -> bool {
303 self.execution_chain_id == other.execution_chain_id
304 && self.source_chain_id == other.source_chain_id
305 && self.network == other.network
306 && self.network_profile == other.network_profile
307 && self.hardfork == other.hardfork
308 && self.instance_id == other.instance_id
309 && self.source_fork_block_number == other.source_fork_block_number
310 && self.source_fork_block_hash == other.source_fork_block_hash
311 }
312
313 pub fn has_same_backend_target(self, other: Self) -> bool {
315 self.has_same_endpoint_identity(other) && self.block_number == other.block_number
316 }
317}
318
319impl Default for EvmOpts {
320 fn default() -> Self {
321 Self {
322 env: Env::default(),
323 fork_url: None,
324 fork_block_number: None,
325 fork_state_by_number: false,
326 fork_retries: None,
327 fork_retry_backoff: None,
328 fork_headers: None,
329 rpc_jwt: None,
330 rpc_headers: None,
331 rpc_timeout: None,
332 rpc_accept_invalid_certs: false,
333 rpc_no_proxy: false,
334 compute_units_per_second: None,
335 no_rpc_rate_limit: false,
336 no_storage_caching: false,
337 no_fork_bal: false,
338 fork_prefetch: false,
339 initial_balance: U256::default(),
340 sender: Address::default(),
341 ffi: false,
342 no_network: false,
343 always_use_create_2_factory: false,
344 verbosity: 0,
345 memory_limit: 0,
346 isolate: false,
347 disable_block_gas_limit: false,
348 enable_tx_gas_limit: false,
349 networks: NetworkConfigs::default(),
350 create2_deployer: DEFAULT_CREATE2_DEPLOYER,
351 fork_endpoint: None,
352 expected_fork_endpoint: None,
353 fork_network_is_inferred: false,
354 fork_chain_id_is_inferred: false,
355 fork_block_number_is_inferred: false,
356 }
357 }
358}
359
360impl EvmOpts {
361 fn clear_endpoint_derived_fork_state(&mut self) {
363 if self.fork_block_number_is_inferred {
364 self.fork_block_number = None;
365 self.fork_block_number_is_inferred = false;
366 }
367 self.fork_endpoint = None;
368 self.expected_fork_endpoint = None;
369 if self.fork_network_is_inferred {
370 self.networks = self.networks.with_rpc_profile(NetworkConfigs::default());
371 self.fork_network_is_inferred = false;
372 }
373 if self.fork_chain_id_is_inferred {
374 self.env.chain_id = None;
375 self.fork_chain_id_is_inferred = false;
376 }
377 }
378
379 pub fn set_fork_url(&mut self, fork_url: String) {
381 if self.fork_url.as_ref() == Some(&fork_url) {
382 return;
383 }
384 self.fork_url = Some(fork_url);
385 self.clear_endpoint_derived_fork_state();
386 }
387
388 pub fn invalidate_fork_endpoint_if_source_changed(&mut self, fork: &Fork) -> bool {
395 let matches = self.fork_url.as_deref().is_some_and(|fork_url| {
396 fork.matches_source(fork_url, self.fork_source_headers(), self.rpc_jwt.as_deref())
397 });
398 if matches {
399 return false;
400 }
401 self.clear_endpoint_derived_fork_state();
402 true
403 }
404
405 pub fn set_explicit_network(&mut self, network: NetworkVariant) {
407 self.networks = network.into();
408 self.fork_network_is_inferred = false;
409 }
410
411 pub fn expect_fork_endpoint(
413 &mut self,
414 identity: ForkEndpointIdentity,
415 network_is_inferred: bool,
416 ) {
417 self.fork_endpoint = Some(identity.clone());
418 self.expected_fork_endpoint = Some(identity);
419 self.fork_network_is_inferred = network_is_inferred;
420 }
421
422 fn ensure_expected_fork_endpoint(&self, identity: &ForkEndpointIdentity) -> eyre::Result<()> {
423 if let Some(expected) = &self.expected_fork_endpoint
424 && expected != identity
425 {
426 eyre::bail!(
427 "fork endpoint {} changed after its execution context was selected",
428 fork_endpoint_description(&identity.endpoint)
429 );
430 }
431 Ok(())
432 }
433
434 fn ensure_inferred_execution_profile(
436 &self,
437 identity: &ForkEndpointIdentity,
438 ) -> eyre::Result<()> {
439 eyre::ensure!(
440 !self.fork_network_is_inferred
441 || self.networks.has_same_execution_profile(&identity.network_profile),
442 "fork endpoint {} changed execution profile from `{}` to `{}`; rebuild the EVM for \
443 the new network",
444 fork_endpoint_description(&identity.endpoint),
445 self.networks.execution_profile_name(),
446 identity.network_profile.execution_profile_name()
447 );
448 Ok(())
449 }
450
451 pub(crate) fn chain_id_override(&self) -> Option<ChainId> {
452 (!self.fork_chain_id_is_inferred).then_some(self.env.chain_id).flatten()
453 }
454
455 fn endpoint_network_fallback(&self) -> NetworkConfigs {
456 if self.networks.has_network_selection() && !self.fork_network_is_inferred {
457 self.networks
458 } else {
459 NetworkConfigs::default()
460 }
461 }
462
463 fn anvil_node_info_probe(&self) -> AnvilNodeInfoProbe {
464 let endpoint = self.fork_url.as_deref();
465 let identified = [self.fork_endpoint.as_ref(), self.expected_fork_endpoint.as_ref()]
466 .into_iter()
467 .flatten()
468 .any(|identity| {
469 Some(identity.endpoint.as_str()) == endpoint && identity.reported_hardfork.is_some()
470 });
471 AnvilNodeInfoProbe::new(identified)
472 }
473
474 fn discovered_non_anvil_chain_id(
484 &self,
485 node_info_probe: AnvilNodeInfoProbe,
486 ) -> Option<ChainId> {
487 if node_info_probe.identified {
488 return None;
489 }
490 let endpoint = self.fork_url.as_deref()?;
491 self.fork_endpoint
492 .as_ref()
493 .filter(|identity| identity.endpoint == endpoint)
494 .map(|identity| identity.execution_chain_id)
495 }
496
497 pub(crate) fn fork_source_headers(&self) -> Option<&[String]> {
498 self.fork_headers.as_deref().or(self.rpc_headers.as_deref())
499 }
500
501 #[cfg(test)]
502 fn test_fork(&self, fork_url: &str, block: BlockNumHash, context: ForkContext) -> Fork {
503 let mut fork = Fork::test(
504 fork_url,
505 self.fork_source_headers(),
506 self.rpc_jwt.as_deref(),
507 self.fork_block_number,
508 block,
509 context,
510 );
511 fork.state_by_number = self.fork_state_by_number;
512 fork
513 }
514
515 pub async fn pin_fork_block(&mut self) -> eyre::Result<Option<BlockNumber>> {
522 if self.fork_block_number.is_none()
523 && let Some(fork_url) = &self.fork_url
524 {
525 self.fork_block_number = Some(
526 self.fork_provider_with_url::<AnyNetwork>(fork_url)?.get_block_number().await?,
527 );
528 self.fork_block_number_is_inferred = true;
529 }
530 Ok(self.fork_block_number)
531 }
532
533 pub(crate) async fn prepare_fork(&self) -> eyre::Result<Option<Fork>> {
541 let Some(fork_url) = &self.fork_url else { return Ok(None) };
542 let client = self.fork_rpc_client(fork_url)?;
543 let provider = RootProvider::<AnyNetwork>::new(client.clone());
544 let target = match self.fork_block_number {
545 Some(block_number) => BlockNumberOrTag::Number(block_number),
546 None => BlockNumberOrTag::Latest,
547 };
548 let mut node_info_probe = self.anvil_node_info_probe();
549 let (block, _, context) =
550 self.resolve_fork_block_with_context(&provider, target, &mut node_info_probe).await?;
551 Ok(Some(Fork::new(self, client, block, context)))
552 }
553
554 pub(crate) async fn can_use_create2_deployer_at(
556 &self,
557 fork: Option<&Fork>,
558 ) -> eyre::Result<bool> {
559 let Some(_) = &self.fork_url else {
560 eyre::ensure!(fork.is_none(), "resolved fork provided without a configured fork");
561 return Ok(self.create2_deployer == DEFAULT_CREATE2_DEPLOYER);
562 };
563 let fork = fork.ok_or_else(|| eyre::eyre!("fork must be resolved"))?;
564 let provider = self.provider_for_fork::<AnyNetwork>(fork)?;
565 self.check_fork_endpoint(&provider, fork).await?;
566 let available = !provider
567 .get_code_at(self.create2_deployer)
568 .block_id(fork.state_block_id())
569 .await?
570 .is_empty();
571 self.check_fork_endpoint(&provider, fork).await?;
572 Ok(available)
573 }
574
575 pub(crate) fn provider_for_fork<N: Network>(
577 &self,
578 fork: &Fork,
579 ) -> eyre::Result<RootProvider<N>> {
580 eyre::ensure!(
581 fork.matches_request(self),
582 "resolved fork does not match the configured source and selector"
583 );
584 Ok(fork.provider())
585 }
586
587 pub(crate) async fn check_fork_endpoint<N: Network, P: Provider<N>>(
588 &self,
589 provider: &P,
590 fork: &Fork,
591 ) -> eyre::Result<()> {
592 let mut node_info_probe = self.anvil_node_info_probe();
593 node_info_probe.identified |= fork.context().hardfork.is_some();
594 let identity = self
595 .resolve_fork_endpoint_once(
596 provider,
597 &mut node_info_probe,
598 EndpointHardforkPolicy::Required,
599 )
600 .await?;
601 self.ensure_expected_fork_endpoint(&identity)?;
602 self.ensure_inferred_execution_profile(&identity)?;
603 eyre::ensure!(
604 fork.context().matches_identity(&identity),
605 "fork endpoint {} changed after its block and execution context were resolved",
606 fork_endpoint_description(self.fork_url.as_deref().unwrap_or_default())
607 );
608 Ok(())
609 }
610
611 pub(crate) async fn transaction_count_at_fork(
613 &self,
614 account: Address,
615 fork: &Fork,
616 ) -> eyre::Result<u64> {
617 let provider = self.provider_for_fork::<AnyNetwork>(fork)?;
618 self.check_fork_endpoint(&provider, fork).await?;
619 let nonce = provider.get_transaction_count(account).block_id(fork.state_block_id()).await?;
620 self.check_fork_endpoint(&provider, fork).await?;
621 Ok(nonce)
622 }
623
624 pub fn fork_provider_with_url<N: Network>(
627 &self,
628 fork_url: &str,
629 ) -> eyre::Result<RootProvider<N>> {
630 Ok(RootProvider::new(self.fork_rpc_client(fork_url)?))
631 }
632
633 pub(crate) fn fork_rpc_client(&self, fork_url: &str) -> eyre::Result<RpcClient> {
634 let mut builder = ProviderBuilder::<AnyNetwork>::new(fork_url)
635 .maybe_max_retry(self.fork_retries)
636 .maybe_initial_backoff(self.fork_retry_backoff)
637 .maybe_headers(self.fork_headers.clone().or_else(|| self.rpc_headers.clone()))
638 .compute_units_per_second(self.get_compute_units_per_second())
639 .accept_invalid_certs(self.rpc_accept_invalid_certs)
640 .no_proxy(self.rpc_no_proxy);
641 if let Some(jwt) = &self.rpc_jwt {
642 builder = builder.jwt(jwt);
643 }
644 if let Some(timeout) = self.rpc_timeout {
645 builder = builder.timeout(std::time::Duration::from_secs(timeout));
646 }
647 builder.build_client()
648 }
649
650 pub async fn infer_network_from_fork(&mut self) -> eyre::Result<()> {
658 let previous_identity = self.fork_endpoint.clone();
659 let Some(fork_url) = self.fork_url.clone() else {
660 self.fork_endpoint = None;
661 self.expected_fork_endpoint = None;
662 if self.fork_network_is_inferred {
663 self.networks = self.networks.with_rpc_profile(NetworkConfigs::default());
664 self.fork_network_is_inferred = false;
665 }
666 if self.fork_chain_id_is_inferred {
667 self.env.chain_id = None;
668 self.fork_chain_id_is_inferred = false;
669 }
670 if self.fork_block_number_is_inferred {
671 self.fork_block_number = None;
672 self.fork_block_number_is_inferred = false;
673 }
674 return Ok(());
675 };
676 let explicit_network =
677 self.networks.has_network_selection() && !self.fork_network_is_inferred;
678 let identity = self.fork_endpoint_identity().await?;
679 self.ensure_expected_fork_endpoint(&identity)?;
680 if self.fork_network_is_inferred
681 && previous_identity.as_ref().is_some_and(|previous| {
682 previous.endpoint == fork_url
683 && previous.network_profile != identity.network_profile
684 })
685 {
686 eyre::bail!(
687 "fork endpoint {} changed execution profile from `{}` to `{}`; rebuild \
688 the EVM for the new network",
689 fork_endpoint_description(&fork_url),
690 self.networks.execution_profile_name(),
691 identity.network_profile.execution_profile_name()
692 );
693 }
694
695 if self.env.chain_id.is_none() || self.fork_chain_id_is_inferred {
696 self.env.chain_id = Some(identity.execution_chain_id);
697 self.fork_chain_id_is_inferred = true;
698 }
699 if !explicit_network {
700 self.networks = self.networks.with_rpc_profile(identity.network_profile);
701 self.fork_network_is_inferred = true;
702 }
703 debug_assert_eq!(identity.endpoint, fork_url);
704 self.fork_endpoint = Some(identity);
705 Ok(())
706 }
707
708 pub async fn discover_fork_endpoint(&self) -> eyre::Result<ForkEndpointIdentity> {
717 let fork_url = self.fork_url.as_deref().ok_or_eyre("fork URL is not configured")?;
718 let provider = self.fork_provider_with_url::<AnyNetwork>(fork_url)?;
719 let unknown_fallback = self.endpoint_network_fallback();
720 let mut node_info_probe = self.anvil_node_info_probe();
721 for _ in 0..3 {
722 let before_chain_id = provider
723 .get_chain_id()
724 .await
725 .wrap_err("failed to retrieve chain ID from fork endpoint")?;
726 let Some(before_node_info) = node_info_probe.request(&provider).await? else {
727 return Self::non_anvil_fork_endpoint_identity(
728 fork_url,
729 before_chain_id,
730 Some(unknown_fallback),
731 );
732 };
733 let before = Self::resolve_fork_endpoint_identity(
734 &provider,
735 fork_url,
736 before_chain_id,
737 Some(before_node_info),
738 Some(unknown_fallback),
739 EndpointHardforkPolicy::Optional,
740 )
741 .await?;
742 let after_chain_id = provider
743 .get_chain_id()
744 .await
745 .wrap_err("failed to confirm chain ID from fork endpoint")?;
746 let after_node_info = node_info_probe.request(&provider).await?;
747 let after = Self::resolve_fork_endpoint_identity(
748 &provider,
749 fork_url,
750 after_chain_id,
751 after_node_info,
752 Some(unknown_fallback),
753 EndpointHardforkPolicy::Optional,
754 )
755 .await?;
756 if before_chain_id == after_chain_id && before == after {
757 return Ok(before);
758 }
759 }
760 eyre::bail!(
761 "fork endpoint {} changed while its identity was being resolved",
762 fork_endpoint_description(fork_url)
763 );
764 }
765
766 pub async fn fork_endpoint_identity(&self) -> eyre::Result<ForkEndpointIdentity> {
773 let fork_url = self.fork_url.as_deref().ok_or_eyre("fork URL is not configured")?;
774 if let Some(execution_chain_id) =
775 self.discovered_non_anvil_chain_id(self.anvil_node_info_probe())
776 {
777 return Self::non_anvil_fork_endpoint_identity(
778 fork_url,
779 execution_chain_id,
780 Some(self.endpoint_network_fallback()),
781 );
782 }
783 self.discover_fork_endpoint().await
784 }
785
786 async fn resolve_fork_endpoint_once<N: Network, P: Provider<N>>(
793 &self,
794 provider: &P,
795 node_info_probe: &mut AnvilNodeInfoProbe,
796 hardfork_policy: EndpointHardforkPolicy,
797 ) -> eyre::Result<ForkEndpointIdentity> {
798 let fork_url = self.fork_url.as_deref().ok_or_eyre("fork URL is not configured")?;
799 if let Some(execution_chain_id) = self.discovered_non_anvil_chain_id(*node_info_probe) {
800 return Self::non_anvil_fork_endpoint_identity(
801 fork_url,
802 execution_chain_id,
803 Some(self.endpoint_network_fallback()),
804 );
805 }
806 let cached_anvil = self.fork_endpoint.as_ref().is_some_and(|identity| {
807 identity.endpoint == fork_url && identity.reported_hardfork.is_some()
808 });
809 let node_info = node_info_probe.request(provider).await?;
810 let execution_chain_id = if cached_anvil && let Some(node_info) = &node_info {
811 node_info.environment.chain_id
812 } else {
813 provider
814 .get_chain_id()
815 .await
816 .wrap_err("failed to retrieve chain ID from fork endpoint")?
817 };
818 Self::resolve_fork_endpoint_identity(
819 provider,
820 fork_url,
821 execution_chain_id,
822 node_info,
823 Some(self.endpoint_network_fallback()),
824 hardfork_policy,
825 )
826 .await
827 }
828
829 async fn resolve_fork_endpoint_identity<N: Network, P: Provider<N>>(
833 provider: &P,
834 endpoint: &str,
835 execution_chain_id: ChainId,
836 node_info: Option<NodeInfo>,
837 unknown_fallback: Option<NetworkConfigs>,
838 hardfork_policy: EndpointHardforkPolicy,
839 ) -> eyre::Result<ForkEndpointIdentity> {
840 match node_info {
841 Some(node_info) => {
842 let (
843 source_chain_id,
844 instance_id,
845 source_fork_block_number,
846 source_fork_block_hash,
847 ) = match provider.raw_request::<_, Metadata>("anvil_metadata".into(), ()).await {
848 Ok(metadata) => {
849 let forked_network = metadata.forked_network;
850 (
851 forked_network.map(|fork| fork.chain_id).unwrap_or(execution_chain_id),
852 Some(metadata.instance_id),
853 forked_network.map(|fork| fork.fork_block_number),
854 forked_network.map(|fork| fork.fork_block_hash),
855 )
856 }
857 Err(error) if is_rpc_method_not_found(&error) => {
858 (execution_chain_id, None, None, None)
859 }
860 Err(error) => {
861 return Err(error)
862 .wrap_err("failed to retrieve Anvil fork source identity");
863 }
864 };
865 let identity_chain_id =
866 if node_info.network.is_some() { execution_chain_id } else { source_chain_id };
867 let network_profile = NetworkConfigs::from_rpc_identity_profile_with_fallback(
868 identity_chain_id,
869 Some(node_info.network.as_deref()),
870 unknown_fallback,
871 )
872 .map_err(eyre::Report::msg)?
873 .ok_or_else(|| {
874 eyre::eyre!("Anvil metadata did not identify an execution profile")
875 })?;
876 let network = network_profile.execution_network();
877 let hardfork = endpoint_hardfork(network, &node_info.hard_fork, hardfork_policy)?;
878
879 Ok(ForkEndpointIdentity {
880 endpoint: endpoint.to_string(),
881 execution_chain_id,
882 source_chain_id,
883 network,
884 network_profile,
885 reported_hardfork: Some(node_info.hard_fork),
886 hardfork,
887 instance_id,
888 source_fork_block_number,
889 source_fork_block_hash,
890 })
891 }
892 None => Self::non_anvil_fork_endpoint_identity(
893 endpoint,
894 execution_chain_id,
895 unknown_fallback,
896 ),
897 }
898 }
899
900 fn non_anvil_fork_endpoint_identity(
902 endpoint: &str,
903 execution_chain_id: ChainId,
904 unknown_fallback: Option<NetworkConfigs>,
905 ) -> eyre::Result<ForkEndpointIdentity> {
906 let network_profile = NetworkConfigs::from_rpc_identity_profile_with_fallback(
907 execution_chain_id,
908 None,
909 unknown_fallback,
910 )
911 .map_err(eyre::Report::msg)?
912 .ok_or_else(|| {
913 eyre::eyre!(
914 "cannot determine network family for unknown chain ID \
915 {execution_chain_id}: the fork endpoint does not expose `anvil_nodeInfo`"
916 )
917 })?;
918 Ok(ForkEndpointIdentity {
919 endpoint: endpoint.to_string(),
920 execution_chain_id,
921 source_chain_id: execution_chain_id,
922 network: network_profile.execution_network(),
923 network_profile,
924 reported_hardfork: None,
925 hardfork: None,
926 instance_id: None,
927 source_fork_block_number: None,
928 source_fork_block_hash: None,
929 })
930 }
931
932 pub async fn env_with_fork_context<
936 SPEC: Into<SpecId> + Default + Copy,
937 BLOCK: FoundryBlock + Default,
938 TX: FoundryTransaction + Default,
939 >(
940 &self,
941 ) -> eyre::Result<(EvmEnv<SPEC, BLOCK>, TX, Option<ForkContext>)> {
942 let Some(fork_url) = &self.fork_url else {
943 return Ok((self.local_evm_env(), self.local_tx_env(), None));
944 };
945 let provider = self.fork_provider_with_url::<AnyNetwork>(fork_url)?;
946 let target = self.fork_block_number.map_or(BlockNumberOrTag::Latest, Into::into);
947 let mut node_info_probe = self.anvil_node_info_probe();
948 for _ in 0..3 {
949 let (block, _, context) = self
950 .resolve_fork_block_with_context(&provider, target, &mut node_info_probe)
951 .await?;
952 let gas_price =
953 option_try_or_else(self.env.gas_price.map(|value| value as u128), async || {
954 provider.get_gas_price().await
955 })
956 .await?;
957 let identity = self
958 .resolve_fork_endpoint_once(
959 &provider,
960 &mut node_info_probe,
961 EndpointHardforkPolicy::Required,
962 )
963 .await?;
964 self.ensure_expected_fork_endpoint(&identity)?;
965 if context.matches_identity(&identity) {
966 let chain_id = self.chain_id_override().unwrap_or(context.execution_chain_id);
967 let env = self.fork_env_from_block::<SPEC, BLOCK, AnyNetwork>(
968 chain_id,
969 context.source_chain_id,
970 &block,
971 );
972 let tx = self.fork_tx_env(gas_price, chain_id);
973 return Ok((env, tx, Some(context)));
974 }
975 }
976 eyre::bail!(
977 "fork endpoint {} changed while its EVM and transaction environments were being \
978 resolved",
979 fork_endpoint_description(fork_url)
980 );
981 }
982
983 pub(crate) async fn env_at_fork<
985 SPEC: Into<SpecId> + Default + Copy,
986 BLOCK: FoundryBlock + Default,
987 TX: FoundryTransaction + Default,
988 >(
989 &self,
990 fork: Option<&Fork>,
991 ) -> eyre::Result<(EvmEnv<SPEC, BLOCK>, TX)> {
992 let Some(_) = &self.fork_url else {
993 eyre::ensure!(fork.is_none(), "resolved fork provided without a configured fork");
994 return Ok((self.local_evm_env(), self.local_tx_env()));
995 };
996 let fork = fork.ok_or_else(|| eyre::eyre!("fork must be resolved"))?;
997 let provider = self.provider_for_fork::<AnyNetwork>(fork)?;
998 self.check_fork_endpoint(&provider, fork).await?;
999 let context = fork.context();
1000 let chain_id = self.chain_id_override().unwrap_or(context.execution_chain_id);
1001 let env = self.fork_env_from_block::<SPEC, BLOCK, AnyNetwork>(
1002 chain_id,
1003 context.source_chain_id,
1004 &fork.block,
1005 );
1006 let gas_price =
1007 option_try_or_else(self.env.gas_price.map(|value| value as u128), async || {
1008 provider.get_gas_price().await
1009 })
1010 .await?;
1011 self.check_fork_endpoint(&provider, fork).await?;
1012 Ok((env, self.fork_tx_env(gas_price, chain_id)))
1013 }
1014
1015 fn fork_env_error_context(&self) -> String {
1016 let mut message = "could not instantiate forked environment".to_string();
1017 if let Some(fork_url) = self.fork_url.as_deref()
1018 && let Ok(url) = Url::parse(fork_url)
1019 && let Some(host) = url.host()
1020 {
1021 write!(message, " with provider {host}").unwrap();
1022 }
1023 message
1024 }
1025
1026 async fn resolve_fork_block_with_context<N: Network, P: Provider<N>>(
1027 &self,
1028 provider: &P,
1029 target: BlockNumberOrTag,
1030 node_info_probe: &mut AnvilNodeInfoProbe,
1031 ) -> eyre::Result<(N::BlockResponse, BlockNumHash, ForkContext)> {
1032 let endpoint = self.fork_url.as_deref().unwrap_or_default();
1033 let mut stable = None;
1034 for _ in 0..3 {
1035 let before = self
1036 .resolve_fork_endpoint_once(
1037 provider,
1038 node_info_probe,
1039 EndpointHardforkPolicy::Required,
1040 )
1041 .await
1042 .wrap_err_with(|| self.fork_env_error_context())?;
1043 let block = provider
1044 .get_block_by_number(target)
1045 .await
1046 .wrap_err_with(|| self.fork_env_error_context())?;
1047 let after = self
1048 .resolve_fork_endpoint_once(
1049 provider,
1050 node_info_probe,
1051 EndpointHardforkPolicy::Required,
1052 )
1053 .await
1054 .wrap_err_with(|| self.fork_env_error_context())?;
1055 if before == after {
1056 stable = Some((before, block));
1057 break;
1058 }
1059 }
1060 let (identity, block) = stable.ok_or_else(|| {
1061 eyre::eyre!(
1062 "fork endpoint {} changed while its block and execution context were being \
1063 resolved",
1064 fork_endpoint_description(endpoint)
1065 )
1066 })?;
1067 self.ensure_expected_fork_endpoint(&identity)?;
1068 self.ensure_inferred_execution_profile(&identity)?;
1069
1070 let block = if let Some(block) = block {
1071 block
1072 } else {
1073 let block_number_message = match target {
1074 BlockNumberOrTag::Number(block_number) => {
1075 format!("block number: {block_number}")
1076 }
1077 block_number => format!("{block_number} block"),
1078 };
1079 let latest_message = if let Ok(latest_block) = provider.get_block_number().await {
1080 if let Some(block_number) = self.fork_block_number
1081 && block_number <= latest_block
1082 {
1083 error!("{NON_ARCHIVE_NODE_WARNING}");
1084 }
1085 format!("; latest block number: {latest_block}")
1086 } else {
1087 Default::default()
1088 };
1089 eyre::bail!("failed to get {block_number_message}{latest_message}");
1090 };
1091 let actual = BlockNumHash::new(block.header().number(), block.header().hash());
1092
1093 let context = ForkContext {
1094 execution_chain_id: identity.execution_chain_id,
1095 source_chain_id: identity.source_chain_id,
1096 network: identity.network,
1097 network_profile: identity.network_profile,
1098 block_number: actual.number,
1099 hardfork: identity.hardfork,
1100 instance_id: identity.instance_id,
1101 source_fork_block_number: identity.source_fork_block_number,
1102 source_fork_block_hash: identity.source_fork_block_hash,
1103 };
1104 Ok((block, actual, context))
1105 }
1106
1107 pub(crate) fn fork_env_from_block<
1108 SPEC: Into<SpecId> + Default + Copy,
1109 BLOCK: FoundryBlock + Default,
1110 N: Network,
1111 >(
1112 &self,
1113 chain_id: ChainId,
1114 source_chain_id: ChainId,
1115 block: &N::BlockResponse,
1116 ) -> EvmEnv<SPEC, BLOCK> {
1117 let mut evm_env = EvmEnv {
1118 cfg_env: self.cfg_env(chain_id),
1119 block_env: block_env_from_header(block.header()),
1120 };
1121
1122 apply_chain_and_block_specific_env_changes_for_chain::<N, _, _>(
1123 &mut evm_env,
1124 block,
1125 source_chain_id,
1126 self.networks,
1127 );
1128
1129 evm_env
1130 }
1131
1132 fn local_evm_env<SPEC: Into<SpecId> + Default + Clone, BLOCK: FoundryBlock + Default>(
1134 &self,
1135 ) -> EvmEnv<SPEC, BLOCK> {
1136 let cfg_env = self.cfg_env(self.env.chain_id.unwrap_or(foundry_common::DEV_CHAIN_ID));
1137 let mut block_env = BLOCK::default();
1138 block_env.set_number(self.env.block_number);
1139 block_env.set_beneficiary(self.env.block_coinbase);
1140 block_env.set_timestamp(self.env.block_timestamp);
1141 block_env.set_difficulty(U256::from(self.env.block_difficulty));
1142 block_env.set_prevrandao(Some(self.env.block_prevrandao));
1143 block_env.set_basefee(self.env.block_base_fee_per_gas);
1144 block_env.set_gas_limit(self.gas_limit());
1145 EvmEnv::new(cfg_env, block_env)
1146 }
1147
1148 fn fork_tx_env<TX: FoundryTransaction + Default>(
1150 &self,
1151 gas_price: u128,
1152 chain_id: ChainId,
1153 ) -> TX {
1154 let mut tx_env = TX::default();
1155 tx_env.set_caller(self.sender);
1156 tx_env.set_chain_id(Some(chain_id));
1157 tx_env.set_gas_price(gas_price);
1158 tx_env.set_gas_limit(self.gas_limit());
1159 tx_env
1160 }
1161
1162 fn local_tx_env<TX: FoundryTransaction + Default>(&self) -> TX {
1164 let mut tx_env = TX::default();
1165 tx_env.set_caller(self.sender);
1166 tx_env.set_gas_price(self.env.gas_price.unwrap_or_default().into());
1167 tx_env.set_gas_limit(self.gas_limit());
1168 tx_env
1169 }
1170
1171 fn cfg_env<SPEC: Into<SpecId> + Default + Clone>(&self, chain_id: ChainId) -> CfgEnv<SPEC> {
1173 let mut cfg = CfgEnv::default();
1174 cfg.chain_id = chain_id;
1175 cfg.memory_limit = self.memory_limit;
1176 cfg.limit_contract_code_size = self.env.code_size_limit.or(Some(usize::MAX));
1177 cfg.disable_eip3607 = true;
1181 cfg.disable_block_gas_limit = self.disable_block_gas_limit;
1182 cfg.disable_nonce_check = true;
1183 if !self.enable_tx_gas_limit {
1186 cfg.tx_gas_limit_cap = Some(u64::MAX);
1187 }
1188 cfg
1189 }
1190
1191 pub fn get_fork(
1213 &self,
1214 config: &Config,
1215 chain_id: u64,
1216 fork_block_number: Option<BlockNumber>,
1217 ) -> Option<CreateFork> {
1218 let url = self.fork_url.clone()?;
1219 let source_chain_id =
1220 self.fork_endpoint.as_ref().map_or(chain_id, |identity| identity.source_chain_id);
1221 let enable_caching = config.enable_caching(&url, source_chain_id);
1222
1223 let mut evm_opts = self.clone();
1225 if evm_opts.fork_block_number.is_none() {
1226 evm_opts.fork_block_number = fork_block_number;
1227 evm_opts.fork_block_number_is_inferred = fork_block_number.is_some();
1228 }
1229
1230 Some(CreateFork { url, enable_caching, evm_opts })
1231 }
1232
1233 pub fn gas_limit(&self) -> u64 {
1235 self.env.block_gas_limit.unwrap_or(self.env.gas_limit).0
1236 }
1237
1238 const fn get_compute_units_per_second(&self) -> u64 {
1243 if self.no_rpc_rate_limit {
1244 u64::MAX
1245 } else if let Some(cups) = self.compute_units_per_second {
1246 cups
1247 } else {
1248 ALCHEMY_FREE_TIER_CUPS
1249 }
1250 }
1251
1252 pub async fn get_remote_chain_id(&self) -> Option<Chain> {
1254 if let Some(url) = &self.fork_url
1255 && let Ok(provider) = self.fork_provider_with_url::<AnyNetwork>(url)
1256 {
1257 trace!(?url, "retrieving chain via eth_chainId");
1258
1259 if let Ok(id) = provider.get_chain_id().await {
1260 return Some(Chain::from(id));
1261 }
1262 }
1263
1264 None
1265 }
1266}
1267
1268#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1270pub enum ExecutionSpecContext {
1271 Local,
1273 Fork {
1275 source_chain_id: ChainId,
1277 endpoint_hardfork: Option<FoundryHardfork>,
1279 },
1280 Script {
1282 source_chain_id: ChainId,
1284 endpoint_hardfork: Option<FoundryHardfork>,
1286 },
1287 Historical {
1289 source_chain_id: ChainId,
1291 endpoint_hardfork: Option<FoundryHardfork>,
1293 },
1294}
1295
1296impl ExecutionSpecContext {
1297 pub const fn fork(
1299 source_chain_id: ChainId,
1300 endpoint_hardfork: Option<FoundryHardfork>,
1301 ) -> Self {
1302 Self::Fork { source_chain_id, endpoint_hardfork }
1303 }
1304
1305 pub const fn local_or_fork(
1307 source_chain_id: Option<ChainId>,
1308 endpoint_hardfork: Option<FoundryHardfork>,
1309 ) -> Self {
1310 if let Some(source_chain_id) = source_chain_id {
1311 Self::Fork { source_chain_id, endpoint_hardfork }
1312 } else {
1313 Self::Local
1314 }
1315 }
1316
1317 pub const fn local_or_script(
1319 source_chain_id: Option<ChainId>,
1320 endpoint_hardfork: Option<FoundryHardfork>,
1321 ) -> Self {
1322 if let Some(source_chain_id) = source_chain_id {
1323 Self::Script { source_chain_id, endpoint_hardfork }
1324 } else {
1325 Self::Local
1326 }
1327 }
1328
1329 pub const fn historical(
1331 source_chain_id: ChainId,
1332 endpoint_hardfork: Option<FoundryHardfork>,
1333 ) -> Self {
1334 Self::Historical { source_chain_id, endpoint_hardfork }
1335 }
1336
1337 fn hardfork<SPEC: ExecutionSpec>(
1338 self,
1339 timestamp: u64,
1340 configured: SPEC,
1341 ) -> Option<FoundryHardfork> {
1342 match self {
1343 Self::Local => None,
1344 Self::Fork { source_chain_id, endpoint_hardfork } => {
1345 SPEC::fork_hardfork(source_chain_id, timestamp, endpoint_hardfork)
1346 }
1347 Self::Script { source_chain_id, endpoint_hardfork } => SPEC::script_fork_hardfork(
1348 source_chain_id,
1349 timestamp,
1350 endpoint_hardfork,
1351 configured,
1352 ),
1353 Self::Historical { source_chain_id, endpoint_hardfork } => endpoint_hardfork
1354 .filter(|&hardfork| SPEC::from_foundry_hardfork(hardfork).is_some())
1355 .or_else(|| SPEC::historical_hardfork(source_chain_id, timestamp)),
1356 }
1357 }
1358}
1359
1360pub fn resolve_execution_spec<SPEC, BLOCK>(
1371 evm_version: EvmVersion,
1372 configured_hardfork: Option<FoundryHardfork>,
1373 evm_env: &mut EvmEnv<SPEC, BLOCK>,
1374 context: ExecutionSpecContext,
1375 explicit_spec: Option<SPEC>,
1376) -> Option<FoundryHardfork>
1377where
1378 SPEC: ExecutionSpec + Into<SpecId> + Copy,
1379 BLOCK: FoundryBlock,
1380{
1381 let configured_spec = evm_spec_id::<SPEC>(evm_version);
1382 let (spec, hardfork) = if let Some(spec) = explicit_spec {
1383 (spec, spec.reported_hardfork())
1384 } else if let Some((hardfork, spec)) = configured_hardfork
1385 .filter(|&hardfork| SPEC::from_foundry_hardfork(hardfork).is_some())
1386 .or_else(|| {
1387 context.hardfork(evm_env.block_env.timestamp().saturating_to(), configured_spec)
1388 })
1389 .and_then(|hardfork| SPEC::from_foundry_hardfork(hardfork).map(|spec| (hardfork, spec)))
1390 {
1391 (spec, Some(hardfork))
1392 } else {
1393 (configured_spec, configured_spec.reported_hardfork())
1394 };
1395 evm_env.cfg_env.set_spec_and_mainnet_gas_params(spec);
1396 hardfork
1397}
1398
1399#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1400pub struct Env {
1401 pub gas_limit: GasLimit,
1403
1404 pub chain_id: Option<u64>,
1406
1407 #[serde(default, skip_serializing_if = "Option::is_none")]
1412 pub gas_price: Option<u64>,
1413
1414 pub block_base_fee_per_gas: u64,
1416
1417 pub tx_origin: Address,
1419
1420 pub block_coinbase: Address,
1422
1423 #[serde(
1425 deserialize_with = "foundry_config::deserialize_u64_to_u256",
1426 serialize_with = "foundry_config::serialize_u64_or_u256"
1427 )]
1428 pub block_timestamp: U256,
1429
1430 #[serde(
1432 deserialize_with = "foundry_config::deserialize_u64_to_u256",
1433 serialize_with = "foundry_config::serialize_u64_or_u256"
1434 )]
1435 pub block_number: U256,
1436
1437 pub block_difficulty: u64,
1439
1440 pub block_prevrandao: B256,
1442
1443 #[serde(default, skip_serializing_if = "Option::is_none")]
1445 pub block_gas_limit: Option<GasLimit>,
1446
1447 #[serde(default, skip_serializing_if = "Option::is_none")]
1449 pub code_size_limit: Option<usize>,
1450}
1451
1452fn fork_endpoint_description(endpoint: &str) -> String {
1453 Url::parse(endpoint)
1454 .ok()
1455 .and_then(|url| url.host_str().map(|host| format!("provider {host}")))
1456 .unwrap_or_else(|| "configured provider".to_string())
1457}
1458
1459async fn option_try_or_else<T, E>(
1460 option: Option<T>,
1461 f: impl AsyncFnOnce() -> Result<T, E>,
1462) -> Result<T, E> {
1463 if let Some(value) = option { Ok(value) } else { f().await }
1464}
1465
1466#[cfg(test)]
1467mod tests {
1468 use super::*;
1469 use alloy_chains::NamedChain;
1470 use alloy_network::TransactionBuilder;
1471 use alloy_primitives::bytes;
1472 use alloy_rpc_types::TransactionRequest;
1473 use alloy_serde::WithOtherFields;
1474 use foundry_evm_hardforks::EthereumHardfork;
1475 use foundry_test_utils::rpc::{
1476 spawn_rpc_proxy_internal_error_after, spawn_rpc_proxy_method_not_found_before,
1477 spawn_rpc_proxy_recording_method, spawn_rpc_proxy_rejecting_method_after,
1478 };
1479 use revm::context::{BlockEnv, TxEnv};
1480
1481 #[cfg(feature = "base")]
1482 use foundry_evm_hardforks::BaseUpgrade;
1483
1484 #[cfg(feature = "optimism")]
1485 use op_revm::OpSpecId;
1486
1487 fn resolved_context(block_number: BlockNumber) -> ForkContext {
1488 ForkContext {
1489 execution_chain_id: 1,
1490 source_chain_id: 1,
1491 network: NetworkVariant::Ethereum,
1492 network_profile: NetworkConfigs::default(),
1493 block_number,
1494 hardfork: None,
1495 instance_id: None,
1496 source_fork_block_number: None,
1497 source_fork_block_hash: None,
1498 }
1499 }
1500
1501 #[test]
1502 fn fork_endpoint_description_redacts_credentials_and_paths() {
1503 let endpoint = "https://user:secret@example.com/v2/private-api-key?token=another-secret";
1504 let description = fork_endpoint_description(endpoint);
1505
1506 assert_eq!(description, "provider example.com");
1507 assert!(!description.contains("secret"));
1508 assert!(!description.contains("private-api-key"));
1509 }
1510
1511 #[test]
1512 fn endpoint_network_fallback_preserves_explicit_selection() {
1513 let default = EvmOpts::default().endpoint_network_fallback();
1514 assert!(!default.has_network_selection());
1515
1516 let explicit = EvmOpts { networks: NetworkConfigs::with_ethereum(), ..Default::default() }
1517 .endpoint_network_fallback();
1518 assert!(explicit.has_network_selection());
1519 assert_eq!(explicit.execution_network(), NetworkVariant::Ethereum);
1520 }
1521
1522 #[test]
1523 fn backend_target_matches_number_pinned_fork_identity() {
1524 let context = ForkContext {
1525 execution_chain_id: 1,
1526 source_chain_id: 1,
1527 network: NetworkVariant::Ethereum,
1528 network_profile: NetworkConfigs::default(),
1529 block_number: 10,
1530 hardfork: None,
1531 instance_id: None,
1532 source_fork_block_number: None,
1533 source_fork_block_hash: None,
1534 };
1535
1536 let mut different_number = context;
1537 different_number.block_number += 1;
1538 assert!(!context.has_same_backend_target(different_number));
1539
1540 let mut different_profile = context;
1541 different_profile.network_profile = NetworkConfigs::with_celo();
1542 assert!(!context.has_same_backend_target(different_profile));
1543
1544 let mut different_hardfork = context;
1545 different_hardfork.hardfork =
1546 Some(FoundryHardfork::Ethereum(foundry_evm_hardforks::EthereumHardfork::Prague));
1547 assert!(!context.has_same_backend_target(different_hardfork));
1548
1549 let mut different_instance = context;
1550 different_instance.instance_id = Some(B256::with_last_byte(3));
1551 assert!(!context.has_same_backend_target(different_instance));
1552 }
1553
1554 #[tokio::test]
1555 async fn expected_fork_endpoint_requires_exact_identity() {
1556 let identity = ForkEndpointIdentity {
1557 endpoint: "http://localhost:8545".to_string(),
1558 execution_chain_id: 1,
1559 source_chain_id: 1,
1560 network: NetworkVariant::Ethereum,
1561 network_profile: NetworkConfigs::default(),
1562 reported_hardfork: Some("Prague".to_string()),
1563 hardfork: Some(FoundryHardfork::Ethereum(
1564 foundry_evm_hardforks::EthereumHardfork::Prague,
1565 )),
1566 instance_id: Some(B256::with_last_byte(1)),
1567 source_fork_block_number: None,
1568 source_fork_block_hash: None,
1569 };
1570 let mut evm_opts = EvmOpts::default();
1571 evm_opts.expect_fork_endpoint(identity.clone(), true);
1572
1573 assert!(evm_opts.ensure_expected_fork_endpoint(&identity).is_ok());
1574 assert_eq!(evm_opts.fork_endpoint, Some(identity.clone()));
1575 assert_eq!(evm_opts.expected_fork_endpoint, Some(identity.clone()));
1576 assert!(evm_opts.fork_network_is_inferred);
1577
1578 let mut changed_instance = identity.clone();
1579 changed_instance.instance_id = Some(B256::with_last_byte(2));
1580 assert!(evm_opts.ensure_expected_fork_endpoint(&changed_instance).is_err());
1581
1582 let mut changed_unknown_hardfork = identity;
1583 changed_unknown_hardfork.reported_hardfork = Some("Future".to_string());
1584 changed_unknown_hardfork.hardfork = None;
1585 assert!(evm_opts.ensure_expected_fork_endpoint(&changed_unknown_hardfork).is_err());
1586
1587 evm_opts.infer_network_from_fork().await.unwrap();
1588 assert_eq!(evm_opts.fork_endpoint, None);
1589 assert_eq!(evm_opts.expected_fork_endpoint, None);
1590 }
1591
1592 #[test]
1593 fn fork_expected_anvil_identity_seeds_strict_node_info_probe() {
1594 let authoritative = ForkEndpointIdentity {
1595 endpoint: "http://localhost:8545".to_string(),
1596 execution_chain_id: 1,
1597 source_chain_id: 1,
1598 network: NetworkVariant::Ethereum,
1599 network_profile: NetworkConfigs::default(),
1600 reported_hardfork: Some("Prague".to_string()),
1601 hardfork: Some(FoundryHardfork::Ethereum(
1602 foundry_evm_hardforks::EthereumHardfork::Prague,
1603 )),
1604 instance_id: Some(B256::with_last_byte(1)),
1605 source_fork_block_number: None,
1606 source_fork_block_hash: None,
1607 };
1608 let mut anonymous = authoritative.clone();
1609 anonymous.reported_hardfork = None;
1610 anonymous.hardfork = None;
1611 anonymous.instance_id = None;
1612 let mut evm_opts = EvmOpts {
1613 fork_url: Some(authoritative.endpoint.clone()),
1614 fork_endpoint: Some(anonymous),
1615 expected_fork_endpoint: Some(authoritative),
1616 ..Default::default()
1617 };
1618
1619 assert!(evm_opts.anvil_node_info_probe().identified);
1620
1621 evm_opts.fork_url = Some("http://localhost:9545".to_string());
1622 assert!(!evm_opts.anvil_node_info_probe().identified);
1623 }
1624
1625 #[test]
1626 fn selecting_new_fork_url_clears_only_endpoint_derived_state() {
1627 let identity = ForkEndpointIdentity {
1628 endpoint: "http://localhost:8545".to_string(),
1629 execution_chain_id: 42,
1630 source_chain_id: 42,
1631 network: NetworkVariant::Ethereum,
1632 network_profile: NetworkConfigs::with_celo(),
1633 reported_hardfork: None,
1634 hardfork: None,
1635 instance_id: None,
1636 source_fork_block_number: None,
1637 source_fork_block_hash: None,
1638 };
1639 let mut inferred = EvmOpts {
1640 fork_url: Some(identity.endpoint.clone()),
1641 fork_block_number: Some(123),
1642 fork_endpoint: Some(identity.clone()),
1643 expected_fork_endpoint: Some(identity.clone()),
1644 networks: identity.network_profile,
1645 fork_network_is_inferred: true,
1646 fork_chain_id_is_inferred: true,
1647 fork_block_number_is_inferred: true,
1648 ..Default::default()
1649 };
1650 inferred.env.chain_id = Some(identity.execution_chain_id);
1651
1652 inferred.set_fork_url("http://localhost:9545".to_string());
1653
1654 assert_eq!(inferred.fork_url.as_deref(), Some("http://localhost:9545"));
1655 assert_eq!(inferred.fork_block_number, None);
1656 assert_eq!(inferred.fork_endpoint, None);
1657 assert_eq!(inferred.expected_fork_endpoint, None);
1658 assert_eq!(inferred.networks, NetworkConfigs::default());
1659 assert_eq!(inferred.env.chain_id, None);
1660 assert!(!inferred.fork_network_is_inferred);
1661 assert!(!inferred.fork_chain_id_is_inferred);
1662 assert!(!inferred.fork_block_number_is_inferred);
1663
1664 let mut explicit = EvmOpts {
1665 fork_url: Some(identity.endpoint.clone()),
1666 fork_block_number: Some(123),
1667 fork_endpoint: Some(identity.clone()),
1668 expected_fork_endpoint: Some(identity),
1669 networks: NetworkConfigs::with_celo(),
1670 ..Default::default()
1671 };
1672 explicit.env.chain_id = Some(42);
1673
1674 explicit.set_fork_url("http://localhost:9545".to_string());
1675
1676 assert_eq!(explicit.networks, NetworkConfigs::with_celo());
1677 assert_eq!(explicit.env.chain_id, Some(42));
1678 assert_eq!(explicit.fork_block_number, Some(123));
1679 assert_eq!(explicit.fork_endpoint, None);
1680 assert_eq!(explicit.expected_fork_endpoint, None);
1681 assert!(!explicit.fork_block_number_is_inferred);
1682 }
1683
1684 #[test]
1685 fn changed_fork_auth_clears_only_endpoint_derived_state() {
1686 let endpoint = "http://localhost:8545";
1687 let identity = ForkEndpointIdentity {
1688 endpoint: endpoint.to_string(),
1689 execution_chain_id: 42,
1690 source_chain_id: 42,
1691 network: NetworkVariant::Ethereum,
1692 network_profile: NetworkConfigs::with_celo(),
1693 reported_hardfork: None,
1694 hardfork: None,
1695 instance_id: Some(B256::with_last_byte(1)),
1696 source_fork_block_number: None,
1697 source_fork_block_hash: None,
1698 };
1699 let context = ForkContext {
1700 execution_chain_id: identity.execution_chain_id,
1701 source_chain_id: identity.source_chain_id,
1702 network: identity.network,
1703 network_profile: identity.network_profile,
1704 block_number: 123,
1705 hardfork: identity.hardfork,
1706 instance_id: identity.instance_id,
1707 source_fork_block_number: identity.source_fork_block_number,
1708 source_fork_block_hash: identity.source_fork_block_hash,
1709 };
1710 let mut evm_opts = EvmOpts {
1711 fork_url: Some(endpoint.to_string()),
1712 fork_block_number: Some(123),
1713 fork_headers: Some(vec!["Authorization: first".to_string()]),
1714 rpc_jwt: Some("first-jwt".to_string()),
1715 fork_endpoint: Some(identity.clone()),
1716 expected_fork_endpoint: Some(identity),
1717 networks: NetworkConfigs::with_celo(),
1718 ..Default::default()
1719 };
1720 evm_opts.env.chain_id = Some(42);
1721 let fork = evm_opts.test_fork(
1722 endpoint,
1723 BlockNumHash::new(context.block_number, B256::with_last_byte(2)),
1724 context,
1725 );
1726
1727 evm_opts.fork_headers = Some(vec!["Authorization: second".to_string()]);
1728 evm_opts.rpc_jwt = Some("second-jwt".to_string());
1729
1730 assert!(evm_opts.invalidate_fork_endpoint_if_source_changed(&fork));
1731 assert_eq!(evm_opts.fork_endpoint, None);
1732 assert_eq!(evm_opts.expected_fork_endpoint, None);
1733 assert_eq!(evm_opts.fork_block_number, Some(123));
1734 assert_eq!(evm_opts.networks, NetworkConfigs::with_celo());
1735 assert_eq!(evm_opts.env.chain_id, Some(42));
1736 }
1737
1738 #[test]
1739 fn changed_fork_selector_preserves_endpoint_identity() {
1740 let endpoint = "http://localhost:8545";
1741 let identity = ForkEndpointIdentity {
1742 endpoint: endpoint.to_string(),
1743 execution_chain_id: 1,
1744 source_chain_id: 1,
1745 network: NetworkVariant::Ethereum,
1746 network_profile: NetworkConfigs::default(),
1747 reported_hardfork: None,
1748 hardfork: None,
1749 instance_id: Some(B256::with_last_byte(1)),
1750 source_fork_block_number: None,
1751 source_fork_block_hash: None,
1752 };
1753 let mut evm_opts = EvmOpts {
1754 fork_url: Some(endpoint.to_string()),
1755 fork_headers: Some(vec!["x-source: stable".to_string()]),
1756 rpc_jwt: Some("stable-jwt".to_string()),
1757 fork_endpoint: Some(identity.clone()),
1758 expected_fork_endpoint: Some(identity.clone()),
1759 ..Default::default()
1760 };
1761 let fork = evm_opts.test_fork(
1762 endpoint,
1763 BlockNumHash::new(1, B256::with_last_byte(2)),
1764 resolved_context(1),
1765 );
1766
1767 evm_opts.fork_block_number = Some(1);
1768
1769 assert!(!evm_opts.invalidate_fork_endpoint_if_source_changed(&fork));
1770 assert_eq!(evm_opts.fork_endpoint, Some(identity.clone()));
1771 assert_eq!(evm_opts.expected_fork_endpoint, Some(identity));
1772 }
1773
1774 #[test]
1775 fn selecting_same_fork_url_preserves_pinned_state() {
1776 let mut evm_opts = EvmOpts {
1777 fork_url: Some("http://localhost:8545".to_string()),
1778 fork_block_number: Some(123),
1779 networks: NetworkConfigs::with_celo(),
1780 fork_network_is_inferred: true,
1781 fork_chain_id_is_inferred: true,
1782 fork_block_number_is_inferred: true,
1783 ..Default::default()
1784 };
1785 evm_opts.env.chain_id = Some(42);
1786
1787 evm_opts.set_fork_url("http://localhost:8545".to_string());
1788
1789 assert_eq!(evm_opts.fork_block_number, Some(123));
1790 assert_eq!(evm_opts.networks, NetworkConfigs::with_celo());
1791 assert_eq!(evm_opts.env.chain_id, Some(42));
1792 assert!(evm_opts.fork_network_is_inferred);
1793 assert!(evm_opts.fork_chain_id_is_inferred);
1794 assert!(evm_opts.fork_block_number_is_inferred);
1795 }
1796
1797 #[cfg(feature = "monad")]
1798 fn monad_env(timestamp: u64) -> EvmEnv<foundry_evm_hardforks::MonadHardfork, BlockEnv> {
1799 let mut block = BlockEnv::default();
1800 block.set_timestamp(U256::from(timestamp));
1801 let mut cfg = CfgEnv::new_with_spec(foundry_evm_hardforks::MonadHardfork::default());
1802 cfg.chain_id = NamedChain::Monad as u64;
1803 EvmEnv::new(cfg, block)
1804 }
1805
1806 #[test]
1807 #[cfg(feature = "monad")]
1808 fn resolve_execution_spec_uses_monad_ten_activation_timestamp() {
1809 let config = Config::default();
1810 let activation =
1811 foundry_evm_hardforks::MonadHardfork::MonadTen.mainnet_activation_timestamp().unwrap();
1812
1813 let mut before = monad_env(activation - 1);
1814 assert_eq!(
1815 resolve_execution_spec(
1816 config.evm_version,
1817 config.hardfork,
1818 &mut before,
1819 ExecutionSpecContext::fork(NamedChain::Monad as u64, None),
1820 None,
1821 ),
1822 Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadNine))
1823 );
1824 assert_eq!(before.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadNine);
1825
1826 let mut after = monad_env(activation);
1827 assert_eq!(
1828 resolve_execution_spec(
1829 config.evm_version,
1830 config.hardfork,
1831 &mut after,
1832 ExecutionSpecContext::fork(NamedChain::Monad as u64, None),
1833 None,
1834 ),
1835 Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadTen))
1836 );
1837 assert_eq!(after.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadTen);
1838 }
1839
1840 #[test]
1841 #[cfg(feature = "monad")]
1842 fn resolve_execution_spec_prefers_exact_endpoint_hardfork() {
1843 let config = Config::default();
1844 let activation =
1845 foundry_evm_hardforks::MonadHardfork::MonadNine.mainnet_activation_timestamp().unwrap();
1846 let mut env = monad_env(activation);
1847 let endpoint_hardfork =
1848 FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadEight);
1849
1850 assert_eq!(
1851 resolve_execution_spec(
1852 config.evm_version,
1853 config.hardfork,
1854 &mut env,
1855 ExecutionSpecContext::fork(NamedChain::Monad as u64, Some(endpoint_hardfork),),
1856 None,
1857 ),
1858 Some(endpoint_hardfork)
1859 );
1860 assert_eq!(env.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadEight);
1861 }
1862
1863 #[test]
1864 #[cfg(feature = "monad")]
1865 fn resolve_execution_spec_ignores_schedule_for_local_env() {
1866 let config = Config::default();
1867 let activation =
1868 foundry_evm_hardforks::MonadHardfork::MonadTen.mainnet_activation_timestamp().unwrap();
1869 let mut env = monad_env(activation - 1);
1870
1871 assert_eq!(
1872 resolve_execution_spec(
1873 config.evm_version,
1874 config.hardfork,
1875 &mut env,
1876 ExecutionSpecContext::Local,
1877 None,
1878 ),
1879 Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadTen))
1880 );
1881 assert_eq!(env.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadTen);
1882 }
1883
1884 #[test]
1885 fn resolve_execution_spec_preserves_ethereum_config_for_local_forks() {
1886 let config = Config::default();
1887 let mut block = BlockEnv::default();
1888 block.set_timestamp(U256::from(1_500_000_000u64));
1889 let mut env = EvmEnv::new(CfgEnv::new_with_spec(SpecId::LONDON), block);
1890
1891 assert_eq!(
1892 resolve_execution_spec(
1893 config.evm_version,
1894 config.hardfork,
1895 &mut env,
1896 ExecutionSpecContext::fork(NamedChain::Mainnet as u64, None),
1897 None,
1898 ),
1899 None
1900 );
1901 assert_eq!(env.cfg_env.spec, config.evm_spec_id::<SpecId>());
1902
1903 let endpoint_hardfork =
1904 FoundryHardfork::Ethereum(foundry_evm_hardforks::EthereumHardfork::Frontier);
1905 assert_eq!(
1906 resolve_execution_spec(
1907 config.evm_version,
1908 config.hardfork,
1909 &mut env,
1910 ExecutionSpecContext::fork(NamedChain::Mainnet as u64, Some(endpoint_hardfork),),
1911 None,
1912 ),
1913 None
1914 );
1915 assert_eq!(env.cfg_env.spec, config.evm_spec_id::<SpecId>());
1916 }
1917
1918 #[test]
1919 fn resolve_execution_spec_follows_ethereum_chain_ahead_for_scripts() {
1920 let amsterdam = EthereumHardfork::Amsterdam.sepolia_activation_timestamp().unwrap();
1921 let sepolia = NamedChain::Sepolia as u64;
1922 let resolve = |timestamp: u64, context| {
1923 let mut block = BlockEnv::default();
1924 block.set_timestamp(U256::from(timestamp));
1925 let mut env = EvmEnv::new(CfgEnv::new_with_spec(SpecId::LONDON), block);
1926 let resolved =
1927 resolve_execution_spec(EvmVersion::Prague, None, &mut env, context, None);
1928 (resolved, env.cfg_env.spec)
1929 };
1930 let ethereum = |hardfork| Some(FoundryHardfork::Ethereum(hardfork));
1931
1932 assert_eq!(
1934 resolve(amsterdam, ExecutionSpecContext::local_or_script(Some(sepolia), None)),
1935 (ethereum(EthereumHardfork::Amsterdam), SpecId::AMSTERDAM)
1936 );
1937 assert_eq!(
1938 resolve(amsterdam - 1, ExecutionSpecContext::local_or_script(Some(sepolia), None)).1,
1939 SpecId::OSAKA
1940 );
1941 assert_eq!(
1943 resolve(
1944 amsterdam,
1945 ExecutionSpecContext::local_or_script(
1946 Some(NamedChain::Dev as u64),
1947 ethereum(EthereumHardfork::Amsterdam)
1948 )
1949 ),
1950 (ethereum(EthereumHardfork::Amsterdam), SpecId::AMSTERDAM)
1951 );
1952 assert_eq!(
1954 resolve(
1955 amsterdam,
1956 ExecutionSpecContext::local_or_script(
1957 Some(sepolia),
1958 ethereum(EthereumHardfork::Prague)
1959 )
1960 ),
1961 (None, SpecId::PRAGUE)
1962 );
1963 }
1964
1965 #[test]
1966 fn resolve_execution_spec_uses_ethereum_schedule_for_historical_execution() {
1967 let config = Config::default();
1968 let timestamp = 1_500_000_000u64;
1969 let expected =
1970 FoundryHardfork::from_chain_and_timestamp(NamedChain::Mainnet as u64, timestamp)
1971 .unwrap();
1972 let mut block = BlockEnv::default();
1973 block.set_timestamp(U256::from(timestamp));
1974 let mut env = EvmEnv::new(CfgEnv::new_with_spec(SpecId::LONDON), block);
1975
1976 assert_eq!(
1977 resolve_execution_spec(
1978 config.evm_version,
1979 config.hardfork,
1980 &mut env,
1981 ExecutionSpecContext::historical(NamedChain::Mainnet as u64, None),
1982 None,
1983 ),
1984 Some(expected)
1985 );
1986 assert_eq!(env.cfg_env.spec, SpecId::from(expected));
1987 }
1988
1989 #[test]
1990 #[cfg(feature = "optimism")]
1991 fn resolve_execution_spec_uses_optimism_schedule_for_local_forks() {
1992 let config =
1993 Config { hardfork: Some("ethereum:london".parse().unwrap()), ..Default::default() };
1994 let chain_id = NamedChain::Optimism as u64;
1995 let timestamp = u64::MAX;
1996 let expected = FoundryHardfork::from_chain_and_timestamp(chain_id, timestamp).unwrap();
1997 let mut block = BlockEnv::default();
1998 block.set_timestamp(U256::from(timestamp));
1999 let mut env = EvmEnv::new(CfgEnv::new_with_spec(OpSpecId::default()), block);
2000
2001 assert_eq!(
2002 resolve_execution_spec(
2003 config.evm_version,
2004 config.hardfork,
2005 &mut env,
2006 ExecutionSpecContext::fork(chain_id, Some("tempo:T3".parse().unwrap())),
2007 None,
2008 ),
2009 Some(expected)
2010 );
2011 assert_eq!(env.cfg_env.spec, OpSpecId::from_foundry_hardfork(expected).unwrap());
2012 }
2013
2014 #[test]
2015 #[cfg(all(feature = "optimism", not(feature = "base")))]
2016 fn resolve_execution_spec_preserves_config_for_base_op_fallback() {
2017 let config = Config { evm_version: EvmVersion::Osaka, ..Default::default() };
2018 let mut block = BlockEnv::default();
2019 block.set_timestamp(U256::from(u64::MAX));
2020 let mut env = EvmEnv::new(CfgEnv::new_with_spec(OpSpecId::default()), block);
2021
2022 assert_eq!(
2023 resolve_execution_spec(
2024 config.evm_version,
2025 config.hardfork,
2026 &mut env,
2027 ExecutionSpecContext::fork(NamedChain::Base as u64, None),
2028 None,
2029 ),
2030 None
2031 );
2032 assert_eq!(env.cfg_env.spec, OpSpecId::KARST);
2033 }
2034
2035 #[test]
2036 #[cfg(feature = "monad")]
2037 fn resolve_execution_spec_honors_explicit_precedence() {
2038 let activation =
2039 foundry_evm_hardforks::MonadHardfork::MonadNine.mainnet_activation_timestamp().unwrap();
2040 let mut configured = Config {
2041 hardfork: Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadNine)),
2042 ..Default::default()
2043 };
2044 let mut env = monad_env(activation - 1);
2045
2046 assert_eq!(
2047 resolve_execution_spec(
2048 configured.evm_version,
2049 configured.hardfork,
2050 &mut env,
2051 ExecutionSpecContext::fork(NamedChain::Monad as u64, None),
2052 None,
2053 ),
2054 configured.hardfork
2055 );
2056 assert_eq!(env.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadNine);
2057
2058 configured.hardfork = None;
2059 assert_eq!(
2060 resolve_execution_spec(
2061 configured.evm_version,
2062 configured.hardfork,
2063 &mut env,
2064 ExecutionSpecContext::fork(NamedChain::Monad as u64, None),
2065 Some(foundry_evm_hardforks::MonadHardfork::MonadEight),
2066 ),
2067 Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadEight))
2068 );
2069 assert_eq!(env.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadEight);
2070 }
2071
2072 #[tokio::test(flavor = "multi_thread")]
2073 #[cfg(feature = "monad")]
2074 async fn fork_context_preserves_source_chain_with_execution_override() {
2075 let activation =
2076 foundry_evm_hardforks::MonadHardfork::MonadNine.mainnet_activation_timestamp().unwrap();
2077 let (_api, handle) = anvil::spawn(
2078 anvil::NodeConfig::test()
2079 .with_networks(NetworkConfigs::with_ethereum())
2080 .with_chain_id(Some(NamedChain::Monad as u64))
2081 .with_genesis_timestamp(Some(activation - 1)),
2082 )
2083 .await;
2084 let mut inferred = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2085 inferred.infer_network_from_fork().await.unwrap();
2086 assert_eq!(inferred.networks, NetworkConfigs::default());
2087
2088 let mut evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2089 evm_opts.env.chain_id = Some(NamedChain::Mainnet as u64);
2090 evm_opts.networks = NetworkConfigs::with_monad();
2091
2092 let (mut evm_env, tx_env, fork) = evm_opts
2093 .env_with_fork_context::<foundry_evm_hardforks::MonadHardfork, BlockEnv, TxEnv>()
2094 .await
2095 .unwrap();
2096 let fork_context = fork.unwrap();
2097
2098 assert_eq!(fork_context.source_chain_id, NamedChain::Monad as u64);
2099 assert_eq!(fork_context.network, NetworkVariant::Ethereum);
2100 assert!(matches!(fork_context.hardfork, Some(FoundryHardfork::Ethereum(_))));
2101 assert_eq!(evm_env.cfg_env.chain_id, NamedChain::Mainnet as u64);
2102 assert_eq!(tx_env.chain_id, Some(NamedChain::Mainnet as u64));
2103 assert_eq!(
2104 resolve_execution_spec(
2105 Config::default().evm_version,
2106 Config::default().hardfork,
2107 &mut evm_env,
2108 ExecutionSpecContext::fork(fork_context.source_chain_id, fork_context.hardfork,),
2109 None,
2110 ),
2111 Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadEight))
2112 );
2113 }
2114
2115 #[tokio::test(flavor = "multi_thread")]
2116 async fn infer_network_default_and_custom_anvil_selects_ethereum() {
2117 for chain_id in [NamedChain::Mainnet as u64, NamedChain::AnvilHardhat as u64, 98_765_432] {
2118 let (_api, handle) =
2119 anvil::spawn(anvil::NodeConfig::test().with_chain_id(Some(chain_id))).await;
2120
2121 let config = Config::figment();
2122 let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2123 evm_opts.fork_url = Some(handle.http_endpoint());
2124 assert_eq!(evm_opts.networks, NetworkConfigs::default());
2125
2126 evm_opts.infer_network_from_fork().await.unwrap();
2127
2128 assert_eq!(evm_opts.env.chain_id, Some(chain_id));
2129 assert!(!evm_opts.networks.is_tempo());
2130 #[cfg(feature = "optimism")]
2131 assert!(!evm_opts.networks.is_optimism());
2132 #[cfg(feature = "base")]
2133 assert!(!evm_opts.networks.is_base());
2134 assert!(!evm_opts.networks.is_celo());
2135 assert_eq!(evm_opts.networks, NetworkConfigs::default());
2136 }
2137 }
2138
2139 #[tokio::test(flavor = "multi_thread")]
2140 async fn infer_network_preserves_explicit_ethereum() {
2141 let networks = NetworkConfigs::with_ethereum();
2142 let mut evm_opts = EvmOpts {
2143 fork_url: Some("http://127.0.0.1:1".to_string()),
2144 networks,
2145 ..Default::default()
2146 };
2147
2148 let error = evm_opts.infer_network_from_fork().await.unwrap_err();
2149
2150 assert!(error.to_string().contains("failed to retrieve chain ID"));
2151 assert_eq!(evm_opts.networks, networks);
2152 assert_eq!(evm_opts.env.chain_id, None);
2153 }
2154
2155 #[tokio::test]
2156 async fn clearing_fork_restores_inferred_network_defaults() {
2157 let mut profiles = vec![NetworkConfigs::with_celo(), NetworkConfigs::with_tempo()];
2158 #[cfg(feature = "optimism")]
2159 profiles.push(NetworkConfigs::with_optimism());
2160 #[cfg(feature = "monad")]
2161 profiles.push(NetworkConfigs::with_monad());
2162 #[cfg(feature = "base")]
2163 profiles.push(NetworkConfigs::with_base());
2164
2165 for networks in profiles {
2166 let mut evm_opts = EvmOpts {
2167 networks,
2168 fork_endpoint: Some(ForkEndpointIdentity {
2169 endpoint: "http://localhost:8545".to_string(),
2170 execution_chain_id: 1,
2171 source_chain_id: 1,
2172 network: networks.execution_network(),
2173 network_profile: networks,
2174 reported_hardfork: None,
2175 hardfork: None,
2176 instance_id: None,
2177 source_fork_block_number: None,
2178 source_fork_block_hash: None,
2179 }),
2180 fork_block_number: Some(123),
2181 fork_network_is_inferred: true,
2182 fork_chain_id_is_inferred: true,
2183 fork_block_number_is_inferred: true,
2184 ..Default::default()
2185 };
2186 evm_opts.env.chain_id = Some(1);
2187
2188 evm_opts.infer_network_from_fork().await.unwrap();
2189
2190 assert_eq!(evm_opts.networks, NetworkConfigs::default());
2191 assert_eq!(evm_opts.fork_endpoint, None);
2192 assert!(!evm_opts.fork_network_is_inferred);
2193 assert_eq!(evm_opts.env.chain_id, None);
2194 assert!(!evm_opts.fork_chain_id_is_inferred);
2195 assert_eq!(evm_opts.fork_block_number, None);
2196 assert!(!evm_opts.fork_block_number_is_inferred);
2197 }
2198 }
2199
2200 #[tokio::test(flavor = "multi_thread")]
2201 async fn fork_network_detects_ethereum_anvil() {
2202 let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2203 let evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2204
2205 let identity = evm_opts.discover_fork_endpoint().await.unwrap();
2206 assert_eq!(
2207 (identity.execution_chain_id, identity.network),
2208 (NamedChain::AnvilHardhat as u64, NetworkVariant::Ethereum)
2209 );
2210 }
2211
2212 #[tokio::test(flavor = "multi_thread")]
2213 async fn fork_network_uses_anvil_metadata_for_unknown_chain() {
2214 let chain_id = 98_765_432;
2215 let (_api, handle) =
2216 anvil::spawn(anvil::NodeConfig::test().with_chain_id(Some(chain_id))).await;
2217 let evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2218
2219 let identity = evm_opts.discover_fork_endpoint().await.unwrap();
2220 assert_eq!(
2221 (identity.execution_chain_id, identity.network),
2222 (chain_id, NetworkVariant::Ethereum)
2223 );
2224 }
2225
2226 #[tokio::test]
2227 #[cfg(feature = "base")]
2228 async fn base_endpoint_identity_uses_generic_chain_profile() {
2229 let endpoint = "http://127.0.0.1:1";
2230 let provider = EvmOpts::default().fork_provider_with_url::<AnyNetwork>(endpoint).unwrap();
2231
2232 let identity = EvmOpts::resolve_fork_endpoint_identity(
2233 &provider,
2234 endpoint,
2235 NamedChain::Base as u64,
2236 None,
2237 None,
2238 EndpointHardforkPolicy::Optional,
2239 )
2240 .await
2241 .unwrap();
2242
2243 assert_eq!(identity.execution_chain_id, NamedChain::Base as u64);
2244 assert_eq!(identity.source_chain_id, NamedChain::Base as u64);
2245 assert_eq!(identity.network, NetworkVariant::Base);
2246 assert!(identity.network_profile.is_base());
2247 assert_eq!(identity.reported_hardfork, None);
2248 assert_eq!(identity.hardfork, None);
2249 }
2250
2251 #[tokio::test(flavor = "multi_thread")]
2252 async fn fork_non_anvil_node_info_rpc_error_is_optional() {
2253 let (_api, handle) =
2254 anvil::spawn(anvil::NodeConfig::test().with_chain_id(Some(NamedChain::Mainnet as u64)))
2255 .await;
2256 let fork_url =
2257 spawn_rpc_proxy_internal_error_after(handle.http_endpoint(), "anvil_nodeInfo", 0).await;
2258 let mut evm_opts = EvmOpts { fork_url: Some(fork_url), ..Default::default() };
2259
2260 evm_opts.infer_network_from_fork().await.unwrap();
2261
2262 assert_eq!(evm_opts.networks, NetworkConfigs::default());
2263 assert_eq!(evm_opts.env.chain_id, Some(NamedChain::Mainnet as u64));
2264 }
2265
2266 #[tokio::test(flavor = "multi_thread")]
2267 async fn fork_node_info_failure_is_strict_after_anvil_is_identified() {
2268 let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2269 let fork_url =
2270 spawn_rpc_proxy_rejecting_method_after(handle.http_endpoint(), "anvil_nodeInfo", 1)
2271 .await;
2272 let evm_opts = EvmOpts { fork_url: Some(fork_url), ..Default::default() };
2273
2274 let error = evm_opts.discover_fork_endpoint().await.unwrap_err();
2275
2276 assert!(
2277 error.to_string().contains("failed to determine network family from endpoint"),
2278 "{error}"
2279 );
2280 }
2281
2282 #[tokio::test(flavor = "multi_thread")]
2283 async fn fork_node_info_probe_becomes_strict_after_initial_failure() {
2284 let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2285 let fork_url =
2286 spawn_rpc_proxy_method_not_found_before(handle.http_endpoint(), "anvil_nodeInfo", 1)
2287 .await;
2288 let evm_opts = EvmOpts { fork_url: Some(fork_url), ..Default::default() };
2289
2290 let fork = evm_opts.prepare_fork().await.unwrap().unwrap();
2291
2292 assert!(fork.context().hardfork.is_some());
2293 assert!(fork.context().instance_id.is_some());
2294 }
2295
2296 #[tokio::test(flavor = "multi_thread")]
2297 async fn fork_cached_anvil_identity_keeps_node_info_probe_strict() {
2298 let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2299 let fork_url =
2300 spawn_rpc_proxy_rejecting_method_after(handle.http_endpoint(), "anvil_nodeInfo", 2)
2301 .await;
2302 let mut evm_opts = EvmOpts { fork_url: Some(fork_url), ..Default::default() };
2303 evm_opts.infer_network_from_fork().await.unwrap();
2304 assert!(evm_opts.fork_endpoint.as_ref().unwrap().reported_hardfork.is_some());
2305
2306 let error = evm_opts.discover_fork_endpoint().await.unwrap_err();
2307
2308 assert!(
2309 error.to_string().contains("failed to determine network family from endpoint"),
2310 "{error}"
2311 );
2312 }
2313
2314 #[tokio::test(flavor = "multi_thread")]
2315 async fn fork_non_anvil_discovery_is_reused_by_resolution() {
2316 let (_api, handle) =
2317 anvil::spawn(anvil::NodeConfig::test().with_chain_id(Some(NamedChain::Mainnet as u64)))
2318 .await;
2319 let endpoint = spawn_rpc_proxy_method_not_found_before(
2320 handle.http_endpoint(),
2321 "anvil_nodeInfo",
2322 usize::MAX,
2323 )
2324 .await;
2325 let (endpoint, chain_ids) = spawn_rpc_proxy_recording_method(endpoint, "eth_chainId").await;
2326 let (endpoint, node_infos) =
2327 spawn_rpc_proxy_recording_method(endpoint, "anvil_nodeInfo").await;
2328 let (endpoint, blocks) =
2329 spawn_rpc_proxy_recording_method(endpoint, "eth_getBlockByNumber").await;
2330 let (endpoint, hashes) =
2331 spawn_rpc_proxy_recording_method(endpoint, "eth_getBlockByHash").await;
2332 let probes = || (chain_ids.lock().unwrap().len(), node_infos.lock().unwrap().len());
2333 let mut evm_opts = EvmOpts { fork_url: Some(endpoint), ..Default::default() };
2334
2335 evm_opts.infer_network_from_fork().await.unwrap();
2336 assert_eq!(probes(), (1, 1));
2337 assert_eq!(evm_opts.fork_endpoint.as_ref().unwrap().reported_hardfork, None);
2338
2339 evm_opts.infer_network_from_fork().await.unwrap();
2340 let backend = crate::backend::Backend::<crate::evm::EthEvmNetwork>::spawn(
2341 evm_opts.get_fork(&Config::default(), NamedChain::Mainnet as u64, None),
2342 )
2343 .unwrap();
2344 backend.env(&evm_opts).await.unwrap();
2345 backend.env(&evm_opts).await.unwrap();
2346 backend.can_use_create2_deployer(evm_opts.create2_deployer).await.unwrap();
2347 backend.transaction_count(evm_opts.sender).await.unwrap();
2348 assert_eq!(probes(), (1, 1));
2349 assert_eq!(blocks.lock().unwrap().len(), 1);
2350 assert!(hashes.lock().unwrap().is_empty());
2351 assert_eq!(evm_opts.fork_block_number, None);
2352
2353 evm_opts.discover_fork_endpoint().await.unwrap();
2355 assert_eq!(probes(), (2, 2));
2356 }
2357
2358 #[tokio::test]
2359 async fn fork_remote_chain_id_does_not_guess_from_url() {
2360 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2361 let address = listener.local_addr().unwrap();
2362 drop(listener);
2363 let evm_opts = EvmOpts {
2364 fork_url: Some(format!("http://{address}/arb-mainnet")),
2365 fork_retries: Some(0),
2366 ..Default::default()
2367 };
2368 assert_eq!(evm_opts.get_remote_chain_id().await, None);
2369 }
2370
2371 #[test]
2372 fn known_network_variant_does_not_guess_unknown_chain() {
2373 assert_eq!(NetworkVariant::from_known_chain_id(98_765_432).unwrap(), None);
2374 }
2375
2376 #[test]
2377 #[cfg(feature = "monad")]
2378 fn known_network_variant_classifies_monad() {
2379 assert_eq!(
2380 NetworkVariant::from_known_chain_id(NamedChain::Monad as u64).unwrap(),
2381 Some(NetworkVariant::Monad)
2382 );
2383 }
2384
2385 #[test]
2386 #[cfg(not(feature = "monad"))]
2387 fn known_network_variant_rejects_disabled_monad() {
2388 assert_eq!(
2389 NetworkVariant::from_known_chain_id(NamedChain::Monad as u64).unwrap_err(),
2390 "network family `monad` is not enabled in this build"
2391 );
2392 }
2393
2394 #[tokio::test(flavor = "multi_thread")]
2395 async fn infer_network_tempo_anvil_via_node_info() {
2396 let (_api, handle) = anvil::spawn(anvil::NodeConfig::test_tempo()).await;
2397
2398 let config = Config::figment();
2399 let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2400 evm_opts.fork_url = Some(handle.http_endpoint());
2401 assert_eq!(evm_opts.networks, NetworkConfigs::default());
2403
2404 evm_opts.infer_network_from_fork().await.unwrap();
2405
2406 assert!(evm_opts.networks.is_tempo(), "should detect tempo via anvil_nodeInfo");
2407 }
2408
2409 #[tokio::test(flavor = "multi_thread")]
2410 async fn infer_network_tempo_propagates_unavailable_rpc() {
2411 let config = Config::figment();
2412 let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2413 evm_opts.fork_url = Some("http://127.0.0.1:1".to_string());
2414 evm_opts.networks = NetworkConfigs::with_tempo();
2415
2416 let error = evm_opts.infer_network_from_fork().await.unwrap_err();
2417
2418 assert!(error.to_string().contains("failed to retrieve chain ID"));
2419 assert!(evm_opts.networks.is_tempo());
2420 }
2421
2422 #[tokio::test]
2423 async fn create2_deployer_availability_requires_resolved_fork_block() {
2424 let evm_opts =
2425 EvmOpts { fork_url: Some("http://127.0.0.1:1".to_string()), ..Default::default() };
2426
2427 let err = evm_opts.can_use_create2_deployer_at(None).await.unwrap_err();
2428 assert!(err.to_string().contains("fork must be resolved"));
2429 }
2430
2431 #[test]
2432 fn resolved_fork_matches_source_headers_and_selector() {
2433 let headers = vec!["Authorization: one".to_string()];
2434 let fork = Fork::test(
2435 "http://127.0.0.1:1",
2436 Some(&headers),
2437 None,
2438 None,
2439 BlockNumHash::new(1, B256::ZERO),
2440 resolved_context(1),
2441 );
2442 let mut evm_opts = EvmOpts {
2443 fork_url: Some("http://127.0.0.1:1".to_string()),
2444 fork_headers: Some(headers),
2445 rpc_headers: Some(vec!["x-fallback: ignored".to_string()]),
2446 ..Default::default()
2447 };
2448
2449 assert!(fork.matches_request(&evm_opts));
2450 let debug = format!("{fork:?}");
2451 assert!(!debug.contains("127.0.0.1"));
2452 assert!(!debug.contains("Authorization"));
2453 assert!(!debug.contains("one"));
2454
2455 evm_opts.fork_headers = Some(vec!["Authorization: two".to_string()]);
2456 assert!(!fork.matches_request(&evm_opts));
2457
2458 evm_opts.fork_headers = Some(vec!["Authorization: one".to_string()]);
2459 evm_opts.fork_block_number = Some(1);
2460 assert!(!fork.matches_request(&evm_opts));
2461
2462 evm_opts.fork_block_number = None;
2463 evm_opts.fork_url = Some("http://127.0.0.1:2".to_string());
2464 assert!(!fork.matches_request(&evm_opts));
2465
2466 let fork = Fork::test(
2467 "http://127.0.0.1:1",
2468 None,
2469 None,
2470 None,
2471 BlockNumHash::new(1, B256::ZERO),
2472 resolved_context(1),
2473 );
2474 evm_opts.fork_url = Some("http://127.0.0.1:1".to_string());
2475 evm_opts.fork_headers = Some(Vec::new());
2476 evm_opts.rpc_headers = None;
2477 assert!(fork.matches_request(&evm_opts));
2478
2479 let rpc_headers = vec!["Authorization: fallback".to_string()];
2480 let fork = Fork::test(
2481 "http://127.0.0.1:1",
2482 Some(&rpc_headers),
2483 Some("secret-jwt"),
2484 None,
2485 BlockNumHash::new(1, B256::ZERO),
2486 resolved_context(1),
2487 );
2488 evm_opts.fork_headers = None;
2489 evm_opts.rpc_headers = Some(rpc_headers);
2490 evm_opts.rpc_jwt = Some("secret-jwt".to_string());
2491 assert!(fork.matches_request(&evm_opts));
2492 assert!(!format!("{fork:?}").contains("secret-jwt"));
2493
2494 evm_opts.rpc_jwt = Some("different-jwt".to_string());
2495 assert!(!fork.matches_request(&evm_opts));
2496 }
2497
2498 #[tokio::test(flavor = "multi_thread")]
2499 async fn create2_deployer_preflight_uses_exact_fork_hash() {
2500 let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2501 let provider = handle.http_provider();
2502 let sender = handle.dev_accounts().next().unwrap();
2503
2504 let init_code = bytes!("6001600c60003960016000f300");
2505 let receipt = provider
2506 .send_transaction(WithOtherFields::new(
2507 TransactionRequest::default().from(sender).with_deploy_code(init_code),
2508 ))
2509 .await
2510 .unwrap()
2511 .get_receipt()
2512 .await
2513 .unwrap();
2514 let deployer = receipt.contract_address.unwrap();
2515 let block_number = receipt.block_number.unwrap();
2516
2517 let evm_opts = EvmOpts {
2518 fork_url: Some(handle.http_endpoint()),
2519 fork_block_number: Some(block_number),
2520 create2_deployer: deployer,
2521 ..Default::default()
2522 };
2523 let fork = evm_opts.prepare_fork().await.unwrap();
2524 let fork = fork.unwrap();
2525 assert!(evm_opts.can_use_create2_deployer_at(Some(&fork)).await.unwrap());
2526
2527 provider
2528 .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
2529 .await
2530 .unwrap();
2531 let replacement = provider.get_block_by_number(block_number.into()).await.unwrap().unwrap();
2532 assert_ne!(replacement.header().hash(), fork.hash());
2533 assert!(
2534 provider.get_code_at(deployer).number(block_number).await.unwrap().is_empty(),
2535 "the replacement block must not contain the deployer"
2536 );
2537
2538 assert!(
2539 !matches!(evm_opts.can_use_create2_deployer_at(Some(&fork)).await, Ok(false)),
2540 "the exact lookup fell back to the replacement block"
2541 );
2542 }
2543
2544 #[tokio::test(flavor = "multi_thread")]
2545 #[cfg(feature = "monad")]
2546 async fn infer_network_monad_anvil_via_node_info() {
2547 for chain_id in [NamedChain::Mainnet as u64, NamedChain::AnvilHardhat as u64, 98_765_432] {
2548 let (_api, handle) =
2549 anvil::spawn(anvil::NodeConfig::test_monad().with_chain_id(Some(chain_id))).await;
2550
2551 let config = Config::figment();
2552 let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2553 evm_opts.fork_url = Some(handle.http_endpoint());
2554 assert_eq!(evm_opts.networks, NetworkConfigs::default());
2555
2556 evm_opts.infer_network_from_fork().await.unwrap();
2557
2558 assert_eq!(evm_opts.env.chain_id, Some(chain_id));
2559 assert!(
2560 evm_opts.networks.is_monad(),
2561 "should detect Monad via anvil_nodeInfo for chain {chain_id}"
2562 );
2563 }
2564 }
2565
2566 #[tokio::test(flavor = "multi_thread")]
2567 #[cfg(feature = "monad")]
2568 async fn fork_context_carries_exact_monad_anvil_hardfork() {
2569 let activation =
2570 foundry_evm_hardforks::MonadHardfork::MonadNine.mainnet_activation_timestamp().unwrap();
2571 let (_api, handle) = anvil::spawn(
2572 anvil::NodeConfig::test_monad()
2573 .with_chain_id(Some(NamedChain::Monad as u64))
2574 .with_genesis_timestamp(Some(activation))
2575 .with_hardfork(Some(foundry_evm_hardforks::MonadHardfork::MonadEight.into())),
2576 )
2577 .await;
2578 let mut evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2579
2580 evm_opts.infer_network_from_fork().await.unwrap();
2581 let (mut evm_env, _, fork) = evm_opts
2582 .env_with_fork_context::<foundry_evm_hardforks::MonadHardfork, BlockEnv, TxEnv>()
2583 .await
2584 .unwrap();
2585 let context = fork.unwrap();
2586
2587 assert_eq!(
2588 context.hardfork,
2589 Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadEight))
2590 );
2591 assert_eq!(
2592 resolve_execution_spec(
2593 Config::default().evm_version,
2594 Config::default().hardfork,
2595 &mut evm_env,
2596 ExecutionSpecContext::fork(context.source_chain_id, context.hardfork),
2597 None,
2598 ),
2599 context.hardfork
2600 );
2601 assert_eq!(evm_env.cfg_env.spec, foundry_evm_hardforks::MonadHardfork::MonadEight);
2602 }
2603
2604 #[tokio::test(flavor = "multi_thread")]
2605 #[cfg(feature = "monad")]
2606 async fn fork_context_refreshes_identity_after_same_url_reset() {
2607 let (_api, monad_eight) = anvil::spawn(
2608 anvil::NodeConfig::test_monad()
2609 .with_chain_id(Some(NamedChain::MonadTestnet as u64))
2610 .with_hardfork(Some(foundry_evm_hardforks::MonadHardfork::MonadEight.into())),
2611 )
2612 .await;
2613 let (_api, monad_nine) = anvil::spawn(
2614 anvil::NodeConfig::test_monad()
2615 .with_chain_id(Some(NamedChain::Monad as u64))
2616 .with_hardfork(Some(foundry_evm_hardforks::MonadHardfork::MonadNine.into())),
2617 )
2618 .await;
2619 let (fork_api, fork_handle) = anvil::spawn(
2620 anvil::NodeConfig::test()
2621 .with_chain_id(Some(NamedChain::Mainnet as u64))
2622 .with_no_storage_caching(true)
2623 .with_eth_rpc_url(Some(monad_eight.http_endpoint()))
2624 .with_fork_block_number(Some(0u64)),
2625 )
2626 .await;
2627
2628 let mut evm_opts =
2629 EvmOpts { fork_url: Some(fork_handle.http_endpoint()), ..Default::default() };
2630 evm_opts.infer_network_from_fork().await.unwrap();
2631 let cached = evm_opts.fork_endpoint.as_ref().unwrap();
2632 assert_eq!(cached.source_chain_id, NamedChain::MonadTestnet as u64);
2633 assert_eq!(
2634 cached.hardfork,
2635 Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadEight))
2636 );
2637
2638 let (_, _, first) = evm_opts
2639 .env_with_fork_context::<foundry_evm_hardforks::MonadHardfork, BlockEnv, TxEnv>()
2640 .await
2641 .unwrap();
2642 assert_eq!(first.unwrap().source_chain_id, NamedChain::MonadTestnet as u64);
2643
2644 fork_api
2645 .anvil_reset(Some(alloy_rpc_types::anvil::Forking {
2646 json_rpc_url: Some(monad_nine.http_endpoint()),
2647 block_number: Some(0),
2648 }))
2649 .await
2650 .unwrap();
2651
2652 let (_, _, second) = evm_opts
2653 .env_with_fork_context::<foundry_evm_hardforks::MonadHardfork, BlockEnv, TxEnv>()
2654 .await
2655 .unwrap();
2656 let second = second.unwrap();
2657 assert_eq!(second.source_chain_id, NamedChain::Monad as u64);
2658 assert_eq!(
2659 second.hardfork,
2660 Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadNine))
2661 );
2662 }
2663
2664 #[tokio::test(flavor = "multi_thread")]
2665 async fn expected_fork_endpoint_detects_same_url_reset() {
2666 let (api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2667 let mut evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2668 evm_opts.infer_network_from_fork().await.unwrap();
2669 let identity = evm_opts.fork_endpoint.clone().unwrap();
2670 let original_instance = identity.instance_id;
2671 let network_is_inferred = evm_opts.fork_network_is_inferred;
2672 evm_opts.expect_fork_endpoint(identity, network_is_inferred);
2673
2674 api.anvil_reset(None).await.unwrap();
2675
2676 assert_ne!(api.instance_id(), original_instance.unwrap());
2677 let error = evm_opts.env_with_fork_context::<SpecId, BlockEnv, TxEnv>().await.unwrap_err();
2678 assert!(
2679 error.to_string().contains("changed after its execution context was selected"),
2680 "{error}"
2681 );
2682 }
2683
2684 #[tokio::test(flavor = "multi_thread")]
2685 async fn prepared_fork_preserves_inferred_execution_profile_guard() {
2686 let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2687 let mut opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2688 let fork = opts.prepare_fork().await.unwrap().unwrap();
2689 opts.networks = NetworkConfigs::with_tempo();
2690 opts.fork_network_is_inferred = true;
2691 let error = opts.env_at_fork::<SpecId, BlockEnv, TxEnv>(Some(&fork)).await.unwrap_err();
2692 assert!(error.to_string().contains("changed execution profile"), "{error}");
2693 opts.fork_network_is_inferred = false;
2694 opts.env_at_fork::<SpecId, BlockEnv, TxEnv>(Some(&fork)).await.unwrap();
2695 }
2696
2697 #[tokio::test(flavor = "multi_thread")]
2698 async fn prepared_fork_reconnects_after_runtime_shutdown() {
2699 let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2700 for endpoint in [handle.http_endpoint(), handle.ws_endpoint()] {
2701 tokio::task::spawn_blocking(move || {
2702 let opts = EvmOpts { fork_url: Some(endpoint), ..Default::default() };
2703 let runtime =
2704 || tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
2705 let fork = runtime().block_on(opts.prepare_fork()).unwrap().unwrap();
2706 let (env, _) = runtime()
2707 .block_on(opts.env_at_fork::<SpecId, BlockEnv, TxEnv>(Some(&fork)))
2708 .unwrap();
2709 assert_eq!(env.block_env.number, U256::from(fork.number()));
2710 })
2711 .await
2712 .unwrap();
2713 }
2714 }
2715
2716 #[tokio::test(flavor = "multi_thread")]
2717 async fn resolved_fork_environment_retains_exact_block() {
2718 let (api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2719 let evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2720 let fork = evm_opts.prepare_fork().await.unwrap().unwrap();
2721 let provider = handle.http_provider();
2722
2723 api.anvil_mine(Some(U256::ONE), None).await.unwrap();
2724 assert!(provider.get_block_number().await.unwrap() > fork.number());
2725
2726 let (evm_env, _) =
2727 evm_opts.env_at_fork::<SpecId, BlockEnv, TxEnv>(Some(&fork)).await.unwrap();
2728 let block = &fork.block;
2729
2730 assert_eq!(block.header().hash(), fork.hash());
2731 assert_eq!(block.header().number(), fork.number());
2732 assert_eq!(evm_env.block_env.number, U256::from(fork.number()));
2733 assert_eq!(evm_env.block_env.timestamp, U256::from(block.header().timestamp()));
2734 }
2735
2736 #[tokio::test(flavor = "multi_thread")]
2737 async fn resolved_fork_detects_same_url_reset_without_cached_expectation() {
2738 let (api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2739 let evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2740 let fork = evm_opts.prepare_fork().await.unwrap().unwrap();
2741 let original_instance = fork.context().instance_id;
2742
2743 api.anvil_reset(None).await.unwrap();
2744
2745 assert_ne!(api.instance_id(), original_instance.unwrap());
2746 let error = evm_opts.env_at_fork::<SpecId, BlockEnv, TxEnv>(Some(&fork)).await.unwrap_err();
2747 assert!(
2748 error
2749 .to_string()
2750 .contains("changed after its block and execution context were resolved"),
2751 "{error}"
2752 );
2753 }
2754
2755 #[test]
2756 #[cfg(feature = "monad")]
2757 fn unknown_endpoint_hardfork_is_optional_only_for_remote_execution() {
2758 assert_eq!(NetworkVariant::from_node_info_name("monad").unwrap(), NetworkVariant::Monad);
2759 assert_eq!(
2760 endpoint_hardfork(
2761 NetworkVariant::Monad,
2762 "MonadFuture",
2763 EndpointHardforkPolicy::Optional
2764 )
2765 .unwrap(),
2766 None
2767 );
2768
2769 let error = endpoint_hardfork(
2770 NetworkVariant::Monad,
2771 "MonadFuture",
2772 EndpointHardforkPolicy::Required,
2773 )
2774 .unwrap_err();
2775 assert!(
2776 error.to_string().contains("unsupported hardfork `MonadFuture` reported for `monad`")
2777 );
2778 assert_eq!(
2779 endpoint_hardfork(NetworkVariant::Monad, "MonadNine", EndpointHardforkPolicy::Required)
2780 .unwrap(),
2781 Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadNine))
2782 );
2783 }
2784
2785 #[test]
2786 #[cfg(feature = "base")]
2787 fn unknown_base_endpoint_hardfork_is_optional_only_for_remote_execution() {
2788 assert_eq!(NetworkVariant::from_node_info_name("base").unwrap(), NetworkVariant::Base);
2789 assert_eq!(
2790 endpoint_hardfork(NetworkVariant::Base, "BaseFuture", EndpointHardforkPolicy::Optional)
2791 .unwrap(),
2792 None
2793 );
2794
2795 let error =
2796 endpoint_hardfork(NetworkVariant::Base, "BaseFuture", EndpointHardforkPolicy::Required)
2797 .unwrap_err();
2798 assert!(
2799 error.to_string().contains("unsupported hardfork `BaseFuture` reported for `base`")
2800 );
2801 assert_eq!(
2802 endpoint_hardfork(NetworkVariant::Base, "Beryl", EndpointHardforkPolicy::Required)
2803 .unwrap(),
2804 Some(FoundryHardfork::Base(BaseUpgrade::Beryl))
2805 );
2806 }
2807
2808 #[tokio::test(flavor = "multi_thread")]
2809 #[cfg(feature = "monad")]
2810 async fn fork_network_detects_monad_anvil() {
2811 let (_api, handle) = anvil::spawn(anvil::NodeConfig::test_monad()).await;
2812 let evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2813
2814 let identity = evm_opts.discover_fork_endpoint().await.unwrap();
2815 assert_eq!(
2816 (identity.execution_chain_id, identity.network),
2817 (NamedChain::AnvilHardhat as u64, NetworkVariant::Monad)
2818 );
2819 }
2820
2821 #[tokio::test(flavor = "multi_thread")]
2822 #[cfg(feature = "monad")]
2823 async fn infer_network_monad_propagates_unavailable_rpc() {
2824 let config = Config::figment();
2825 let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2826 evm_opts.fork_url = Some("http://127.0.0.1:1".to_string());
2827 evm_opts.networks = NetworkConfigs::with_monad();
2828
2829 let error = evm_opts.infer_network_from_fork().await.unwrap_err();
2830
2831 assert!(error.to_string().contains("failed to retrieve chain ID"));
2832 assert!(evm_opts.networks.is_monad());
2833 }
2834
2835 #[tokio::test(flavor = "multi_thread")]
2836 #[cfg(feature = "base")]
2837 async fn infer_network_base_propagates_unavailable_rpc() {
2838 let mut evm_opts = EvmOpts {
2839 fork_url: Some("http://127.0.0.1:1".to_string()),
2840 networks: NetworkConfigs::with_base(),
2841 ..Default::default()
2842 };
2843
2844 let error = evm_opts.infer_network_from_fork().await.unwrap_err();
2845
2846 assert!(error.to_string().contains("failed to retrieve chain ID"));
2847 assert!(evm_opts.networks.is_base());
2848 assert_eq!(evm_opts.fork_endpoint, None);
2849 }
2850
2851 #[tokio::test(flavor = "multi_thread")]
2852 async fn flaky_infer_network_tempo_moderato_rpc() {
2853 let config = Config::figment();
2854 let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2855 evm_opts.fork_url = Some("https://rpc.moderato.tempo.xyz".to_string());
2856 assert_eq!(evm_opts.networks, NetworkConfigs::default());
2857
2858 evm_opts.infer_network_from_fork().await.unwrap();
2859
2860 assert!(evm_opts.networks.is_tempo(), "should detect tempo from Moderato chain ID");
2862 }
2863
2864 #[tokio::test(flavor = "multi_thread")]
2865 async fn get_fork_pins_block_number_from_env() {
2866 let endpoint = foundry_test_utils::rpc::next_http_rpc_endpoint();
2867
2868 let config = Config::figment();
2869 let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2870 evm_opts.fork_url = Some(endpoint.clone());
2871 assert!(evm_opts.fork_block_number.is_none());
2873
2874 let (evm_env, _, fork) =
2876 evm_opts.env_with_fork_context::<SpecId, BlockEnv, TxEnv>().await.unwrap();
2877 let fork_block = fork.as_ref().map(|context| context.block_number);
2878 assert!(fork_block.is_some(), "should have resolved a fork block number");
2879 let resolved_block = fork_block.unwrap();
2880 assert!(resolved_block > 0, "should have resolved to a real block number");
2881
2882 let fork =
2884 evm_opts.get_fork(&Config::default(), evm_env.cfg_env.chain_id, fork_block).unwrap();
2885
2886 assert_eq!(
2888 fork.evm_opts.fork_block_number,
2889 Some(resolved_block),
2890 "get_fork should pin fork_block_number to the block from env"
2891 );
2892 assert!(fork.evm_opts.fork_block_number_is_inferred);
2893 }
2894
2895 #[tokio::test(flavor = "multi_thread")]
2896 async fn fork_backend_rejects_changed_selected_endpoint() {
2897 let (api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
2898 let mut opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() };
2899 opts.infer_network_from_fork().await.unwrap();
2900 opts.expect_fork_endpoint(
2901 opts.fork_endpoint.clone().unwrap(),
2902 opts.fork_network_is_inferred,
2903 );
2904 let request = opts.get_fork(&Config::default(), 31337, None);
2905 api.anvil_reset(None).await.unwrap();
2906 let error =
2907 crate::backend::Backend::<crate::evm::EthEvmNetwork>::spawn(request).unwrap_err();
2908 assert!(
2909 error.to_string().contains("changed after its execution context was selected"),
2910 "{error}"
2911 );
2912 }
2913
2914 #[tokio::test(flavor = "multi_thread")]
2919 async fn flaky_get_fork_uses_l2_block_number_on_arbitrum() {
2920 let endpoint =
2921 foundry_test_utils::rpc::next_rpc_endpoint(foundry_config::NamedChain::Arbitrum);
2922
2923 let config = Config::figment();
2924 let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2925 evm_opts.fork_url = Some(endpoint.clone());
2926 assert!(evm_opts.fork_block_number.is_none());
2927
2928 let (evm_env, _, fork) =
2929 evm_opts.env_with_fork_context::<SpecId, BlockEnv, TxEnv>().await.unwrap();
2930 let fork_block = fork
2931 .as_ref()
2932 .map(|context| context.block_number)
2933 .expect("should have resolved a fork block number");
2934
2935 let block_env_number: u64 = evm_env.block_env.number.to();
2938 assert!(
2939 fork_block > block_env_number,
2940 "fork_block ({fork_block}) should be the L2 block, which is larger than \
2941 block_env.number ({block_env_number}) which is the L1 block on Arbitrum"
2942 );
2943
2944 let fork = evm_opts
2946 .get_fork(&Config::default(), evm_env.cfg_env.chain_id, Some(fork_block))
2947 .unwrap();
2948 assert_eq!(
2949 fork.evm_opts.fork_block_number,
2950 Some(fork_block),
2951 "get_fork should pin to the L2 block number, not the L1 block number"
2952 );
2953 }
2954
2955 #[tokio::test(flavor = "multi_thread")]
2956 async fn get_fork_preserves_explicit_block_number() {
2957 let endpoint = foundry_test_utils::rpc::next_http_rpc_endpoint();
2958
2959 let config = Config::figment();
2960 let mut evm_opts = config.extract::<EvmOpts>().unwrap();
2961 evm_opts.fork_url = Some(endpoint.clone());
2962 evm_opts.fork_block_number = Some(12345678);
2964
2965 let (evm_env, _, fork) =
2966 evm_opts.env_with_fork_context::<SpecId, BlockEnv, TxEnv>().await.unwrap();
2967 let fork_block = fork.as_ref().map(|context| context.block_number);
2968
2969 let fork =
2970 evm_opts.get_fork(&Config::default(), evm_env.cfg_env.chain_id, fork_block).unwrap();
2971
2972 assert_eq!(
2974 fork.evm_opts.fork_block_number,
2975 Some(12345678),
2976 "get_fork should preserve explicitly set fork_block_number"
2977 );
2978 assert!(!fork.evm_opts.fork_block_number_is_inferred);
2979 }
2980}