1use crate::celo::transfer::{CELO_TRANSFER_ADDRESS, PRECOMPILE_ID_CELO_TRANSFER};
16use alloy_chains::{
17 Chain, NamedChain,
18 NamedChain::{Chiado, Gnosis, Moonbase, Moonbeam, MoonbeamDev, Moonriver, Rsk, RskTestnet},
19};
20use alloy_eips::{eip1559::BaseFeeParams, eip7840::BlobParams};
21use alloy_evm::precompiles::{DynPrecompile, PrecompilesMap};
22use alloy_primitives::{Address, ChainId, address, map::AddressHashMap};
23use clap::Parser;
24use foundry_evm_hardforks::{
25 EthereumHardfork, ExecutionSpec, FoundryHardfork, TempoHardfork, latest_active_tempo_hardfork,
26};
27use revm::precompile::{
28 Precompile as RevmPrecompile,
29 secp256r1::{P256VERIFY, P256VERIFY_OSAKA},
30};
31use serde::{Deserialize, Serialize};
32use std::collections::BTreeMap;
33use tempo_contracts::precompiles::{
34 ACCOUNT_KEYCHAIN_ADDRESS, ADDRESS_REGISTRY_ADDRESS, CURRENT_COMMITTEE_ADDRESS,
35 NONCE_PRECOMPILE_ADDRESS, RECEIVE_POLICY_GUARD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS,
36 STABLECOIN_DEX_ADDRESS, STORAGE_CREDITS_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
37 TIP20_CHANNEL_RESERVE_ADDRESS, TIP20_FACTORY_ADDRESS, TIP403_REGISTRY_ADDRESS,
38 VALIDATOR_CONFIG_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
39};
40
41#[cfg(feature = "base")]
42use base_common_precompiles::{
43 ActivationRegistryStorage, B20FactoryStorage, NonceManagerStorage, PolicyRegistryStorage,
44 TxContextStorage,
45};
46#[cfg(feature = "base")]
47use foundry_evm_hardforks::BaseUpgrade;
48
49#[cfg(feature = "optimism")]
50use foundry_evm_hardforks::OpHardfork;
51
52#[cfg(feature = "monad")]
53type MonadHardfork = foundry_evm_hardforks::MonadHardfork;
54
55pub const MONAD_CHEATCODE_ADDRESS: Address = address!("0xc0FFeeCD43A10e1C2b0De63c6CDCFe5B7d0e0CEA");
57
58pub mod arbitrum;
59pub mod celo;
60
61#[cfg(feature = "optimism")]
62mod optimism;
63
64pub const TEMPO_PRECOMPILES: &[(&str, Address)] = &[
66 ("Nonce", NONCE_PRECOMPILE_ADDRESS),
67 ("StablecoinDex", STABLECOIN_DEX_ADDRESS),
68 ("TIP20Factory", TIP20_FACTORY_ADDRESS),
69 ("TIP403Registry", TIP403_REGISTRY_ADDRESS),
70 ("FeeManager", TIP_FEE_MANAGER_ADDRESS),
71 ("ValidatorConfig", VALIDATOR_CONFIG_ADDRESS),
72 ("ValidatorConfigV2", VALIDATOR_CONFIG_V2_ADDRESS),
73 ("AccountKeychain", ACCOUNT_KEYCHAIN_ADDRESS),
74 ("SignatureVerifier", SIGNATURE_VERIFIER_ADDRESS),
75 ("AddressRegistry", ADDRESS_REGISTRY_ADDRESS),
76 ("TIP20ChannelReserve", TIP20_CHANNEL_RESERVE_ADDRESS),
77 ("ReceivePolicyGuard", RECEIVE_POLICY_GUARD_ADDRESS),
78 ("StorageCredits", STORAGE_CREDITS_ADDRESS),
79 ("CurrentCommittee", CURRENT_COMMITTEE_ADDRESS),
80];
81
82#[cfg(feature = "monad")]
83const MONAD_PRECOMPILE_LABELS: &[(&str, Address)] = &[
84 ("Staking", monad_revm::staking::STAKING_ADDRESS),
85 ("ReserveBalance", monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS),
86];
87
88#[cfg(feature = "monad")]
89const MONAD_PRECOMPILES: &[(&str, Address)] = &[
90 ("MonadStaking", monad_revm::staking::STAKING_ADDRESS),
91 ("MonadReserveBalance", monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS),
92];
93
94#[cfg(feature = "base")]
95const BASE_PRECOMPILES: &[(&str, Address)] = &[
96 ("B20Factory", B20FactoryStorage::ADDRESS),
97 ("ActivationRegistry", ActivationRegistryStorage::ADDRESS),
98 ("PolicyRegistry", PolicyRegistryStorage::ADDRESS),
99 ("TxContext", TxContextStorage::ADDRESS),
100 ("NonceManager", NonceManagerStorage::ADDRESS),
101];
102
103#[cfg(feature = "base")]
115pub const BASE_CODE_SENTINEL_ADDRESSES: &[Address] =
116 &[ActivationRegistryStorage::ADDRESS, PolicyRegistryStorage::ADDRESS];
117
118const BSC_P256_ADDRESS: Address = address!("0000000000000000000000000000000000000100");
120
121const BSC_MAINNET_CHAIN_ID: u64 = 56;
122const BSC_TESTNET_CHAIN_ID: u64 = 97;
123const BSC_MAINNET_HABER_TIMESTAMP: u64 = 1_718_863_500;
124const BSC_TESTNET_HABER_TIMESTAMP: u64 = 1_716_962_820;
125const BSC_MAINNET_OSAKA_TIMESTAMP: u64 = 1_777_343_400;
126const BSC_TESTNET_OSAKA_TIMESTAMP: u64 = 1_774_319_400;
127
128pub const TEMPO_PRECOMPILE_ADDRESSES: &[Address] = &{
130 let mut addresses = [Address::ZERO; TEMPO_PRECOMPILES.len()];
131 let mut i = 0;
132 while i < addresses.len() {
133 addresses[i] = TEMPO_PRECOMPILES[i].1;
134 i += 1;
135 }
136 addresses
137};
138
139#[derive(
140 Clone,
141 Copy,
142 Debug,
143 Default,
144 PartialEq,
145 Eq,
146 PartialOrd,
147 Ord,
148 Hash,
149 Serialize,
150 Deserialize,
151 clap::ValueEnum,
152)]
153#[serde(rename_all = "lowercase")]
154#[clap(rename_all = "lowercase")]
155pub enum NetworkVariant {
156 #[default]
157 Ethereum,
158 #[cfg(feature = "base")]
159 Base,
160 #[cfg(feature = "optimism")]
161 Optimism,
162 Tempo,
163 #[cfg(feature = "monad")]
164 Monad,
165}
166
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
169pub struct NetworkContractSizeLimits {
170 pub runtime: usize,
172 pub initcode: usize,
174}
175
176impl std::str::FromStr for NetworkVariant {
177 type Err = String;
178
179 fn from_str(s: &str) -> Result<Self, Self::Err> {
180 match s {
181 "ethereum" => Ok(Self::Ethereum),
182 #[cfg(feature = "base")]
183 "base" => Ok(Self::Base),
184 #[cfg(feature = "optimism")]
185 "optimism" => Ok(Self::Optimism),
186 "tempo" => Ok(Self::Tempo),
187 #[cfg(feature = "monad")]
188 "monad" => Ok(Self::Monad),
189 _ => Err(format!("unknown network variant: {s}")),
190 }
191 }
192}
193
194impl NetworkVariant {
195 pub fn from_known_chain_id(chain_id: ChainId) -> Result<Option<Self>, String> {
201 let chain = Chain::from_id(chain_id);
202 if chain.is_tempo() {
203 return Ok(Some(Self::Tempo));
204 }
205 if matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia)) {
206 return Ok(Some(Self::Ethereum));
207 }
208 #[cfg(feature = "base")]
213 if matches!(chain.named(), Some(NamedChain::Base | NamedChain::BaseSepolia)) {
214 return Ok(Some(Self::Base));
215 }
216 if matches!(chain.named(), Some(NamedChain::Monad | NamedChain::MonadTestnet)) {
217 #[cfg(feature = "monad")]
218 return Ok(Some(Self::Monad));
219 #[cfg(not(feature = "monad"))]
220 return Err("network family `monad` is not enabled in this build".to_string());
221 }
222 if chain.is_optimism() {
223 #[cfg(feature = "optimism")]
224 return Ok(Some(Self::Optimism));
225 #[cfg(not(feature = "optimism"))]
226 return Err("network family `optimism` is not enabled in this build".to_string());
227 }
228 Ok(chain.named().map(|_| Self::Ethereum))
229 }
230
231 pub fn from_node_info_name(network: &str) -> Result<Self, String> {
233 match network {
234 "ethereum" => Ok(Self::Ethereum),
235 #[cfg(feature = "base")]
236 "base" => Ok(Self::Base),
237 #[cfg(not(feature = "base"))]
238 "base" => Err("network family `base` is not enabled in this build".to_string()),
239 #[cfg(feature = "optimism")]
240 "optimism" => Ok(Self::Optimism),
241 #[cfg(not(feature = "optimism"))]
242 "optimism" => Err("network family `optimism` is not enabled in this build".to_string()),
243 "tempo" => Ok(Self::Tempo),
244 #[cfg(feature = "monad")]
245 "monad" => Ok(Self::Monad),
246 #[cfg(not(feature = "monad"))]
247 "monad" => Err("network family `monad` is not enabled in this build".to_string()),
248 network => {
249 Err(format!("unsupported network family `{network}` reported by fork endpoint"))
250 }
251 }
252 }
253
254 pub fn parse_hardfork(self, hardfork: &str) -> Result<FoundryHardfork, String> {
256 format!("{}:{hardfork}", self.name()).parse()
257 }
258
259 pub fn historical_hardfork(self, chain_id: ChainId, timestamp: u64) -> Option<FoundryHardfork> {
264 match self {
265 Self::Ethereum => {
266 EthereumHardfork::from_chain_and_timestamp(Chain::from_id(chain_id), timestamp)
267 .map(Into::into)
268 }
269 Self::Tempo => {
270 TempoHardfork::from_chain_and_timestamp(chain_id, timestamp).map(Into::into)
271 }
272 #[cfg(feature = "optimism")]
273 Self::Optimism => {
274 OpHardfork::from_chain_and_timestamp(Chain::from_id(chain_id), timestamp)
275 .map(Into::into)
276 }
277 #[cfg(feature = "monad")]
278 Self::Monad => {
279 MonadHardfork::from_chain_and_timestamp(chain_id, timestamp).map(Into::into)
280 }
281 #[cfg(feature = "base")]
282 Self::Base => {
283 BaseUpgrade::from_chain_and_timestamp(chain_id, timestamp).map(Into::into)
284 }
285 }
286 }
287
288 pub fn hardfork_at(self, chain_id: ChainId, timestamp: u64) -> FoundryHardfork {
293 self.historical_hardfork(chain_id, timestamp).unwrap_or_else(|| match self {
294 Self::Ethereum => EthereumHardfork::default().into(),
295 Self::Tempo => latest_active_tempo_hardfork().into(),
296 #[cfg(feature = "optimism")]
297 Self::Optimism => OpHardfork::default().into(),
298 #[cfg(feature = "monad")]
299 Self::Monad => MonadHardfork::default().into(),
300 #[cfg(feature = "base")]
301 Self::Base => BaseUpgrade::default().into(),
302 })
303 }
304
305 pub const fn is_ethereum(&self) -> bool {
307 matches!(self, Self::Ethereum)
308 }
309
310 #[cfg(feature = "base")]
312 pub const fn is_base(&self) -> bool {
313 matches!(self, Self::Base)
314 }
315
316 pub const fn is_optimism(&self) -> bool {
318 #[cfg(feature = "optimism")]
319 {
320 matches!(self, Self::Optimism)
321 }
322 #[cfg(not(feature = "optimism"))]
323 {
324 false
325 }
326 }
327
328 pub const fn is_tempo(&self) -> bool {
330 matches!(self, Self::Tempo)
331 }
332
333 #[cfg(feature = "monad")]
335 pub const fn is_monad(&self) -> bool {
336 matches!(self, Self::Monad)
337 }
338
339 #[cfg(not(feature = "monad"))]
341 pub const fn is_monad(&self) -> bool {
342 false
343 }
344
345 pub const fn name(&self) -> &'static str {
347 match self {
348 Self::Ethereum => "ethereum",
349 #[cfg(feature = "base")]
350 Self::Base => "base",
351 #[cfg(feature = "optimism")]
352 Self::Optimism => "optimism",
353 Self::Tempo => "tempo",
354 #[cfg(feature = "monad")]
355 Self::Monad => "monad",
356 }
357 }
358
359 pub const fn hardfork_namespace(&self) -> Option<&'static str> {
361 match self {
362 Self::Ethereum => None,
363 #[cfg(feature = "base")]
364 Self::Base => Some("base"),
365 #[cfg(feature = "optimism")]
366 Self::Optimism => Some("optimism"),
367 Self::Tempo => Some("tempo"),
368 #[cfg(feature = "monad")]
369 Self::Monad => Some("monad"),
370 }
371 }
372}
373
374impl std::fmt::Display for NetworkVariant {
375 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376 f.write_str(self.name())
377 }
378}
379
380impl From<ChainId> for NetworkVariant {
381 fn from(chain_id: ChainId) -> Self {
382 Self::from_known_chain_id(chain_id).ok().flatten().unwrap_or(Self::Ethereum)
383 }
384}
385
386#[derive(Clone, Debug, Default, Parser, Deserialize, Copy, PartialEq, Eq, Hash)]
387pub struct NetworkConfigs {
388 #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "tempo"])]
390 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
391 #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
392 #[serde(default)]
393 pub(crate) network: Option<NetworkVariant>,
394 #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "tempo"])]
396 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
397 #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
398 celo: bool,
399 #[cfg(feature = "optimism")]
401 #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
402 #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
403 #[serde(default)]
406 pub(crate) optimism: bool,
407 #[arg(long, hide = true, conflicts_with_all = ["network", "celo"])]
409 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
410 #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
411 #[serde(default)]
414 tempo: bool,
415 #[cfg(feature = "monad")]
417 #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
418 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
419 #[serde(default)]
422 monad: bool,
423 #[arg(skip)]
425 #[serde(default)]
426 bypass_prevrandao: bool,
427}
428
429impl Serialize for NetworkConfigs {
433 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
434 use serde::ser::SerializeStruct;
435 let mut s = serializer.serialize_struct("NetworkConfigs", 3)?;
436 s.serialize_field("network", &self.resolved_network())?;
437 s.serialize_field("celo", &self.celo)?;
438 s.serialize_field("bypass_prevrandao", &self.bypass_prevrandao)?;
439 s.end()
440 }
441}
442
443impl NetworkConfigs {
444 pub fn validate(&self) -> Result<(), String> {
449 let mut selectors = Vec::new();
450 if let Some(network) = self.network {
451 selectors.push((network.name(), format!("network = \"{}\"", network.name())));
452 }
453 if self.celo {
454 selectors.push(("celo", "celo = true".to_string()));
455 }
456 #[cfg(feature = "optimism")]
457 if self.optimism {
458 selectors.push(("optimism", "optimism = true".to_string()));
459 }
460 if self.tempo {
461 selectors.push(("tempo", "tempo = true".to_string()));
462 }
463 #[cfg(feature = "monad")]
464 if self.monad {
465 selectors.push(("monad", "monad = true".to_string()));
466 }
467
468 if let Some((family, selector)) = selectors.first()
469 && let Some((_, conflicting)) =
470 selectors.iter().find(|(candidate, _)| candidate != family)
471 {
472 return Err(format!(
473 "network selectors `{selector}` and `{conflicting}` conflict; select only one \
474 network"
475 ));
476 }
477
478 Ok(())
479 }
480
481 pub fn with_ethereum() -> Self {
482 Self { network: Some(NetworkVariant::Ethereum), ..Default::default() }
483 }
484
485 pub fn with_celo() -> Self {
486 Self { celo: true, ..Default::default() }
487 }
488
489 pub fn with_tempo() -> Self {
490 Self { network: Some(NetworkVariant::Tempo), tempo: true, ..Default::default() }
491 }
492
493 #[cfg(feature = "monad")]
494 pub fn with_monad() -> Self {
495 Self { network: Some(NetworkVariant::Monad), monad: true, ..Default::default() }
496 }
497
498 #[cfg(feature = "base")]
499 pub fn with_base() -> Self {
500 Self { network: Some(NetworkVariant::Base), ..Default::default() }
501 }
502
503 pub const fn is_tempo(&self) -> bool {
504 if let Some(network) = self.resolved_network() { network.is_tempo() } else { false }
505 }
506
507 pub const fn is_optimism(&self) -> bool {
511 if let Some(network) = self.resolved_network() { network.is_optimism() } else { false }
512 }
513
514 #[cfg(feature = "monad")]
515 pub const fn is_monad(&self) -> bool {
516 if let Some(network) = self.resolved_network() { network.is_monad() } else { false }
517 }
518
519 #[cfg(not(feature = "monad"))]
520 pub const fn is_monad(&self) -> bool {
521 false
522 }
523
524 #[cfg(feature = "base")]
525 pub const fn is_base(&self) -> bool {
526 matches!(self.resolved_network(), Some(NetworkVariant::Base))
527 }
528
529 pub fn executed_hardfork(&self, hardfork: FoundryHardfork) -> FoundryHardfork {
536 if self.is_tempo() {
537 return TempoHardfork::from(hardfork).into();
538 }
539 #[cfg(feature = "monad")]
540 if self.is_monad() {
541 return MonadHardfork::from(hardfork).into();
542 }
543 #[cfg(feature = "base")]
544 if self.is_base() {
545 return BaseUpgrade::from(hardfork).into();
546 }
547 hardfork
548 }
549
550 pub const fn is_celo(&self) -> bool {
551 self.celo
552 }
553
554 pub const fn resolved_network(&self) -> Option<NetworkVariant> {
556 if let Some(n) = self.network {
557 return Some(n);
558 }
559 #[cfg(feature = "optimism")]
560 if self.optimism {
561 return Some(NetworkVariant::Optimism);
562 }
563 if self.tempo {
564 return Some(NetworkVariant::Tempo);
565 }
566 #[cfg(feature = "monad")]
567 if self.monad {
568 return Some(NetworkVariant::Monad);
569 }
570 None
571 }
572
573 pub const fn has_network_selection(&self) -> bool {
575 self.celo || self.resolved_network().is_some()
576 }
577
578 pub const fn execution_family_name(&self) -> &'static str {
580 self.execution_network().name()
581 }
582
583 pub const fn execution_profile_name(&self) -> &'static str {
585 if self.celo { "celo" } else { self.execution_family_name() }
586 }
587
588 pub const fn execution_network(&self) -> NetworkVariant {
590 if let Some(network) = self.resolved_network() { network } else { NetworkVariant::Ethereum }
591 }
592
593 pub fn has_same_execution_profile(&self, other: &Self) -> bool {
595 self.celo == other.celo
596 && match (self.resolved_network(), other.resolved_network()) {
597 (Some(left), Some(right)) => left == right,
598 (Some(left), None) => left.is_ethereum(),
599 (None, Some(right)) => right.is_ethereum(),
600 (None, None) => true,
601 }
602 }
603
604 pub const fn supports_fork_source(&self, source: &Self) -> bool {
612 match self.execution_network() {
613 #[cfg(feature = "base")]
614 NetworkVariant::Base => source.is_base(),
615 #[cfg(feature = "optimism")]
616 NetworkVariant::Optimism => match source.execution_network() {
617 NetworkVariant::Optimism => true,
618 #[cfg(feature = "base")]
619 NetworkVariant::Base => true,
620 _ => false,
621 },
622 #[cfg(feature = "monad")]
623 NetworkVariant::Monad => source.is_monad(),
624 _ => !source.is_monad(),
625 }
626 }
627
628 pub fn active_network_name(&self) -> Option<&'static str> {
630 self.resolved_network().and_then(|network| network.hardfork_namespace())
631 }
632
633 pub fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams {
638 #[cfg(feature = "base")]
639 if self.is_base() {
640 let canyon_active =
641 BaseUpgrade::from_chain_and_timestamp(NamedChain::Base as u64, timestamp)
642 .is_some_and(|upgrade| upgrade >= BaseUpgrade::Canyon);
643 return if canyon_active {
644 BaseFeeParams::new(250, 6)
645 } else {
646 BaseFeeParams::new(50, 6)
647 };
648 }
649 #[cfg(feature = "optimism")]
650 if self.is_optimism() {
651 return self.op_base_fee_params(timestamp);
652 }
653 BaseFeeParams::ethereum()
654 }
655
656 pub fn next_block_blob_excess_gas(
662 &self,
663 blob_params: BlobParams,
664 parent_excess_blob_gas: u64,
665 parent_blob_gas_used: u64,
666 parent_base_fee: u64,
667 ) -> u64 {
668 #[cfg(feature = "base")]
669 if self.is_base() {
670 return 0;
671 }
672 if self.is_optimism() {
673 return 0;
674 }
675 blob_params.next_block_excess_blob_gas_osaka(
676 parent_excess_blob_gas,
677 parent_blob_gas_used,
678 parent_base_fee,
679 )
680 }
681
682 #[cfg(feature = "monad")]
684 pub fn contract_size_limits(&self) -> Option<NetworkContractSizeLimits> {
685 self.is_monad().then_some(NetworkContractSizeLimits {
686 runtime: monad_revm::MONAD_MAX_CODE_SIZE,
687 initcode: monad_revm::MONAD_MAX_INITCODE_SIZE,
688 })
689 }
690
691 #[cfg(not(feature = "monad"))]
693 pub const fn contract_size_limits(&self) -> Option<NetworkContractSizeLimits> {
694 None
695 }
696
697 pub fn bypass_prevrandao(&self, chain_id: u64) -> bool {
698 if let Ok(
699 Moonbeam | Moonbase | Moonriver | MoonbeamDev | Rsk | RskTestnet | Gnosis | Chiado,
700 ) = NamedChain::try_from(chain_id)
701 {
702 return true;
703 }
704 self.bypass_prevrandao
705 }
706
707 pub fn try_with_chain_id(self, chain_id: u64) -> Result<Self, String> {
713 if self.has_network_selection() {
714 return Ok(self);
715 }
716
717 match NetworkVariant::from_known_chain_id(chain_id).map_err(|error| {
718 format!("cannot infer execution network from chain ID {chain_id}: {error}")
719 })? {
720 Some(network) => Ok(self.with_rpc_identity(network, chain_id)),
721 None => Ok(self),
722 }
723 }
724
725 pub fn with_chain_id(self, chain_id: u64) -> Self {
730 self.try_with_chain_id(chain_id).unwrap_or(self)
731 }
732
733 pub fn with_rpc_network(self, network: NetworkVariant) -> Self {
738 let mut resolved = if network.is_ethereum() { Self::default() } else { network.into() };
739 resolved.bypass_prevrandao = self.bypass_prevrandao;
740 resolved
741 }
742
743 pub fn with_rpc_identity(self, network: NetworkVariant, chain_id: ChainId) -> Self {
749 let mut resolved = self.with_rpc_network(network);
750 if network.is_ethereum()
751 && matches!(
752 Chain::from_id(chain_id).named(),
753 Some(NamedChain::Celo | NamedChain::CeloSepolia)
754 )
755 {
756 resolved.celo = true;
757 }
758 resolved
759 }
760
761 pub fn canonical_execution_profile(self) -> Self {
763 if self.is_celo() {
764 Self::with_celo()
765 } else if let Some(network) = self.resolved_network()
766 && !network.is_ethereum()
767 {
768 network.into()
769 } else {
770 Self::default()
771 }
772 }
773
774 pub fn with_execution_profile(self, profile: Self) -> Self {
776 let mut resolved = if profile.is_celo() {
777 Self::with_celo()
778 } else {
779 profile.resolved_network().map(Into::into).unwrap_or_default()
780 };
781 resolved.bypass_prevrandao = self.bypass_prevrandao;
782 resolved
783 }
784
785 pub fn with_rpc_profile(self, profile: Self) -> Self {
787 self.with_execution_profile(profile.canonical_execution_profile())
788 }
789
790 pub fn from_node_info_profile(profile: &str) -> Result<Self, String> {
792 if profile == "celo" {
793 return Ok(Self::with_celo());
794 }
795 let network = NetworkVariant::from_node_info_name(profile)?;
796 Ok(if network.is_ethereum() { Self::default() } else { network.into() })
797 }
798
799 pub fn from_rpc_identity_profile_with_fallback(
806 chain_id: ChainId,
807 node_info_profile: Option<Option<&str>>,
808 unknown_fallback: Option<Self>,
809 ) -> Result<Option<Self>, String> {
810 let known_profile = || {
811 NetworkVariant::from_known_chain_id(chain_id).map(|network| {
812 network.map(|network| Self::default().with_rpc_identity(network, chain_id))
813 })
814 };
815 let fallback_is_explicit =
816 unknown_fallback.is_some_and(|fallback| fallback.has_network_selection());
817 let fallback = unknown_fallback.map(Self::canonical_execution_profile);
818 if let Some(Some(profile)) = node_info_profile {
819 return Self::from_node_info_profile(profile).map(Some);
820 }
821 if fallback_is_explicit && let Some(fallback) = fallback {
822 return Ok(Some(fallback));
823 }
824 match node_info_profile {
825 Some(None) => Ok(known_profile()?.or(fallback).or(Some(Self::default()))),
826 None => Ok(known_profile()?.or(fallback)),
827 Some(Some(_)) => unreachable!(),
828 }
829 }
830
831 pub fn normalize_for_hardfork(self, hardfork: FoundryHardfork) -> Result<Self, String> {
836 if self.has_network_selection() {
837 let configured = self.execution_network();
838 if configured.hardfork_namespace() != hardfork.namespace() {
839 return Err(format!(
840 "hardfork `{}` conflicts with network config `{}`",
841 String::from(hardfork),
842 self.execution_profile_name(),
843 ));
844 }
845 }
846
847 let network = match hardfork {
848 FoundryHardfork::Ethereum(_) => self,
849 FoundryHardfork::Tempo(_) => Self::with_tempo(),
850 #[cfg(feature = "base")]
851 FoundryHardfork::Base(_) => Self::with_base(),
852 #[cfg(feature = "optimism")]
853 FoundryHardfork::Optimism(_) => Self::with_optimism(),
854 #[cfg(feature = "monad")]
855 FoundryHardfork::Monad(_) => Self::with_monad(),
856 };
857
858 Ok(network)
859 }
860
861 pub fn inject_precompiles(self, precompiles: &mut PrecompilesMap) {
863 if self.is_celo() {
864 precompiles.apply_precompile(&CELO_TRANSFER_ADDRESS, move |_| {
865 Some(celo::transfer::precompile())
866 });
867 }
868 }
869
870 pub fn precompiles(self, hardfork: Option<FoundryHardfork>) -> BTreeMap<String, Address> {
872 let mut precompiles = BTreeMap::new();
873 if self.is_celo() {
874 precompiles
875 .insert(PRECOMPILE_ID_CELO_TRANSFER.name().to_string(), CELO_TRANSFER_ADDRESS);
876 }
877 if self.is_tempo() {
878 let tempo_hardfork = hardfork.and_then(TempoHardfork::from_foundry_hardfork);
879 precompiles.extend(
880 TEMPO_PRECOMPILES
881 .iter()
882 .copied()
883 .filter(|(_, address)| {
884 tempo_hardfork.is_none_or(|hardfork| {
885 is_tempo_precompile_active_at(*address, hardfork)
886 })
887 })
888 .map(|(label, address)| (label.to_string(), address)),
889 );
890 }
891 #[cfg(feature = "monad")]
892 if self.is_monad() {
893 let monad_hardfork = hardfork.and_then(MonadHardfork::from_foundry_hardfork);
894 precompiles.extend(
895 MONAD_PRECOMPILES
896 .iter()
897 .copied()
898 .filter(|(_, address)| {
899 monad_hardfork.is_none_or(|hardfork| {
900 is_monad_precompile_active_at(*address, hardfork)
901 })
902 })
903 .map(|(label, address)| (label.to_string(), address)),
904 );
905 }
906 #[cfg(feature = "base")]
907 if self.is_base() {
908 let base_upgrade = hardfork.and_then(|hardfork| match hardfork {
909 FoundryHardfork::Base(upgrade) => Some(upgrade),
910 _ => None,
911 });
912 precompiles.extend(
913 BASE_PRECOMPILES
914 .iter()
915 .copied()
916 .filter(|(_, address)| {
917 base_upgrade
918 .is_none_or(|upgrade| is_base_precompile_active_at(*address, upgrade))
919 })
920 .map(|(label, address)| (label.to_string(), address)),
921 );
922 }
923 precompiles
924 }
925}
926
927pub fn apply_bsc_p256_precompile(
929 precompiles: &mut PrecompilesMap,
930 chain_id: ChainId,
931 timestamp: u64,
932) {
933 let Some(p256verify) = bsc_p256_precompile(chain_id, timestamp) else { return };
934 precompiles.apply_precompile(&BSC_P256_ADDRESS, move |_| {
935 p256verify.map(|p256verify| {
936 DynPrecompile::new(p256verify.id().clone(), move |input| {
937 p256verify.execute(input.data, input.gas(), input.reservoir)
938 })
939 })
940 });
941}
942
943impl From<NetworkVariant> for NetworkConfigs {
944 fn from(network: NetworkVariant) -> Self {
945 match network {
946 NetworkVariant::Ethereum => Self::with_ethereum(),
947 NetworkVariant::Tempo => {
948 Self { network: Some(network), tempo: true, ..Default::default() }
949 }
950 #[cfg(feature = "monad")]
951 NetworkVariant::Monad => {
952 Self { network: Some(network), monad: true, ..Default::default() }
953 }
954 #[cfg(feature = "base")]
955 NetworkVariant::Base => Self { network: Some(network), ..Default::default() },
956 #[cfg(feature = "optimism")]
957 NetworkVariant::Optimism => {
958 Self { network: Some(network), optimism: true, ..Default::default() }
959 }
960 }
961 }
962}
963
964const fn bsc_p256_precompile(chain_id: ChainId, timestamp: u64) -> Option<Option<RevmPrecompile>> {
967 let (haber_timestamp, osaka_timestamp) = match chain_id {
968 BSC_MAINNET_CHAIN_ID => (BSC_MAINNET_HABER_TIMESTAMP, BSC_MAINNET_OSAKA_TIMESTAMP),
969 BSC_TESTNET_CHAIN_ID => (BSC_TESTNET_HABER_TIMESTAMP, BSC_TESTNET_OSAKA_TIMESTAMP),
970 _ => return None,
971 };
972
973 if timestamp < haber_timestamp {
974 Some(None)
975 } else if timestamp < osaka_timestamp {
976 Some(Some(P256VERIFY))
977 } else {
978 Some(Some(P256VERIFY_OSAKA))
979 }
980}
981
982pub fn resolved_precompile_labels(hardfork: Option<FoundryHardfork>) -> AddressHashMap<String> {
985 match hardfork {
986 Some(FoundryHardfork::Tempo(hardfork)) => TEMPO_PRECOMPILES
987 .iter()
988 .filter(|(_, address)| is_tempo_precompile_active_at(*address, hardfork))
989 .map(|(label, address)| (*address, (*label).to_string()))
990 .collect(),
991 #[cfg(feature = "monad")]
992 Some(FoundryHardfork::Monad(hardfork)) => MONAD_PRECOMPILE_LABELS
993 .iter()
994 .filter(|(_, address)| is_monad_precompile_active_at(*address, hardfork))
995 .map(|(label, address)| (*address, (*label).to_string()))
996 .collect(),
997 #[cfg(feature = "base")]
998 Some(FoundryHardfork::Base(upgrade)) => BASE_PRECOMPILES
999 .iter()
1000 .filter(|(_, address)| is_base_precompile_active_at(*address, upgrade))
1001 .map(|(label, address)| (*address, (*label).to_string()))
1002 .collect(),
1003 _ => AddressHashMap::default(),
1004 }
1005}
1006
1007pub fn is_tempo_precompile_active_at(address: Address, hardfork: TempoHardfork) -> bool {
1009 if address == CURRENT_COMMITTEE_ADDRESS {
1010 hardfork.is_t8()
1011 } else if address == TIP20_CHANNEL_RESERVE_ADDRESS {
1012 hardfork.is_t5()
1013 } else if address == RECEIVE_POLICY_GUARD_ADDRESS {
1014 hardfork.is_t6()
1015 } else if address == STORAGE_CREDITS_ADDRESS {
1016 hardfork.is_t7()
1017 } else if address == ADDRESS_REGISTRY_ADDRESS || address == SIGNATURE_VERIFIER_ADDRESS {
1018 hardfork.is_t3()
1019 } else {
1020 true
1021 }
1022}
1023
1024pub fn active_tempo_precompile_addresses(hardfork: TempoHardfork) -> impl Iterator<Item = Address> {
1026 TEMPO_PRECOMPILE_ADDRESSES
1027 .iter()
1028 .copied()
1029 .filter(move |&address| is_tempo_precompile_active_at(address, hardfork))
1030}
1031
1032#[cfg(feature = "monad")]
1034pub fn is_monad_precompile_active_at(address: Address, hardfork: MonadHardfork) -> bool {
1035 address == monad_revm::staking::STAKING_ADDRESS
1036 || (address == monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS
1037 && MonadHardfork::MonadNine.is_enabled_in(hardfork))
1038}
1039
1040#[cfg(feature = "base")]
1042pub fn is_base_precompile_active_at(address: Address, upgrade: BaseUpgrade) -> bool {
1043 if matches!(address, TxContextStorage::ADDRESS | NonceManagerStorage::ADDRESS) {
1044 upgrade >= BaseUpgrade::Cobalt
1045 } else if matches!(
1046 address,
1047 B20FactoryStorage::ADDRESS
1048 | ActivationRegistryStorage::ADDRESS
1049 | PolicyRegistryStorage::ADDRESS
1050 ) {
1051 upgrade >= BaseUpgrade::Beryl
1052 } else {
1053 false
1054 }
1055}
1056
1057#[cfg(feature = "base")]
1059pub fn active_base_precompiles(
1060 upgrade: BaseUpgrade,
1061) -> impl Iterator<Item = (&'static str, Address)> {
1062 BASE_PRECOMPILES
1063 .iter()
1064 .copied()
1065 .filter(move |(_, address)| is_base_precompile_active_at(*address, upgrade))
1066}
1067
1068#[cfg(test)]
1069mod tests {
1070 use super::*;
1071 use revm::precompile::{
1072 Precompiles,
1073 secp256r1::{P256VERIFY_BASE_GAS_FEE, P256VERIFY_BASE_GAS_FEE_OSAKA},
1074 };
1075
1076 #[test]
1079 fn network_variant_predicates() {
1080 assert!(NetworkVariant::Ethereum.is_ethereum());
1081 assert!(!NetworkVariant::Ethereum.is_optimism());
1082 assert!(!NetworkVariant::Ethereum.is_tempo());
1083 assert!(NetworkVariant::Tempo.is_tempo());
1084 assert!(!NetworkVariant::Tempo.is_ethereum());
1085 assert!(!NetworkVariant::Tempo.is_optimism());
1086
1087 #[cfg(feature = "monad")]
1088 {
1089 assert!(!NetworkVariant::Ethereum.is_monad());
1090 assert!(!NetworkVariant::Tempo.is_monad());
1091 assert!(NetworkVariant::Monad.is_monad());
1092 assert!(!NetworkVariant::Monad.is_ethereum());
1093 assert!(!NetworkVariant::Monad.is_optimism());
1094 assert!(!NetworkVariant::Monad.is_tempo());
1095 }
1096
1097 #[cfg(feature = "optimism")]
1098 {
1099 assert!(NetworkVariant::Optimism.is_optimism());
1100 assert!(!NetworkVariant::Optimism.is_ethereum());
1101 assert!(!NetworkVariant::Optimism.is_tempo());
1102
1103 #[cfg(feature = "monad")]
1104 assert!(!NetworkVariant::Optimism.is_monad());
1105 }
1106
1107 #[cfg(feature = "base")]
1108 {
1109 assert!(NetworkVariant::Base.is_base());
1110 assert!(!NetworkVariant::Base.is_ethereum());
1111 assert!(!NetworkVariant::Base.is_optimism());
1112 assert!(!NetworkVariant::Base.is_tempo());
1113 }
1114 }
1115
1116 #[test]
1117 #[cfg(all(feature = "base", feature = "optimism"))]
1118 fn historical_hardfork_uses_selected_execution_family() {
1119 let timestamp = 1_710_374_401;
1120
1121 assert_eq!(
1122 NetworkVariant::Base.historical_hardfork(NamedChain::Base as u64, timestamp),
1123 Some(BaseUpgrade::Ecotone.into())
1124 );
1125 assert_eq!(
1126 NetworkVariant::Optimism.historical_hardfork(NamedChain::Base as u64, timestamp),
1127 Some(OpHardfork::Ecotone.into())
1128 );
1129 assert_eq!(
1130 NetworkVariant::Ethereum.historical_hardfork(NamedChain::Base as u64, timestamp),
1131 None
1132 );
1133 assert_eq!(
1134 NetworkVariant::Tempo.historical_hardfork(NamedChain::Base as u64, timestamp),
1135 None
1136 );
1137 }
1138
1139 #[test]
1140 #[cfg(feature = "monad")]
1141 fn fork_sources_only_isolate_monad() {
1142 let non_monad = vec![
1143 NetworkConfigs::default(),
1144 NetworkConfigs::with_ethereum(),
1145 NetworkConfigs::with_celo(),
1146 NetworkConfigs::with_tempo(),
1147 ];
1148
1149 for execution in &non_monad {
1150 for source in &non_monad {
1151 assert!(execution.supports_fork_source(source));
1152 }
1153 assert!(!execution.supports_fork_source(&NetworkConfigs::with_monad()));
1154 assert!(!NetworkConfigs::with_monad().supports_fork_source(execution));
1155 }
1156 assert!(NetworkConfigs::with_monad().supports_fork_source(&NetworkConfigs::with_monad()));
1157 }
1158
1159 #[test]
1160 #[cfg(feature = "optimism")]
1161 fn optimism_fork_sources_require_op_stack_state() {
1162 let optimism = NetworkConfigs::with_optimism();
1163 for source in [
1164 NetworkConfigs::default(),
1165 NetworkConfigs::with_ethereum(),
1166 NetworkConfigs::with_celo(),
1167 NetworkConfigs::with_tempo(),
1168 ] {
1169 assert!(!optimism.supports_fork_source(&source));
1170 assert!(source.supports_fork_source(&optimism));
1171 }
1172 assert!(optimism.supports_fork_source(&optimism));
1173 #[cfg(feature = "monad")]
1174 assert!(!optimism.supports_fork_source(&NetworkConfigs::with_monad()));
1175 }
1176
1177 #[test]
1178 #[cfg(feature = "base")]
1179 fn fork_sources_preserve_base_state_source_compatibility() {
1180 #[cfg_attr(not(feature = "optimism"), allow(unused_mut))]
1181 let mut non_base = vec![
1182 NetworkConfigs::default(),
1183 NetworkConfigs::with_ethereum(),
1184 NetworkConfigs::with_celo(),
1185 NetworkConfigs::with_tempo(),
1186 ];
1187 #[cfg(feature = "optimism")]
1188 non_base.push(NetworkConfigs::with_optimism());
1189 for execution in &non_base {
1190 assert!(execution.supports_fork_source(&NetworkConfigs::with_base()));
1191 assert!(!NetworkConfigs::with_base().supports_fork_source(execution));
1192 }
1193 assert!(NetworkConfigs::with_base().supports_fork_source(&NetworkConfigs::with_base()));
1194
1195 #[cfg(feature = "monad")]
1196 {
1197 assert!(
1198 !NetworkConfigs::with_monad().supports_fork_source(&NetworkConfigs::with_base())
1199 );
1200 assert!(
1201 !NetworkConfigs::with_base().supports_fork_source(&NetworkConfigs::with_monad())
1202 );
1203 }
1204 }
1205
1206 #[test]
1207 fn known_chain_identity_does_not_guess_unknown_networks() {
1208 assert_eq!(NetworkVariant::from_known_chain_id(98_765_432).unwrap(), None);
1209 assert_eq!(
1210 NetworkVariant::from_known_chain_id(NamedChain::Mainnet as u64).unwrap(),
1211 Some(NetworkVariant::Ethereum)
1212 );
1213 assert_eq!(NetworkVariant::from(98_765_432), NetworkVariant::Ethereum);
1214 }
1215
1216 #[test]
1217 fn fallible_chain_id_inference_preserves_unknown_chains() {
1218 let networks = NetworkConfigs { bypass_prevrandao: true, ..Default::default() };
1219 assert_eq!(networks.try_with_chain_id(98_765_432).unwrap(), networks);
1220 }
1221
1222 #[test]
1223 #[cfg(not(feature = "monad"))]
1224 fn fallible_chain_id_inference_rejects_disabled_monad() {
1225 for chain_id in [NamedChain::Monad as u64, NamedChain::MonadTestnet as u64] {
1226 let unavailable = "network family `monad` is not enabled in this build";
1227 let expected =
1228 format!("cannot infer execution network from chain ID {chain_id}: {unavailable}");
1229 assert_eq!(
1230 NetworkConfigs::default().try_with_chain_id(chain_id).unwrap_err(),
1231 expected
1232 );
1233 }
1234 }
1235
1236 #[test]
1237 #[cfg(not(feature = "optimism"))]
1238 fn fallible_chain_id_inference_rejects_disabled_optimism() {
1239 let chain_id = NamedChain::Optimism as u64;
1240 assert_eq!(
1241 NetworkConfigs::default().try_with_chain_id(chain_id).unwrap_err(),
1242 format!(
1243 "cannot infer execution network from chain ID {chain_id}: network family \
1244 `optimism` is not enabled in this build"
1245 )
1246 );
1247 }
1248
1249 #[test]
1250 fn explicit_ethereum_overrides_known_chain_inference() {
1251 let ethereum = NetworkConfigs::with_ethereum();
1252 for chain_id in [NamedChain::Monad as u64, NamedChain::Optimism as u64] {
1253 assert_eq!(ethereum.try_with_chain_id(chain_id).unwrap(), ethereum);
1254 }
1255 }
1256
1257 #[test]
1258 fn fallible_chain_id_inference_preserves_orthogonal_configuration() {
1259 let networks = NetworkConfigs { bypass_prevrandao: true, ..Default::default() };
1260 let inferred = networks.try_with_chain_id(NamedChain::Tempo as u64).unwrap();
1261
1262 assert!(inferred.is_tempo());
1263 assert!(inferred.bypass_prevrandao(NamedChain::Mainnet as u64));
1264 }
1265
1266 #[test]
1267 #[cfg(feature = "optimism")]
1268 fn fallible_chain_id_inference_detects_optimism() {
1269 let chain_id = NamedChain::Optimism as u64;
1270 assert_eq!(NetworkVariant::from(chain_id), NetworkVariant::Optimism);
1271 assert!(NetworkConfigs::default().try_with_chain_id(chain_id).unwrap().is_optimism());
1272 }
1273
1274 #[test]
1275 fn rpc_identity_preserves_orthogonal_configuration() {
1276 let base = NetworkConfigs { bypass_prevrandao: true, ..NetworkConfigs::default() };
1277 let ethereum = base.with_rpc_network(NetworkVariant::Ethereum);
1278 assert!(ethereum.bypass_prevrandao(NamedChain::Mainnet as u64));
1279
1280 let ethereum = NetworkConfigs::default().with_rpc_network(NetworkVariant::Ethereum);
1281 assert_eq!(ethereum, NetworkConfigs::default());
1282
1283 assert!(
1284 NetworkConfigs::default()
1285 .with_rpc_identity(NetworkVariant::Ethereum, NamedChain::Celo as u64)
1286 .is_celo()
1287 );
1288 assert!(
1289 !NetworkConfigs::default()
1290 .with_rpc_identity(NetworkVariant::Ethereum, NamedChain::Monad as u64)
1291 .is_monad()
1292 );
1293 }
1294
1295 #[test]
1296 fn rpc_metadata_overrides_known_execution_chain_identity() {
1297 assert_eq!(
1298 NetworkConfigs::from_rpc_identity_profile_with_fallback(
1299 NamedChain::Mainnet as u64,
1300 Some(Some("tempo")),
1301 None,
1302 )
1303 .unwrap(),
1304 Some(NetworkConfigs::with_tempo())
1305 );
1306 assert_eq!(
1307 NetworkConfigs::from_rpc_identity_profile_with_fallback(
1308 NamedChain::Tempo as u64,
1309 Some(None),
1310 None,
1311 )
1312 .unwrap(),
1313 Some(NetworkConfigs::with_tempo())
1314 );
1315 assert_eq!(
1316 NetworkConfigs::from_rpc_identity_profile_with_fallback(98_765_432, None, None)
1317 .unwrap(),
1318 None
1319 );
1320 assert_eq!(
1321 NetworkConfigs::from_rpc_identity_profile_with_fallback(98_765_432, Some(None), None)
1322 .unwrap(),
1323 Some(NetworkConfigs::default())
1324 );
1325 #[cfg(feature = "optimism")]
1326 assert_eq!(
1327 NetworkConfigs::from_rpc_identity_profile_with_fallback(
1328 NamedChain::Optimism as u64,
1329 Some(None),
1330 None,
1331 )
1332 .unwrap(),
1333 Some(NetworkConfigs::from(NetworkVariant::Optimism))
1334 );
1335 }
1336
1337 #[test]
1338 fn rpc_profile_distinguishes_celo_from_ethereum_factory() {
1339 let custom_chain_id = 98_765_432;
1340 assert!(
1341 NetworkConfigs::from_rpc_identity_profile_with_fallback(
1342 custom_chain_id,
1343 Some(Some("celo")),
1344 None,
1345 )
1346 .unwrap()
1347 .unwrap()
1348 .is_celo()
1349 );
1350 assert!(
1351 !NetworkConfigs::from_rpc_identity_profile_with_fallback(
1352 NamedChain::Celo as u64,
1353 Some(Some("ethereum")),
1354 None,
1355 )
1356 .unwrap()
1357 .unwrap()
1358 .is_celo()
1359 );
1360 assert!(
1361 NetworkConfigs::from_rpc_identity_profile_with_fallback(
1362 NamedChain::Celo as u64,
1363 Some(None),
1364 None,
1365 )
1366 .unwrap()
1367 .unwrap()
1368 .is_celo()
1369 );
1370 assert!(
1371 NetworkConfigs::from_rpc_identity_profile_with_fallback(
1372 custom_chain_id,
1373 None,
1374 Some(NetworkConfigs::with_celo()),
1375 )
1376 .unwrap()
1377 .unwrap()
1378 .is_celo()
1379 );
1380 }
1381
1382 #[test]
1383 fn rpc_profile_keeps_moonbeam_on_default_ethereum() {
1384 let profile = NetworkConfigs::from_rpc_identity_profile_with_fallback(
1385 NamedChain::Moonbeam as u64,
1386 None,
1387 None,
1388 )
1389 .unwrap()
1390 .unwrap();
1391
1392 assert_eq!(profile, NetworkConfigs::default());
1393 assert!(profile.bypass_prevrandao(NamedChain::Moonbeam as u64));
1394 }
1395
1396 #[test]
1397 fn default_fallback_still_infers_known_execution_profile() {
1398 for node_info in [None, Some(None)] {
1399 assert_eq!(
1400 NetworkConfigs::from_rpc_identity_profile_with_fallback(
1401 NamedChain::Tempo as u64,
1402 node_info,
1403 Some(NetworkConfigs::default()),
1404 )
1405 .unwrap(),
1406 Some(NetworkConfigs::with_tempo())
1407 );
1408 }
1409 }
1410
1411 #[test]
1412 fn explicit_ethereum_overrides_known_execution_profile() {
1413 for node_info in [None, Some(None)] {
1414 assert_eq!(
1415 NetworkConfigs::from_rpc_identity_profile_with_fallback(
1416 NamedChain::Tempo as u64,
1417 node_info,
1418 Some(NetworkConfigs::with_ethereum()),
1419 )
1420 .unwrap(),
1421 Some(NetworkConfigs::default())
1422 );
1423 }
1424 }
1425
1426 #[test]
1427 #[cfg(not(feature = "monad"))]
1428 fn explicit_ethereum_overrides_disabled_monad_without_node_info() {
1429 assert_eq!(
1430 NetworkConfigs::from_rpc_identity_profile_with_fallback(
1431 NamedChain::Monad as u64,
1432 None,
1433 Some(NetworkConfigs::with_ethereum()),
1434 )
1435 .unwrap(),
1436 Some(NetworkConfigs::default())
1437 );
1438 }
1439
1440 #[test]
1441 #[cfg(not(feature = "monad"))]
1442 fn explicit_ethereum_overrides_disabled_monad_with_legacy_node_info() {
1443 assert_eq!(
1444 NetworkConfigs::from_rpc_identity_profile_with_fallback(
1445 NamedChain::Monad as u64,
1446 Some(None),
1447 Some(NetworkConfigs::with_ethereum()),
1448 )
1449 .unwrap(),
1450 Some(NetworkConfigs::default())
1451 );
1452 }
1453
1454 #[test]
1455 #[cfg(not(feature = "monad"))]
1456 fn disabled_monad_without_explicit_profile_still_errors() {
1457 for node_info in [None, Some(None)] {
1458 assert_eq!(
1459 NetworkConfigs::from_rpc_identity_profile_with_fallback(
1460 NamedChain::Monad as u64,
1461 node_info,
1462 None,
1463 )
1464 .unwrap_err(),
1465 "network family `monad` is not enabled in this build"
1466 );
1467 }
1468 }
1469
1470 #[test]
1471 fn network_selection_distinguishes_default_and_explicit_ethereum() {
1472 assert!(!NetworkConfigs::default().has_network_selection());
1473 assert!(NetworkConfigs::with_ethereum().has_network_selection());
1474 assert!(NetworkConfigs::with_celo().has_network_selection());
1475 }
1476
1477 #[test]
1478 fn celo_uses_ethereum_factory_with_distinct_precompiles() {
1479 assert!(
1480 !NetworkConfigs::with_celo().has_same_execution_profile(&NetworkConfigs::default())
1481 );
1482 assert!(
1483 NetworkConfigs::with_ethereum().has_same_execution_profile(&NetworkConfigs::default())
1484 );
1485 assert_eq!(NetworkConfigs::with_celo().execution_family_name(), "ethereum");
1486 assert_eq!(NetworkConfigs::with_celo().execution_profile_name(), "celo");
1487 }
1488
1489 #[test]
1490 fn authoritative_execution_profile_preserves_orthogonal_settings() {
1491 let inline = NetworkConfigs { bypass_prevrandao: true, ..NetworkConfigs::with_tempo() };
1492
1493 #[cfg_attr(not(any(feature = "optimism", feature = "monad")), allow(unused_mut))]
1494 let mut profiles = vec![
1495 NetworkConfigs::with_ethereum(),
1496 NetworkConfigs::with_tempo(),
1497 NetworkConfigs::with_celo(),
1498 ];
1499 #[cfg(feature = "optimism")]
1500 profiles.push(NetworkVariant::Optimism.into());
1501 #[cfg(feature = "monad")]
1502 profiles.push(NetworkConfigs::with_monad());
1503
1504 for profile in profiles {
1505 let resolved = inline.with_execution_profile(profile);
1506 assert!(resolved.has_same_execution_profile(&profile));
1507 assert!(resolved.has_network_selection());
1508 assert!(resolved.bypass_prevrandao(NamedChain::Mainnet as u64));
1509 }
1510
1511 let ethereum = inline.with_execution_profile(NetworkConfigs::with_ethereum());
1512 assert_eq!(
1513 ethereum.try_with_chain_id(NamedChain::Tempo as u64).unwrap(),
1514 ethereum,
1515 "an authoritative Ethereum profile must prevent later endpoint inference",
1516 );
1517
1518 let rpc_ethereum = inline.with_rpc_profile(NetworkConfigs::with_ethereum());
1519 assert!(!rpc_ethereum.has_network_selection());
1520 }
1521
1522 #[test]
1523 fn chain_id_inference_preserves_explicit_networks() {
1524 assert!(
1525 NetworkConfigs::default().try_with_chain_id(NamedChain::Celo as u64).unwrap().is_celo()
1526 );
1527 let celo = NetworkConfigs { bypass_prevrandao: true, ..Default::default() }
1528 .try_with_chain_id(NamedChain::Celo as u64)
1529 .unwrap();
1530 assert!(celo.is_celo());
1531 assert!(celo.bypass_prevrandao(NamedChain::Mainnet as u64));
1532
1533 let explicit = [
1534 NetworkConfigs::with_ethereum(),
1535 NetworkConfigs::with_tempo(),
1536 NetworkConfigs::with_celo(),
1537 ];
1538 for networks in explicit {
1539 assert_eq!(networks.try_with_chain_id(NamedChain::Celo as u64).unwrap(), networks);
1540 }
1541
1542 #[cfg(feature = "monad")]
1543 {
1544 let monad = NetworkConfigs::with_monad();
1545 assert_eq!(monad.try_with_chain_id(NamedChain::Celo as u64).unwrap(), monad);
1546 }
1547 }
1548
1549 #[test]
1550 #[cfg(feature = "monad")]
1551 fn rpc_metadata_identifies_custom_monad_networks() {
1552 assert_eq!(NetworkVariant::from_node_info_name("monad").unwrap(), NetworkVariant::Monad);
1553 assert!(NetworkConfigs::default().with_rpc_network(NetworkVariant::Monad).is_monad());
1554 }
1555
1556 #[test]
1557 #[cfg(feature = "monad")]
1558 fn parses_endpoint_hardfork_in_network_namespace() {
1559 assert_eq!(
1560 NetworkVariant::Monad.parse_hardfork("MonadEight").unwrap(),
1561 FoundryHardfork::Monad(MonadHardfork::MonadEight)
1562 );
1563 }
1564
1565 #[test]
1566 fn rpc_metadata_rejects_unknown_network_family() {
1567 assert_eq!(
1568 NetworkVariant::from_node_info_name("unknown").unwrap_err(),
1569 "unsupported network family `unknown` reported by fork endpoint"
1570 );
1571 }
1572
1573 #[test]
1574 fn explicit_ethereum_families_reject_namespaced_hardforks() {
1575 #[cfg_attr(not(feature = "monad"), allow(unused_mut))]
1576 let mut incompatible = vec![FoundryHardfork::Tempo(TempoHardfork::T0)];
1577 #[cfg(feature = "monad")]
1578 incompatible.push(FoundryHardfork::Monad(MonadHardfork::MonadEight));
1579
1580 for networks in [NetworkConfigs::with_ethereum(), NetworkConfigs::with_celo()] {
1581 for hardfork in &incompatible {
1582 assert_eq!(
1583 networks.normalize_for_hardfork(*hardfork).unwrap_err(),
1584 format!(
1585 "hardfork `{}` conflicts with network config `{}`",
1586 String::from(*hardfork),
1587 networks.execution_profile_name()
1588 )
1589 );
1590 }
1591 }
1592
1593 let celo = NetworkConfigs::with_celo();
1594 assert_eq!(
1595 celo.normalize_for_hardfork(FoundryHardfork::Ethereum(
1596 foundry_evm_hardforks::EthereumHardfork::Prague
1597 ))
1598 .unwrap(),
1599 celo
1600 );
1601 }
1602
1603 #[cfg(feature = "base")]
1604 #[test]
1605 fn base_precompile_labels_follow_upgrade_boundaries() {
1606 let config = NetworkConfigs::with_base();
1607
1608 assert!(resolved_precompile_labels(Some(BaseUpgrade::Azul.into())).is_empty());
1609
1610 let beryl = resolved_precompile_labels(Some(BaseUpgrade::Beryl.into()));
1611 assert_eq!(beryl.get(&B20FactoryStorage::ADDRESS), Some(&"B20Factory".to_string()));
1612 assert_eq!(
1613 beryl.get(&ActivationRegistryStorage::ADDRESS),
1614 Some(&"ActivationRegistry".to_string())
1615 );
1616 assert_eq!(beryl.get(&PolicyRegistryStorage::ADDRESS), Some(&"PolicyRegistry".to_string()));
1617 assert!(!beryl.contains_key(&TxContextStorage::ADDRESS));
1618 assert!(!beryl.contains_key(&NonceManagerStorage::ADDRESS));
1619
1620 let cobalt = resolved_precompile_labels(Some(BaseUpgrade::Cobalt.into()));
1621 assert_eq!(cobalt.len(), BASE_PRECOMPILES.len());
1622
1623 assert!(config.precompiles(Some(BaseUpgrade::Azul.into())).is_empty());
1625 let beryl = config.precompiles(Some(BaseUpgrade::Beryl.into()));
1626 assert_eq!(beryl.get("B20Factory"), Some(&B20FactoryStorage::ADDRESS));
1627 assert!(!beryl.contains_key("NonceManager"));
1628 assert_eq!(
1629 config.precompiles(Some(BaseUpgrade::Cobalt.into())).get("NonceManager"),
1630 Some(&NonceManagerStorage::ADDRESS)
1631 );
1632 }
1633
1634 #[test]
1635 fn new_tempo_flag_equivalent_to_legacy() {
1636 let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1637 let via_old = NetworkConfigs { tempo: true, ..Default::default() };
1638 assert_eq!(via_new.is_tempo(), via_old.is_tempo());
1639 assert_eq!(via_new.active_network_name(), via_old.active_network_name());
1640 assert_eq!(via_new.precompiles(None), via_old.precompiles(None));
1641 }
1642
1643 fn bsc_p256_gas_used(chain_id: ChainId, timestamp: u64) -> Option<u64> {
1644 bsc_p256_precompile(chain_id, timestamp)
1645 .flatten()
1646 .map(|precompile| precompile.execute(&[], u64::MAX, 0).unwrap().gas_used)
1647 }
1648
1649 fn assert_bsc_p256_boundaries(chain_id: ChainId, haber_timestamp: u64, osaka_timestamp: u64) {
1650 assert!(matches!(bsc_p256_precompile(chain_id, haber_timestamp - 1), Some(None)));
1651 assert_eq!(bsc_p256_gas_used(chain_id, haber_timestamp), Some(P256VERIFY_BASE_GAS_FEE));
1652 assert_eq!(bsc_p256_gas_used(chain_id, osaka_timestamp - 1), Some(P256VERIFY_BASE_GAS_FEE));
1653 assert_eq!(
1654 bsc_p256_gas_used(chain_id, osaka_timestamp),
1655 Some(P256VERIFY_BASE_GAS_FEE_OSAKA)
1656 );
1657 }
1658
1659 #[test]
1660 fn selects_bsc_p256_at_mainnet_boundaries() {
1661 assert_bsc_p256_boundaries(
1662 BSC_MAINNET_CHAIN_ID,
1663 BSC_MAINNET_HABER_TIMESTAMP,
1664 BSC_MAINNET_OSAKA_TIMESTAMP,
1665 );
1666 }
1667
1668 #[test]
1669 fn selects_bsc_p256_at_testnet_boundaries() {
1670 assert_bsc_p256_boundaries(
1671 BSC_TESTNET_CHAIN_ID,
1672 BSC_TESTNET_HABER_TIMESTAMP,
1673 BSC_TESTNET_OSAKA_TIMESTAMP,
1674 );
1675 }
1676
1677 #[test]
1678 fn removes_bsc_p256_before_haber() {
1679 let mut precompiles = PrecompilesMap::from_static(Precompiles::osaka());
1680 assert!(precompiles.get(&BSC_P256_ADDRESS).is_some());
1681 apply_bsc_p256_precompile(
1682 &mut precompiles,
1683 BSC_MAINNET_CHAIN_ID,
1684 BSC_MAINNET_HABER_TIMESTAMP - 1,
1685 );
1686 assert!(precompiles.get(&BSC_P256_ADDRESS).is_none());
1687 }
1688
1689 #[test]
1690 fn canonical_tempo_network_reports_precompiles() {
1691 let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1692
1693 assert_eq!(
1694 cfg.precompiles(None).get("TIP20ChannelReserve"),
1695 Some(&TIP20_CHANNEL_RESERVE_ADDRESS)
1696 );
1697 assert!(
1698 !cfg.precompiles(Some(TempoHardfork::T4.into())).contains_key("TIP20ChannelReserve")
1699 );
1700 assert!(
1701 !cfg.precompiles(Some(TempoHardfork::T4.into())).contains_key("ReceivePolicyGuard")
1702 );
1703 assert!(!cfg.precompiles(Some(TempoHardfork::T2.into())).contains_key("AddressRegistry"));
1704 assert!(!cfg.precompiles(Some(TempoHardfork::T2.into())).contains_key("SignatureVerifier"));
1705 assert_eq!(
1706 cfg.precompiles(Some(TempoHardfork::T3.into())).get("AddressRegistry"),
1707 Some(&ADDRESS_REGISTRY_ADDRESS)
1708 );
1709 assert_eq!(
1710 cfg.precompiles(Some(TempoHardfork::T3.into())).get("SignatureVerifier"),
1711 Some(&SIGNATURE_VERIFIER_ADDRESS)
1712 );
1713 assert_eq!(
1714 resolved_precompile_labels(Some(TempoHardfork::T5.into()))
1715 .get(&TIP20_CHANNEL_RESERVE_ADDRESS),
1716 Some(&"TIP20ChannelReserve".to_string())
1717 );
1718 assert!(
1719 !resolved_precompile_labels(Some(TempoHardfork::T5.into()))
1720 .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
1721 );
1722 assert!(
1723 resolved_precompile_labels(Some(TempoHardfork::T6.into()))
1724 .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
1725 );
1726 }
1727
1728 #[test]
1729 #[cfg(feature = "monad")]
1730 fn canonical_monad_network_reports_hardfork_gated_precompiles() {
1731 let cfg = NetworkConfigs { network: Some(NetworkVariant::Monad), ..Default::default() };
1732
1733 assert_eq!(
1734 cfg.precompiles(Some(MonadHardfork::MonadEight.into())).get("MonadStaking"),
1735 Some(&monad_revm::staking::STAKING_ADDRESS)
1736 );
1737 assert!(
1738 !cfg.precompiles(Some(MonadHardfork::MonadEight.into()))
1739 .contains_key("MonadReserveBalance")
1740 );
1741 assert_eq!(
1742 cfg.precompiles(Some(MonadHardfork::MonadNine.into())).get("MonadReserveBalance"),
1743 Some(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
1744 );
1745 assert_eq!(
1746 resolved_precompile_labels(Some(MonadHardfork::MonadNine.into()))
1747 .get(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS),
1748 Some(&"ReserveBalance".to_string())
1749 );
1750 assert!(
1751 !resolved_precompile_labels(Some(MonadHardfork::MonadEight.into()))
1752 .contains_key(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
1753 );
1754 }
1755
1756 #[test]
1757 fn storage_credits_precompile_activates_at_t7() {
1758 assert!(!is_tempo_precompile_active_at(STORAGE_CREDITS_ADDRESS, TempoHardfork::T6));
1759 assert!(is_tempo_precompile_active_at(STORAGE_CREDITS_ADDRESS, TempoHardfork::T7));
1760 assert!(TEMPO_PRECOMPILE_ADDRESSES.contains(&STORAGE_CREDITS_ADDRESS));
1761
1762 let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1764 assert!(!cfg.precompiles(Some(TempoHardfork::T6.into())).contains_key("StorageCredits"));
1765 assert!(cfg.precompiles(Some(TempoHardfork::T7.into())).contains_key("StorageCredits"));
1766 }
1767
1768 #[test]
1769 fn current_committee_precompile_activates_at_t8() {
1770 assert!(!is_tempo_precompile_active_at(CURRENT_COMMITTEE_ADDRESS, TempoHardfork::T7));
1771 assert!(is_tempo_precompile_active_at(CURRENT_COMMITTEE_ADDRESS, TempoHardfork::T8));
1772 assert!(TEMPO_PRECOMPILE_ADDRESSES.contains(&CURRENT_COMMITTEE_ADDRESS));
1773
1774 let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1775 assert!(!cfg.precompiles(Some(TempoHardfork::T7.into())).contains_key("CurrentCommittee"));
1776 assert!(cfg.precompiles(Some(TempoHardfork::T8.into())).contains_key("CurrentCommittee"));
1777 }
1778
1779 #[test]
1782 fn active_network_name_tempo() {
1783 let cfg = NetworkConfigs::with_tempo();
1784 assert_eq!(cfg.active_network_name(), Some("tempo"));
1785 }
1786
1787 #[test]
1788 #[cfg(feature = "monad")]
1789 fn active_network_name_monad() {
1790 let cfg = NetworkConfigs::with_monad();
1791 assert_eq!(cfg.active_network_name(), Some("monad"));
1792 assert!(cfg.is_monad());
1793 }
1794
1795 #[test]
1796 #[cfg(feature = "monad")]
1797 fn contract_size_limits_monad() {
1798 let limits = NetworkConfigs::with_monad().contract_size_limits().unwrap();
1799 assert_eq!(limits.runtime, monad_revm::MONAD_MAX_CODE_SIZE);
1800 assert_eq!(limits.initcode, monad_revm::MONAD_MAX_INITCODE_SIZE);
1801 assert!(NetworkConfigs::default().contract_size_limits().is_none());
1802 }
1803
1804 #[test]
1805 fn active_network_name_default_is_none() {
1806 let cfg = NetworkConfigs::default();
1807 assert_eq!(cfg.active_network_name(), None);
1808 assert!(!cfg.is_optimism());
1809 }
1810
1811 #[test]
1815 #[cfg(all(not(feature = "base"), feature = "optimism"))]
1816 fn chain_id_inference_falls_back_to_optimism_without_base() {
1817 for chain_id in [NamedChain::Base as u64, NamedChain::BaseSepolia as u64] {
1818 let configs = NetworkConfigs::default()
1819 .try_with_chain_id(chain_id)
1820 .unwrap_or_else(|error| panic!("chain ID {chain_id} must still resolve: {error}"));
1821 assert!(configs.is_optimism(), "chain ID {chain_id} must resolve to Optimism");
1822 }
1823 }
1824
1825 #[test]
1826 #[cfg(not(feature = "base"))]
1827 fn node_info_rejects_disabled_base() {
1828 assert_eq!(
1829 NetworkVariant::from_node_info_name("base").unwrap_err(),
1830 "network family `base` is not enabled in this build"
1831 );
1832 }
1833
1834 #[test]
1837 fn serde_roundtrip_tempo() {
1838 let original = NetworkConfigs::with_tempo();
1839 let json = serde_json::to_string(&original).unwrap();
1840 let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
1841 assert!(restored.is_tempo());
1842 }
1843
1844 #[test]
1845 fn serde_legacy_tempo_bool_deserialized() {
1846 let json = r#"{"tempo": true, "celo": false, "bypass_prevrandao": false}"#;
1848 let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
1849 assert!(cfg.is_tempo());
1850 }
1851
1852 #[test]
1853 #[cfg(feature = "monad")]
1854 fn serde_roundtrip_monad() {
1855 let original = NetworkConfigs::with_monad();
1856 let json = serde_json::to_string(&original).unwrap();
1857 let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
1858 assert!(restored.is_monad());
1859 assert!(!restored.is_tempo());
1860 }
1861
1862 #[test]
1863 #[cfg(feature = "monad")]
1864 fn serde_legacy_monad_bool_deserialized() {
1865 let json = r#"{"monad": true, "celo": false, "bypass_prevrandao": false}"#;
1866 let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
1867 assert!(cfg.is_monad());
1868 }
1869
1870 #[test]
1871 fn serde_serializes_legacy_alias_as_canonical_network() {
1872 #[cfg_attr(not(feature = "monad"), allow(unused_mut))]
1873 let mut cases = vec![(NetworkConfigs { tempo: true, ..Default::default() }, "tempo")];
1874 #[cfg(feature = "monad")]
1875 cases.push((NetworkConfigs { monad: true, ..Default::default() }, "monad"));
1876
1877 for (cfg, expected) in cases {
1878 let json = serde_json::to_value(cfg).unwrap();
1879 assert_eq!(json["network"], serde_json::json!(expected));
1880 assert!(json.get("tempo").is_none(), "legacy `tempo` key should not be serialized");
1881 assert!(json.get("monad").is_none(), "legacy `monad` key should not be serialized");
1882 }
1883 }
1884
1885 #[test]
1886 fn serde_new_network_field_deserialized() {
1887 let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#;
1888 let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap();
1889 assert!(cfg_tempo.is_tempo());
1890
1891 #[cfg(feature = "monad")]
1892 {
1893 let json_monad = r#"{"network": "monad", "celo": false, "bypass_prevrandao": false}"#;
1894 let cfg_monad: NetworkConfigs = serde_json::from_str(json_monad).unwrap();
1895 assert!(cfg_monad.is_monad());
1896 }
1897 }
1898
1899 #[test]
1900 fn validates_flattened_network_selectors() {
1901 let valid = [
1902 NetworkConfigs::default(),
1903 NetworkConfigs::with_ethereum(),
1904 NetworkConfigs::with_celo(),
1905 NetworkConfigs::with_tempo(),
1906 NetworkConfigs {
1907 network: Some(NetworkVariant::Tempo),
1908 tempo: true,
1909 ..Default::default()
1910 },
1911 ];
1912 for networks in valid {
1913 networks.validate().unwrap();
1914 }
1915
1916 let conflicts = [
1917 (
1918 NetworkConfigs {
1919 network: Some(NetworkVariant::Tempo),
1920 celo: true,
1921 ..Default::default()
1922 },
1923 "network selectors `network = \"tempo\"` and `celo = true` conflict",
1924 ),
1925 (
1926 NetworkConfigs { celo: true, tempo: true, ..Default::default() },
1927 "network selectors `celo = true` and `tempo = true` conflict",
1928 ),
1929 ];
1930 for (networks, expected) in conflicts {
1931 assert!(networks.validate().unwrap_err().contains(expected));
1932 }
1933 }
1934
1935 #[test]
1936 #[cfg(feature = "monad")]
1937 fn validates_flattened_monad_network_selectors() {
1938 for networks in [
1939 NetworkConfigs::with_monad(),
1940 NetworkConfigs {
1941 network: Some(NetworkVariant::Monad),
1942 monad: true,
1943 ..Default::default()
1944 },
1945 ] {
1946 networks.validate().unwrap();
1947 }
1948
1949 let conflicts = [
1950 (
1951 NetworkConfigs {
1952 network: Some(NetworkVariant::Monad),
1953 celo: true,
1954 ..Default::default()
1955 },
1956 "network selectors `network = \"monad\"` and `celo = true` conflict",
1957 ),
1958 (
1959 NetworkConfigs {
1960 network: Some(NetworkVariant::Monad),
1961 tempo: true,
1962 ..Default::default()
1963 },
1964 "network selectors `network = \"monad\"` and `tempo = true` conflict",
1965 ),
1966 (
1967 NetworkConfigs { celo: true, monad: true, ..Default::default() },
1968 "network selectors `celo = true` and `monad = true` conflict",
1969 ),
1970 (
1971 NetworkConfigs { tempo: true, monad: true, ..Default::default() },
1972 "network selectors `tempo = true` and `monad = true` conflict",
1973 ),
1974 ];
1975 for (networks, expected) in conflicts {
1976 assert!(networks.validate().unwrap_err().contains(expected));
1977 }
1978 }
1979
1980 #[test]
1981 #[cfg(feature = "monad")]
1982 fn chain_id_detects_monad_network() {
1983 assert_eq!(NetworkVariant::from(143), NetworkVariant::Monad);
1984 assert_eq!(NetworkVariant::from(10143), NetworkVariant::Monad);
1985
1986 assert!(NetworkConfigs::default().try_with_chain_id(143).unwrap().is_monad());
1987 }
1988
1989 #[cfg(feature = "base")]
1990 mod base {
1991 use super::*;
1992
1993 #[test]
1994 fn active_network_name_base() {
1995 let cfg = NetworkConfigs::with_base();
1996 assert_eq!(cfg.active_network_name(), Some("base"));
1997 }
1998
1999 #[test]
2000 fn base_fee_params_follow_canyon_boundary() {
2001 const CANYON_TIMESTAMP: u64 = 1_704_992_401;
2002
2003 let config = NetworkConfigs::with_base();
2004 assert_eq!(config.base_fee_params(CANYON_TIMESTAMP - 1), BaseFeeParams::new(50, 6));
2005 assert_eq!(config.base_fee_params(CANYON_TIMESTAMP), BaseFeeParams::new(250, 6));
2006 }
2007
2008 #[test]
2009 fn base_does_not_inherit_ethereum_blob_excess_gas() {
2010 let params = BlobParams::prague();
2011 assert_eq!(
2012 NetworkConfigs::with_base().next_block_blob_excess_gas(
2013 params,
2014 0,
2015 params.target_blob_gas_per_block() + 1,
2016 1,
2017 ),
2018 0
2019 );
2020 }
2021
2022 #[test]
2023 fn serde_roundtrip_base() {
2024 let original = NetworkConfigs::with_base();
2025 let json = serde_json::to_string(&original).unwrap();
2026 let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
2027 assert!(restored.is_base());
2028 assert!(!restored.is_tempo());
2029 }
2030
2031 #[test]
2032 fn serde_base_field_deserialized() {
2033 let json_base = r#"{"network": "base", "celo": false, "bypass_prevrandao": false}"#;
2034 let cfg_base: NetworkConfigs = serde_json::from_str(json_base).unwrap();
2035 assert!(cfg_base.is_base());
2036 }
2037
2038 #[test]
2039 fn chain_id_detects_base_networks() {
2040 assert_eq!(NetworkVariant::from(8453), NetworkVariant::Base);
2041 assert_eq!(NetworkVariant::from(84532), NetworkVariant::Base);
2042 assert!(NetworkConfigs::default().try_with_chain_id(8453).unwrap().is_base());
2043 assert_eq!(NetworkVariant::from_node_info_name("base").unwrap(), NetworkVariant::Base);
2044 }
2045
2046 #[test]
2047 fn hardfork_infers_base_network() {
2048 assert_eq!(
2049 NetworkConfigs::default()
2050 .normalize_for_hardfork(FoundryHardfork::Base(BaseUpgrade::Beryl))
2051 .unwrap()
2052 .resolved_network(),
2053 Some(NetworkVariant::Base)
2054 );
2055 }
2056 }
2057
2058 #[cfg(feature = "optimism")]
2059 mod optimism {
2060 use super::*;
2061
2062 #[test]
2063 fn new_optimism_flag_equivalent_to_legacy() {
2064 let via_new =
2065 NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() };
2066 let via_old = NetworkConfigs { optimism: true, ..Default::default() };
2067 assert_eq!(via_new.is_optimism(), via_old.is_optimism());
2068 assert_eq!(via_new.is_tempo(), via_old.is_tempo());
2069 assert_eq!(via_new.active_network_name(), via_old.active_network_name());
2070 }
2071
2072 #[test]
2073 fn matching_optimism_selectors_are_valid() {
2074 NetworkConfigs::with_optimism().validate().unwrap();
2075 }
2076
2077 #[test]
2078 fn active_network_name_optimism() {
2079 let cfg = NetworkConfigs::with_optimism();
2080 assert_eq!(cfg.active_network_name(), Some("optimism"));
2081 }
2082
2083 #[test]
2084 fn conflicting_optimism_and_tempo_selectors_are_rejected() {
2085 let cfg = NetworkConfigs {
2086 network: Some(NetworkVariant::Optimism),
2087 tempo: true,
2088 ..Default::default()
2089 };
2090 assert_eq!(
2091 cfg.validate().unwrap_err(),
2092 "network selectors `network = \"optimism\"` and `tempo = true` conflict; select \
2093 only one network"
2094 );
2095 }
2096
2097 #[test]
2098 fn serde_roundtrip_optimism() {
2099 let original = NetworkConfigs::with_optimism();
2100 let json = serde_json::to_string(&original).unwrap();
2101 let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
2102 assert!(restored.is_optimism());
2103 assert!(!restored.is_tempo());
2104 }
2105
2106 #[test]
2107 fn serde_optimism_field_deserialized() {
2108 let json_optimism =
2109 r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#;
2110 let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap();
2111 assert!(cfg_optimism.is_optimism());
2112 }
2113 }
2114
2115 #[test]
2116 fn executed_hardfork_follows_the_network_family() {
2117 let prague = FoundryHardfork::Ethereum(EthereumHardfork::Prague);
2120 assert_eq!(NetworkConfigs::default().executed_hardfork(prague), prague);
2121 assert_eq!(
2122 NetworkConfigs::with_tempo().executed_hardfork(prague),
2123 FoundryHardfork::Tempo(TempoHardfork::from(prague))
2124 );
2125 }
2126}