Skip to main content

foundry_evm_networks/
lib.rs

1//! # foundry-evm-networks
2//!
3//! Runtime selection and shared configuration for Foundry's EVM network families.
4//!
5//! [`NetworkConfigs`] describes the active execution profile selected by configuration, CLI flags,
6//! hardforks, or fork endpoint discovery. Cargo features only determine which optional
7//! [`NetworkVariant`] values are compiled into a binary; they do not select a network at runtime.
8//!
9//! Concrete Alloy network and EVM factory types are associated by `FoundryEvmNetwork` in
10//! `foundry-evm-core`. See the [custom EVM integration guide] for the cross-crate ownership and
11//! state-lifecycle contract.
12//!
13//! [custom EVM integration guide]: https://github.com/foundry-rs/foundry/blob/master/docs/dev/networks.md
14
15use 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
55/// The Monad cheatcode handler address.
56pub const MONAD_CHEATCODE_ADDRESS: Address = address!("0xc0FFeeCD43A10e1C2b0De63c6CDCFe5B7d0e0CEA");
57
58pub mod arbitrum;
59pub mod celo;
60
61#[cfg(feature = "optimism")]
62mod optimism;
63
64/// Labels of all well-known Tempo precompiles, keyed by address.
65pub 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/// Fixed Base precompiles that expose at least one function returning no data.
104///
105/// Solidity guards high-level calls to such functions with an `extcodesize` check, which a
106/// code-less account fails in the caller, so these must carry code. Base mainnet plants a one-byte
107/// sentinel on exactly these two. The factory, nonce manager, and transaction context return data
108/// from every function and are code-less on chain, so stubbing them would diverge — a contract
109/// guarding calls with an `isContract` probe would pass locally and revert on Base.
110///
111/// The nonce manager separately receives a stub at Zenith from Base's own
112/// `ensure_eip8130_system_accounts` transition, for EIP-161 state clearing rather than for
113/// `extcodesize`. That transition owns it; this list must not.
114#[cfg(feature = "base")]
115pub const BASE_CODE_SENTINEL_ADDRESSES: &[Address] =
116    &[ActivationRegistryStorage::ADDRESS, PolicyRegistryStorage::ADDRESS];
117
118/// BSC secp256r1 precompile address introduced by the Haber hardfork.
119const 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
128/// All well-known Tempo precompile addresses.
129pub 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/// Runtime and initcode byte-size limits for a configured network.
168#[derive(Clone, Copy, Debug, PartialEq, Eq)]
169pub struct NetworkContractSizeLimits {
170    /// Maximum deployed runtime bytecode size.
171    pub runtime: usize,
172    /// Maximum initcode bytecode size.
173    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    /// Returns the network family identified by a known chain ID.
196    ///
197    /// Unknown chain IDs return `None` so callers can consult endpoint metadata instead of
198    /// assuming Ethereum. If a known chain belongs to a feature-gated family that is not enabled,
199    /// this returns an error rather than silently selecting a different EVM.
200    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        // Only claim Base when the feature is on. `is_optimism()` below already covers Base chain
209        // IDs, and that is what shipped binaries resolve them to today, so erroring here would
210        // regress builds that never asked for Base. Monad errors instead because no shipped EVM
211        // approximates it.
212        #[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    /// Parses an explicit network family reported by `anvil_nodeInfo`.
232    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    /// Parses a hardfork name reported by an RPC endpoint in this network's namespace.
255    pub fn parse_hardfork(self, hardfork: &str) -> Result<FoundryHardfork, String> {
256        format!("{}:{hardfork}", self.name()).parse()
257    }
258
259    /// Returns this network's hardfork at a historical source-chain timestamp.
260    ///
261    /// Unknown source chains return `None`; the selected network owns the lookup so a chain ID
262    /// shared by multiple execution families cannot select another family's hardfork.
263    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    /// Returns the active hardfork for this network at the given chain and timestamp.
289    ///
290    /// Unknown chain IDs fall back to the network's default hardfork. The selected network owns
291    /// the lookup so an explicit network choice is not overridden by the chain ID's family.
292    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    /// Returns `true` if this is the Ethereum network variant.
306    pub const fn is_ethereum(&self) -> bool {
307        matches!(self, Self::Ethereum)
308    }
309
310    /// Returns `true` if this is the Base network variant.
311    #[cfg(feature = "base")]
312    pub const fn is_base(&self) -> bool {
313        matches!(self, Self::Base)
314    }
315
316    /// Returns `true` if this is the Optimism network variant.
317    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    /// Returns `true` if this is the Tempo network variant.
329    pub const fn is_tempo(&self) -> bool {
330        matches!(self, Self::Tempo)
331    }
332
333    /// Returns `true` if this is the Monad network variant.
334    #[cfg(feature = "monad")]
335    pub const fn is_monad(&self) -> bool {
336        matches!(self, Self::Monad)
337    }
338
339    /// Returns `false` when Monad support is not compiled in.
340    #[cfg(not(feature = "monad"))]
341    pub const fn is_monad(&self) -> bool {
342        false
343    }
344
345    /// Returns the network variant name.
346    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    /// Returns the hardfork namespace used by this network family.
360    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    /// Enable a specific network family.
389    #[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    /// Enable Celo network features.
395    #[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    /// Enable Optimism network features (deprecated: use --network optimism).
400    #[cfg(feature = "optimism")]
401    #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
402    #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
403    // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the
404    // canonical form is `network = "optimism"`.
405    #[serde(default)]
406    pub(crate) optimism: bool,
407    /// Enable Tempo network features (deprecated: use --network tempo).
408    #[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    // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the
412    // canonical form is `network = "tempo"`.
413    #[serde(default)]
414    tempo: bool,
415    /// Enable Monad network features (deprecated: use --network monad).
416    #[cfg(feature = "monad")]
417    #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
418    #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
419    // Deserialize-only legacy alias: accepted in foundry.toml but never serialized - the
420    // canonical form is `network = "monad"`.
421    #[serde(default)]
422    monad: bool,
423    /// Whether to bypass prevrandao.
424    #[arg(skip)]
425    #[serde(default)]
426    bypass_prevrandao: bool,
427}
428
429// Custom `Serialize` impl: always emits the *resolved* network as the canonical
430// `network = "..."` field, and never emits legacy network aliases. This avoids contradictory
431// canonical and legacy selectors, and ensures old foundry.toml keys round-trip canonically.
432impl 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    /// Validates that all configured network selectors resolve to the same execution profile.
445    ///
446    /// Canonical and legacy selectors for the same family remain compatible. Selectors for
447    /// different families are rejected instead of being silently resolved by field priority.
448    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    /// Returns whether Optimism network features are enabled.
508    ///
509    /// Always returns `false` when built without the `optimism` feature.
510    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    /// Coerces `hardfork` into this network's family.
530    ///
531    /// Execution and fee rules apply the same lossy `From` conversions, so a cross-namespace
532    /// override such as `--hardfork prague` on a Tempo node runs as a Tempo hardfork. Callers that
533    /// need to describe what actually executed, like trace decoding, go through here rather than
534    /// carrying the configured value.
535    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    /// Returns the resolved network variant, folding legacy flags.
555    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    /// Returns whether a network family was selected in this configuration.
574    pub const fn has_network_selection(&self) -> bool {
575        self.celo || self.resolved_network().is_some()
576    }
577
578    /// Returns the execution family represented by this configuration.
579    pub const fn execution_family_name(&self) -> &'static str {
580        self.execution_network().name()
581    }
582
583    /// Returns a label for the complete execution configuration.
584    pub const fn execution_profile_name(&self) -> &'static str {
585        if self.celo { "celo" } else { self.execution_family_name() }
586    }
587
588    /// Returns the concrete execution network, treating an unresolved configuration as Ethereum.
589    pub const fn execution_network(&self) -> NetworkVariant {
590        if let Some(network) = self.resolved_network() { network } else { NetworkVariant::Ethereum }
591    }
592
593    /// Returns whether both configurations can use the same instantiated EVM backend.
594    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    /// Returns whether this execution configuration can use `source` as a fork state source
605    /// without rebuilding the instantiated EVM.
606    ///
607    /// Monad uses a distinct EVM factory and instruction provider, so forks cannot cross the
608    /// Monad boundary. Base execution requires a Base source. Optimism execution requires an OP
609    /// Stack source for its fee accounting. Ethereum and Tempo can use OP Stack state sources
610    /// without switching engines.
611    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    /// Returns the name of the currently active non-Ethereum network, or `None` for plain Ethereum.
629    pub fn active_network_name(&self) -> Option<&'static str> {
630        self.resolved_network().and_then(|network| network.hardfork_namespace())
631    }
632
633    /// Returns the base fee parameters for the configured network.
634    ///
635    /// For OP Stack networks, returns Canyon parameters if the Canyon hardfork is active at the
636    /// given timestamp, otherwise returns pre-Canyon parameters.
637    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    /// Calculates the blob excess gas inherited by the next block.
657    ///
658    /// OP Stack headers use the blob fields for protocol metadata rather than EIP-4844 blobs, so
659    /// their excess blob gas remains zero. Other execution profiles use the configured Ethereum
660    /// blob schedule.
661    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    /// Returns contract size limits for networks that override Ethereum defaults.
683    #[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    /// Returns contract size limits for networks that override Ethereum defaults.
692    #[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    /// Infers an execution profile from `chain_id` unless one was selected explicitly.
708    ///
709    /// Unknown chain IDs preserve the unresolved configuration so endpoint metadata can still
710    /// identify their execution profile. A known chain whose feature is unavailable is rejected
711    /// instead of silently falling back to Ethereum semantics.
712    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    /// Best-effort execution-profile inference for trusted, programmatic chain IDs.
726    ///
727    /// User-provided chain IDs must be handled with [`Self::try_with_chain_id`] so an unavailable
728    /// feature cannot silently select Ethereum semantics.
729    pub fn with_chain_id(self, chain_id: u64) -> Self {
730        self.try_with_chain_id(chain_id).unwrap_or(self)
731    }
732
733    /// Applies an RPC endpoint's resolved EVM family to this configuration.
734    ///
735    /// Successful endpoint metadata is authoritative, including when a local Anvil uses a
736    /// well-known chain ID as an execution override. Orthogonal settings are preserved.
737    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    /// Applies an RPC endpoint's execution family and chain-specific Ethereum configuration.
744    ///
745    /// The reported family is authoritative. A known Celo chain ID additionally enables Celo's
746    /// precompiles because Celo shares the Ethereum EVM factory rather than having its own
747    /// [`NetworkVariant`].
748    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    /// Returns the canonical endpoint-visible execution profile.
762    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    /// Applies an authoritative execution profile while preserving orthogonal settings.
775    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    /// Applies an endpoint-reported execution profile while preserving orthogonal settings.
786    pub fn with_rpc_profile(self, profile: Self) -> Self {
787        self.with_execution_profile(profile.canonical_execution_profile())
788    }
789
790    /// Parses the execution profile reported by `anvil_nodeInfo`.
791    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    /// Resolves an RPC endpoint's complete execution profile.
800    ///
801    /// Explicit metadata is authoritative. When metadata is absent or omits the profile, a
802    /// caller-supplied explicit profile takes precedence over chain-ID inference. Otherwise,
803    /// legacy Anvil responses and ordinary RPC endpoints recover Celo from a canonical Celo chain
804    /// ID and preserve the historical behavior for custom chain IDs.
805    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    /// Validates `hardfork` against the current `NetworkConfigs` and, if consistent, returns an
832    /// updated instance with the network implied by the enabled hardfork.
833    ///
834    /// Returns `Err` when the hardfork's network family conflicts with the configured one.
835    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    /// Inject precompiles for configured networks.
862    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    /// Returns precompiles for configured networks at the given hardfork.
871    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
927/// Applies the BSC P256 precompile active at the given timestamp.
928pub 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
964/// Returns the BSC P256 precompile for the given timestamp. The outer option distinguishes BSC
965/// chains from unrelated chains, while the inner option disables P256 before Haber.
966const 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
982/// Returns custom precompile labels for an already resolved execution hardfork.
983/// This metadata lookup does not discover or select an execution network.
984pub 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
1007/// Returns whether a well-known Tempo precompile address is active at `hardfork`.
1008pub 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
1024/// Returns the well-known Tempo precompile addresses active at `hardfork`.
1025pub 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/// Returns whether a well-known Monad precompile address is active at `hardfork`.
1033#[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/// Returns whether a fixed Base precompile is active at `upgrade`.
1041#[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/// Returns the fixed Base precompiles active at `upgrade`.
1058#[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    // --- Equivalence: new flag == legacy flag ---
1077
1078    #[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        // The name-keyed precompile map must honor the same upgrade boundaries.
1624        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        // The hardfork-filtered precompile map must honor the same T7 activation.
1763        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    // --- resolved() / active_network_name ---
1780
1781    #[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    /// Base chain IDs resolved to Optimism before Base support existed, and `is_optimism()` still
1812    /// covers them, so a build without the `base` feature must keep resolving them rather than
1813    /// erroring. Shipped release binaries are exactly that build.
1814    #[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    // --- Serde round-trip ---
1835
1836    #[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        // Old foundry.toml format: `tempo = true`
1847        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        // A cross-namespace override runs as the configured network's hardfork, so the value used
2118        // to describe execution has to be coerced the same way.
2119        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}