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")]
53use 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    #[cfg_attr(
638        not(any(feature = "base", feature = "optimism")),
639        allow(clippy::missing_const_for_fn)
640    )]
641    pub fn base_fee_params(
642        &self,
643        #[cfg_attr(not(any(feature = "base", feature = "optimism")), allow(unused_variables))]
644        timestamp: u64,
645    ) -> BaseFeeParams {
646        #[cfg(feature = "base")]
647        if self.is_base() {
648            let canyon_active =
649                BaseUpgrade::from_chain_and_timestamp(NamedChain::Base as u64, timestamp)
650                    .is_some_and(|upgrade| upgrade >= BaseUpgrade::Canyon);
651            return if canyon_active {
652                BaseFeeParams::new(250, 6)
653            } else {
654                BaseFeeParams::new(50, 6)
655            };
656        }
657        #[cfg(feature = "optimism")]
658        if self.is_optimism() {
659            return self.op_base_fee_params(timestamp);
660        }
661        BaseFeeParams::ethereum()
662    }
663
664    /// Calculates the blob excess gas inherited by the next block.
665    ///
666    /// OP Stack headers use the blob fields for protocol metadata rather than EIP-4844 blobs, so
667    /// their excess blob gas remains zero. Other execution profiles use the configured Ethereum
668    /// blob schedule.
669    pub fn next_block_blob_excess_gas(
670        &self,
671        blob_params: BlobParams,
672        parent_excess_blob_gas: u64,
673        parent_blob_gas_used: u64,
674        parent_base_fee: u64,
675    ) -> u64 {
676        #[cfg(feature = "base")]
677        if self.is_base() {
678            return 0;
679        }
680        if self.is_optimism() {
681            return 0;
682        }
683        blob_params.next_block_excess_blob_gas_osaka(
684            parent_excess_blob_gas,
685            parent_blob_gas_used,
686            parent_base_fee,
687        )
688    }
689
690    /// Returns contract size limits for networks that override Ethereum defaults.
691    #[cfg(feature = "monad")]
692    pub fn contract_size_limits(&self) -> Option<NetworkContractSizeLimits> {
693        self.is_monad().then_some(NetworkContractSizeLimits {
694            runtime: monad_revm::MONAD_MAX_CODE_SIZE,
695            initcode: monad_revm::MONAD_MAX_INITCODE_SIZE,
696        })
697    }
698
699    /// Returns contract size limits for networks that override Ethereum defaults.
700    #[cfg(not(feature = "monad"))]
701    pub const fn contract_size_limits(&self) -> Option<NetworkContractSizeLimits> {
702        None
703    }
704
705    pub fn bypass_prevrandao(&self, chain_id: u64) -> bool {
706        if let Ok(
707            Moonbeam | Moonbase | Moonriver | MoonbeamDev | Rsk | RskTestnet | Gnosis | Chiado,
708        ) = NamedChain::try_from(chain_id)
709        {
710            return true;
711        }
712        self.bypass_prevrandao
713    }
714
715    /// Infers an execution profile from `chain_id` unless one was selected explicitly.
716    ///
717    /// Unknown chain IDs preserve the unresolved configuration so endpoint metadata can still
718    /// identify their execution profile. A known chain whose feature is unavailable is rejected
719    /// instead of silently falling back to Ethereum semantics.
720    pub fn try_with_chain_id(self, chain_id: u64) -> Result<Self, String> {
721        if self.has_network_selection() {
722            return Ok(self);
723        }
724
725        match NetworkVariant::from_known_chain_id(chain_id).map_err(|error| {
726            format!("cannot infer execution network from chain ID {chain_id}: {error}")
727        })? {
728            Some(network) => Ok(self.with_rpc_identity(network, chain_id)),
729            None => Ok(self),
730        }
731    }
732
733    /// Best-effort execution-profile inference for trusted, programmatic chain IDs.
734    ///
735    /// User-provided chain IDs must be handled with [`Self::try_with_chain_id`] so an unavailable
736    /// feature cannot silently select Ethereum semantics.
737    pub fn with_chain_id(self, chain_id: u64) -> Self {
738        self.try_with_chain_id(chain_id).unwrap_or(self)
739    }
740
741    /// Applies an RPC endpoint's resolved EVM family to this configuration.
742    ///
743    /// Successful endpoint metadata is authoritative, including when a local Anvil uses a
744    /// well-known chain ID as an execution override. Orthogonal settings are preserved.
745    pub fn with_rpc_network(self, network: NetworkVariant) -> Self {
746        let mut resolved = if network.is_ethereum() { Self::default() } else { network.into() };
747        resolved.bypass_prevrandao = self.bypass_prevrandao;
748        resolved
749    }
750
751    /// Applies an RPC endpoint's execution family and chain-specific Ethereum configuration.
752    ///
753    /// The reported family is authoritative. A known Celo chain ID additionally enables Celo's
754    /// precompiles because Celo shares the Ethereum EVM factory rather than having its own
755    /// [`NetworkVariant`].
756    pub fn with_rpc_identity(self, network: NetworkVariant, chain_id: ChainId) -> Self {
757        let mut resolved = self.with_rpc_network(network);
758        if network.is_ethereum()
759            && matches!(
760                Chain::from_id(chain_id).named(),
761                Some(NamedChain::Celo | NamedChain::CeloSepolia)
762            )
763        {
764            resolved.celo = true;
765        }
766        resolved
767    }
768
769    /// Returns the canonical endpoint-visible execution profile.
770    pub fn canonical_execution_profile(self) -> Self {
771        if self.is_celo() {
772            Self::with_celo()
773        } else if let Some(network) = self.resolved_network()
774            && !network.is_ethereum()
775        {
776            network.into()
777        } else {
778            Self::default()
779        }
780    }
781
782    /// Applies an authoritative execution profile while preserving orthogonal settings.
783    pub fn with_execution_profile(self, profile: Self) -> Self {
784        let mut resolved = if profile.is_celo() {
785            Self::with_celo()
786        } else {
787            profile.resolved_network().map(Into::into).unwrap_or_default()
788        };
789        resolved.bypass_prevrandao = self.bypass_prevrandao;
790        resolved
791    }
792
793    /// Applies an endpoint-reported execution profile while preserving orthogonal settings.
794    pub fn with_rpc_profile(self, profile: Self) -> Self {
795        self.with_execution_profile(profile.canonical_execution_profile())
796    }
797
798    /// Parses the execution profile reported by `anvil_nodeInfo`.
799    pub fn from_node_info_profile(profile: &str) -> Result<Self, String> {
800        if profile == "celo" {
801            return Ok(Self::with_celo());
802        }
803        let network = NetworkVariant::from_node_info_name(profile)?;
804        Ok(if network.is_ethereum() { Self::default() } else { network.into() })
805    }
806
807    /// Resolves an RPC endpoint's complete execution profile.
808    ///
809    /// Explicit metadata is authoritative. When metadata is absent or omits the profile, a
810    /// caller-supplied explicit profile takes precedence over chain-ID inference. Otherwise,
811    /// legacy Anvil responses and ordinary RPC endpoints recover Celo from a canonical Celo chain
812    /// ID and preserve the historical behavior for custom chain IDs.
813    pub fn from_rpc_identity_profile_with_fallback(
814        chain_id: ChainId,
815        node_info_profile: Option<Option<&str>>,
816        unknown_fallback: Option<Self>,
817    ) -> Result<Option<Self>, String> {
818        let known_profile = || {
819            NetworkVariant::from_known_chain_id(chain_id).map(|network| {
820                network.map(|network| Self::default().with_rpc_identity(network, chain_id))
821            })
822        };
823        let fallback_is_explicit =
824            unknown_fallback.is_some_and(|fallback| fallback.has_network_selection());
825        let fallback = unknown_fallback.map(Self::canonical_execution_profile);
826        if let Some(Some(profile)) = node_info_profile {
827            return Self::from_node_info_profile(profile).map(Some);
828        }
829        if fallback_is_explicit && let Some(fallback) = fallback {
830            return Ok(Some(fallback));
831        }
832        match node_info_profile {
833            Some(None) => Ok(known_profile()?.or(fallback).or(Some(Self::default()))),
834            None => Ok(known_profile()?.or(fallback)),
835            Some(Some(_)) => unreachable!(),
836        }
837    }
838
839    /// Validates `hardfork` against the current `NetworkConfigs` and, if consistent, returns an
840    /// updated instance with the network implied by the enabled hardfork.
841    ///
842    /// Returns `Err` when the hardfork's network family conflicts with the configured one.
843    pub fn normalize_for_hardfork(self, hardfork: FoundryHardfork) -> Result<Self, String> {
844        if self.has_network_selection() {
845            let configured = self.execution_network();
846            if configured.hardfork_namespace() != hardfork.namespace() {
847                return Err(format!(
848                    "hardfork `{}` conflicts with network config `{}`",
849                    String::from(hardfork),
850                    self.execution_profile_name(),
851                ));
852            }
853        }
854
855        let network = match hardfork {
856            FoundryHardfork::Ethereum(_) => self,
857            FoundryHardfork::Tempo(_) => Self::with_tempo(),
858            #[cfg(feature = "base")]
859            FoundryHardfork::Base(_) => Self::with_base(),
860            #[cfg(feature = "optimism")]
861            FoundryHardfork::Optimism(_) => Self::with_optimism(),
862            #[cfg(feature = "monad")]
863            FoundryHardfork::Monad(_) => Self::with_monad(),
864        };
865
866        Ok(network)
867    }
868
869    /// Inject precompiles for configured networks.
870    pub fn inject_precompiles(self, precompiles: &mut PrecompilesMap) {
871        if self.is_celo() {
872            precompiles.apply_precompile(&CELO_TRANSFER_ADDRESS, move |_| {
873                Some(celo::transfer::precompile())
874            });
875        }
876    }
877
878    /// Returns precompiles for configured networks at the given hardfork.
879    pub fn precompiles(self, hardfork: Option<FoundryHardfork>) -> BTreeMap<String, Address> {
880        let mut precompiles = BTreeMap::new();
881        if self.is_celo() {
882            precompiles
883                .insert(PRECOMPILE_ID_CELO_TRANSFER.name().to_string(), CELO_TRANSFER_ADDRESS);
884        }
885        if self.is_tempo() {
886            let tempo_hardfork = hardfork.and_then(TempoHardfork::from_foundry_hardfork);
887            precompiles.extend(
888                TEMPO_PRECOMPILES
889                    .iter()
890                    .copied()
891                    .filter(|(_, address)| {
892                        tempo_hardfork.is_none_or(|hardfork| {
893                            is_tempo_precompile_active_at(*address, hardfork)
894                        })
895                    })
896                    .map(|(label, address)| (label.to_string(), address)),
897            );
898        }
899        #[cfg(feature = "monad")]
900        if self.is_monad() {
901            let monad_hardfork = hardfork.and_then(MonadHardfork::from_foundry_hardfork);
902            precompiles.extend(
903                MONAD_PRECOMPILES
904                    .iter()
905                    .copied()
906                    .filter(|(_, address)| {
907                        monad_hardfork.is_none_or(|hardfork| {
908                            is_monad_precompile_active_at(*address, hardfork)
909                        })
910                    })
911                    .map(|(label, address)| (label.to_string(), address)),
912            );
913        }
914        #[cfg(feature = "base")]
915        if self.is_base() {
916            let base_upgrade = hardfork.and_then(|hardfork| match hardfork {
917                FoundryHardfork::Base(upgrade) => Some(upgrade),
918                _ => None,
919            });
920            precompiles.extend(
921                BASE_PRECOMPILES
922                    .iter()
923                    .copied()
924                    .filter(|(_, address)| {
925                        base_upgrade
926                            .is_none_or(|upgrade| is_base_precompile_active_at(*address, upgrade))
927                    })
928                    .map(|(label, address)| (label.to_string(), address)),
929            );
930        }
931        precompiles
932    }
933}
934
935/// Applies the BSC P256 precompile active at the given timestamp.
936pub fn apply_bsc_p256_precompile(
937    precompiles: &mut PrecompilesMap,
938    chain_id: ChainId,
939    timestamp: u64,
940) {
941    let Some(p256verify) = bsc_p256_precompile(chain_id, timestamp) else { return };
942    precompiles.apply_precompile(&BSC_P256_ADDRESS, move |_| {
943        p256verify.map(|p256verify| {
944            DynPrecompile::new(p256verify.id().clone(), move |input| {
945                p256verify.execute(input.data, input.gas(), input.reservoir)
946            })
947        })
948    });
949}
950
951impl From<NetworkVariant> for NetworkConfigs {
952    fn from(network: NetworkVariant) -> Self {
953        match network {
954            NetworkVariant::Ethereum => Self::with_ethereum(),
955            NetworkVariant::Tempo => {
956                Self { network: Some(network), tempo: true, ..Default::default() }
957            }
958            #[cfg(feature = "monad")]
959            NetworkVariant::Monad => {
960                Self { network: Some(network), monad: true, ..Default::default() }
961            }
962            #[cfg(feature = "base")]
963            NetworkVariant::Base => Self { network: Some(network), ..Default::default() },
964            #[cfg(feature = "optimism")]
965            NetworkVariant::Optimism => {
966                Self { network: Some(network), optimism: true, ..Default::default() }
967            }
968        }
969    }
970}
971
972/// Returns the BSC P256 precompile for the given timestamp. The outer option distinguishes BSC
973/// chains from unrelated chains, while the inner option disables P256 before Haber.
974const fn bsc_p256_precompile(chain_id: ChainId, timestamp: u64) -> Option<Option<RevmPrecompile>> {
975    let (haber_timestamp, osaka_timestamp) = match chain_id {
976        BSC_MAINNET_CHAIN_ID => (BSC_MAINNET_HABER_TIMESTAMP, BSC_MAINNET_OSAKA_TIMESTAMP),
977        BSC_TESTNET_CHAIN_ID => (BSC_TESTNET_HABER_TIMESTAMP, BSC_TESTNET_OSAKA_TIMESTAMP),
978        _ => return None,
979    };
980
981    if timestamp < haber_timestamp {
982        Some(None)
983    } else if timestamp < osaka_timestamp {
984        Some(Some(P256VERIFY))
985    } else {
986        Some(Some(P256VERIFY_OSAKA))
987    }
988}
989
990/// Returns custom precompile labels for an already resolved execution hardfork.
991/// This metadata lookup does not discover or select an execution network.
992pub fn resolved_precompile_labels(hardfork: Option<FoundryHardfork>) -> AddressHashMap<String> {
993    match hardfork {
994        Some(FoundryHardfork::Tempo(hardfork)) => TEMPO_PRECOMPILES
995            .iter()
996            .filter(|(_, address)| is_tempo_precompile_active_at(*address, hardfork))
997            .map(|(label, address)| (*address, (*label).to_string()))
998            .collect(),
999        #[cfg(feature = "monad")]
1000        Some(FoundryHardfork::Monad(hardfork)) => MONAD_PRECOMPILE_LABELS
1001            .iter()
1002            .filter(|(_, address)| is_monad_precompile_active_at(*address, hardfork))
1003            .map(|(label, address)| (*address, (*label).to_string()))
1004            .collect(),
1005        #[cfg(feature = "base")]
1006        Some(FoundryHardfork::Base(upgrade)) => BASE_PRECOMPILES
1007            .iter()
1008            .filter(|(_, address)| is_base_precompile_active_at(*address, upgrade))
1009            .map(|(label, address)| (*address, (*label).to_string()))
1010            .collect(),
1011        _ => AddressHashMap::default(),
1012    }
1013}
1014
1015/// Returns whether a well-known Tempo precompile address is active at `hardfork`.
1016pub fn is_tempo_precompile_active_at(address: Address, hardfork: TempoHardfork) -> bool {
1017    if address == CURRENT_COMMITTEE_ADDRESS {
1018        hardfork.is_t8()
1019    } else if address == TIP20_CHANNEL_RESERVE_ADDRESS {
1020        hardfork.is_t5()
1021    } else if address == RECEIVE_POLICY_GUARD_ADDRESS {
1022        hardfork.is_t6()
1023    } else if address == STORAGE_CREDITS_ADDRESS {
1024        hardfork.is_t7()
1025    } else if address == ADDRESS_REGISTRY_ADDRESS || address == SIGNATURE_VERIFIER_ADDRESS {
1026        hardfork.is_t3()
1027    } else {
1028        true
1029    }
1030}
1031
1032/// Returns the well-known Tempo precompile addresses active at `hardfork`.
1033pub fn active_tempo_precompile_addresses(hardfork: TempoHardfork) -> impl Iterator<Item = Address> {
1034    TEMPO_PRECOMPILE_ADDRESSES
1035        .iter()
1036        .copied()
1037        .filter(move |&address| is_tempo_precompile_active_at(address, hardfork))
1038}
1039
1040/// Returns whether a well-known Monad precompile address is active at `hardfork`.
1041#[cfg(feature = "monad")]
1042pub fn is_monad_precompile_active_at(address: Address, hardfork: MonadHardfork) -> bool {
1043    address == monad_revm::staking::STAKING_ADDRESS
1044        || (address == monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS
1045            && MonadHardfork::MonadNine.is_enabled_in(hardfork))
1046}
1047
1048/// Returns whether a fixed Base precompile is active at `upgrade`.
1049#[cfg(feature = "base")]
1050pub fn is_base_precompile_active_at(address: Address, upgrade: BaseUpgrade) -> bool {
1051    if matches!(address, TxContextStorage::ADDRESS | NonceManagerStorage::ADDRESS) {
1052        upgrade >= BaseUpgrade::Cobalt
1053    } else if matches!(
1054        address,
1055        B20FactoryStorage::ADDRESS
1056            | ActivationRegistryStorage::ADDRESS
1057            | PolicyRegistryStorage::ADDRESS
1058    ) {
1059        upgrade >= BaseUpgrade::Beryl
1060    } else {
1061        false
1062    }
1063}
1064
1065/// Returns the fixed Base precompiles active at `upgrade`.
1066#[cfg(feature = "base")]
1067pub fn active_base_precompiles(
1068    upgrade: BaseUpgrade,
1069) -> impl Iterator<Item = (&'static str, Address)> {
1070    BASE_PRECOMPILES
1071        .iter()
1072        .copied()
1073        .filter(move |(_, address)| is_base_precompile_active_at(*address, upgrade))
1074}
1075
1076#[cfg(test)]
1077mod tests {
1078    use super::*;
1079    use revm::precompile::{
1080        Precompiles,
1081        secp256r1::{P256VERIFY_BASE_GAS_FEE, P256VERIFY_BASE_GAS_FEE_OSAKA},
1082    };
1083
1084    // --- Equivalence: new flag == legacy flag ---
1085
1086    #[test]
1087    fn network_variant_predicates() {
1088        assert!(NetworkVariant::Ethereum.is_ethereum());
1089        assert!(!NetworkVariant::Ethereum.is_optimism());
1090        assert!(!NetworkVariant::Ethereum.is_tempo());
1091        assert!(NetworkVariant::Tempo.is_tempo());
1092        assert!(!NetworkVariant::Tempo.is_ethereum());
1093        assert!(!NetworkVariant::Tempo.is_optimism());
1094
1095        #[cfg(feature = "monad")]
1096        {
1097            assert!(!NetworkVariant::Ethereum.is_monad());
1098            assert!(!NetworkVariant::Tempo.is_monad());
1099            assert!(NetworkVariant::Monad.is_monad());
1100            assert!(!NetworkVariant::Monad.is_ethereum());
1101            assert!(!NetworkVariant::Monad.is_optimism());
1102            assert!(!NetworkVariant::Monad.is_tempo());
1103        }
1104
1105        #[cfg(feature = "optimism")]
1106        {
1107            assert!(NetworkVariant::Optimism.is_optimism());
1108            assert!(!NetworkVariant::Optimism.is_ethereum());
1109            assert!(!NetworkVariant::Optimism.is_tempo());
1110
1111            #[cfg(feature = "monad")]
1112            assert!(!NetworkVariant::Optimism.is_monad());
1113        }
1114
1115        #[cfg(feature = "base")]
1116        {
1117            assert!(NetworkVariant::Base.is_base());
1118            assert!(!NetworkVariant::Base.is_ethereum());
1119            assert!(!NetworkVariant::Base.is_optimism());
1120            assert!(!NetworkVariant::Base.is_tempo());
1121        }
1122    }
1123
1124    #[test]
1125    #[cfg(all(feature = "base", feature = "optimism"))]
1126    fn historical_hardfork_uses_selected_execution_family() {
1127        let timestamp = 1_710_374_401;
1128
1129        assert_eq!(
1130            NetworkVariant::Base.historical_hardfork(NamedChain::Base as u64, timestamp),
1131            Some(BaseUpgrade::Ecotone.into())
1132        );
1133        assert_eq!(
1134            NetworkVariant::Optimism.historical_hardfork(NamedChain::Base as u64, timestamp),
1135            Some(OpHardfork::Ecotone.into())
1136        );
1137        assert_eq!(
1138            NetworkVariant::Ethereum.historical_hardfork(NamedChain::Base as u64, timestamp),
1139            None
1140        );
1141        assert_eq!(
1142            NetworkVariant::Tempo.historical_hardfork(NamedChain::Base as u64, timestamp),
1143            None
1144        );
1145    }
1146
1147    #[test]
1148    #[cfg(feature = "monad")]
1149    fn fork_sources_only_isolate_monad() {
1150        let non_monad = vec![
1151            NetworkConfigs::default(),
1152            NetworkConfigs::with_ethereum(),
1153            NetworkConfigs::with_celo(),
1154            NetworkConfigs::with_tempo(),
1155        ];
1156
1157        for execution in &non_monad {
1158            for source in &non_monad {
1159                assert!(execution.supports_fork_source(source));
1160            }
1161            assert!(!execution.supports_fork_source(&NetworkConfigs::with_monad()));
1162            assert!(!NetworkConfigs::with_monad().supports_fork_source(execution));
1163        }
1164        assert!(NetworkConfigs::with_monad().supports_fork_source(&NetworkConfigs::with_monad()));
1165    }
1166
1167    #[test]
1168    #[cfg(feature = "optimism")]
1169    fn optimism_fork_sources_require_op_stack_state() {
1170        let optimism = NetworkConfigs::with_optimism();
1171        for source in [
1172            NetworkConfigs::default(),
1173            NetworkConfigs::with_ethereum(),
1174            NetworkConfigs::with_celo(),
1175            NetworkConfigs::with_tempo(),
1176        ] {
1177            assert!(!optimism.supports_fork_source(&source));
1178            assert!(source.supports_fork_source(&optimism));
1179        }
1180        assert!(optimism.supports_fork_source(&optimism));
1181        #[cfg(feature = "monad")]
1182        assert!(!optimism.supports_fork_source(&NetworkConfigs::with_monad()));
1183    }
1184
1185    #[test]
1186    #[cfg(feature = "base")]
1187    fn fork_sources_preserve_base_state_source_compatibility() {
1188        #[cfg_attr(not(feature = "optimism"), allow(unused_mut))]
1189        let mut non_base = vec![
1190            NetworkConfigs::default(),
1191            NetworkConfigs::with_ethereum(),
1192            NetworkConfigs::with_celo(),
1193            NetworkConfigs::with_tempo(),
1194        ];
1195        #[cfg(feature = "optimism")]
1196        non_base.push(NetworkConfigs::with_optimism());
1197        for execution in &non_base {
1198            assert!(execution.supports_fork_source(&NetworkConfigs::with_base()));
1199            assert!(!NetworkConfigs::with_base().supports_fork_source(execution));
1200        }
1201        assert!(NetworkConfigs::with_base().supports_fork_source(&NetworkConfigs::with_base()));
1202
1203        #[cfg(feature = "monad")]
1204        {
1205            assert!(
1206                !NetworkConfigs::with_monad().supports_fork_source(&NetworkConfigs::with_base())
1207            );
1208            assert!(
1209                !NetworkConfigs::with_base().supports_fork_source(&NetworkConfigs::with_monad())
1210            );
1211        }
1212    }
1213
1214    #[test]
1215    fn known_chain_identity_does_not_guess_unknown_networks() {
1216        assert_eq!(NetworkVariant::from_known_chain_id(98_765_432).unwrap(), None);
1217        assert_eq!(
1218            NetworkVariant::from_known_chain_id(NamedChain::Mainnet as u64).unwrap(),
1219            Some(NetworkVariant::Ethereum)
1220        );
1221        assert_eq!(NetworkVariant::from(98_765_432), NetworkVariant::Ethereum);
1222    }
1223
1224    #[test]
1225    fn fallible_chain_id_inference_preserves_unknown_chains() {
1226        let networks = NetworkConfigs { bypass_prevrandao: true, ..Default::default() };
1227        assert_eq!(networks.try_with_chain_id(98_765_432).unwrap(), networks);
1228    }
1229
1230    #[test]
1231    #[cfg(not(feature = "monad"))]
1232    fn fallible_chain_id_inference_rejects_disabled_monad() {
1233        for chain_id in [NamedChain::Monad as u64, NamedChain::MonadTestnet as u64] {
1234            let unavailable = "network family `monad` is not enabled in this build";
1235            let expected =
1236                format!("cannot infer execution network from chain ID {chain_id}: {unavailable}");
1237            assert_eq!(
1238                NetworkConfigs::default().try_with_chain_id(chain_id).unwrap_err(),
1239                expected
1240            );
1241        }
1242    }
1243
1244    #[test]
1245    #[cfg(not(feature = "optimism"))]
1246    fn fallible_chain_id_inference_rejects_disabled_optimism() {
1247        let chain_id = NamedChain::Optimism as u64;
1248        assert_eq!(
1249            NetworkConfigs::default().try_with_chain_id(chain_id).unwrap_err(),
1250            format!(
1251                "cannot infer execution network from chain ID {chain_id}: network family \
1252                 `optimism` is not enabled in this build"
1253            )
1254        );
1255    }
1256
1257    #[test]
1258    fn explicit_ethereum_overrides_known_chain_inference() {
1259        let ethereum = NetworkConfigs::with_ethereum();
1260        for chain_id in [NamedChain::Monad as u64, NamedChain::Optimism as u64] {
1261            assert_eq!(ethereum.try_with_chain_id(chain_id).unwrap(), ethereum);
1262        }
1263    }
1264
1265    #[test]
1266    fn fallible_chain_id_inference_preserves_orthogonal_configuration() {
1267        let networks = NetworkConfigs { bypass_prevrandao: true, ..Default::default() };
1268        let inferred = networks.try_with_chain_id(NamedChain::Tempo as u64).unwrap();
1269
1270        assert!(inferred.is_tempo());
1271        assert!(inferred.bypass_prevrandao(NamedChain::Mainnet as u64));
1272    }
1273
1274    #[test]
1275    #[cfg(feature = "optimism")]
1276    fn fallible_chain_id_inference_detects_optimism() {
1277        let chain_id = NamedChain::Optimism as u64;
1278        assert_eq!(NetworkVariant::from(chain_id), NetworkVariant::Optimism);
1279        assert!(NetworkConfigs::default().try_with_chain_id(chain_id).unwrap().is_optimism());
1280    }
1281
1282    #[test]
1283    fn rpc_identity_preserves_orthogonal_configuration() {
1284        let base = NetworkConfigs { bypass_prevrandao: true, ..NetworkConfigs::default() };
1285        let ethereum = base.with_rpc_network(NetworkVariant::Ethereum);
1286        assert!(ethereum.bypass_prevrandao(NamedChain::Mainnet as u64));
1287
1288        let ethereum = NetworkConfigs::default().with_rpc_network(NetworkVariant::Ethereum);
1289        assert_eq!(ethereum, NetworkConfigs::default());
1290
1291        assert!(
1292            NetworkConfigs::default()
1293                .with_rpc_identity(NetworkVariant::Ethereum, NamedChain::Celo as u64)
1294                .is_celo()
1295        );
1296        assert!(
1297            !NetworkConfigs::default()
1298                .with_rpc_identity(NetworkVariant::Ethereum, NamedChain::Monad as u64)
1299                .is_monad()
1300        );
1301    }
1302
1303    #[test]
1304    fn rpc_metadata_overrides_known_execution_chain_identity() {
1305        assert_eq!(
1306            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1307                NamedChain::Mainnet as u64,
1308                Some(Some("tempo")),
1309                None,
1310            )
1311            .unwrap(),
1312            Some(NetworkConfigs::with_tempo())
1313        );
1314        assert_eq!(
1315            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1316                NamedChain::Tempo as u64,
1317                Some(None),
1318                None,
1319            )
1320            .unwrap(),
1321            Some(NetworkConfigs::with_tempo())
1322        );
1323        assert_eq!(
1324            NetworkConfigs::from_rpc_identity_profile_with_fallback(98_765_432, None, None)
1325                .unwrap(),
1326            None
1327        );
1328        assert_eq!(
1329            NetworkConfigs::from_rpc_identity_profile_with_fallback(98_765_432, Some(None), None)
1330                .unwrap(),
1331            Some(NetworkConfigs::default())
1332        );
1333        #[cfg(feature = "optimism")]
1334        assert_eq!(
1335            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1336                NamedChain::Optimism as u64,
1337                Some(None),
1338                None,
1339            )
1340            .unwrap(),
1341            Some(NetworkConfigs::from(NetworkVariant::Optimism))
1342        );
1343    }
1344
1345    #[test]
1346    fn rpc_profile_distinguishes_celo_from_ethereum_factory() {
1347        let custom_chain_id = 98_765_432;
1348        assert!(
1349            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1350                custom_chain_id,
1351                Some(Some("celo")),
1352                None,
1353            )
1354            .unwrap()
1355            .unwrap()
1356            .is_celo()
1357        );
1358        assert!(
1359            !NetworkConfigs::from_rpc_identity_profile_with_fallback(
1360                NamedChain::Celo as u64,
1361                Some(Some("ethereum")),
1362                None,
1363            )
1364            .unwrap()
1365            .unwrap()
1366            .is_celo()
1367        );
1368        assert!(
1369            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1370                NamedChain::Celo as u64,
1371                Some(None),
1372                None,
1373            )
1374            .unwrap()
1375            .unwrap()
1376            .is_celo()
1377        );
1378        assert!(
1379            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1380                custom_chain_id,
1381                None,
1382                Some(NetworkConfigs::with_celo()),
1383            )
1384            .unwrap()
1385            .unwrap()
1386            .is_celo()
1387        );
1388    }
1389
1390    #[test]
1391    fn rpc_profile_keeps_moonbeam_on_default_ethereum() {
1392        let profile = NetworkConfigs::from_rpc_identity_profile_with_fallback(
1393            NamedChain::Moonbeam as u64,
1394            None,
1395            None,
1396        )
1397        .unwrap()
1398        .unwrap();
1399
1400        assert_eq!(profile, NetworkConfigs::default());
1401        assert!(profile.bypass_prevrandao(NamedChain::Moonbeam as u64));
1402    }
1403
1404    #[test]
1405    fn default_fallback_still_infers_known_execution_profile() {
1406        for node_info in [None, Some(None)] {
1407            assert_eq!(
1408                NetworkConfigs::from_rpc_identity_profile_with_fallback(
1409                    NamedChain::Tempo as u64,
1410                    node_info,
1411                    Some(NetworkConfigs::default()),
1412                )
1413                .unwrap(),
1414                Some(NetworkConfigs::with_tempo())
1415            );
1416        }
1417    }
1418
1419    #[test]
1420    fn explicit_ethereum_overrides_known_execution_profile() {
1421        for node_info in [None, Some(None)] {
1422            assert_eq!(
1423                NetworkConfigs::from_rpc_identity_profile_with_fallback(
1424                    NamedChain::Tempo as u64,
1425                    node_info,
1426                    Some(NetworkConfigs::with_ethereum()),
1427                )
1428                .unwrap(),
1429                Some(NetworkConfigs::default())
1430            );
1431        }
1432    }
1433
1434    #[test]
1435    #[cfg(not(feature = "monad"))]
1436    fn explicit_ethereum_overrides_disabled_monad_without_node_info() {
1437        assert_eq!(
1438            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1439                NamedChain::Monad as u64,
1440                None,
1441                Some(NetworkConfigs::with_ethereum()),
1442            )
1443            .unwrap(),
1444            Some(NetworkConfigs::default())
1445        );
1446    }
1447
1448    #[test]
1449    #[cfg(not(feature = "monad"))]
1450    fn explicit_ethereum_overrides_disabled_monad_with_legacy_node_info() {
1451        assert_eq!(
1452            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1453                NamedChain::Monad as u64,
1454                Some(None),
1455                Some(NetworkConfigs::with_ethereum()),
1456            )
1457            .unwrap(),
1458            Some(NetworkConfigs::default())
1459        );
1460    }
1461
1462    #[test]
1463    #[cfg(not(feature = "monad"))]
1464    fn disabled_monad_without_explicit_profile_still_errors() {
1465        for node_info in [None, Some(None)] {
1466            assert_eq!(
1467                NetworkConfigs::from_rpc_identity_profile_with_fallback(
1468                    NamedChain::Monad as u64,
1469                    node_info,
1470                    None,
1471                )
1472                .unwrap_err(),
1473                "network family `monad` is not enabled in this build"
1474            );
1475        }
1476    }
1477
1478    #[test]
1479    fn network_selection_distinguishes_default_and_explicit_ethereum() {
1480        assert!(!NetworkConfigs::default().has_network_selection());
1481        assert!(NetworkConfigs::with_ethereum().has_network_selection());
1482        assert!(NetworkConfigs::with_celo().has_network_selection());
1483    }
1484
1485    #[test]
1486    fn celo_uses_ethereum_factory_with_distinct_precompiles() {
1487        assert!(
1488            !NetworkConfigs::with_celo().has_same_execution_profile(&NetworkConfigs::default())
1489        );
1490        assert!(
1491            NetworkConfigs::with_ethereum().has_same_execution_profile(&NetworkConfigs::default())
1492        );
1493        assert_eq!(NetworkConfigs::with_celo().execution_family_name(), "ethereum");
1494        assert_eq!(NetworkConfigs::with_celo().execution_profile_name(), "celo");
1495    }
1496
1497    #[test]
1498    fn authoritative_execution_profile_preserves_orthogonal_settings() {
1499        let inline = NetworkConfigs { bypass_prevrandao: true, ..NetworkConfigs::with_tempo() };
1500
1501        #[cfg_attr(not(any(feature = "optimism", feature = "monad")), allow(unused_mut))]
1502        let mut profiles = vec![
1503            NetworkConfigs::with_ethereum(),
1504            NetworkConfigs::with_tempo(),
1505            NetworkConfigs::with_celo(),
1506        ];
1507        #[cfg(feature = "optimism")]
1508        profiles.push(NetworkVariant::Optimism.into());
1509        #[cfg(feature = "monad")]
1510        profiles.push(NetworkConfigs::with_monad());
1511
1512        for profile in profiles {
1513            let resolved = inline.with_execution_profile(profile);
1514            assert!(resolved.has_same_execution_profile(&profile));
1515            assert!(resolved.has_network_selection());
1516            assert!(resolved.bypass_prevrandao(NamedChain::Mainnet as u64));
1517        }
1518
1519        let ethereum = inline.with_execution_profile(NetworkConfigs::with_ethereum());
1520        assert_eq!(
1521            ethereum.try_with_chain_id(NamedChain::Tempo as u64).unwrap(),
1522            ethereum,
1523            "an authoritative Ethereum profile must prevent later endpoint inference",
1524        );
1525
1526        let rpc_ethereum = inline.with_rpc_profile(NetworkConfigs::with_ethereum());
1527        assert!(!rpc_ethereum.has_network_selection());
1528    }
1529
1530    #[test]
1531    fn chain_id_inference_preserves_explicit_networks() {
1532        assert!(
1533            NetworkConfigs::default().try_with_chain_id(NamedChain::Celo as u64).unwrap().is_celo()
1534        );
1535        let celo = NetworkConfigs { bypass_prevrandao: true, ..Default::default() }
1536            .try_with_chain_id(NamedChain::Celo as u64)
1537            .unwrap();
1538        assert!(celo.is_celo());
1539        assert!(celo.bypass_prevrandao(NamedChain::Mainnet as u64));
1540
1541        let explicit = [
1542            NetworkConfigs::with_ethereum(),
1543            NetworkConfigs::with_tempo(),
1544            NetworkConfigs::with_celo(),
1545        ];
1546        for networks in explicit {
1547            assert_eq!(networks.try_with_chain_id(NamedChain::Celo as u64).unwrap(), networks);
1548        }
1549
1550        #[cfg(feature = "monad")]
1551        {
1552            let monad = NetworkConfigs::with_monad();
1553            assert_eq!(monad.try_with_chain_id(NamedChain::Celo as u64).unwrap(), monad);
1554        }
1555    }
1556
1557    #[test]
1558    #[cfg(feature = "monad")]
1559    fn rpc_metadata_identifies_custom_monad_networks() {
1560        assert_eq!(NetworkVariant::from_node_info_name("monad").unwrap(), NetworkVariant::Monad);
1561        assert!(NetworkConfigs::default().with_rpc_network(NetworkVariant::Monad).is_monad());
1562    }
1563
1564    #[test]
1565    #[cfg(feature = "monad")]
1566    fn parses_endpoint_hardfork_in_network_namespace() {
1567        assert_eq!(
1568            NetworkVariant::Monad.parse_hardfork("MonadEight").unwrap(),
1569            FoundryHardfork::Monad(MonadHardfork::MonadEight)
1570        );
1571    }
1572
1573    #[test]
1574    fn rpc_metadata_rejects_unknown_network_family() {
1575        assert_eq!(
1576            NetworkVariant::from_node_info_name("unknown").unwrap_err(),
1577            "unsupported network family `unknown` reported by fork endpoint"
1578        );
1579    }
1580
1581    #[test]
1582    fn explicit_ethereum_families_reject_namespaced_hardforks() {
1583        #[cfg_attr(not(feature = "monad"), allow(unused_mut))]
1584        let mut incompatible = vec![FoundryHardfork::Tempo(TempoHardfork::T0)];
1585        #[cfg(feature = "monad")]
1586        incompatible.push(FoundryHardfork::Monad(MonadHardfork::MonadEight));
1587
1588        for networks in [NetworkConfigs::with_ethereum(), NetworkConfigs::with_celo()] {
1589            for hardfork in &incompatible {
1590                assert_eq!(
1591                    networks.normalize_for_hardfork(*hardfork).unwrap_err(),
1592                    format!(
1593                        "hardfork `{}` conflicts with network config `{}`",
1594                        String::from(*hardfork),
1595                        networks.execution_profile_name()
1596                    )
1597                );
1598            }
1599        }
1600
1601        let celo = NetworkConfigs::with_celo();
1602        assert_eq!(
1603            celo.normalize_for_hardfork(FoundryHardfork::Ethereum(
1604                foundry_evm_hardforks::EthereumHardfork::Prague
1605            ))
1606            .unwrap(),
1607            celo
1608        );
1609    }
1610
1611    #[cfg(feature = "base")]
1612    #[test]
1613    fn base_precompile_labels_follow_upgrade_boundaries() {
1614        let config = NetworkConfigs::with_base();
1615
1616        assert!(resolved_precompile_labels(Some(BaseUpgrade::Azul.into())).is_empty());
1617
1618        let beryl = resolved_precompile_labels(Some(BaseUpgrade::Beryl.into()));
1619        assert_eq!(beryl.get(&B20FactoryStorage::ADDRESS), Some(&"B20Factory".to_string()));
1620        assert_eq!(
1621            beryl.get(&ActivationRegistryStorage::ADDRESS),
1622            Some(&"ActivationRegistry".to_string())
1623        );
1624        assert_eq!(beryl.get(&PolicyRegistryStorage::ADDRESS), Some(&"PolicyRegistry".to_string()));
1625        assert!(!beryl.contains_key(&TxContextStorage::ADDRESS));
1626        assert!(!beryl.contains_key(&NonceManagerStorage::ADDRESS));
1627
1628        let cobalt = resolved_precompile_labels(Some(BaseUpgrade::Cobalt.into()));
1629        assert_eq!(cobalt.len(), BASE_PRECOMPILES.len());
1630
1631        // The name-keyed precompile map must honor the same upgrade boundaries.
1632        assert!(config.precompiles(Some(BaseUpgrade::Azul.into())).is_empty());
1633        let beryl = config.precompiles(Some(BaseUpgrade::Beryl.into()));
1634        assert_eq!(beryl.get("B20Factory"), Some(&B20FactoryStorage::ADDRESS));
1635        assert!(!beryl.contains_key("NonceManager"));
1636        assert_eq!(
1637            config.precompiles(Some(BaseUpgrade::Cobalt.into())).get("NonceManager"),
1638            Some(&NonceManagerStorage::ADDRESS)
1639        );
1640    }
1641
1642    #[test]
1643    fn new_tempo_flag_equivalent_to_legacy() {
1644        let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1645        let via_old = NetworkConfigs { tempo: true, ..Default::default() };
1646        assert_eq!(via_new.is_tempo(), via_old.is_tempo());
1647        assert_eq!(via_new.active_network_name(), via_old.active_network_name());
1648        assert_eq!(via_new.precompiles(None), via_old.precompiles(None));
1649    }
1650
1651    fn bsc_p256_gas_used(chain_id: ChainId, timestamp: u64) -> Option<u64> {
1652        bsc_p256_precompile(chain_id, timestamp)
1653            .flatten()
1654            .map(|precompile| precompile.execute(&[], u64::MAX, 0).unwrap().gas_used)
1655    }
1656
1657    fn assert_bsc_p256_boundaries(chain_id: ChainId, haber_timestamp: u64, osaka_timestamp: u64) {
1658        assert!(matches!(bsc_p256_precompile(chain_id, haber_timestamp - 1), Some(None)));
1659        assert_eq!(bsc_p256_gas_used(chain_id, haber_timestamp), Some(P256VERIFY_BASE_GAS_FEE));
1660        assert_eq!(bsc_p256_gas_used(chain_id, osaka_timestamp - 1), Some(P256VERIFY_BASE_GAS_FEE));
1661        assert_eq!(
1662            bsc_p256_gas_used(chain_id, osaka_timestamp),
1663            Some(P256VERIFY_BASE_GAS_FEE_OSAKA)
1664        );
1665    }
1666
1667    #[test]
1668    fn selects_bsc_p256_at_mainnet_boundaries() {
1669        assert_bsc_p256_boundaries(
1670            BSC_MAINNET_CHAIN_ID,
1671            BSC_MAINNET_HABER_TIMESTAMP,
1672            BSC_MAINNET_OSAKA_TIMESTAMP,
1673        );
1674    }
1675
1676    #[test]
1677    fn selects_bsc_p256_at_testnet_boundaries() {
1678        assert_bsc_p256_boundaries(
1679            BSC_TESTNET_CHAIN_ID,
1680            BSC_TESTNET_HABER_TIMESTAMP,
1681            BSC_TESTNET_OSAKA_TIMESTAMP,
1682        );
1683    }
1684
1685    #[test]
1686    fn removes_bsc_p256_before_haber() {
1687        let mut precompiles = PrecompilesMap::from_static(Precompiles::osaka());
1688        assert!(precompiles.get(&BSC_P256_ADDRESS).is_some());
1689        apply_bsc_p256_precompile(
1690            &mut precompiles,
1691            BSC_MAINNET_CHAIN_ID,
1692            BSC_MAINNET_HABER_TIMESTAMP - 1,
1693        );
1694        assert!(precompiles.get(&BSC_P256_ADDRESS).is_none());
1695    }
1696
1697    #[test]
1698    fn canonical_tempo_network_reports_precompiles() {
1699        let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1700
1701        assert_eq!(
1702            cfg.precompiles(None).get("TIP20ChannelReserve"),
1703            Some(&TIP20_CHANNEL_RESERVE_ADDRESS)
1704        );
1705        assert!(
1706            !cfg.precompiles(Some(TempoHardfork::T4.into())).contains_key("TIP20ChannelReserve")
1707        );
1708        assert!(
1709            !cfg.precompiles(Some(TempoHardfork::T4.into())).contains_key("ReceivePolicyGuard")
1710        );
1711        assert!(!cfg.precompiles(Some(TempoHardfork::T2.into())).contains_key("AddressRegistry"));
1712        assert!(!cfg.precompiles(Some(TempoHardfork::T2.into())).contains_key("SignatureVerifier"));
1713        assert_eq!(
1714            cfg.precompiles(Some(TempoHardfork::T3.into())).get("AddressRegistry"),
1715            Some(&ADDRESS_REGISTRY_ADDRESS)
1716        );
1717        assert_eq!(
1718            cfg.precompiles(Some(TempoHardfork::T3.into())).get("SignatureVerifier"),
1719            Some(&SIGNATURE_VERIFIER_ADDRESS)
1720        );
1721        assert_eq!(
1722            resolved_precompile_labels(Some(TempoHardfork::T5.into()))
1723                .get(&TIP20_CHANNEL_RESERVE_ADDRESS),
1724            Some(&"TIP20ChannelReserve".to_string())
1725        );
1726        assert!(
1727            !resolved_precompile_labels(Some(TempoHardfork::T5.into()))
1728                .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
1729        );
1730        assert!(
1731            resolved_precompile_labels(Some(TempoHardfork::T6.into()))
1732                .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
1733        );
1734    }
1735
1736    #[test]
1737    #[cfg(feature = "monad")]
1738    fn canonical_monad_network_reports_hardfork_gated_precompiles() {
1739        let cfg = NetworkConfigs { network: Some(NetworkVariant::Monad), ..Default::default() };
1740
1741        assert_eq!(
1742            cfg.precompiles(Some(MonadHardfork::MonadEight.into())).get("MonadStaking"),
1743            Some(&monad_revm::staking::STAKING_ADDRESS)
1744        );
1745        assert!(
1746            !cfg.precompiles(Some(MonadHardfork::MonadEight.into()))
1747                .contains_key("MonadReserveBalance")
1748        );
1749        assert_eq!(
1750            cfg.precompiles(Some(MonadHardfork::MonadNine.into())).get("MonadReserveBalance"),
1751            Some(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
1752        );
1753        assert_eq!(
1754            resolved_precompile_labels(Some(MonadHardfork::MonadNine.into()))
1755                .get(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS),
1756            Some(&"ReserveBalance".to_string())
1757        );
1758        assert!(
1759            !resolved_precompile_labels(Some(MonadHardfork::MonadEight.into()))
1760                .contains_key(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
1761        );
1762    }
1763
1764    #[test]
1765    fn storage_credits_precompile_activates_at_t7() {
1766        assert!(!is_tempo_precompile_active_at(STORAGE_CREDITS_ADDRESS, TempoHardfork::T6));
1767        assert!(is_tempo_precompile_active_at(STORAGE_CREDITS_ADDRESS, TempoHardfork::T7));
1768        assert!(TEMPO_PRECOMPILE_ADDRESSES.contains(&STORAGE_CREDITS_ADDRESS));
1769
1770        // The hardfork-filtered precompile map must honor the same T7 activation.
1771        let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1772        assert!(!cfg.precompiles(Some(TempoHardfork::T6.into())).contains_key("StorageCredits"));
1773        assert!(cfg.precompiles(Some(TempoHardfork::T7.into())).contains_key("StorageCredits"));
1774    }
1775
1776    #[test]
1777    fn current_committee_precompile_activates_at_t8() {
1778        assert!(!is_tempo_precompile_active_at(CURRENT_COMMITTEE_ADDRESS, TempoHardfork::T7));
1779        assert!(is_tempo_precompile_active_at(CURRENT_COMMITTEE_ADDRESS, TempoHardfork::T8));
1780        assert!(TEMPO_PRECOMPILE_ADDRESSES.contains(&CURRENT_COMMITTEE_ADDRESS));
1781
1782        let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1783        assert!(!cfg.precompiles(Some(TempoHardfork::T7.into())).contains_key("CurrentCommittee"));
1784        assert!(cfg.precompiles(Some(TempoHardfork::T8.into())).contains_key("CurrentCommittee"));
1785    }
1786
1787    // --- resolved() / active_network_name ---
1788
1789    #[test]
1790    fn active_network_name_tempo() {
1791        let cfg = NetworkConfigs::with_tempo();
1792        assert_eq!(cfg.active_network_name(), Some("tempo"));
1793    }
1794
1795    #[test]
1796    #[cfg(feature = "monad")]
1797    fn active_network_name_monad() {
1798        let cfg = NetworkConfigs::with_monad();
1799        assert_eq!(cfg.active_network_name(), Some("monad"));
1800        assert!(cfg.is_monad());
1801    }
1802
1803    #[test]
1804    #[cfg(feature = "monad")]
1805    fn contract_size_limits_monad() {
1806        let limits = NetworkConfigs::with_monad().contract_size_limits().unwrap();
1807        assert_eq!(limits.runtime, monad_revm::MONAD_MAX_CODE_SIZE);
1808        assert_eq!(limits.initcode, monad_revm::MONAD_MAX_INITCODE_SIZE);
1809        assert!(NetworkConfigs::default().contract_size_limits().is_none());
1810    }
1811
1812    #[test]
1813    fn active_network_name_default_is_none() {
1814        let cfg = NetworkConfigs::default();
1815        assert_eq!(cfg.active_network_name(), None);
1816        assert!(!cfg.is_optimism());
1817    }
1818
1819    /// Base chain IDs resolved to Optimism before Base support existed, and `is_optimism()` still
1820    /// covers them, so a build without the `base` feature must keep resolving them rather than
1821    /// erroring. Shipped release binaries are exactly that build.
1822    #[test]
1823    #[cfg(all(not(feature = "base"), feature = "optimism"))]
1824    fn chain_id_inference_falls_back_to_optimism_without_base() {
1825        for chain_id in [NamedChain::Base as u64, NamedChain::BaseSepolia as u64] {
1826            let configs = NetworkConfigs::default()
1827                .try_with_chain_id(chain_id)
1828                .unwrap_or_else(|error| panic!("chain ID {chain_id} must still resolve: {error}"));
1829            assert!(configs.is_optimism(), "chain ID {chain_id} must resolve to Optimism");
1830        }
1831    }
1832
1833    #[test]
1834    #[cfg(not(feature = "base"))]
1835    fn node_info_rejects_disabled_base() {
1836        assert_eq!(
1837            NetworkVariant::from_node_info_name("base").unwrap_err(),
1838            "network family `base` is not enabled in this build"
1839        );
1840    }
1841
1842    // --- Serde round-trip ---
1843
1844    #[test]
1845    fn serde_roundtrip_tempo() {
1846        let original = NetworkConfigs::with_tempo();
1847        let json = serde_json::to_string(&original).unwrap();
1848        let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
1849        assert!(restored.is_tempo());
1850    }
1851
1852    #[test]
1853    fn serde_legacy_tempo_bool_deserialized() {
1854        // Old foundry.toml format: `tempo = true`
1855        let json = r#"{"tempo": true, "celo": false, "bypass_prevrandao": false}"#;
1856        let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
1857        assert!(cfg.is_tempo());
1858    }
1859
1860    #[test]
1861    #[cfg(feature = "monad")]
1862    fn serde_roundtrip_monad() {
1863        let original = NetworkConfigs::with_monad();
1864        let json = serde_json::to_string(&original).unwrap();
1865        let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
1866        assert!(restored.is_monad());
1867        assert!(!restored.is_tempo());
1868    }
1869
1870    #[test]
1871    #[cfg(feature = "monad")]
1872    fn serde_legacy_monad_bool_deserialized() {
1873        let json = r#"{"monad": true, "celo": false, "bypass_prevrandao": false}"#;
1874        let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
1875        assert!(cfg.is_monad());
1876    }
1877
1878    #[test]
1879    fn serde_serializes_legacy_alias_as_canonical_network() {
1880        #[cfg_attr(not(feature = "monad"), allow(unused_mut))]
1881        let mut cases = vec![(NetworkConfigs { tempo: true, ..Default::default() }, "tempo")];
1882        #[cfg(feature = "monad")]
1883        cases.push((NetworkConfigs { monad: true, ..Default::default() }, "monad"));
1884
1885        for (cfg, expected) in cases {
1886            let json = serde_json::to_value(cfg).unwrap();
1887            assert_eq!(json["network"], serde_json::json!(expected));
1888            assert!(json.get("tempo").is_none(), "legacy `tempo` key should not be serialized");
1889            assert!(json.get("monad").is_none(), "legacy `monad` key should not be serialized");
1890        }
1891    }
1892
1893    #[test]
1894    fn serde_new_network_field_deserialized() {
1895        let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#;
1896        let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap();
1897        assert!(cfg_tempo.is_tempo());
1898
1899        #[cfg(feature = "monad")]
1900        {
1901            let json_monad = r#"{"network": "monad", "celo": false, "bypass_prevrandao": false}"#;
1902            let cfg_monad: NetworkConfigs = serde_json::from_str(json_monad).unwrap();
1903            assert!(cfg_monad.is_monad());
1904        }
1905    }
1906
1907    #[test]
1908    fn validates_flattened_network_selectors() {
1909        let valid = [
1910            NetworkConfigs::default(),
1911            NetworkConfigs::with_ethereum(),
1912            NetworkConfigs::with_celo(),
1913            NetworkConfigs::with_tempo(),
1914            NetworkConfigs {
1915                network: Some(NetworkVariant::Tempo),
1916                tempo: true,
1917                ..Default::default()
1918            },
1919        ];
1920        for networks in valid {
1921            networks.validate().unwrap();
1922        }
1923
1924        let conflicts = [
1925            (
1926                NetworkConfigs {
1927                    network: Some(NetworkVariant::Tempo),
1928                    celo: true,
1929                    ..Default::default()
1930                },
1931                "network selectors `network = \"tempo\"` and `celo = true` conflict",
1932            ),
1933            (
1934                NetworkConfigs { celo: true, tempo: true, ..Default::default() },
1935                "network selectors `celo = true` and `tempo = true` conflict",
1936            ),
1937        ];
1938        for (networks, expected) in conflicts {
1939            assert!(networks.validate().unwrap_err().contains(expected));
1940        }
1941    }
1942
1943    #[test]
1944    #[cfg(feature = "monad")]
1945    fn validates_flattened_monad_network_selectors() {
1946        for networks in [
1947            NetworkConfigs::with_monad(),
1948            NetworkConfigs {
1949                network: Some(NetworkVariant::Monad),
1950                monad: true,
1951                ..Default::default()
1952            },
1953        ] {
1954            networks.validate().unwrap();
1955        }
1956
1957        let conflicts = [
1958            (
1959                NetworkConfigs {
1960                    network: Some(NetworkVariant::Monad),
1961                    celo: true,
1962                    ..Default::default()
1963                },
1964                "network selectors `network = \"monad\"` and `celo = true` conflict",
1965            ),
1966            (
1967                NetworkConfigs {
1968                    network: Some(NetworkVariant::Monad),
1969                    tempo: true,
1970                    ..Default::default()
1971                },
1972                "network selectors `network = \"monad\"` and `tempo = true` conflict",
1973            ),
1974            (
1975                NetworkConfigs { celo: true, monad: true, ..Default::default() },
1976                "network selectors `celo = true` and `monad = true` conflict",
1977            ),
1978            (
1979                NetworkConfigs { tempo: true, monad: true, ..Default::default() },
1980                "network selectors `tempo = true` and `monad = true` conflict",
1981            ),
1982        ];
1983        for (networks, expected) in conflicts {
1984            assert!(networks.validate().unwrap_err().contains(expected));
1985        }
1986    }
1987
1988    #[test]
1989    #[cfg(feature = "monad")]
1990    fn chain_id_detects_monad_network() {
1991        assert_eq!(NetworkVariant::from(143), NetworkVariant::Monad);
1992        assert_eq!(NetworkVariant::from(10143), NetworkVariant::Monad);
1993
1994        assert!(NetworkConfigs::default().try_with_chain_id(143).unwrap().is_monad());
1995    }
1996
1997    #[cfg(feature = "base")]
1998    mod base {
1999        use super::*;
2000
2001        #[test]
2002        fn active_network_name_base() {
2003            let cfg = NetworkConfigs::with_base();
2004            assert_eq!(cfg.active_network_name(), Some("base"));
2005        }
2006
2007        #[test]
2008        fn base_fee_params_follow_canyon_boundary() {
2009            const CANYON_TIMESTAMP: u64 = 1_704_992_401;
2010
2011            let config = NetworkConfigs::with_base();
2012            assert_eq!(config.base_fee_params(CANYON_TIMESTAMP - 1), BaseFeeParams::new(50, 6));
2013            assert_eq!(config.base_fee_params(CANYON_TIMESTAMP), BaseFeeParams::new(250, 6));
2014        }
2015
2016        #[test]
2017        fn base_does_not_inherit_ethereum_blob_excess_gas() {
2018            let params = BlobParams::prague();
2019            assert_eq!(
2020                NetworkConfigs::with_base().next_block_blob_excess_gas(
2021                    params,
2022                    0,
2023                    params.target_blob_gas_per_block() + 1,
2024                    1,
2025                ),
2026                0
2027            );
2028        }
2029
2030        #[test]
2031        fn serde_roundtrip_base() {
2032            let original = NetworkConfigs::with_base();
2033            let json = serde_json::to_string(&original).unwrap();
2034            let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
2035            assert!(restored.is_base());
2036            assert!(!restored.is_tempo());
2037        }
2038
2039        #[test]
2040        fn serde_base_field_deserialized() {
2041            let json_base = r#"{"network": "base", "celo": false, "bypass_prevrandao": false}"#;
2042            let cfg_base: NetworkConfigs = serde_json::from_str(json_base).unwrap();
2043            assert!(cfg_base.is_base());
2044        }
2045
2046        #[test]
2047        fn chain_id_detects_base_networks() {
2048            assert_eq!(NetworkVariant::from(8453), NetworkVariant::Base);
2049            assert_eq!(NetworkVariant::from(84532), NetworkVariant::Base);
2050            assert!(NetworkConfigs::default().try_with_chain_id(8453).unwrap().is_base());
2051            assert_eq!(NetworkVariant::from_node_info_name("base").unwrap(), NetworkVariant::Base);
2052        }
2053
2054        #[test]
2055        fn hardfork_infers_base_network() {
2056            assert_eq!(
2057                NetworkConfigs::default()
2058                    .normalize_for_hardfork(FoundryHardfork::Base(BaseUpgrade::Beryl))
2059                    .unwrap()
2060                    .resolved_network(),
2061                Some(NetworkVariant::Base)
2062            );
2063        }
2064    }
2065
2066    #[cfg(feature = "optimism")]
2067    mod optimism {
2068        use super::*;
2069
2070        #[test]
2071        fn new_optimism_flag_equivalent_to_legacy() {
2072            let via_new =
2073                NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() };
2074            let via_old = NetworkConfigs { optimism: true, ..Default::default() };
2075            assert_eq!(via_new.is_optimism(), via_old.is_optimism());
2076            assert_eq!(via_new.is_tempo(), via_old.is_tempo());
2077            assert_eq!(via_new.active_network_name(), via_old.active_network_name());
2078        }
2079
2080        #[test]
2081        fn matching_optimism_selectors_are_valid() {
2082            NetworkConfigs::with_optimism().validate().unwrap();
2083        }
2084
2085        #[test]
2086        fn active_network_name_optimism() {
2087            let cfg = NetworkConfigs::with_optimism();
2088            assert_eq!(cfg.active_network_name(), Some("optimism"));
2089        }
2090
2091        #[test]
2092        fn conflicting_optimism_and_tempo_selectors_are_rejected() {
2093            let cfg = NetworkConfigs {
2094                network: Some(NetworkVariant::Optimism),
2095                tempo: true,
2096                ..Default::default()
2097            };
2098            assert_eq!(
2099                cfg.validate().unwrap_err(),
2100                "network selectors `network = \"optimism\"` and `tempo = true` conflict; select \
2101                 only one network"
2102            );
2103        }
2104
2105        #[test]
2106        fn serde_roundtrip_optimism() {
2107            let original = NetworkConfigs::with_optimism();
2108            let json = serde_json::to_string(&original).unwrap();
2109            let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
2110            assert!(restored.is_optimism());
2111            assert!(!restored.is_tempo());
2112        }
2113
2114        #[test]
2115        fn serde_optimism_field_deserialized() {
2116            let json_optimism =
2117                r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#;
2118            let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap();
2119            assert!(cfg_optimism.is_optimism());
2120        }
2121    }
2122
2123    #[test]
2124    fn executed_hardfork_follows_the_network_family() {
2125        // A cross-namespace override runs as the configured network's hardfork, so the value used
2126        // to describe execution has to be coerced the same way.
2127        let prague = FoundryHardfork::Ethereum(EthereumHardfork::Prague);
2128        assert_eq!(NetworkConfigs::default().executed_hardfork(prague), prague);
2129        assert_eq!(
2130            NetworkConfigs::with_tempo().executed_hardfork(prague),
2131            FoundryHardfork::Tempo(TempoHardfork::from(prague))
2132        );
2133    }
2134}