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::{
16    CELO_TRANSFER_ADDRESS, CELO_TRANSFER_LABEL, PRECOMPILE_ID_CELO_TRANSFER,
17};
18use alloy_chains::{
19    Chain, NamedChain,
20    NamedChain::{Chiado, Gnosis, Moonbase, Moonbeam, MoonbeamDev, Moonriver, Rsk, RskTestnet},
21};
22use alloy_eips::{eip1559::BaseFeeParams, eip7840::BlobParams};
23use alloy_evm::precompiles::{DynPrecompile, PrecompilesMap};
24use alloy_primitives::{Address, ChainId, address, map::AddressHashMap};
25use clap::Parser;
26use foundry_evm_hardforks::{
27    EthereumHardfork, ExecutionSpec, FoundryHardfork, TempoHardfork, latest_active_tempo_hardfork,
28};
29use revm::precompile::{
30    Precompile as RevmPrecompile,
31    secp256r1::{P256VERIFY, P256VERIFY_OSAKA},
32};
33use serde::{Deserialize, Serialize};
34use std::collections::BTreeMap;
35use tempo_contracts::precompiles::{
36    ACCOUNT_KEYCHAIN_ADDRESS, ADDRESS_REGISTRY_ADDRESS, CURRENT_COMMITTEE_ADDRESS,
37    NONCE_PRECOMPILE_ADDRESS, RECEIVE_POLICY_GUARD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS,
38    STABLECOIN_DEX_ADDRESS, STORAGE_CREDITS_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
39    TIP20_CHANNEL_RESERVE_ADDRESS, TIP20_FACTORY_ADDRESS, TIP403_REGISTRY_ADDRESS,
40    VALIDATOR_CONFIG_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
41};
42
43#[cfg(feature = "base")]
44use base_common_precompiles::{
45    ActivationRegistryStorage, B20FactoryStorage, NonceManagerStorage, PolicyRegistryStorage,
46    TxContextStorage,
47};
48#[cfg(feature = "base")]
49use foundry_evm_hardforks::BaseUpgrade;
50
51#[cfg(feature = "optimism")]
52use foundry_evm_hardforks::OpHardfork;
53
54#[cfg(feature = "monad")]
55type MonadHardfork = foundry_evm_hardforks::MonadHardfork;
56
57/// The Monad cheatcode handler address.
58pub const MONAD_CHEATCODE_ADDRESS: Address = address!("0xc0FFeeCD43A10e1C2b0De63c6CDCFe5B7d0e0CEA");
59
60pub mod arbitrum;
61pub mod celo;
62
63#[cfg(feature = "optimism")]
64mod optimism;
65
66const TEMPO_PRECOMPILES: &[(&str, Address)] = &[
67    ("Nonce", NONCE_PRECOMPILE_ADDRESS),
68    ("StablecoinDex", STABLECOIN_DEX_ADDRESS),
69    ("TIP20Factory", TIP20_FACTORY_ADDRESS),
70    ("TIP403Registry", TIP403_REGISTRY_ADDRESS),
71    ("FeeManager", TIP_FEE_MANAGER_ADDRESS),
72    ("ValidatorConfig", VALIDATOR_CONFIG_ADDRESS),
73    ("ValidatorConfigV2", VALIDATOR_CONFIG_V2_ADDRESS),
74    ("AccountKeychain", ACCOUNT_KEYCHAIN_ADDRESS),
75    ("SignatureVerifier", SIGNATURE_VERIFIER_ADDRESS),
76    ("AddressRegistry", ADDRESS_REGISTRY_ADDRESS),
77    ("TIP20ChannelReserve", TIP20_CHANNEL_RESERVE_ADDRESS),
78    ("ReceivePolicyGuard", RECEIVE_POLICY_GUARD_ADDRESS),
79    ("StorageCredits", STORAGE_CREDITS_ADDRESS),
80    ("CurrentCommittee", CURRENT_COMMITTEE_ADDRESS),
81];
82
83#[cfg(feature = "monad")]
84const MONAD_PRECOMPILE_LABELS: &[(&str, Address)] = &[
85    ("Staking", monad_revm::staking::STAKING_ADDRESS),
86    ("ReserveBalance", monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS),
87];
88
89#[cfg(feature = "monad")]
90const MONAD_PRECOMPILES: &[(&str, Address)] = &[
91    ("MonadStaking", monad_revm::staking::STAKING_ADDRESS),
92    ("MonadReserveBalance", monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS),
93];
94
95#[cfg(feature = "base")]
96const BASE_PRECOMPILES: &[(&str, Address)] = &[
97    ("B20Factory", B20FactoryStorage::ADDRESS),
98    ("ActivationRegistry", ActivationRegistryStorage::ADDRESS),
99    ("PolicyRegistry", PolicyRegistryStorage::ADDRESS),
100    ("TxContext", TxContextStorage::ADDRESS),
101    ("NonceManager", NonceManagerStorage::ADDRESS),
102];
103
104/// All fixed Base precompile addresses.
105#[cfg(feature = "base")]
106pub const BASE_PRECOMPILE_ADDRESSES: &[Address] = &[
107    B20FactoryStorage::ADDRESS,
108    ActivationRegistryStorage::ADDRESS,
109    PolicyRegistryStorage::ADDRESS,
110    TxContextStorage::ADDRESS,
111    NonceManagerStorage::ADDRESS,
112];
113
114/// Fixed Base precompiles that expose at least one function returning no data.
115///
116/// Solidity guards high-level calls to such functions with an `extcodesize` check, which a
117/// code-less account fails in the caller, so these must carry code. Base mainnet plants a one-byte
118/// sentinel on exactly these two. The factory, nonce manager, and transaction context return data
119/// from every function and are code-less on chain, so stubbing them would diverge — a contract
120/// guarding calls with an `isContract` probe would pass locally and revert on Base.
121///
122/// The nonce manager separately receives a stub at Zenith from Base's own
123/// `ensure_eip8130_system_accounts` transition, for EIP-161 state clearing rather than for
124/// `extcodesize`. That transition owns it; this list must not.
125#[cfg(feature = "base")]
126pub const BASE_CODE_SENTINEL_ADDRESSES: &[Address] =
127    &[ActivationRegistryStorage::ADDRESS, PolicyRegistryStorage::ADDRESS];
128
129/// BSC secp256r1 precompile address introduced by the Haber hardfork.
130const BSC_P256_ADDRESS: Address = address!("0000000000000000000000000000000000000100");
131
132const BSC_MAINNET_CHAIN_ID: u64 = 56;
133const BSC_TESTNET_CHAIN_ID: u64 = 97;
134const BSC_MAINNET_HABER_TIMESTAMP: u64 = 1_718_863_500;
135const BSC_TESTNET_HABER_TIMESTAMP: u64 = 1_716_962_820;
136const BSC_MAINNET_OSAKA_TIMESTAMP: u64 = 1_777_343_400;
137const BSC_TESTNET_OSAKA_TIMESTAMP: u64 = 1_774_319_400;
138
139/// All well-known Tempo precompile addresses.
140pub const TEMPO_PRECOMPILE_ADDRESSES: &[Address] = &[
141    NONCE_PRECOMPILE_ADDRESS,
142    STABLECOIN_DEX_ADDRESS,
143    TIP20_FACTORY_ADDRESS,
144    TIP403_REGISTRY_ADDRESS,
145    TIP_FEE_MANAGER_ADDRESS,
146    VALIDATOR_CONFIG_ADDRESS,
147    VALIDATOR_CONFIG_V2_ADDRESS,
148    ACCOUNT_KEYCHAIN_ADDRESS,
149    SIGNATURE_VERIFIER_ADDRESS,
150    ADDRESS_REGISTRY_ADDRESS,
151    TIP20_CHANNEL_RESERVE_ADDRESS,
152    RECEIVE_POLICY_GUARD_ADDRESS,
153    STORAGE_CREDITS_ADDRESS,
154    CURRENT_COMMITTEE_ADDRESS,
155];
156
157#[derive(
158    Clone,
159    Copy,
160    Debug,
161    Default,
162    PartialEq,
163    Eq,
164    PartialOrd,
165    Ord,
166    Hash,
167    Serialize,
168    Deserialize,
169    clap::ValueEnum,
170)]
171#[serde(rename_all = "lowercase")]
172#[clap(rename_all = "lowercase")]
173pub enum NetworkVariant {
174    #[default]
175    Ethereum,
176    #[cfg(feature = "base")]
177    Base,
178    #[cfg(feature = "optimism")]
179    Optimism,
180    Tempo,
181    #[cfg(feature = "monad")]
182    Monad,
183}
184
185/// Runtime and initcode byte-size limits for a configured network.
186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub struct NetworkContractSizeLimits {
188    /// Maximum deployed runtime bytecode size.
189    pub runtime: usize,
190    /// Maximum initcode bytecode size.
191    pub initcode: usize,
192}
193
194impl std::str::FromStr for NetworkVariant {
195    type Err = String;
196
197    fn from_str(s: &str) -> Result<Self, Self::Err> {
198        match s {
199            "ethereum" => Ok(Self::Ethereum),
200            #[cfg(feature = "base")]
201            "base" => Ok(Self::Base),
202            #[cfg(feature = "optimism")]
203            "optimism" => Ok(Self::Optimism),
204            "tempo" => Ok(Self::Tempo),
205            #[cfg(feature = "monad")]
206            "monad" => Ok(Self::Monad),
207            _ => Err(format!("unknown network variant: {s}")),
208        }
209    }
210}
211
212impl NetworkVariant {
213    /// Returns the network family identified by a known chain ID.
214    ///
215    /// Unknown chain IDs return `None` so callers can consult endpoint metadata instead of
216    /// assuming Ethereum. If a known chain belongs to a feature-gated family that is not enabled,
217    /// this returns an error rather than silently selecting a different EVM.
218    pub fn from_known_chain_id(chain_id: ChainId) -> Result<Option<Self>, String> {
219        let chain = Chain::from_id(chain_id);
220        if chain.is_tempo() {
221            return Ok(Some(Self::Tempo));
222        }
223        if matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia)) {
224            return Ok(Some(Self::Ethereum));
225        }
226        // Only claim Base when the feature is on. `is_optimism()` below already covers Base chain
227        // IDs, and that is what shipped binaries resolve them to today, so erroring here would
228        // regress builds that never asked for Base. Monad errors instead because no shipped EVM
229        // approximates it.
230        #[cfg(feature = "base")]
231        if matches!(chain.named(), Some(NamedChain::Base | NamedChain::BaseSepolia)) {
232            return Ok(Some(Self::Base));
233        }
234        if matches!(chain.named(), Some(NamedChain::Monad | NamedChain::MonadTestnet)) {
235            #[cfg(feature = "monad")]
236            return Ok(Some(Self::Monad));
237            #[cfg(not(feature = "monad"))]
238            return Err("network family `monad` is not enabled in this build".to_string());
239        }
240        if chain.is_optimism() {
241            #[cfg(feature = "optimism")]
242            return Ok(Some(Self::Optimism));
243            #[cfg(not(feature = "optimism"))]
244            return Err("network family `optimism` is not enabled in this build".to_string());
245        }
246        Ok(chain.named().map(|_| Self::Ethereum))
247    }
248
249    /// Parses an explicit network family reported by `anvil_nodeInfo`.
250    pub fn from_node_info_name(network: &str) -> Result<Self, String> {
251        match network {
252            "ethereum" => Ok(Self::Ethereum),
253            #[cfg(feature = "base")]
254            "base" => Ok(Self::Base),
255            #[cfg(not(feature = "base"))]
256            "base" => Err("network family `base` is not enabled in this build".to_string()),
257            #[cfg(feature = "optimism")]
258            "optimism" => Ok(Self::Optimism),
259            #[cfg(not(feature = "optimism"))]
260            "optimism" => Err("network family `optimism` is not enabled in this build".to_string()),
261            "tempo" => Ok(Self::Tempo),
262            #[cfg(feature = "monad")]
263            "monad" => Ok(Self::Monad),
264            #[cfg(not(feature = "monad"))]
265            "monad" => Err("network family `monad` is not enabled in this build".to_string()),
266            network => {
267                Err(format!("unsupported network family `{network}` reported by fork endpoint"))
268            }
269        }
270    }
271
272    /// Resolves an RPC endpoint's network family.
273    ///
274    /// The outer `node_info_network` option distinguishes an unavailable `anvil_nodeInfo` method
275    /// from a successful legacy response that omitted the network. An explicit name is
276    /// authoritative even when the endpoint exposes a well-known execution chain ID. A legacy
277    /// omission falls back to the known chain ID, or Ethereum for a custom chain.
278    pub fn from_rpc_identity(
279        chain_id: ChainId,
280        node_info_network: Option<Option<&str>>,
281    ) -> Result<Option<Self>, String> {
282        Self::from_rpc_identity_with_fallback(chain_id, node_info_network, None)
283    }
284
285    /// Resolves an RPC endpoint's network family with a caller-selected unknown-chain fallback.
286    ///
287    /// Explicit endpoint metadata is authoritative. Legacy `anvil_nodeInfo` responses that omit
288    /// the family first use a known chain ID, then the supplied fallback, and finally Ethereum to
289    /// preserve historical behavior. Endpoints without `anvil_nodeInfo` use only a known chain ID
290    /// or the supplied fallback.
291    pub fn from_rpc_identity_with_fallback(
292        chain_id: ChainId,
293        node_info_network: Option<Option<&str>>,
294        unknown_fallback: Option<Self>,
295    ) -> Result<Option<Self>, String> {
296        match node_info_network {
297            Some(Some(network)) => Self::from_node_info_name(network).map(Some),
298            Some(None) => Ok(Self::from_known_chain_id(chain_id)?
299                .or(unknown_fallback)
300                .or(Some(Self::Ethereum))),
301            None => Ok(Self::from_known_chain_id(chain_id)?.or(unknown_fallback)),
302        }
303    }
304
305    /// Parses a hardfork name reported by an RPC endpoint in this network's namespace.
306    pub fn parse_hardfork(self, hardfork: &str) -> Result<FoundryHardfork, String> {
307        format!("{}:{hardfork}", self.name()).parse()
308    }
309
310    /// Returns this network's hardfork at a historical source-chain timestamp.
311    ///
312    /// Unknown source chains return `None`; the selected network owns the lookup so a chain ID
313    /// shared by multiple execution families cannot select another family's hardfork.
314    pub fn historical_hardfork(self, chain_id: ChainId, timestamp: u64) -> Option<FoundryHardfork> {
315        match self {
316            Self::Ethereum => {
317                EthereumHardfork::from_chain_and_timestamp(Chain::from_id(chain_id), timestamp)
318                    .map(Into::into)
319            }
320            Self::Tempo => {
321                TempoHardfork::from_chain_and_timestamp(chain_id, timestamp).map(Into::into)
322            }
323            #[cfg(feature = "optimism")]
324            Self::Optimism => {
325                OpHardfork::from_chain_and_timestamp(Chain::from_id(chain_id), timestamp)
326                    .map(Into::into)
327            }
328            #[cfg(feature = "monad")]
329            Self::Monad => {
330                MonadHardfork::from_chain_and_timestamp(chain_id, timestamp).map(Into::into)
331            }
332            #[cfg(feature = "base")]
333            Self::Base => {
334                BaseUpgrade::from_chain_and_timestamp(chain_id, timestamp).map(Into::into)
335            }
336        }
337    }
338
339    /// Returns the active hardfork for this network at the given chain and timestamp.
340    ///
341    /// Unknown chain IDs fall back to the network's default hardfork. The selected network owns
342    /// the lookup so an explicit network choice is not overridden by the chain ID's family.
343    pub fn hardfork_at(self, chain_id: ChainId, timestamp: u64) -> FoundryHardfork {
344        self.historical_hardfork(chain_id, timestamp).unwrap_or_else(|| match self {
345            Self::Ethereum => EthereumHardfork::default().into(),
346            Self::Tempo => latest_active_tempo_hardfork().into(),
347            #[cfg(feature = "optimism")]
348            Self::Optimism => OpHardfork::default().into(),
349            #[cfg(feature = "monad")]
350            Self::Monad => MonadHardfork::default().into(),
351            #[cfg(feature = "base")]
352            Self::Base => BaseUpgrade::default().into(),
353        })
354    }
355
356    /// Returns `true` if this is the Ethereum network variant.
357    pub const fn is_ethereum(&self) -> bool {
358        matches!(self, Self::Ethereum)
359    }
360
361    /// Returns `true` if this is the Base network variant.
362    #[cfg(feature = "base")]
363    pub const fn is_base(&self) -> bool {
364        matches!(self, Self::Base)
365    }
366
367    /// Returns `true` if this is the Optimism network variant.
368    pub const fn is_optimism(&self) -> bool {
369        #[cfg(feature = "optimism")]
370        {
371            matches!(self, Self::Optimism)
372        }
373        #[cfg(not(feature = "optimism"))]
374        {
375            false
376        }
377    }
378
379    /// Returns `true` if this is the Tempo network variant.
380    pub const fn is_tempo(&self) -> bool {
381        matches!(self, Self::Tempo)
382    }
383
384    /// Returns `true` if this is the Monad network variant.
385    #[cfg(feature = "monad")]
386    pub const fn is_monad(&self) -> bool {
387        matches!(self, Self::Monad)
388    }
389
390    /// Returns `false` when Monad support is not compiled in.
391    #[cfg(not(feature = "monad"))]
392    pub const fn is_monad(&self) -> bool {
393        false
394    }
395
396    /// Returns the network variant name.
397    pub const fn name(&self) -> &'static str {
398        match self {
399            Self::Ethereum => "ethereum",
400            #[cfg(feature = "base")]
401            Self::Base => "base",
402            #[cfg(feature = "optimism")]
403            Self::Optimism => "optimism",
404            Self::Tempo => "tempo",
405            #[cfg(feature = "monad")]
406            Self::Monad => "monad",
407        }
408    }
409
410    /// Returns the hardfork namespace used by this network family.
411    pub const fn hardfork_namespace(&self) -> Option<&'static str> {
412        match self {
413            Self::Ethereum => None,
414            #[cfg(feature = "base")]
415            Self::Base => Some("base"),
416            #[cfg(feature = "optimism")]
417            Self::Optimism => Some("optimism"),
418            Self::Tempo => Some("tempo"),
419            #[cfg(feature = "monad")]
420            Self::Monad => Some("monad"),
421        }
422    }
423}
424
425impl std::fmt::Display for NetworkVariant {
426    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427        f.write_str(self.name())
428    }
429}
430
431impl From<ChainId> for NetworkVariant {
432    fn from(chain_id: ChainId) -> Self {
433        Self::from_known_chain_id(chain_id).ok().flatten().unwrap_or(Self::Ethereum)
434    }
435}
436
437#[derive(Clone, Debug, Default, Parser, Deserialize, Copy, PartialEq, Eq, Hash)]
438pub struct NetworkConfigs {
439    /// Enable a specific network family.
440    #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "tempo"])]
441    #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
442    #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
443    #[serde(default)]
444    pub(crate) network: Option<NetworkVariant>,
445    /// Enable Celo network features.
446    #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "tempo"])]
447    #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
448    #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
449    celo: bool,
450    /// Enable Optimism network features (deprecated: use --network optimism).
451    #[cfg(feature = "optimism")]
452    #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
453    #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
454    // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the
455    // canonical form is `network = "optimism"`.
456    #[serde(default)]
457    pub(crate) optimism: bool,
458    /// Enable Tempo network features (deprecated: use --network tempo).
459    #[arg(long, hide = true, conflicts_with_all = ["network", "celo"])]
460    #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
461    #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
462    // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the
463    // canonical form is `network = "tempo"`.
464    #[serde(default)]
465    tempo: bool,
466    /// Enable Monad network features (deprecated: use --network monad).
467    #[cfg(feature = "monad")]
468    #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
469    #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
470    // Deserialize-only legacy alias: accepted in foundry.toml but never serialized - the
471    // canonical form is `network = "monad"`.
472    #[serde(default)]
473    monad: bool,
474    /// Whether to bypass prevrandao.
475    #[arg(skip)]
476    #[serde(default)]
477    bypass_prevrandao: bool,
478}
479
480// Custom `Serialize` impl: always emits the *resolved* network as the canonical
481// `network = "..."` field, and never emits legacy network aliases. This avoids contradictory
482// canonical and legacy selectors, and ensures old foundry.toml keys round-trip canonically.
483impl Serialize for NetworkConfigs {
484    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
485        use serde::ser::SerializeStruct;
486        let mut s = serializer.serialize_struct("NetworkConfigs", 3)?;
487        s.serialize_field("network", &self.resolved_network())?;
488        s.serialize_field("celo", &self.celo)?;
489        s.serialize_field("bypass_prevrandao", &self.bypass_prevrandao)?;
490        s.end()
491    }
492}
493
494impl NetworkConfigs {
495    /// Validates that all configured network selectors resolve to the same execution profile.
496    ///
497    /// Canonical and legacy selectors for the same family remain compatible. Selectors for
498    /// different families are rejected instead of being silently resolved by field priority.
499    pub fn validate(&self) -> Result<(), String> {
500        let mut selectors = Vec::new();
501        if let Some(network) = self.network {
502            selectors.push((network.name(), format!("network = \"{}\"", network.name())));
503        }
504        if self.celo {
505            selectors.push(("celo", "celo = true".to_string()));
506        }
507        #[cfg(feature = "optimism")]
508        if self.optimism {
509            selectors.push(("optimism", "optimism = true".to_string()));
510        }
511        if self.tempo {
512            selectors.push(("tempo", "tempo = true".to_string()));
513        }
514        #[cfg(feature = "monad")]
515        if self.monad {
516            selectors.push(("monad", "monad = true".to_string()));
517        }
518
519        if let Some((family, selector)) = selectors.first()
520            && let Some((_, conflicting)) =
521                selectors.iter().find(|(candidate, _)| candidate != family)
522        {
523            return Err(format!(
524                "network selectors `{selector}` and `{conflicting}` conflict; select only one \
525                 network"
526            ));
527        }
528
529        Ok(())
530    }
531
532    pub fn with_ethereum() -> Self {
533        Self { network: Some(NetworkVariant::Ethereum), ..Default::default() }
534    }
535
536    pub fn with_celo() -> Self {
537        Self { celo: true, ..Default::default() }
538    }
539
540    pub fn with_tempo() -> Self {
541        Self { network: Some(NetworkVariant::Tempo), tempo: true, ..Default::default() }
542    }
543
544    #[cfg(feature = "monad")]
545    pub fn with_monad() -> Self {
546        Self { network: Some(NetworkVariant::Monad), monad: true, ..Default::default() }
547    }
548
549    #[cfg(feature = "base")]
550    pub fn with_base() -> Self {
551        Self { network: Some(NetworkVariant::Base), ..Default::default() }
552    }
553
554    pub const fn is_tempo(&self) -> bool {
555        if let Some(network) = self.resolved_network() { network.is_tempo() } else { false }
556    }
557
558    /// Returns whether Optimism network features are enabled.
559    ///
560    /// Always returns `false` when built without the `optimism` feature.
561    pub const fn is_optimism(&self) -> bool {
562        if let Some(network) = self.resolved_network() { network.is_optimism() } else { false }
563    }
564
565    #[cfg(feature = "monad")]
566    pub const fn is_monad(&self) -> bool {
567        if let Some(network) = self.resolved_network() { network.is_monad() } else { false }
568    }
569
570    #[cfg(not(feature = "monad"))]
571    pub const fn is_monad(&self) -> bool {
572        false
573    }
574
575    #[cfg(feature = "base")]
576    pub const fn is_base(&self) -> bool {
577        matches!(self.resolved_network(), Some(NetworkVariant::Base))
578    }
579
580    /// Coerces `hardfork` into this network's family.
581    ///
582    /// Execution and fee rules apply the same lossy `From` conversions, so a cross-namespace
583    /// override such as `--hardfork prague` on a Tempo node runs as a Tempo hardfork. Callers that
584    /// need to describe what actually executed, like trace decoding, go through here rather than
585    /// carrying the configured value.
586    pub fn executed_hardfork(&self, hardfork: FoundryHardfork) -> FoundryHardfork {
587        if self.is_tempo() {
588            return TempoHardfork::from(hardfork).into();
589        }
590        #[cfg(feature = "monad")]
591        if self.is_monad() {
592            return MonadHardfork::from(hardfork).into();
593        }
594        #[cfg(feature = "base")]
595        if self.is_base() {
596            return BaseUpgrade::from(hardfork).into();
597        }
598        hardfork
599    }
600
601    pub const fn is_celo(&self) -> bool {
602        self.celo
603    }
604
605    /// Returns the resolved network variant, folding legacy flags.
606    pub const fn resolved_network(&self) -> Option<NetworkVariant> {
607        if let Some(n) = self.network {
608            return Some(n);
609        }
610        #[cfg(feature = "optimism")]
611        if self.optimism {
612            return Some(NetworkVariant::Optimism);
613        }
614        if self.tempo {
615            return Some(NetworkVariant::Tempo);
616        }
617        #[cfg(feature = "monad")]
618        if self.monad {
619            return Some(NetworkVariant::Monad);
620        }
621        None
622    }
623
624    /// Returns whether a network family was selected in this configuration.
625    pub const fn has_network_selection(&self) -> bool {
626        self.celo || self.resolved_network().is_some()
627    }
628
629    /// Returns the execution family represented by this configuration.
630    pub const fn execution_family_name(&self) -> &'static str {
631        self.execution_network().name()
632    }
633
634    /// Returns a label for the complete execution configuration.
635    pub const fn execution_profile_name(&self) -> &'static str {
636        if self.celo { "celo" } else { self.execution_family_name() }
637    }
638
639    /// Returns the concrete execution network, treating an unresolved configuration as Ethereum.
640    pub const fn execution_network(&self) -> NetworkVariant {
641        if let Some(network) = self.resolved_network() { network } else { NetworkVariant::Ethereum }
642    }
643
644    /// Returns whether both configurations can use the same instantiated EVM backend.
645    pub fn has_same_execution_profile(&self, other: &Self) -> bool {
646        self.celo == other.celo
647            && match (self.resolved_network(), other.resolved_network()) {
648                (Some(left), Some(right)) => left == right,
649                (Some(left), None) => left.is_ethereum(),
650                (None, Some(right)) => right.is_ethereum(),
651                (None, None) => true,
652            }
653    }
654
655    /// Returns whether this execution configuration can use `source` as a fork state source
656    /// without rebuilding the instantiated EVM.
657    ///
658    /// Monad uses a distinct EVM factory and instruction provider, so forks cannot cross the
659    /// Monad boundary. Base execution requires a Base source, while existing Ethereum, Optimism,
660    /// and Tempo execution can continue using Base as a state source without switching engines.
661    pub const fn supports_fork_source(&self, source: &Self) -> bool {
662        #[cfg(feature = "base")]
663        if self.is_base() && !source.is_base() {
664            return false;
665        }
666        self.is_monad() == source.is_monad()
667    }
668
669    /// Returns the name of the currently active non-Ethereum network, or `None` for plain Ethereum.
670    pub fn active_network_name(&self) -> Option<&'static str> {
671        self.resolved_network().and_then(|network| network.hardfork_namespace())
672    }
673
674    /// Returns the base fee parameters for the configured network.
675    ///
676    /// For OP Stack networks, returns Canyon parameters if the Canyon hardfork is active at the
677    /// given timestamp, otherwise returns pre-Canyon parameters.
678    pub fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams {
679        #[cfg(feature = "base")]
680        if self.is_base() {
681            let canyon_active =
682                BaseUpgrade::from_chain_and_timestamp(NamedChain::Base as u64, timestamp)
683                    .is_some_and(|upgrade| upgrade >= BaseUpgrade::Canyon);
684            return if canyon_active {
685                BaseFeeParams::new(250, 6)
686            } else {
687                BaseFeeParams::new(50, 6)
688            };
689        }
690        #[cfg(feature = "optimism")]
691        if self.is_optimism() {
692            return self.op_base_fee_params(timestamp);
693        }
694        BaseFeeParams::ethereum()
695    }
696
697    /// Calculates the blob excess gas inherited by the next block.
698    ///
699    /// OP Stack headers use the blob fields for protocol metadata rather than EIP-4844 blobs, so
700    /// their excess blob gas remains zero. Other execution profiles use the configured Ethereum
701    /// blob schedule.
702    pub fn next_block_blob_excess_gas(
703        &self,
704        blob_params: BlobParams,
705        parent_excess_blob_gas: u64,
706        parent_blob_gas_used: u64,
707        parent_base_fee: u64,
708    ) -> u64 {
709        #[cfg(feature = "base")]
710        if self.is_base() {
711            return 0;
712        }
713        if self.is_optimism() {
714            return 0;
715        }
716        blob_params.next_block_excess_blob_gas_osaka(
717            parent_excess_blob_gas,
718            parent_blob_gas_used,
719            parent_base_fee,
720        )
721    }
722
723    /// Returns contract size limits for networks that override Ethereum defaults.
724    #[cfg(feature = "monad")]
725    pub fn contract_size_limits(&self) -> Option<NetworkContractSizeLimits> {
726        self.is_monad().then_some(NetworkContractSizeLimits {
727            runtime: monad_revm::MONAD_MAX_CODE_SIZE,
728            initcode: monad_revm::MONAD_MAX_INITCODE_SIZE,
729        })
730    }
731
732    /// Returns contract size limits for networks that override Ethereum defaults.
733    #[cfg(not(feature = "monad"))]
734    pub const fn contract_size_limits(&self) -> Option<NetworkContractSizeLimits> {
735        None
736    }
737
738    pub fn bypass_prevrandao(&self, chain_id: u64) -> bool {
739        if let Ok(
740            Moonbeam | Moonbase | Moonriver | MoonbeamDev | Rsk | RskTestnet | Gnosis | Chiado,
741        ) = NamedChain::try_from(chain_id)
742        {
743            return true;
744        }
745        self.bypass_prevrandao
746    }
747
748    /// Infers an execution profile from `chain_id` unless one was selected explicitly.
749    ///
750    /// Unknown chain IDs preserve the unresolved configuration so endpoint metadata can still
751    /// identify their execution profile. A known chain whose feature is unavailable is rejected
752    /// instead of silently falling back to Ethereum semantics.
753    pub fn try_with_chain_id(self, chain_id: u64) -> Result<Self, String> {
754        if self.has_network_selection() {
755            return Ok(self);
756        }
757
758        match NetworkVariant::from_known_chain_id(chain_id).map_err(|error| {
759            format!("cannot infer execution network from chain ID {chain_id}: {error}")
760        })? {
761            Some(network) => Ok(self.with_rpc_identity(network, chain_id)),
762            None => Ok(self),
763        }
764    }
765
766    /// Best-effort execution-profile inference for trusted, programmatic chain IDs.
767    ///
768    /// User-provided chain IDs must be handled with [`Self::try_with_chain_id`] so an unavailable
769    /// feature cannot silently select Ethereum semantics.
770    pub fn with_chain_id(self, chain_id: u64) -> Self {
771        self.try_with_chain_id(chain_id).unwrap_or(self)
772    }
773
774    /// Applies an RPC endpoint's resolved EVM family to this configuration.
775    ///
776    /// Successful endpoint metadata is authoritative, including when a local Anvil uses a
777    /// well-known chain ID as an execution override. Orthogonal settings are preserved.
778    pub fn with_rpc_network(self, network: NetworkVariant) -> Self {
779        let mut resolved = if network.is_ethereum() { Self::default() } else { network.into() };
780        resolved.bypass_prevrandao = self.bypass_prevrandao;
781        resolved
782    }
783
784    /// Applies an RPC endpoint's execution family and chain-specific Ethereum configuration.
785    ///
786    /// The reported family is authoritative. A known Celo chain ID additionally enables Celo's
787    /// precompiles because Celo shares the Ethereum EVM factory rather than having its own
788    /// [`NetworkVariant`].
789    pub fn with_rpc_identity(self, network: NetworkVariant, chain_id: ChainId) -> Self {
790        let mut resolved = self.with_rpc_network(network);
791        if network.is_ethereum()
792            && matches!(
793                Chain::from_id(chain_id).named(),
794                Some(NamedChain::Celo | NamedChain::CeloSepolia)
795            )
796        {
797            resolved.celo = true;
798        }
799        resolved
800    }
801
802    /// Returns the canonical endpoint-visible execution profile.
803    pub fn canonical_execution_profile(self) -> Self {
804        if self.is_celo() {
805            Self::with_celo()
806        } else if let Some(network) = self.resolved_network()
807            && !network.is_ethereum()
808        {
809            network.into()
810        } else {
811            Self::default()
812        }
813    }
814
815    /// Applies an authoritative execution profile while preserving orthogonal settings.
816    pub fn with_execution_profile(self, profile: Self) -> Self {
817        let mut resolved = if profile.is_celo() {
818            Self::with_celo()
819        } else {
820            profile.resolved_network().map(Into::into).unwrap_or_default()
821        };
822        resolved.bypass_prevrandao = self.bypass_prevrandao;
823        resolved
824    }
825
826    /// Applies an endpoint-reported execution profile while preserving orthogonal settings.
827    pub fn with_rpc_profile(self, profile: Self) -> Self {
828        self.with_execution_profile(profile.canonical_execution_profile())
829    }
830
831    /// Parses the execution profile reported by `anvil_nodeInfo`.
832    pub fn from_node_info_profile(profile: &str) -> Result<Self, String> {
833        if profile == "celo" {
834            return Ok(Self::with_celo());
835        }
836        let network = NetworkVariant::from_node_info_name(profile)?;
837        Ok(if network.is_ethereum() { Self::default() } else { network.into() })
838    }
839
840    /// Resolves an RPC endpoint's complete execution profile.
841    ///
842    /// Explicit metadata is authoritative. When metadata is absent or omits the profile, a
843    /// caller-supplied explicit profile takes precedence over chain-ID inference. Otherwise,
844    /// legacy Anvil responses and ordinary RPC endpoints recover Celo from a canonical Celo chain
845    /// ID and preserve the historical behavior for custom chain IDs.
846    pub fn from_rpc_identity_profile_with_fallback(
847        chain_id: ChainId,
848        node_info_profile: Option<Option<&str>>,
849        unknown_fallback: Option<Self>,
850    ) -> Result<Option<Self>, String> {
851        let known_profile = || {
852            NetworkVariant::from_known_chain_id(chain_id).map(|network| {
853                network.map(|network| Self::default().with_rpc_identity(network, chain_id))
854            })
855        };
856        let fallback_is_explicit =
857            unknown_fallback.is_some_and(|fallback| fallback.has_network_selection());
858        let fallback = unknown_fallback.map(Self::canonical_execution_profile);
859        if let Some(Some(profile)) = node_info_profile {
860            return Self::from_node_info_profile(profile).map(Some);
861        }
862        if fallback_is_explicit && let Some(fallback) = fallback {
863            return Ok(Some(fallback));
864        }
865        match node_info_profile {
866            Some(None) => Ok(known_profile()?.or(fallback).or(Some(Self::default()))),
867            None => Ok(known_profile()?.or(fallback)),
868            Some(Some(_)) => unreachable!(),
869        }
870    }
871
872    /// Validates `hardfork` against the current `NetworkConfigs` and, if consistent, returns an
873    /// updated instance with the network implied by the enabled hardfork.
874    ///
875    /// Returns `Err` when the hardfork's network family conflicts with the configured one.
876    pub fn normalize_for_hardfork(self, hardfork: FoundryHardfork) -> Result<Self, String> {
877        if self.has_network_selection() {
878            let configured = self.execution_network();
879            if configured.hardfork_namespace() != hardfork.namespace() {
880                return Err(format!(
881                    "hardfork `{}` conflicts with network config `{}`",
882                    String::from(hardfork),
883                    self.execution_profile_name(),
884                ));
885            }
886        }
887
888        let network = match hardfork {
889            FoundryHardfork::Ethereum(_) => self,
890            FoundryHardfork::Tempo(_) => Self::with_tempo(),
891            #[cfg(feature = "base")]
892            FoundryHardfork::Base(_) => Self::with_base(),
893            #[cfg(feature = "optimism")]
894            FoundryHardfork::Optimism(_) => Self::with_optimism(),
895            #[cfg(feature = "monad")]
896            FoundryHardfork::Monad(_) => Self::with_monad(),
897        };
898
899        Ok(network)
900    }
901
902    /// Inject precompiles for configured networks.
903    pub fn inject_precompiles(self, precompiles: &mut PrecompilesMap) {
904        if self.is_celo() {
905            precompiles.apply_precompile(&CELO_TRANSFER_ADDRESS, move |_| {
906                Some(celo::transfer::precompile())
907            });
908        }
909    }
910
911    /// Returns precompile labels for configured networks at the given hardfork, to be used in
912    /// traces.
913    pub fn precompiles_label(self, hardfork: Option<FoundryHardfork>) -> AddressHashMap<String> {
914        let mut labels = AddressHashMap::default();
915        if self.is_celo() {
916            labels.insert(CELO_TRANSFER_ADDRESS, CELO_TRANSFER_LABEL.to_string());
917        }
918        if self.is_tempo() {
919            let tempo_hardfork = hardfork.and_then(TempoHardfork::from_foundry_hardfork);
920            labels.extend(
921                TEMPO_PRECOMPILES
922                    .iter()
923                    .copied()
924                    .filter(|(_, address)| {
925                        tempo_hardfork.is_none_or(|hardfork| {
926                            is_tempo_precompile_active_at(*address, hardfork)
927                        })
928                    })
929                    .map(|(label, address)| (address, label.to_string())),
930            );
931        }
932        #[cfg(feature = "monad")]
933        if self.is_monad() {
934            let monad_hardfork = hardfork.and_then(MonadHardfork::from_foundry_hardfork);
935            labels.extend(
936                MONAD_PRECOMPILE_LABELS
937                    .iter()
938                    .copied()
939                    .filter(|(_, address)| {
940                        monad_hardfork.is_none_or(|hardfork| {
941                            is_monad_precompile_active_at(*address, hardfork)
942                        })
943                    })
944                    .map(|(label, address)| (address, label.to_string())),
945            );
946        }
947        #[cfg(feature = "base")]
948        if self.is_base() {
949            let base_upgrade = hardfork.and_then(|hardfork| match hardfork {
950                FoundryHardfork::Base(upgrade) => Some(upgrade),
951                _ => None,
952            });
953            labels.extend(
954                BASE_PRECOMPILES
955                    .iter()
956                    .copied()
957                    .filter(|(_, address)| {
958                        base_upgrade
959                            .is_none_or(|upgrade| is_base_precompile_active_at(*address, upgrade))
960                    })
961                    .map(|(label, address)| (address, label.to_string())),
962            );
963        }
964        labels
965    }
966
967    /// Returns precompiles for configured networks at the given hardfork.
968    pub fn precompiles(self, hardfork: Option<FoundryHardfork>) -> BTreeMap<String, Address> {
969        let mut precompiles = BTreeMap::new();
970        if self.is_celo() {
971            precompiles
972                .insert(PRECOMPILE_ID_CELO_TRANSFER.name().to_string(), CELO_TRANSFER_ADDRESS);
973        }
974        if self.is_tempo() {
975            let tempo_hardfork = hardfork.and_then(TempoHardfork::from_foundry_hardfork);
976            precompiles.extend(
977                TEMPO_PRECOMPILES
978                    .iter()
979                    .copied()
980                    .filter(|(_, address)| {
981                        tempo_hardfork.is_none_or(|hardfork| {
982                            is_tempo_precompile_active_at(*address, hardfork)
983                        })
984                    })
985                    .map(|(label, address)| (label.to_string(), address)),
986            );
987        }
988        #[cfg(feature = "monad")]
989        if self.is_monad() {
990            let monad_hardfork = hardfork.and_then(MonadHardfork::from_foundry_hardfork);
991            precompiles.extend(
992                MONAD_PRECOMPILES
993                    .iter()
994                    .copied()
995                    .filter(|(_, address)| {
996                        monad_hardfork.is_none_or(|hardfork| {
997                            is_monad_precompile_active_at(*address, hardfork)
998                        })
999                    })
1000                    .map(|(label, address)| (label.to_string(), address)),
1001            );
1002        }
1003        #[cfg(feature = "base")]
1004        if self.is_base() {
1005            let base_upgrade = hardfork.and_then(|hardfork| match hardfork {
1006                FoundryHardfork::Base(upgrade) => Some(upgrade),
1007                _ => None,
1008            });
1009            precompiles.extend(
1010                BASE_PRECOMPILES
1011                    .iter()
1012                    .copied()
1013                    .filter(|(_, address)| {
1014                        base_upgrade
1015                            .is_none_or(|upgrade| is_base_precompile_active_at(*address, upgrade))
1016                    })
1017                    .map(|(label, address)| (label.to_string(), address)),
1018            );
1019        }
1020        precompiles
1021    }
1022}
1023
1024/// Applies the BSC P256 precompile active at the given timestamp.
1025pub fn apply_bsc_p256_precompile(
1026    precompiles: &mut PrecompilesMap,
1027    chain_id: ChainId,
1028    timestamp: u64,
1029) {
1030    let Some(p256verify) = bsc_p256_precompile(chain_id, timestamp) else { return };
1031    precompiles.apply_precompile(&BSC_P256_ADDRESS, move |_| {
1032        p256verify.map(|p256verify| {
1033            DynPrecompile::new(p256verify.id().clone(), move |input| {
1034                p256verify.execute(input.data, input.gas, input.reservoir)
1035            })
1036        })
1037    });
1038}
1039
1040impl From<NetworkVariant> for NetworkConfigs {
1041    fn from(network: NetworkVariant) -> Self {
1042        match network {
1043            NetworkVariant::Ethereum => Self::with_ethereum(),
1044            NetworkVariant::Tempo => {
1045                Self { network: Some(network), tempo: true, ..Default::default() }
1046            }
1047            #[cfg(feature = "monad")]
1048            NetworkVariant::Monad => {
1049                Self { network: Some(network), monad: true, ..Default::default() }
1050            }
1051            #[cfg(feature = "base")]
1052            NetworkVariant::Base => Self { network: Some(network), ..Default::default() },
1053            #[cfg(feature = "optimism")]
1054            NetworkVariant::Optimism => {
1055                Self { network: Some(network), optimism: true, ..Default::default() }
1056            }
1057        }
1058    }
1059}
1060
1061/// Returns the BSC P256 precompile for the given timestamp. The outer option distinguishes BSC
1062/// chains from unrelated chains, while the inner option disables P256 before Haber.
1063const fn bsc_p256_precompile(chain_id: ChainId, timestamp: u64) -> Option<Option<RevmPrecompile>> {
1064    let (haber_timestamp, osaka_timestamp) = match chain_id {
1065        BSC_MAINNET_CHAIN_ID => (BSC_MAINNET_HABER_TIMESTAMP, BSC_MAINNET_OSAKA_TIMESTAMP),
1066        BSC_TESTNET_CHAIN_ID => (BSC_TESTNET_HABER_TIMESTAMP, BSC_TESTNET_OSAKA_TIMESTAMP),
1067        _ => return None,
1068    };
1069
1070    if timestamp < haber_timestamp {
1071        Some(None)
1072    } else if timestamp < osaka_timestamp {
1073        Some(Some(P256VERIFY))
1074    } else {
1075        Some(Some(P256VERIFY_OSAKA))
1076    }
1077}
1078
1079/// Returns custom precompile labels for an already resolved execution hardfork.
1080/// This metadata lookup does not discover or select an execution network.
1081pub fn resolved_precompile_labels(hardfork: Option<FoundryHardfork>) -> AddressHashMap<String> {
1082    match hardfork {
1083        Some(FoundryHardfork::Tempo(hardfork)) => TEMPO_PRECOMPILES
1084            .iter()
1085            .filter(|(_, address)| is_tempo_precompile_active_at(*address, hardfork))
1086            .map(|(label, address)| (*address, (*label).to_string()))
1087            .collect(),
1088        #[cfg(feature = "monad")]
1089        Some(FoundryHardfork::Monad(hardfork)) => MONAD_PRECOMPILE_LABELS
1090            .iter()
1091            .filter(|(_, address)| is_monad_precompile_active_at(*address, hardfork))
1092            .map(|(label, address)| (*address, (*label).to_string()))
1093            .collect(),
1094        #[cfg(feature = "base")]
1095        Some(FoundryHardfork::Base(upgrade)) => BASE_PRECOMPILES
1096            .iter()
1097            .filter(|(_, address)| is_base_precompile_active_at(*address, upgrade))
1098            .map(|(label, address)| (*address, (*label).to_string()))
1099            .collect(),
1100        _ => AddressHashMap::default(),
1101    }
1102}
1103
1104/// Returns whether a well-known Tempo precompile address is active at `hardfork`.
1105pub fn is_tempo_precompile_active_at(address: Address, hardfork: TempoHardfork) -> bool {
1106    if address == CURRENT_COMMITTEE_ADDRESS {
1107        hardfork.is_t8()
1108    } else if address == TIP20_CHANNEL_RESERVE_ADDRESS {
1109        hardfork.is_t5()
1110    } else if address == RECEIVE_POLICY_GUARD_ADDRESS {
1111        hardfork.is_t6()
1112    } else if address == STORAGE_CREDITS_ADDRESS {
1113        hardfork.is_t7()
1114    } else if address == ADDRESS_REGISTRY_ADDRESS || address == SIGNATURE_VERIFIER_ADDRESS {
1115        hardfork.is_t3()
1116    } else {
1117        true
1118    }
1119}
1120
1121/// Returns the well-known Tempo precompile addresses active at `hardfork`.
1122pub fn active_tempo_precompile_addresses(hardfork: TempoHardfork) -> impl Iterator<Item = Address> {
1123    TEMPO_PRECOMPILE_ADDRESSES
1124        .iter()
1125        .copied()
1126        .filter(move |&address| is_tempo_precompile_active_at(address, hardfork))
1127}
1128
1129/// Returns whether a well-known Monad precompile address is active at `hardfork`.
1130#[cfg(feature = "monad")]
1131pub fn is_monad_precompile_active_at(address: Address, hardfork: MonadHardfork) -> bool {
1132    address == monad_revm::staking::STAKING_ADDRESS
1133        || (address == monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS
1134            && MonadHardfork::MonadNine.is_enabled_in(hardfork))
1135}
1136
1137/// Returns whether a fixed Base precompile is active at `upgrade`.
1138#[cfg(feature = "base")]
1139pub fn is_base_precompile_active_at(address: Address, upgrade: BaseUpgrade) -> bool {
1140    if matches!(address, TxContextStorage::ADDRESS | NonceManagerStorage::ADDRESS) {
1141        upgrade >= BaseUpgrade::Cobalt
1142    } else if matches!(
1143        address,
1144        B20FactoryStorage::ADDRESS
1145            | ActivationRegistryStorage::ADDRESS
1146            | PolicyRegistryStorage::ADDRESS
1147    ) {
1148        upgrade >= BaseUpgrade::Beryl
1149    } else {
1150        false
1151    }
1152}
1153
1154/// Returns the fixed Base precompiles active at `upgrade`.
1155#[cfg(feature = "base")]
1156pub fn active_base_precompiles(
1157    upgrade: BaseUpgrade,
1158) -> impl Iterator<Item = (&'static str, Address)> {
1159    BASE_PRECOMPILES
1160        .iter()
1161        .copied()
1162        .filter(move |(_, address)| is_base_precompile_active_at(*address, upgrade))
1163}
1164
1165#[cfg(test)]
1166mod tests {
1167    use super::*;
1168    use revm::precompile::{
1169        Precompiles,
1170        secp256r1::{P256VERIFY_BASE_GAS_FEE, P256VERIFY_BASE_GAS_FEE_OSAKA},
1171    };
1172
1173    // --- Equivalence: new flag == legacy flag ---
1174
1175    #[test]
1176    fn network_variant_predicates() {
1177        assert!(NetworkVariant::Ethereum.is_ethereum());
1178        assert!(!NetworkVariant::Ethereum.is_optimism());
1179        assert!(!NetworkVariant::Ethereum.is_tempo());
1180        assert!(NetworkVariant::Tempo.is_tempo());
1181        assert!(!NetworkVariant::Tempo.is_ethereum());
1182        assert!(!NetworkVariant::Tempo.is_optimism());
1183
1184        #[cfg(feature = "monad")]
1185        {
1186            assert!(!NetworkVariant::Ethereum.is_monad());
1187            assert!(!NetworkVariant::Tempo.is_monad());
1188            assert!(NetworkVariant::Monad.is_monad());
1189            assert!(!NetworkVariant::Monad.is_ethereum());
1190            assert!(!NetworkVariant::Monad.is_optimism());
1191            assert!(!NetworkVariant::Monad.is_tempo());
1192        }
1193
1194        #[cfg(feature = "optimism")]
1195        {
1196            assert!(NetworkVariant::Optimism.is_optimism());
1197            assert!(!NetworkVariant::Optimism.is_ethereum());
1198            assert!(!NetworkVariant::Optimism.is_tempo());
1199
1200            #[cfg(feature = "monad")]
1201            assert!(!NetworkVariant::Optimism.is_monad());
1202        }
1203
1204        #[cfg(feature = "base")]
1205        {
1206            assert!(NetworkVariant::Base.is_base());
1207            assert!(!NetworkVariant::Base.is_ethereum());
1208            assert!(!NetworkVariant::Base.is_optimism());
1209            assert!(!NetworkVariant::Base.is_tempo());
1210        }
1211    }
1212
1213    #[test]
1214    #[cfg(all(feature = "base", feature = "optimism"))]
1215    fn historical_hardfork_uses_selected_execution_family() {
1216        let timestamp = 1_710_374_401;
1217
1218        assert_eq!(
1219            NetworkVariant::Base.historical_hardfork(NamedChain::Base as u64, timestamp),
1220            Some(BaseUpgrade::Ecotone.into())
1221        );
1222        assert_eq!(
1223            NetworkVariant::Optimism.historical_hardfork(NamedChain::Base as u64, timestamp),
1224            Some(OpHardfork::Ecotone.into())
1225        );
1226        assert_eq!(
1227            NetworkVariant::Ethereum.historical_hardfork(NamedChain::Base as u64, timestamp),
1228            None
1229        );
1230        assert_eq!(
1231            NetworkVariant::Tempo.historical_hardfork(NamedChain::Base as u64, timestamp),
1232            None
1233        );
1234    }
1235
1236    #[test]
1237    #[cfg(feature = "monad")]
1238    fn fork_sources_only_isolate_monad() {
1239        let mut non_monad = vec![
1240            NetworkConfigs::default(),
1241            NetworkConfigs::with_ethereum(),
1242            NetworkConfigs::with_celo(),
1243            NetworkConfigs::with_tempo(),
1244        ];
1245        #[cfg(feature = "optimism")]
1246        non_monad.push(NetworkConfigs::with_optimism());
1247
1248        for execution in &non_monad {
1249            for source in &non_monad {
1250                assert!(execution.supports_fork_source(source));
1251            }
1252            assert!(!execution.supports_fork_source(&NetworkConfigs::with_monad()));
1253            assert!(!NetworkConfigs::with_monad().supports_fork_source(execution));
1254        }
1255        assert!(NetworkConfigs::with_monad().supports_fork_source(&NetworkConfigs::with_monad()));
1256    }
1257
1258    #[test]
1259    #[cfg(feature = "base")]
1260    fn fork_sources_preserve_base_state_source_compatibility() {
1261        #[cfg_attr(not(feature = "optimism"), allow(unused_mut))]
1262        let mut non_base = vec![
1263            NetworkConfigs::default(),
1264            NetworkConfigs::with_ethereum(),
1265            NetworkConfigs::with_celo(),
1266            NetworkConfigs::with_tempo(),
1267        ];
1268        #[cfg(feature = "optimism")]
1269        non_base.push(NetworkConfigs::with_optimism());
1270        for execution in &non_base {
1271            assert!(execution.supports_fork_source(&NetworkConfigs::with_base()));
1272            assert!(!NetworkConfigs::with_base().supports_fork_source(execution));
1273        }
1274        assert!(NetworkConfigs::with_base().supports_fork_source(&NetworkConfigs::with_base()));
1275
1276        #[cfg(feature = "monad")]
1277        {
1278            assert!(
1279                !NetworkConfigs::with_monad().supports_fork_source(&NetworkConfigs::with_base())
1280            );
1281            assert!(
1282                !NetworkConfigs::with_base().supports_fork_source(&NetworkConfigs::with_monad())
1283            );
1284        }
1285    }
1286
1287    #[test]
1288    fn known_chain_identity_does_not_guess_unknown_networks() {
1289        assert_eq!(NetworkVariant::from_known_chain_id(98_765_432).unwrap(), None);
1290        assert_eq!(
1291            NetworkVariant::from_known_chain_id(NamedChain::Mainnet as u64).unwrap(),
1292            Some(NetworkVariant::Ethereum)
1293        );
1294        assert_eq!(NetworkVariant::from(98_765_432), NetworkVariant::Ethereum);
1295    }
1296
1297    #[test]
1298    fn fallible_chain_id_inference_preserves_unknown_chains() {
1299        let networks = NetworkConfigs { bypass_prevrandao: true, ..Default::default() };
1300        assert_eq!(networks.try_with_chain_id(98_765_432).unwrap(), networks);
1301    }
1302
1303    #[test]
1304    #[cfg(not(feature = "monad"))]
1305    fn fallible_chain_id_inference_rejects_disabled_monad() {
1306        for chain_id in [NamedChain::Monad as u64, NamedChain::MonadTestnet as u64] {
1307            let unavailable = "network family `monad` is not enabled in this build";
1308            let expected =
1309                format!("cannot infer execution network from chain ID {chain_id}: {unavailable}");
1310            assert_eq!(
1311                NetworkConfigs::default().try_with_chain_id(chain_id).unwrap_err(),
1312                expected
1313            );
1314        }
1315    }
1316
1317    #[test]
1318    #[cfg(not(feature = "optimism"))]
1319    fn fallible_chain_id_inference_rejects_disabled_optimism() {
1320        let chain_id = NamedChain::Optimism as u64;
1321        assert_eq!(
1322            NetworkConfigs::default().try_with_chain_id(chain_id).unwrap_err(),
1323            format!(
1324                "cannot infer execution network from chain ID {chain_id}: network family \
1325                 `optimism` is not enabled in this build"
1326            )
1327        );
1328    }
1329
1330    #[test]
1331    fn explicit_ethereum_overrides_known_chain_inference() {
1332        let ethereum = NetworkConfigs::with_ethereum();
1333        for chain_id in [NamedChain::Monad as u64, NamedChain::Optimism as u64] {
1334            assert_eq!(ethereum.try_with_chain_id(chain_id).unwrap(), ethereum);
1335        }
1336    }
1337
1338    #[test]
1339    fn fallible_chain_id_inference_preserves_orthogonal_configuration() {
1340        let networks = NetworkConfigs { bypass_prevrandao: true, ..Default::default() };
1341        let inferred = networks.try_with_chain_id(NamedChain::Tempo as u64).unwrap();
1342
1343        assert!(inferred.is_tempo());
1344        assert!(inferred.bypass_prevrandao(NamedChain::Mainnet as u64));
1345    }
1346
1347    #[test]
1348    #[cfg(feature = "optimism")]
1349    fn fallible_chain_id_inference_detects_optimism() {
1350        let chain_id = NamedChain::Optimism as u64;
1351        assert_eq!(NetworkVariant::from(chain_id), NetworkVariant::Optimism);
1352        assert!(NetworkConfigs::default().try_with_chain_id(chain_id).unwrap().is_optimism());
1353    }
1354
1355    #[test]
1356    fn rpc_identity_preserves_orthogonal_configuration() {
1357        let base = NetworkConfigs { bypass_prevrandao: true, ..NetworkConfigs::default() };
1358        let ethereum = base.with_rpc_network(NetworkVariant::Ethereum);
1359        assert!(ethereum.bypass_prevrandao(NamedChain::Mainnet as u64));
1360
1361        let ethereum = NetworkConfigs::default().with_rpc_network(NetworkVariant::Ethereum);
1362        assert_eq!(ethereum, NetworkConfigs::default());
1363
1364        assert!(
1365            NetworkConfigs::default()
1366                .with_rpc_identity(NetworkVariant::Ethereum, NamedChain::Celo as u64)
1367                .is_celo()
1368        );
1369        assert!(
1370            !NetworkConfigs::default()
1371                .with_rpc_identity(NetworkVariant::Ethereum, NamedChain::Monad as u64)
1372                .is_monad()
1373        );
1374    }
1375
1376    #[test]
1377    fn rpc_metadata_overrides_known_execution_chain_identity() {
1378        assert_eq!(
1379            NetworkVariant::from_rpc_identity(NamedChain::Mainnet as u64, Some(Some("tempo")))
1380                .unwrap(),
1381            Some(NetworkVariant::Tempo)
1382        );
1383        assert_eq!(
1384            NetworkVariant::from_rpc_identity(NamedChain::Tempo as u64, Some(None)).unwrap(),
1385            Some(NetworkVariant::Tempo)
1386        );
1387        assert_eq!(NetworkVariant::from_rpc_identity(98_765_432, None).unwrap(), None);
1388        assert_eq!(
1389            NetworkVariant::from_rpc_identity(98_765_432, Some(None)).unwrap(),
1390            Some(NetworkVariant::Ethereum)
1391        );
1392        #[cfg(feature = "optimism")]
1393        assert_eq!(
1394            NetworkVariant::from_rpc_identity(NamedChain::Optimism as u64, Some(None)).unwrap(),
1395            Some(NetworkVariant::Optimism)
1396        );
1397        assert_eq!(
1398            NetworkVariant::from_rpc_identity(NamedChain::Celo as u64, Some(None)).unwrap(),
1399            Some(NetworkVariant::Ethereum)
1400        );
1401    }
1402
1403    #[test]
1404    fn rpc_profile_distinguishes_celo_from_ethereum_factory() {
1405        let custom_chain_id = 98_765_432;
1406        assert!(
1407            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1408                custom_chain_id,
1409                Some(Some("celo")),
1410                None,
1411            )
1412            .unwrap()
1413            .unwrap()
1414            .is_celo()
1415        );
1416        assert!(
1417            !NetworkConfigs::from_rpc_identity_profile_with_fallback(
1418                NamedChain::Celo as u64,
1419                Some(Some("ethereum")),
1420                None,
1421            )
1422            .unwrap()
1423            .unwrap()
1424            .is_celo()
1425        );
1426        assert!(
1427            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1428                NamedChain::Celo as u64,
1429                Some(None),
1430                None,
1431            )
1432            .unwrap()
1433            .unwrap()
1434            .is_celo()
1435        );
1436        assert!(
1437            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1438                custom_chain_id,
1439                None,
1440                Some(NetworkConfigs::with_celo()),
1441            )
1442            .unwrap()
1443            .unwrap()
1444            .is_celo()
1445        );
1446    }
1447
1448    #[test]
1449    fn rpc_profile_keeps_moonbeam_on_default_ethereum() {
1450        let profile = NetworkConfigs::from_rpc_identity_profile_with_fallback(
1451            NamedChain::Moonbeam as u64,
1452            None,
1453            None,
1454        )
1455        .unwrap()
1456        .unwrap();
1457
1458        assert_eq!(profile, NetworkConfigs::default());
1459        assert!(profile.bypass_prevrandao(NamedChain::Moonbeam as u64));
1460    }
1461
1462    #[test]
1463    fn default_fallback_still_infers_known_execution_profile() {
1464        for node_info in [None, Some(None)] {
1465            assert_eq!(
1466                NetworkConfigs::from_rpc_identity_profile_with_fallback(
1467                    NamedChain::Tempo as u64,
1468                    node_info,
1469                    Some(NetworkConfigs::default()),
1470                )
1471                .unwrap(),
1472                Some(NetworkConfigs::with_tempo())
1473            );
1474        }
1475    }
1476
1477    #[test]
1478    fn explicit_ethereum_overrides_known_execution_profile() {
1479        for node_info in [None, Some(None)] {
1480            assert_eq!(
1481                NetworkConfigs::from_rpc_identity_profile_with_fallback(
1482                    NamedChain::Tempo as u64,
1483                    node_info,
1484                    Some(NetworkConfigs::with_ethereum()),
1485                )
1486                .unwrap(),
1487                Some(NetworkConfigs::default())
1488            );
1489        }
1490    }
1491
1492    #[test]
1493    #[cfg(not(feature = "monad"))]
1494    fn explicit_ethereum_overrides_disabled_monad_without_node_info() {
1495        assert_eq!(
1496            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1497                NamedChain::Monad as u64,
1498                None,
1499                Some(NetworkConfigs::with_ethereum()),
1500            )
1501            .unwrap(),
1502            Some(NetworkConfigs::default())
1503        );
1504    }
1505
1506    #[test]
1507    #[cfg(not(feature = "monad"))]
1508    fn explicit_ethereum_overrides_disabled_monad_with_legacy_node_info() {
1509        assert_eq!(
1510            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1511                NamedChain::Monad as u64,
1512                Some(None),
1513                Some(NetworkConfigs::with_ethereum()),
1514            )
1515            .unwrap(),
1516            Some(NetworkConfigs::default())
1517        );
1518    }
1519
1520    #[test]
1521    #[cfg(not(feature = "monad"))]
1522    fn disabled_monad_without_explicit_profile_still_errors() {
1523        for node_info in [None, Some(None)] {
1524            assert_eq!(
1525                NetworkConfigs::from_rpc_identity_profile_with_fallback(
1526                    NamedChain::Monad as u64,
1527                    node_info,
1528                    None,
1529                )
1530                .unwrap_err(),
1531                "network family `monad` is not enabled in this build"
1532            );
1533        }
1534    }
1535
1536    #[test]
1537    #[cfg(feature = "monad")]
1538    fn rpc_identity_fallback_preserves_explicit_custom_networks() {
1539        let custom_chain_id = 98_765_432;
1540        for node_info in [None, Some(None)] {
1541            assert_eq!(
1542                NetworkVariant::from_rpc_identity_with_fallback(
1543                    custom_chain_id,
1544                    node_info,
1545                    Some(NetworkVariant::Monad),
1546                )
1547                .unwrap(),
1548                Some(NetworkVariant::Monad)
1549            );
1550        }
1551
1552        assert_eq!(
1553            NetworkVariant::from_rpc_identity_with_fallback(
1554                NamedChain::Mainnet as u64,
1555                Some(None),
1556                Some(NetworkVariant::Monad),
1557            )
1558            .unwrap(),
1559            Some(NetworkVariant::Ethereum)
1560        );
1561        assert_eq!(
1562            NetworkVariant::from_rpc_identity_with_fallback(
1563                custom_chain_id,
1564                Some(Some("tempo")),
1565                Some(NetworkVariant::Monad),
1566            )
1567            .unwrap(),
1568            Some(NetworkVariant::Tempo)
1569        );
1570    }
1571
1572    #[test]
1573    fn network_selection_distinguishes_default_and_explicit_ethereum() {
1574        assert!(!NetworkConfigs::default().has_network_selection());
1575        assert!(NetworkConfigs::with_ethereum().has_network_selection());
1576        assert!(NetworkConfigs::with_celo().has_network_selection());
1577    }
1578
1579    #[test]
1580    fn celo_uses_ethereum_factory_with_distinct_precompiles() {
1581        assert!(
1582            !NetworkConfigs::with_celo().has_same_execution_profile(&NetworkConfigs::default())
1583        );
1584        assert!(
1585            NetworkConfigs::with_ethereum().has_same_execution_profile(&NetworkConfigs::default())
1586        );
1587        assert_eq!(NetworkConfigs::with_celo().execution_family_name(), "ethereum");
1588        assert_eq!(NetworkConfigs::with_celo().execution_profile_name(), "celo");
1589    }
1590
1591    #[test]
1592    fn authoritative_execution_profile_preserves_orthogonal_settings() {
1593        let inline = NetworkConfigs { bypass_prevrandao: true, ..NetworkConfigs::with_tempo() };
1594
1595        #[cfg_attr(not(any(feature = "optimism", feature = "monad")), allow(unused_mut))]
1596        let mut profiles = vec![
1597            NetworkConfigs::with_ethereum(),
1598            NetworkConfigs::with_tempo(),
1599            NetworkConfigs::with_celo(),
1600        ];
1601        #[cfg(feature = "optimism")]
1602        profiles.push(NetworkVariant::Optimism.into());
1603        #[cfg(feature = "monad")]
1604        profiles.push(NetworkConfigs::with_monad());
1605
1606        for profile in profiles {
1607            let resolved = inline.with_execution_profile(profile);
1608            assert!(resolved.has_same_execution_profile(&profile));
1609            assert!(resolved.has_network_selection());
1610            assert!(resolved.bypass_prevrandao(NamedChain::Mainnet as u64));
1611        }
1612
1613        let ethereum = inline.with_execution_profile(NetworkConfigs::with_ethereum());
1614        assert_eq!(
1615            ethereum.try_with_chain_id(NamedChain::Tempo as u64).unwrap(),
1616            ethereum,
1617            "an authoritative Ethereum profile must prevent later endpoint inference",
1618        );
1619
1620        let rpc_ethereum = inline.with_rpc_profile(NetworkConfigs::with_ethereum());
1621        assert!(!rpc_ethereum.has_network_selection());
1622    }
1623
1624    #[test]
1625    fn chain_id_inference_preserves_explicit_networks() {
1626        assert!(
1627            NetworkConfigs::default().try_with_chain_id(NamedChain::Celo as u64).unwrap().is_celo()
1628        );
1629        let celo = NetworkConfigs { bypass_prevrandao: true, ..Default::default() }
1630            .try_with_chain_id(NamedChain::Celo as u64)
1631            .unwrap();
1632        assert!(celo.is_celo());
1633        assert!(celo.bypass_prevrandao(NamedChain::Mainnet as u64));
1634
1635        let explicit = [
1636            NetworkConfigs::with_ethereum(),
1637            NetworkConfigs::with_tempo(),
1638            NetworkConfigs::with_celo(),
1639        ];
1640        for networks in explicit {
1641            assert_eq!(networks.try_with_chain_id(NamedChain::Celo as u64).unwrap(), networks);
1642        }
1643
1644        #[cfg(feature = "monad")]
1645        {
1646            let monad = NetworkConfigs::with_monad();
1647            assert_eq!(monad.try_with_chain_id(NamedChain::Celo as u64).unwrap(), monad);
1648        }
1649    }
1650
1651    #[test]
1652    #[cfg(feature = "monad")]
1653    fn rpc_metadata_identifies_custom_monad_networks() {
1654        assert_eq!(NetworkVariant::from_node_info_name("monad").unwrap(), NetworkVariant::Monad);
1655        assert!(NetworkConfigs::default().with_rpc_network(NetworkVariant::Monad).is_monad());
1656    }
1657
1658    #[test]
1659    #[cfg(feature = "monad")]
1660    fn parses_endpoint_hardfork_in_network_namespace() {
1661        assert_eq!(
1662            NetworkVariant::Monad.parse_hardfork("MonadEight").unwrap(),
1663            FoundryHardfork::Monad(MonadHardfork::MonadEight)
1664        );
1665    }
1666
1667    #[test]
1668    fn rpc_metadata_rejects_unknown_network_family() {
1669        assert_eq!(
1670            NetworkVariant::from_node_info_name("unknown").unwrap_err(),
1671            "unsupported network family `unknown` reported by fork endpoint"
1672        );
1673    }
1674
1675    #[test]
1676    fn explicit_ethereum_families_reject_namespaced_hardforks() {
1677        #[cfg_attr(not(feature = "monad"), allow(unused_mut))]
1678        let mut incompatible = vec![FoundryHardfork::Tempo(TempoHardfork::T0)];
1679        #[cfg(feature = "monad")]
1680        incompatible.push(FoundryHardfork::Monad(MonadHardfork::MonadEight));
1681
1682        for networks in [NetworkConfigs::with_ethereum(), NetworkConfigs::with_celo()] {
1683            for hardfork in &incompatible {
1684                assert_eq!(
1685                    networks.normalize_for_hardfork(*hardfork).unwrap_err(),
1686                    format!(
1687                        "hardfork `{}` conflicts with network config `{}`",
1688                        String::from(*hardfork),
1689                        networks.execution_profile_name()
1690                    )
1691                );
1692            }
1693        }
1694
1695        let celo = NetworkConfigs::with_celo();
1696        assert_eq!(
1697            celo.normalize_for_hardfork(FoundryHardfork::Ethereum(
1698                foundry_evm_hardforks::EthereumHardfork::Prague
1699            ))
1700            .unwrap(),
1701            celo
1702        );
1703    }
1704
1705    #[cfg(feature = "base")]
1706    #[test]
1707    fn base_precompile_labels_follow_upgrade_boundaries() {
1708        let config = NetworkConfigs::with_base();
1709
1710        assert!(config.precompiles_label(Some(BaseUpgrade::Azul.into())).is_empty());
1711
1712        let beryl = config.precompiles_label(Some(BaseUpgrade::Beryl.into()));
1713        assert_eq!(beryl.get(&B20FactoryStorage::ADDRESS), Some(&"B20Factory".to_string()));
1714        assert_eq!(
1715            beryl.get(&ActivationRegistryStorage::ADDRESS),
1716            Some(&"ActivationRegistry".to_string())
1717        );
1718        assert_eq!(beryl.get(&PolicyRegistryStorage::ADDRESS), Some(&"PolicyRegistry".to_string()));
1719        assert!(!beryl.contains_key(&TxContextStorage::ADDRESS));
1720        assert!(!beryl.contains_key(&NonceManagerStorage::ADDRESS));
1721
1722        let cobalt = config.precompiles_label(Some(BaseUpgrade::Cobalt.into()));
1723        assert_eq!(cobalt.len(), BASE_PRECOMPILES.len());
1724        assert_eq!(config.precompiles_label(None).len(), BASE_PRECOMPILES.len());
1725
1726        for upgrade in [BaseUpgrade::Azul, BaseUpgrade::Beryl, BaseUpgrade::Cobalt] {
1727            assert_eq!(
1728                resolved_precompile_labels(Some(upgrade.into())),
1729                config.precompiles_label(Some(upgrade.into()))
1730            );
1731        }
1732
1733        // The name-keyed precompile map must honor the same upgrade boundaries.
1734        assert!(config.precompiles(Some(BaseUpgrade::Azul.into())).is_empty());
1735        let beryl = config.precompiles(Some(BaseUpgrade::Beryl.into()));
1736        assert_eq!(beryl.get("B20Factory"), Some(&B20FactoryStorage::ADDRESS));
1737        assert!(!beryl.contains_key("NonceManager"));
1738        assert_eq!(
1739            config.precompiles(Some(BaseUpgrade::Cobalt.into())).get("NonceManager"),
1740            Some(&NonceManagerStorage::ADDRESS)
1741        );
1742
1743        // A non-Base profile must not pick up Base labels even when an upgrade is supplied.
1744        assert!(
1745            NetworkConfigs::default()
1746                .precompiles_label(Some(BaseUpgrade::Cobalt.into()))
1747                .is_empty()
1748        );
1749    }
1750
1751    #[test]
1752    fn new_tempo_flag_equivalent_to_legacy() {
1753        let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1754        let via_old = NetworkConfigs { tempo: true, ..Default::default() };
1755        assert_eq!(via_new.is_tempo(), via_old.is_tempo());
1756        assert_eq!(via_new.active_network_name(), via_old.active_network_name());
1757        assert_eq!(via_new.precompiles(None), via_old.precompiles(None));
1758        assert_eq!(via_new.precompiles_label(None), via_old.precompiles_label(None));
1759    }
1760
1761    fn bsc_p256_gas_used(chain_id: ChainId, timestamp: u64) -> Option<u64> {
1762        bsc_p256_precompile(chain_id, timestamp)
1763            .flatten()
1764            .map(|precompile| precompile.execute(&[], u64::MAX, 0).unwrap().gas_used)
1765    }
1766
1767    fn assert_bsc_p256_boundaries(chain_id: ChainId, haber_timestamp: u64, osaka_timestamp: u64) {
1768        assert!(matches!(bsc_p256_precompile(chain_id, haber_timestamp - 1), Some(None)));
1769        assert_eq!(bsc_p256_gas_used(chain_id, haber_timestamp), Some(P256VERIFY_BASE_GAS_FEE));
1770        assert_eq!(bsc_p256_gas_used(chain_id, osaka_timestamp - 1), Some(P256VERIFY_BASE_GAS_FEE));
1771        assert_eq!(
1772            bsc_p256_gas_used(chain_id, osaka_timestamp),
1773            Some(P256VERIFY_BASE_GAS_FEE_OSAKA)
1774        );
1775    }
1776
1777    #[test]
1778    fn selects_bsc_p256_at_mainnet_boundaries() {
1779        assert_bsc_p256_boundaries(
1780            BSC_MAINNET_CHAIN_ID,
1781            BSC_MAINNET_HABER_TIMESTAMP,
1782            BSC_MAINNET_OSAKA_TIMESTAMP,
1783        );
1784    }
1785
1786    #[test]
1787    fn selects_bsc_p256_at_testnet_boundaries() {
1788        assert_bsc_p256_boundaries(
1789            BSC_TESTNET_CHAIN_ID,
1790            BSC_TESTNET_HABER_TIMESTAMP,
1791            BSC_TESTNET_OSAKA_TIMESTAMP,
1792        );
1793    }
1794
1795    #[test]
1796    fn removes_bsc_p256_before_haber() {
1797        let mut precompiles = PrecompilesMap::from_static(Precompiles::osaka());
1798        assert!(precompiles.get(&BSC_P256_ADDRESS).is_some());
1799        apply_bsc_p256_precompile(
1800            &mut precompiles,
1801            BSC_MAINNET_CHAIN_ID,
1802            BSC_MAINNET_HABER_TIMESTAMP - 1,
1803        );
1804        assert!(precompiles.get(&BSC_P256_ADDRESS).is_none());
1805    }
1806
1807    #[test]
1808    fn canonical_tempo_network_reports_precompiles() {
1809        let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1810
1811        assert_eq!(
1812            cfg.precompiles(None).get("TIP20ChannelReserve"),
1813            Some(&TIP20_CHANNEL_RESERVE_ADDRESS)
1814        );
1815        assert!(
1816            !cfg.precompiles(Some(TempoHardfork::T4.into())).contains_key("TIP20ChannelReserve")
1817        );
1818        assert!(
1819            !cfg.precompiles(Some(TempoHardfork::T4.into())).contains_key("ReceivePolicyGuard")
1820        );
1821        assert!(!cfg.precompiles(Some(TempoHardfork::T2.into())).contains_key("AddressRegistry"));
1822        assert!(!cfg.precompiles(Some(TempoHardfork::T2.into())).contains_key("SignatureVerifier"));
1823        assert_eq!(
1824            cfg.precompiles(Some(TempoHardfork::T3.into())).get("AddressRegistry"),
1825            Some(&ADDRESS_REGISTRY_ADDRESS)
1826        );
1827        assert_eq!(
1828            cfg.precompiles(Some(TempoHardfork::T3.into())).get("SignatureVerifier"),
1829            Some(&SIGNATURE_VERIFIER_ADDRESS)
1830        );
1831        assert_eq!(
1832            cfg.precompiles_label(Some(TempoHardfork::T5.into()))
1833                .get(&TIP20_CHANNEL_RESERVE_ADDRESS),
1834            Some(&"TIP20ChannelReserve".to_string())
1835        );
1836        assert!(cfg.precompiles_label(None).contains_key(&TIP20_CHANNEL_RESERVE_ADDRESS));
1837        assert!(
1838            !cfg.precompiles_label(Some(TempoHardfork::T5.into()))
1839                .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
1840        );
1841        assert!(
1842            cfg.precompiles_label(Some(TempoHardfork::T6.into()))
1843                .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
1844        );
1845    }
1846
1847    #[test]
1848    #[cfg(feature = "monad")]
1849    fn canonical_monad_network_reports_hardfork_gated_precompiles() {
1850        let cfg = NetworkConfigs { network: Some(NetworkVariant::Monad), ..Default::default() };
1851
1852        assert_eq!(
1853            cfg.precompiles(Some(MonadHardfork::MonadEight.into())).get("MonadStaking"),
1854            Some(&monad_revm::staking::STAKING_ADDRESS)
1855        );
1856        assert!(
1857            !cfg.precompiles(Some(MonadHardfork::MonadEight.into()))
1858                .contains_key("MonadReserveBalance")
1859        );
1860        assert_eq!(
1861            cfg.precompiles(Some(MonadHardfork::MonadNine.into())).get("MonadReserveBalance"),
1862            Some(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
1863        );
1864        assert_eq!(
1865            cfg.precompiles_label(Some(MonadHardfork::MonadNine.into()))
1866                .get(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS),
1867            Some(&"ReserveBalance".to_string())
1868        );
1869        assert!(
1870            cfg.precompiles_label(None)
1871                .contains_key(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
1872        );
1873        assert!(
1874            !cfg.precompiles_label(Some(MonadHardfork::MonadEight.into()))
1875                .contains_key(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
1876        );
1877    }
1878
1879    #[test]
1880    fn storage_credits_precompile_activates_at_t7() {
1881        assert!(!is_tempo_precompile_active_at(STORAGE_CREDITS_ADDRESS, TempoHardfork::T6));
1882        assert!(is_tempo_precompile_active_at(STORAGE_CREDITS_ADDRESS, TempoHardfork::T7));
1883        assert!(TEMPO_PRECOMPILE_ADDRESSES.contains(&STORAGE_CREDITS_ADDRESS));
1884
1885        // The hardfork-filtered precompile map must honor the same T7 activation.
1886        let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1887        assert!(!cfg.precompiles(Some(TempoHardfork::T6.into())).contains_key("StorageCredits"));
1888        assert!(cfg.precompiles(Some(TempoHardfork::T7.into())).contains_key("StorageCredits"));
1889    }
1890
1891    #[test]
1892    fn current_committee_precompile_activates_at_t8() {
1893        assert!(!is_tempo_precompile_active_at(CURRENT_COMMITTEE_ADDRESS, TempoHardfork::T7));
1894        assert!(is_tempo_precompile_active_at(CURRENT_COMMITTEE_ADDRESS, TempoHardfork::T8));
1895        assert!(TEMPO_PRECOMPILE_ADDRESSES.contains(&CURRENT_COMMITTEE_ADDRESS));
1896
1897        let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1898        assert!(!cfg.precompiles(Some(TempoHardfork::T7.into())).contains_key("CurrentCommittee"));
1899        assert!(cfg.precompiles(Some(TempoHardfork::T8.into())).contains_key("CurrentCommittee"));
1900    }
1901
1902    // --- resolved() / active_network_name ---
1903
1904    #[test]
1905    fn active_network_name_tempo() {
1906        let cfg = NetworkConfigs::with_tempo();
1907        assert_eq!(cfg.active_network_name(), Some("tempo"));
1908    }
1909
1910    #[test]
1911    #[cfg(feature = "monad")]
1912    fn active_network_name_monad() {
1913        let cfg = NetworkConfigs::with_monad();
1914        assert_eq!(cfg.active_network_name(), Some("monad"));
1915        assert!(cfg.is_monad());
1916    }
1917
1918    #[test]
1919    #[cfg(feature = "monad")]
1920    fn contract_size_limits_monad() {
1921        let limits = NetworkConfigs::with_monad().contract_size_limits().unwrap();
1922        assert_eq!(limits.runtime, monad_revm::MONAD_MAX_CODE_SIZE);
1923        assert_eq!(limits.initcode, monad_revm::MONAD_MAX_INITCODE_SIZE);
1924        assert!(NetworkConfigs::default().contract_size_limits().is_none());
1925    }
1926
1927    #[test]
1928    fn active_network_name_default_is_none() {
1929        let cfg = NetworkConfigs::default();
1930        assert_eq!(cfg.active_network_name(), None);
1931        assert!(!cfg.is_optimism());
1932    }
1933
1934    /// Base chain IDs resolved to Optimism before Base support existed, and `is_optimism()` still
1935    /// covers them, so a build without the `base` feature must keep resolving them rather than
1936    /// erroring. Shipped release binaries are exactly that build.
1937    #[test]
1938    #[cfg(all(not(feature = "base"), feature = "optimism"))]
1939    fn chain_id_inference_falls_back_to_optimism_without_base() {
1940        for chain_id in [NamedChain::Base as u64, NamedChain::BaseSepolia as u64] {
1941            let configs = NetworkConfigs::default()
1942                .try_with_chain_id(chain_id)
1943                .unwrap_or_else(|error| panic!("chain ID {chain_id} must still resolve: {error}"));
1944            assert!(configs.is_optimism(), "chain ID {chain_id} must resolve to Optimism");
1945        }
1946    }
1947
1948    #[test]
1949    #[cfg(not(feature = "base"))]
1950    fn node_info_rejects_disabled_base() {
1951        assert_eq!(
1952            NetworkVariant::from_node_info_name("base").unwrap_err(),
1953            "network family `base` is not enabled in this build"
1954        );
1955    }
1956
1957    // --- Serde round-trip ---
1958
1959    #[test]
1960    fn serde_roundtrip_tempo() {
1961        let original = NetworkConfigs::with_tempo();
1962        let json = serde_json::to_string(&original).unwrap();
1963        let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
1964        assert!(restored.is_tempo());
1965    }
1966
1967    #[test]
1968    fn serde_legacy_tempo_bool_deserialized() {
1969        // Old foundry.toml format: `tempo = true`
1970        let json = r#"{"tempo": true, "celo": false, "bypass_prevrandao": false}"#;
1971        let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
1972        assert!(cfg.is_tempo());
1973    }
1974
1975    #[test]
1976    #[cfg(feature = "monad")]
1977    fn serde_roundtrip_monad() {
1978        let original = NetworkConfigs::with_monad();
1979        let json = serde_json::to_string(&original).unwrap();
1980        let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
1981        assert!(restored.is_monad());
1982        assert!(!restored.is_tempo());
1983    }
1984
1985    #[test]
1986    #[cfg(feature = "monad")]
1987    fn serde_legacy_monad_bool_deserialized() {
1988        let json = r#"{"monad": true, "celo": false, "bypass_prevrandao": false}"#;
1989        let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
1990        assert!(cfg.is_monad());
1991    }
1992
1993    #[test]
1994    fn serde_serializes_legacy_alias_as_canonical_network() {
1995        #[cfg_attr(not(feature = "monad"), allow(unused_mut))]
1996        let mut cases = vec![(NetworkConfigs { tempo: true, ..Default::default() }, "tempo")];
1997        #[cfg(feature = "monad")]
1998        cases.push((NetworkConfigs { monad: true, ..Default::default() }, "monad"));
1999
2000        for (cfg, expected) in cases {
2001            let json = serde_json::to_value(cfg).unwrap();
2002            assert_eq!(json["network"], serde_json::json!(expected));
2003            assert!(json.get("tempo").is_none(), "legacy `tempo` key should not be serialized");
2004            assert!(json.get("monad").is_none(), "legacy `monad` key should not be serialized");
2005        }
2006    }
2007
2008    #[test]
2009    fn serde_new_network_field_deserialized() {
2010        let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#;
2011        let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap();
2012        assert!(cfg_tempo.is_tempo());
2013
2014        #[cfg(feature = "monad")]
2015        {
2016            let json_monad = r#"{"network": "monad", "celo": false, "bypass_prevrandao": false}"#;
2017            let cfg_monad: NetworkConfigs = serde_json::from_str(json_monad).unwrap();
2018            assert!(cfg_monad.is_monad());
2019        }
2020    }
2021
2022    #[test]
2023    fn validates_flattened_network_selectors() {
2024        let valid = [
2025            NetworkConfigs::default(),
2026            NetworkConfigs::with_ethereum(),
2027            NetworkConfigs::with_celo(),
2028            NetworkConfigs::with_tempo(),
2029            NetworkConfigs {
2030                network: Some(NetworkVariant::Tempo),
2031                tempo: true,
2032                ..Default::default()
2033            },
2034        ];
2035        for networks in valid {
2036            networks.validate().unwrap();
2037        }
2038
2039        let conflicts = [
2040            (
2041                NetworkConfigs {
2042                    network: Some(NetworkVariant::Tempo),
2043                    celo: true,
2044                    ..Default::default()
2045                },
2046                "network selectors `network = \"tempo\"` and `celo = true` conflict",
2047            ),
2048            (
2049                NetworkConfigs { celo: true, tempo: true, ..Default::default() },
2050                "network selectors `celo = true` and `tempo = true` conflict",
2051            ),
2052        ];
2053        for (networks, expected) in conflicts {
2054            assert!(networks.validate().unwrap_err().contains(expected));
2055        }
2056    }
2057
2058    #[test]
2059    #[cfg(feature = "monad")]
2060    fn validates_flattened_monad_network_selectors() {
2061        for networks in [
2062            NetworkConfigs::with_monad(),
2063            NetworkConfigs {
2064                network: Some(NetworkVariant::Monad),
2065                monad: true,
2066                ..Default::default()
2067            },
2068        ] {
2069            networks.validate().unwrap();
2070        }
2071
2072        let conflicts = [
2073            (
2074                NetworkConfigs {
2075                    network: Some(NetworkVariant::Monad),
2076                    celo: true,
2077                    ..Default::default()
2078                },
2079                "network selectors `network = \"monad\"` and `celo = true` conflict",
2080            ),
2081            (
2082                NetworkConfigs {
2083                    network: Some(NetworkVariant::Monad),
2084                    tempo: true,
2085                    ..Default::default()
2086                },
2087                "network selectors `network = \"monad\"` and `tempo = true` conflict",
2088            ),
2089            (
2090                NetworkConfigs { celo: true, monad: true, ..Default::default() },
2091                "network selectors `celo = true` and `monad = true` conflict",
2092            ),
2093            (
2094                NetworkConfigs { tempo: true, monad: true, ..Default::default() },
2095                "network selectors `tempo = true` and `monad = true` conflict",
2096            ),
2097        ];
2098        for (networks, expected) in conflicts {
2099            assert!(networks.validate().unwrap_err().contains(expected));
2100        }
2101    }
2102
2103    #[test]
2104    #[cfg(feature = "monad")]
2105    fn chain_id_detects_monad_network() {
2106        assert_eq!(NetworkVariant::from(143), NetworkVariant::Monad);
2107        assert_eq!(NetworkVariant::from(10143), NetworkVariant::Monad);
2108
2109        assert!(NetworkConfigs::default().try_with_chain_id(143).unwrap().is_monad());
2110    }
2111
2112    #[cfg(feature = "base")]
2113    mod base {
2114        use super::*;
2115
2116        #[test]
2117        fn active_network_name_base() {
2118            let cfg = NetworkConfigs::with_base();
2119            assert_eq!(cfg.active_network_name(), Some("base"));
2120        }
2121
2122        #[test]
2123        fn base_fee_params_follow_canyon_boundary() {
2124            const CANYON_TIMESTAMP: u64 = 1_704_992_401;
2125
2126            let config = NetworkConfigs::with_base();
2127            assert_eq!(config.base_fee_params(CANYON_TIMESTAMP - 1), BaseFeeParams::new(50, 6));
2128            assert_eq!(config.base_fee_params(CANYON_TIMESTAMP), BaseFeeParams::new(250, 6));
2129        }
2130
2131        #[test]
2132        fn base_does_not_inherit_ethereum_blob_excess_gas() {
2133            let params = BlobParams::prague();
2134            assert_eq!(
2135                NetworkConfigs::with_base().next_block_blob_excess_gas(
2136                    params,
2137                    0,
2138                    params.target_blob_gas_per_block() + 1,
2139                    1,
2140                ),
2141                0
2142            );
2143        }
2144
2145        #[test]
2146        fn serde_roundtrip_base() {
2147            let original = NetworkConfigs::with_base();
2148            let json = serde_json::to_string(&original).unwrap();
2149            let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
2150            assert!(restored.is_base());
2151            assert!(!restored.is_tempo());
2152        }
2153
2154        #[test]
2155        fn serde_base_field_deserialized() {
2156            let json_base = r#"{"network": "base", "celo": false, "bypass_prevrandao": false}"#;
2157            let cfg_base: NetworkConfigs = serde_json::from_str(json_base).unwrap();
2158            assert!(cfg_base.is_base());
2159        }
2160
2161        #[test]
2162        fn chain_id_detects_base_networks() {
2163            assert_eq!(NetworkVariant::from(8453), NetworkVariant::Base);
2164            assert_eq!(NetworkVariant::from(84532), NetworkVariant::Base);
2165            assert!(NetworkConfigs::default().try_with_chain_id(8453).unwrap().is_base());
2166            assert_eq!(NetworkVariant::from_node_info_name("base").unwrap(), NetworkVariant::Base);
2167        }
2168
2169        #[test]
2170        fn hardfork_infers_base_network() {
2171            assert_eq!(
2172                NetworkConfigs::default()
2173                    .normalize_for_hardfork(FoundryHardfork::Base(BaseUpgrade::Beryl))
2174                    .unwrap()
2175                    .resolved_network(),
2176                Some(NetworkVariant::Base)
2177            );
2178        }
2179    }
2180
2181    #[cfg(feature = "optimism")]
2182    mod optimism {
2183        use super::*;
2184
2185        #[test]
2186        fn new_optimism_flag_equivalent_to_legacy() {
2187            let via_new =
2188                NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() };
2189            let via_old = NetworkConfigs { optimism: true, ..Default::default() };
2190            assert_eq!(via_new.is_optimism(), via_old.is_optimism());
2191            assert_eq!(via_new.is_tempo(), via_old.is_tempo());
2192            assert_eq!(via_new.active_network_name(), via_old.active_network_name());
2193        }
2194
2195        #[test]
2196        fn matching_optimism_selectors_are_valid() {
2197            NetworkConfigs::with_optimism().validate().unwrap();
2198        }
2199
2200        #[test]
2201        fn active_network_name_optimism() {
2202            let cfg = NetworkConfigs::with_optimism();
2203            assert_eq!(cfg.active_network_name(), Some("optimism"));
2204        }
2205
2206        #[test]
2207        fn conflicting_optimism_and_tempo_selectors_are_rejected() {
2208            let cfg = NetworkConfigs {
2209                network: Some(NetworkVariant::Optimism),
2210                tempo: true,
2211                ..Default::default()
2212            };
2213            assert_eq!(
2214                cfg.validate().unwrap_err(),
2215                "network selectors `network = \"optimism\"` and `tempo = true` conflict; select \
2216                 only one network"
2217            );
2218        }
2219
2220        #[test]
2221        fn serde_roundtrip_optimism() {
2222            let original = NetworkConfigs::with_optimism();
2223            let json = serde_json::to_string(&original).unwrap();
2224            let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
2225            assert!(restored.is_optimism());
2226            assert!(!restored.is_tempo());
2227        }
2228
2229        #[test]
2230        fn serde_optimism_field_deserialized() {
2231            let json_optimism =
2232                r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#;
2233            let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap();
2234            assert!(cfg_optimism.is_optimism());
2235        }
2236    }
2237
2238    #[test]
2239    fn executed_hardfork_follows_the_network_family() {
2240        // A cross-namespace override runs as the configured network's hardfork, so the value used
2241        // to describe execution has to be coerced the same way.
2242        let prague = FoundryHardfork::Ethereum(EthereumHardfork::Prague);
2243        assert_eq!(NetworkConfigs::default().executed_hardfork(prague), prague);
2244        assert_eq!(
2245            NetworkConfigs::with_tempo().executed_hardfork(prague),
2246            FoundryHardfork::Tempo(TempoHardfork::from(prague))
2247        );
2248    }
2249}