Skip to main content

foundry_evm_networks/
lib.rs

1//! # foundry-evm-networks
2//!
3//! Foundry EVM network configuration.
4
5use crate::celo::transfer::{
6    CELO_TRANSFER_ADDRESS, CELO_TRANSFER_LABEL, PRECOMPILE_ID_CELO_TRANSFER,
7};
8use alloy_chains::{
9    Chain, NamedChain,
10    NamedChain::{Chiado, Gnosis, Moonbase, Moonbeam, MoonbeamDev, Moonriver, Rsk, RskTestnet},
11};
12use alloy_eips::eip1559::BaseFeeParams;
13use alloy_evm::precompiles::{DynPrecompile, PrecompilesMap};
14use alloy_primitives::{Address, ChainId, address, map::AddressHashMap};
15use clap::Parser;
16use foundry_evm_hardforks::{FoundryHardfork, TempoHardfork};
17use revm::precompile::{
18    Precompile as RevmPrecompile,
19    secp256r1::{P256VERIFY, P256VERIFY_OSAKA},
20};
21use serde::{Deserialize, Serialize};
22use std::collections::BTreeMap;
23use tempo_contracts::precompiles::{
24    ACCOUNT_KEYCHAIN_ADDRESS, ADDRESS_REGISTRY_ADDRESS, CURRENT_COMMITTEE_ADDRESS,
25    NONCE_PRECOMPILE_ADDRESS, RECEIVE_POLICY_GUARD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS,
26    STABLECOIN_DEX_ADDRESS, STORAGE_CREDITS_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
27    TIP20_CHANNEL_RESERVE_ADDRESS, TIP20_FACTORY_ADDRESS, TIP403_REGISTRY_ADDRESS,
28    VALIDATOR_CONFIG_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
29};
30
31pub mod arbitrum;
32pub mod celo;
33
34#[cfg(feature = "optimism")]
35mod optimism;
36
37const TEMPO_PRECOMPILES: &[(&str, Address)] = &[
38    ("Nonce", NONCE_PRECOMPILE_ADDRESS),
39    ("StablecoinDex", STABLECOIN_DEX_ADDRESS),
40    ("TIP20Factory", TIP20_FACTORY_ADDRESS),
41    ("TIP403Registry", TIP403_REGISTRY_ADDRESS),
42    ("FeeManager", TIP_FEE_MANAGER_ADDRESS),
43    ("ValidatorConfig", VALIDATOR_CONFIG_ADDRESS),
44    ("ValidatorConfigV2", VALIDATOR_CONFIG_V2_ADDRESS),
45    ("AccountKeychain", ACCOUNT_KEYCHAIN_ADDRESS),
46    ("SignatureVerifier", SIGNATURE_VERIFIER_ADDRESS),
47    ("AddressRegistry", ADDRESS_REGISTRY_ADDRESS),
48    ("TIP20ChannelReserve", TIP20_CHANNEL_RESERVE_ADDRESS),
49    ("ReceivePolicyGuard", RECEIVE_POLICY_GUARD_ADDRESS),
50    ("StorageCredits", STORAGE_CREDITS_ADDRESS),
51    ("CurrentCommittee", CURRENT_COMMITTEE_ADDRESS),
52];
53
54/// BSC secp256r1 precompile address introduced by the Haber hardfork.
55const BSC_P256_ADDRESS: Address = address!("0000000000000000000000000000000000000100");
56
57const BSC_MAINNET_CHAIN_ID: u64 = 56;
58const BSC_TESTNET_CHAIN_ID: u64 = 97;
59const BSC_MAINNET_HABER_TIMESTAMP: u64 = 1_718_863_500;
60const BSC_TESTNET_HABER_TIMESTAMP: u64 = 1_716_962_820;
61const BSC_MAINNET_OSAKA_TIMESTAMP: u64 = 1_777_343_400;
62const BSC_TESTNET_OSAKA_TIMESTAMP: u64 = 1_774_319_400;
63
64/// Returns the BSC P256 precompile for the given timestamp. The outer option distinguishes BSC
65/// chains from unrelated chains, while the inner option disables P256 before Haber.
66const fn bsc_p256_precompile(chain_id: ChainId, timestamp: u64) -> Option<Option<RevmPrecompile>> {
67    let (haber_timestamp, osaka_timestamp) = match chain_id {
68        BSC_MAINNET_CHAIN_ID => (BSC_MAINNET_HABER_TIMESTAMP, BSC_MAINNET_OSAKA_TIMESTAMP),
69        BSC_TESTNET_CHAIN_ID => (BSC_TESTNET_HABER_TIMESTAMP, BSC_TESTNET_OSAKA_TIMESTAMP),
70        _ => return None,
71    };
72
73    if timestamp < haber_timestamp {
74        Some(None)
75    } else if timestamp < osaka_timestamp {
76        Some(Some(P256VERIFY))
77    } else {
78        Some(Some(P256VERIFY_OSAKA))
79    }
80}
81
82/// All well-known Tempo precompile addresses.
83pub const TEMPO_PRECOMPILE_ADDRESSES: &[Address] = &[
84    NONCE_PRECOMPILE_ADDRESS,
85    STABLECOIN_DEX_ADDRESS,
86    TIP20_FACTORY_ADDRESS,
87    TIP403_REGISTRY_ADDRESS,
88    TIP_FEE_MANAGER_ADDRESS,
89    VALIDATOR_CONFIG_ADDRESS,
90    VALIDATOR_CONFIG_V2_ADDRESS,
91    ACCOUNT_KEYCHAIN_ADDRESS,
92    SIGNATURE_VERIFIER_ADDRESS,
93    ADDRESS_REGISTRY_ADDRESS,
94    TIP20_CHANNEL_RESERVE_ADDRESS,
95    RECEIVE_POLICY_GUARD_ADDRESS,
96    STORAGE_CREDITS_ADDRESS,
97    CURRENT_COMMITTEE_ADDRESS,
98];
99
100/// Returns whether a well-known Tempo precompile address is active at `hardfork`.
101pub fn is_tempo_precompile_active_at(address: Address, hardfork: TempoHardfork) -> bool {
102    if address == CURRENT_COMMITTEE_ADDRESS {
103        hardfork.is_t8()
104    } else if address == TIP20_CHANNEL_RESERVE_ADDRESS {
105        hardfork.is_t5()
106    } else if address == RECEIVE_POLICY_GUARD_ADDRESS {
107        hardfork.is_t6()
108    } else if address == STORAGE_CREDITS_ADDRESS {
109        hardfork.is_t7()
110    } else if address == ADDRESS_REGISTRY_ADDRESS || address == SIGNATURE_VERIFIER_ADDRESS {
111        hardfork.is_t3()
112    } else {
113        true
114    }
115}
116
117/// Returns the well-known Tempo precompile addresses active at `hardfork`.
118pub fn active_tempo_precompile_addresses(hardfork: TempoHardfork) -> impl Iterator<Item = Address> {
119    TEMPO_PRECOMPILE_ADDRESSES
120        .iter()
121        .copied()
122        .filter(move |&address| is_tempo_precompile_active_at(address, hardfork))
123}
124
125#[derive(
126    Clone,
127    Copy,
128    Debug,
129    Default,
130    PartialEq,
131    Eq,
132    PartialOrd,
133    Ord,
134    Hash,
135    Serialize,
136    Deserialize,
137    clap::ValueEnum,
138)]
139#[serde(rename_all = "lowercase")]
140#[clap(rename_all = "lowercase")]
141pub enum NetworkVariant {
142    #[default]
143    Ethereum,
144    #[cfg(feature = "optimism")]
145    Optimism,
146    Tempo,
147}
148
149impl std::str::FromStr for NetworkVariant {
150    type Err = String;
151
152    fn from_str(s: &str) -> Result<Self, Self::Err> {
153        match s {
154            "ethereum" => Ok(Self::Ethereum),
155            #[cfg(feature = "optimism")]
156            "optimism" => Ok(Self::Optimism),
157            "tempo" => Ok(Self::Tempo),
158            _ => Err(format!("unknown network variant: {s}")),
159        }
160    }
161}
162
163impl NetworkVariant {
164    pub const fn name(&self) -> &'static str {
165        match self {
166            Self::Ethereum => "ethereum",
167            #[cfg(feature = "optimism")]
168            Self::Optimism => "optimism",
169            Self::Tempo => "tempo",
170        }
171    }
172}
173
174impl std::fmt::Display for NetworkVariant {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        f.write_str(self.name())
177    }
178}
179
180impl From<ChainId> for NetworkVariant {
181    fn from(chain_id: ChainId) -> Self {
182        let chain = Chain::from_id(chain_id);
183        if chain.is_tempo() {
184            return Self::Tempo;
185        }
186        #[cfg(feature = "optimism")]
187        if chain.is_optimism() {
188            return Self::Optimism;
189        }
190        Self::Ethereum
191    }
192}
193
194#[derive(Clone, Debug, Default, Parser, Deserialize, Copy, PartialEq, Eq)]
195pub struct NetworkConfigs {
196    /// Enable a specific network family.
197    #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "tempo"])]
198    #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
199    #[serde(default)]
200    pub(crate) network: Option<NetworkVariant>,
201    /// Enable Celo network features.
202    #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "tempo"])]
203    #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
204    celo: bool,
205    /// Enable Optimism network features (deprecated: use --network optimism).
206    #[cfg(feature = "optimism")]
207    #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
208    // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the
209    // canonical form is `network = "optimism"`.
210    #[serde(default)]
211    pub(crate) optimism: bool,
212    /// Enable Tempo network features (deprecated: use --network tempo).
213    #[arg(long, hide = true, conflicts_with_all = ["network", "celo"])]
214    #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
215    // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the
216    // canonical form is `network = "tempo"`.
217    #[serde(default)]
218    tempo: bool,
219    /// Whether to bypass prevrandao.
220    #[arg(skip)]
221    #[serde(default)]
222    bypass_prevrandao: bool,
223}
224
225// Custom `Serialize` impl: always emits the *resolved* network as the canonical
226// `network = "..."` field, and never emits the legacy `tempo` / `optimism` aliases. This avoids
227// confusing output like `network = "tempo"` next to `tempo = false`, and ensures `tempo = true`
228// in foundry.toml round-trips as `network = "tempo"`.
229impl Serialize for NetworkConfigs {
230    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
231        use serde::ser::SerializeStruct;
232        let mut s = serializer.serialize_struct("NetworkConfigs", 3)?;
233        s.serialize_field("network", &self.resolved_network())?;
234        s.serialize_field("celo", &self.celo)?;
235        s.serialize_field("bypass_prevrandao", &self.bypass_prevrandao)?;
236        s.end()
237    }
238}
239
240impl NetworkConfigs {
241    pub fn with_celo() -> Self {
242        Self { celo: true, ..Default::default() }
243    }
244
245    pub fn with_tempo() -> Self {
246        Self { network: Some(NetworkVariant::Tempo), tempo: true, ..Default::default() }
247    }
248
249    pub const fn is_tempo(&self) -> bool {
250        matches!(self.resolved_network(), Some(NetworkVariant::Tempo))
251    }
252
253    pub const fn is_celo(&self) -> bool {
254        self.celo
255    }
256
257    /// Returns the resolved network variant, folding legacy flags.
258    pub const fn resolved_network(&self) -> Option<NetworkVariant> {
259        if let Some(n) = self.network {
260            return Some(n);
261        }
262        #[cfg(feature = "optimism")]
263        if self.optimism {
264            return Some(NetworkVariant::Optimism);
265        }
266        if self.tempo {
267            return Some(NetworkVariant::Tempo);
268        }
269        None
270    }
271
272    /// Returns the name of the currently active non-Ethereum network, or `None` for plain Ethereum.
273    pub fn active_network_name(&self) -> Option<&'static str> {
274        self.resolved_network().and_then(|n| match n {
275            NetworkVariant::Ethereum => None,
276            _ => Some(n.name()),
277        })
278    }
279
280    /// Returns the base fee parameters for the configured network.
281    ///
282    /// For Optimism networks, returns Canyon parameters if the Canyon hardfork is active
283    /// at the given timestamp, otherwise returns pre-Canyon parameters.
284    #[cfg(feature = "optimism")]
285    pub fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams {
286        if self.is_optimism() {
287            return self.op_base_fee_params(timestamp);
288        }
289        BaseFeeParams::ethereum()
290    }
291
292    /// Returns the base fee parameters for the configured network.
293    #[cfg(not(feature = "optimism"))]
294    pub const fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams {
295        let _ = timestamp;
296        BaseFeeParams::ethereum()
297    }
298
299    pub fn bypass_prevrandao(&self, chain_id: u64) -> bool {
300        if let Ok(
301            Moonbeam | Moonbase | Moonriver | MoonbeamDev | Rsk | RskTestnet | Gnosis | Chiado,
302        ) = NamedChain::try_from(chain_id)
303        {
304            return true;
305        }
306        self.bypass_prevrandao
307    }
308
309    pub fn with_chain_id(self, chain_id: u64) -> Self {
310        let chain = Chain::from_id(chain_id);
311        if self.resolved_network().is_some() {
312            return if !self.celo
313                && matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia))
314            {
315                Self::with_celo()
316            } else {
317                self
318            };
319        }
320        if chain.is_tempo() {
321            return Self::with_tempo();
322        }
323        #[cfg(feature = "optimism")]
324        if chain.is_optimism() {
325            return Self::with_optimism();
326        }
327        self
328    }
329
330    /// Validates `hardfork` against the current `NetworkConfigs` and, if consistent, returns an
331    /// updated instance with the network implied by the enabled hardfork.
332    ///
333    /// Returns `Err` when the hardfork's network family conflicts with the configured one.
334    pub fn normalize_for_hardfork(self, hardfork: FoundryHardfork) -> Result<Self, String> {
335        if let Some(configured) =
336            self.active_network_name().filter(|&n| Some(n) != hardfork.namespace())
337        {
338            return Err(format!(
339                "hardfork `{}` conflicts with network config `{configured}`",
340                String::from(hardfork),
341            ));
342        }
343
344        let network = match hardfork {
345            FoundryHardfork::Ethereum(_) => self,
346            FoundryHardfork::Tempo(_) => Self::with_tempo(),
347            #[cfg(feature = "optimism")]
348            FoundryHardfork::Optimism(_) => Self::with_optimism(),
349        };
350
351        Ok(network)
352    }
353
354    /// Inject precompiles for configured networks.
355    pub fn inject_precompiles(self, precompiles: &mut PrecompilesMap) {
356        if self.celo {
357            precompiles.apply_precompile(&CELO_TRANSFER_ADDRESS, move |_| {
358                Some(celo::transfer::precompile())
359            });
360        }
361    }
362
363    /// Injects chain-specific precompiles active at the given timestamp.
364    pub fn inject_chain_precompiles(
365        self,
366        precompiles: &mut PrecompilesMap,
367        chain_id: ChainId,
368        timestamp: u64,
369    ) {
370        let Some(p256verify) = bsc_p256_precompile(chain_id, timestamp) else { return };
371        precompiles.apply_precompile(&BSC_P256_ADDRESS, move |_| {
372            p256verify.map(|p256verify| {
373                DynPrecompile::new(p256verify.id().clone(), move |input| {
374                    p256verify.execute(input.data, input.gas, input.reservoir)
375                })
376            })
377        });
378    }
379
380    /// Returns precompiles label for configured networks, to be used in traces.
381    pub fn precompiles_label(
382        self,
383        tempo_hardfork: Option<TempoHardfork>,
384    ) -> AddressHashMap<String> {
385        let mut labels = AddressHashMap::default();
386        if self.celo {
387            labels.insert(CELO_TRANSFER_ADDRESS, CELO_TRANSFER_LABEL.to_string());
388        }
389        if self.is_tempo() {
390            labels.extend(
391                TEMPO_PRECOMPILES
392                    .iter()
393                    .copied()
394                    .filter(|(_, address)| {
395                        tempo_hardfork.is_none_or(|hardfork| {
396                            is_tempo_precompile_active_at(*address, hardfork)
397                        })
398                    })
399                    .map(|(label, address)| (address, label.to_string())),
400            );
401        }
402        labels
403    }
404
405    /// Returns precompiles for configured networks.
406    pub fn precompiles(self, tempo_hardfork: Option<TempoHardfork>) -> BTreeMap<String, Address> {
407        let mut precompiles = BTreeMap::new();
408        if self.celo {
409            precompiles
410                .insert(PRECOMPILE_ID_CELO_TRANSFER.name().to_string(), CELO_TRANSFER_ADDRESS);
411        }
412        if self.is_tempo() {
413            precompiles.extend(
414                TEMPO_PRECOMPILES
415                    .iter()
416                    .copied()
417                    .filter(|(_, address)| {
418                        tempo_hardfork.is_none_or(|hardfork| {
419                            is_tempo_precompile_active_at(*address, hardfork)
420                        })
421                    })
422                    .map(|(label, address)| (label.to_string(), address)),
423            );
424        }
425        precompiles
426    }
427}
428
429impl From<NetworkVariant> for NetworkConfigs {
430    fn from(network: NetworkVariant) -> Self {
431        match network {
432            NetworkVariant::Ethereum => Self::default(),
433            NetworkVariant::Tempo => {
434                Self { network: Some(network), tempo: true, ..Default::default() }
435            }
436            #[cfg(feature = "optimism")]
437            NetworkVariant::Optimism => {
438                Self { network: Some(network), optimism: true, ..Default::default() }
439            }
440        }
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447    use revm::precompile::{
448        Precompiles,
449        secp256r1::{P256VERIFY_BASE_GAS_FEE, P256VERIFY_BASE_GAS_FEE_OSAKA},
450    };
451
452    // --- Equivalence: new flag == legacy flag ---
453
454    #[test]
455    fn new_tempo_flag_equivalent_to_legacy() {
456        let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
457        let via_old = NetworkConfigs { tempo: true, ..Default::default() };
458        assert_eq!(via_new.is_tempo(), via_old.is_tempo());
459        assert_eq!(via_new.active_network_name(), via_old.active_network_name());
460        assert_eq!(via_new.precompiles(None), via_old.precompiles(None));
461        assert_eq!(via_new.precompiles_label(None), via_old.precompiles_label(None));
462    }
463
464    fn bsc_p256_gas_used(chain_id: ChainId, timestamp: u64) -> Option<u64> {
465        bsc_p256_precompile(chain_id, timestamp)
466            .flatten()
467            .map(|precompile| precompile.execute(&[], u64::MAX, 0).unwrap().gas_used)
468    }
469
470    fn assert_bsc_p256_boundaries(chain_id: ChainId, haber_timestamp: u64, osaka_timestamp: u64) {
471        assert!(matches!(bsc_p256_precompile(chain_id, haber_timestamp - 1), Some(None)));
472        assert_eq!(bsc_p256_gas_used(chain_id, haber_timestamp), Some(P256VERIFY_BASE_GAS_FEE));
473        assert_eq!(bsc_p256_gas_used(chain_id, osaka_timestamp - 1), Some(P256VERIFY_BASE_GAS_FEE));
474        assert_eq!(
475            bsc_p256_gas_used(chain_id, osaka_timestamp),
476            Some(P256VERIFY_BASE_GAS_FEE_OSAKA)
477        );
478    }
479
480    #[test]
481    fn selects_bsc_p256_at_mainnet_boundaries() {
482        assert_bsc_p256_boundaries(
483            BSC_MAINNET_CHAIN_ID,
484            BSC_MAINNET_HABER_TIMESTAMP,
485            BSC_MAINNET_OSAKA_TIMESTAMP,
486        );
487    }
488
489    #[test]
490    fn selects_bsc_p256_at_testnet_boundaries() {
491        assert_bsc_p256_boundaries(
492            BSC_TESTNET_CHAIN_ID,
493            BSC_TESTNET_HABER_TIMESTAMP,
494            BSC_TESTNET_OSAKA_TIMESTAMP,
495        );
496    }
497
498    #[test]
499    fn removes_bsc_p256_before_haber() {
500        let mut precompiles = PrecompilesMap::from_static(Precompiles::osaka());
501        assert!(precompiles.get(&BSC_P256_ADDRESS).is_some());
502        NetworkConfigs::default().inject_chain_precompiles(
503            &mut precompiles,
504            BSC_MAINNET_CHAIN_ID,
505            BSC_MAINNET_HABER_TIMESTAMP - 1,
506        );
507        assert!(precompiles.get(&BSC_P256_ADDRESS).is_none());
508    }
509
510    #[test]
511    fn canonical_tempo_network_reports_precompiles() {
512        let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
513
514        assert_eq!(
515            cfg.precompiles(None).get("TIP20ChannelReserve"),
516            Some(&TIP20_CHANNEL_RESERVE_ADDRESS)
517        );
518        assert!(!cfg.precompiles(Some(TempoHardfork::T4)).contains_key("TIP20ChannelReserve"));
519        assert!(!cfg.precompiles(Some(TempoHardfork::T4)).contains_key("ReceivePolicyGuard"));
520        assert!(!cfg.precompiles(Some(TempoHardfork::T2)).contains_key("AddressRegistry"));
521        assert!(!cfg.precompiles(Some(TempoHardfork::T2)).contains_key("SignatureVerifier"));
522        assert_eq!(
523            cfg.precompiles(Some(TempoHardfork::T3)).get("AddressRegistry"),
524            Some(&ADDRESS_REGISTRY_ADDRESS)
525        );
526        assert_eq!(
527            cfg.precompiles(Some(TempoHardfork::T3)).get("SignatureVerifier"),
528            Some(&SIGNATURE_VERIFIER_ADDRESS)
529        );
530        assert_eq!(
531            cfg.precompiles_label(Some(TempoHardfork::T5)).get(&TIP20_CHANNEL_RESERVE_ADDRESS),
532            Some(&"TIP20ChannelReserve".to_string())
533        );
534        assert!(cfg.precompiles_label(None).contains_key(&TIP20_CHANNEL_RESERVE_ADDRESS));
535        assert!(
536            !cfg.precompiles_label(Some(TempoHardfork::T5))
537                .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
538        );
539        assert!(
540            cfg.precompiles_label(Some(TempoHardfork::T6))
541                .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
542        );
543    }
544
545    #[test]
546    fn storage_credits_precompile_activates_at_t7() {
547        assert!(!is_tempo_precompile_active_at(STORAGE_CREDITS_ADDRESS, TempoHardfork::T6));
548        assert!(is_tempo_precompile_active_at(STORAGE_CREDITS_ADDRESS, TempoHardfork::T7));
549        assert!(TEMPO_PRECOMPILE_ADDRESSES.contains(&STORAGE_CREDITS_ADDRESS));
550
551        // The hardfork-filtered precompile map must honor the same T7 activation.
552        let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
553        assert!(!cfg.precompiles(Some(TempoHardfork::T6)).contains_key("StorageCredits"));
554        assert!(cfg.precompiles(Some(TempoHardfork::T7)).contains_key("StorageCredits"));
555    }
556
557    #[test]
558    fn current_committee_precompile_activates_at_t8() {
559        assert!(!is_tempo_precompile_active_at(CURRENT_COMMITTEE_ADDRESS, TempoHardfork::T7));
560        assert!(is_tempo_precompile_active_at(CURRENT_COMMITTEE_ADDRESS, TempoHardfork::T8));
561        assert!(TEMPO_PRECOMPILE_ADDRESSES.contains(&CURRENT_COMMITTEE_ADDRESS));
562
563        let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
564        assert!(!cfg.precompiles(Some(TempoHardfork::T7)).contains_key("CurrentCommittee"));
565        assert!(cfg.precompiles(Some(TempoHardfork::T8)).contains_key("CurrentCommittee"));
566    }
567
568    // --- resolved() / active_network_name ---
569
570    #[test]
571    fn active_network_name_tempo() {
572        let cfg = NetworkConfigs::with_tempo();
573        assert_eq!(cfg.active_network_name(), Some("tempo"));
574    }
575
576    #[test]
577    fn active_network_name_default_is_none() {
578        assert_eq!(NetworkConfigs::default().active_network_name(), None);
579    }
580
581    // --- Serde round-trip ---
582
583    #[test]
584    fn serde_roundtrip_tempo() {
585        let original = NetworkConfigs::with_tempo();
586        let json = serde_json::to_string(&original).unwrap();
587        let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
588        assert!(restored.is_tempo());
589    }
590
591    #[test]
592    fn serde_legacy_tempo_bool_deserialized() {
593        // Old foundry.toml format: `tempo = true`
594        let json = r#"{"tempo": true, "celo": false, "bypass_prevrandao": false}"#;
595        let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
596        assert!(cfg.is_tempo());
597    }
598
599    #[test]
600    fn serde_serializes_legacy_alias_as_canonical_network() {
601        // Legacy `tempo = true` should serialize as the canonical `network = "tempo"`,
602        // and the legacy `tempo` / `optimism` keys must not appear in the output.
603        let cfg = NetworkConfigs { tempo: true, ..Default::default() };
604        let json = serde_json::to_value(cfg).unwrap();
605        assert_eq!(json["network"], serde_json::json!("tempo"));
606        assert!(json.get("tempo").is_none(), "legacy `tempo` key should not be serialized");
607        assert!(json.get("optimism").is_none(), "legacy `optimism` key should not be serialized");
608    }
609
610    #[test]
611    fn serde_new_network_field_deserialized() {
612        let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#;
613        let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap();
614        assert!(cfg_tempo.is_tempo());
615    }
616
617    #[cfg(feature = "optimism")]
618    mod optimism {
619        use super::*;
620
621        #[test]
622        fn new_optimism_flag_equivalent_to_legacy() {
623            let via_new =
624                NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() };
625            let via_old = NetworkConfigs { optimism: true, ..Default::default() };
626            assert_eq!(via_new.is_optimism(), via_old.is_optimism());
627            assert_eq!(via_new.is_tempo(), via_old.is_tempo());
628            assert_eq!(via_new.active_network_name(), via_old.active_network_name());
629        }
630
631        #[test]
632        fn active_network_name_optimism() {
633            let cfg = NetworkConfigs::with_optimism();
634            assert_eq!(cfg.active_network_name(), Some("optimism"));
635        }
636
637        #[test]
638        fn new_flag_wins_over_legacy_when_both_set() {
639            // --network optimism --tempo: network field wins
640            let cfg = NetworkConfigs {
641                network: Some(NetworkVariant::Optimism),
642                tempo: true,
643                ..Default::default()
644            };
645            assert!(cfg.is_optimism());
646            assert!(!cfg.is_tempo());
647        }
648
649        #[test]
650        fn serde_roundtrip_optimism() {
651            let original = NetworkConfigs::with_optimism();
652            let json = serde_json::to_string(&original).unwrap();
653            let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
654            assert!(restored.is_optimism());
655            assert!(!restored.is_tempo());
656        }
657
658        #[test]
659        fn serde_optimism_field_deserialized() {
660            let json_optimism =
661                r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#;
662            let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap();
663            assert!(cfg_optimism.is_optimism());
664        }
665    }
666}