Skip to main content

foundry_evm_hardforks/
lib.rs

1//! EVM hardfork definitions for Foundry.
2//!
3//! Provides [`FoundryHardfork`], a unified enum over Ethereum, Optimism, Tempo, and Monad hardforks
4//! with `FromStr`/`Serialize`/`Deserialize` support for CLI and config usage.
5
6use alloy_chains::Chain;
7use alloy_rpc_types::BlockNumberOrTag;
8use foundry_compilers::artifacts::EvmVersion;
9use revm::primitives::hardfork::SpecId;
10use serde::{Deserialize, Serialize};
11use std::{
12    str::FromStr,
13    time::{SystemTime, UNIX_EPOCH},
14};
15
16#[cfg(all(feature = "optimism", not(feature = "base")))]
17use alloy_chains::NamedChain;
18#[cfg(feature = "optimism")]
19use op_revm::OpSpecId;
20
21pub use alloy_hardforks::EthereumHardfork;
22pub use tempo_hardfork::TempoHardfork;
23
24#[cfg(feature = "base")]
25pub use base_common_evm::BaseSpecId;
26#[cfg(feature = "base")]
27pub use base_common_genesis::BaseUpgrade;
28
29#[cfg(feature = "monad")]
30pub use monad_revm::MonadHardfork;
31
32#[cfg(feature = "optimism")]
33pub use alloy_op_hardforks::OpHardfork;
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
36#[serde(into = "String")]
37pub enum FoundryHardfork {
38    Ethereum(EthereumHardfork),
39    #[cfg(feature = "optimism")]
40    Optimism(OpHardfork),
41    #[cfg(feature = "base")]
42    Base(BaseUpgrade),
43    Tempo(TempoHardfork),
44    #[cfg(feature = "monad")]
45    Monad(MonadHardfork),
46}
47
48impl From<FoundryHardfork> for String {
49    fn from(fork: FoundryHardfork) -> Self {
50        match fork {
51            FoundryHardfork::Ethereum(h) => format!("{h}"),
52            #[cfg(feature = "optimism")]
53            FoundryHardfork::Optimism(h) => format!("optimism:{h}"),
54            #[cfg(feature = "base")]
55            FoundryHardfork::Base(h) => format!("base:{h}"),
56            FoundryHardfork::Tempo(h) => format!("tempo:{h}"),
57            #[cfg(feature = "monad")]
58            FoundryHardfork::Monad(h) => format!("monad:{h}"),
59        }
60    }
61}
62
63impl<'de> Deserialize<'de> for FoundryHardfork {
64    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65    where
66        D: serde::Deserializer<'de>,
67    {
68        let s = String::deserialize(deserializer)?;
69        Self::from_str(&s).map_err(serde::de::Error::custom)
70    }
71}
72
73impl FromStr for FoundryHardfork {
74    type Err = String;
75
76    fn from_str(s: &str) -> Result<Self, Self::Err> {
77        let raw = s.trim();
78
79        let Some((ns, fork_raw)) = raw.split_once(':') else {
80            return EthereumHardfork::from_str(raw)
81                .map(Self::Ethereum)
82                .map_err(|_| format!("unknown ethereum hardfork '{raw}'"));
83        };
84
85        let ns = ns.trim().to_ascii_lowercase();
86        let fork = fork_raw.trim().to_ascii_lowercase().replace(['-', ' '], "_");
87
88        match ns.as_str() {
89            "eth" | "ethereum" => EthereumHardfork::from_str(&fork)
90                .map(Self::Ethereum)
91                .map_err(|_| format!("unknown ethereum hardfork '{fork_raw}'")),
92
93            #[cfg(feature = "optimism")]
94            "op" | "optimism" => OpHardfork::from_str(&fork)
95                .map(Self::Optimism)
96                .map_err(|_| format!("unknown optimism hardfork '{fork_raw}'")),
97            #[cfg(not(feature = "optimism"))]
98            "op" | "optimism" => {
99                Err(format!("hardfork namespace '{ns}' requires the `optimism` feature"))
100            }
101
102            #[cfg(feature = "base")]
103            "base" => BaseUpgrade::from_str(&fork)
104                .map(Self::Base)
105                .map_err(|_| format!("unknown base hardfork '{fork_raw}'")),
106            #[cfg(not(feature = "base"))]
107            "base" => Err(format!("hardfork namespace '{ns}' requires the `base` feature")),
108
109            "t" | "tempo" => TempoHardfork::from_str(&fork)
110                .map(Self::Tempo)
111                .map_err(|_| format!("unknown tempo hardfork '{fork_raw}'")),
112
113            #[cfg(feature = "monad")]
114            "m" | "monad" => MonadHardfork::from_str(&fork)
115                .map(Self::Monad)
116                .map_err(|_| format!("unknown monad hardfork '{fork_raw}'")),
117            #[cfg(not(feature = "monad"))]
118            "m" | "monad" => Err(format!("hardfork namespace '{ns}' requires the `monad` feature")),
119
120            _ => Err(format!("unknown hardfork namespace '{ns}'")),
121        }
122    }
123}
124
125impl FoundryHardfork {
126    pub const fn ethereum(h: EthereumHardfork) -> Self {
127        Self::Ethereum(h)
128    }
129
130    #[cfg(feature = "optimism")]
131    pub const fn optimism(h: OpHardfork) -> Self {
132        Self::Optimism(h)
133    }
134
135    #[cfg(feature = "base")]
136    pub const fn base(h: BaseUpgrade) -> Self {
137        Self::Base(h)
138    }
139
140    pub const fn tempo(h: TempoHardfork) -> Self {
141        Self::Tempo(h)
142    }
143
144    #[cfg(feature = "monad")]
145    pub const fn monad(h: MonadHardfork) -> Self {
146        Self::Monad(h)
147    }
148
149    /// Returns the hardfork name without a network namespace prefix.
150    pub fn name(&self) -> String {
151        match self {
152            Self::Ethereum(h) => format!("{h}"),
153            #[cfg(feature = "optimism")]
154            Self::Optimism(h) => format!("{h}"),
155            #[cfg(feature = "base")]
156            Self::Base(h) => format!("{h}"),
157            Self::Tempo(h) => format!("{h}"),
158            #[cfg(feature = "monad")]
159            Self::Monad(h) => format!("{h}"),
160        }
161    }
162
163    /// Returns the network namespace for this hardfork, or `None` for plain Ethereum.
164    ///
165    /// Mirrors the namespace prefix used in the `"network:hardfork"` serialization format.
166    pub const fn namespace(&self) -> Option<&'static str> {
167        match self {
168            Self::Ethereum(_) => None,
169            #[cfg(feature = "optimism")]
170            Self::Optimism(_) => Some("optimism"),
171            #[cfg(feature = "base")]
172            Self::Base(_) => Some("base"),
173            Self::Tempo(_) => Some("tempo"),
174            #[cfg(feature = "monad")]
175            Self::Monad(_) => Some("monad"),
176        }
177    }
178
179    /// Auto-detect the active hardfork for a given chain at a specific timestamp.
180    ///
181    /// Tries Ethereum, then Optimism, Tempo, and Monad. Returns `None` for unknown chains.
182    pub fn from_chain_and_timestamp(chain_id: u64, timestamp: u64) -> Option<Self> {
183        let chain = Chain::from_id(chain_id);
184        if let Some(fork) = EthereumHardfork::from_chain_and_timestamp(chain, timestamp) {
185            return Some(Self::Ethereum(fork));
186        }
187        #[cfg(feature = "base")]
188        if let Some(fork) = BaseUpgrade::from_chain_and_timestamp(chain_id, timestamp) {
189            return Some(Self::Base(fork));
190        }
191        #[cfg(feature = "optimism")]
192        if let Some(fork) = OpHardfork::from_chain_and_timestamp(chain, timestamp) {
193            return Some(Self::Optimism(fork));
194        }
195        if let Some(fork) = TempoHardfork::from_chain_and_timestamp(chain_id, timestamp) {
196            return Some(Self::Tempo(fork));
197        }
198        #[cfg(feature = "monad")]
199        if let Some(fork) = MonadHardfork::from_chain_and_timestamp(chain_id, timestamp) {
200            return Some(Self::Monad(fork));
201        }
202        None
203    }
204}
205
206impl From<EthereumHardfork> for FoundryHardfork {
207    fn from(value: EthereumHardfork) -> Self {
208        Self::Ethereum(value)
209    }
210}
211
212impl From<FoundryHardfork> for EthereumHardfork {
213    fn from(fork: FoundryHardfork) -> Self {
214        match fork {
215            FoundryHardfork::Ethereum(hardfork) => hardfork,
216            _ => Self::default(),
217        }
218    }
219}
220
221#[cfg(feature = "optimism")]
222impl From<OpHardfork> for FoundryHardfork {
223    fn from(value: OpHardfork) -> Self {
224        Self::Optimism(value)
225    }
226}
227
228#[cfg(feature = "optimism")]
229impl From<FoundryHardfork> for OpHardfork {
230    fn from(fork: FoundryHardfork) -> Self {
231        match fork {
232            FoundryHardfork::Optimism(hardfork) => hardfork,
233            _ => Self::default(),
234        }
235    }
236}
237
238#[cfg(feature = "base")]
239impl From<BaseUpgrade> for FoundryHardfork {
240    fn from(value: BaseUpgrade) -> Self {
241        Self::Base(value)
242    }
243}
244
245#[cfg(feature = "base")]
246impl From<FoundryHardfork> for BaseUpgrade {
247    fn from(fork: FoundryHardfork) -> Self {
248        match fork {
249            FoundryHardfork::Base(upgrade) => upgrade,
250            _ => Self::default(),
251        }
252    }
253}
254
255#[cfg(feature = "base")]
256impl From<FoundryHardfork> for BaseSpecId {
257    fn from(fork: FoundryHardfork) -> Self {
258        match fork {
259            FoundryHardfork::Base(upgrade) => Self::new(upgrade),
260            _ => Self::default(),
261        }
262    }
263}
264
265impl From<TempoHardfork> for FoundryHardfork {
266    fn from(value: TempoHardfork) -> Self {
267        Self::Tempo(value)
268    }
269}
270
271impl From<FoundryHardfork> for TempoHardfork {
272    fn from(fork: FoundryHardfork) -> Self {
273        match fork {
274            FoundryHardfork::Tempo(hardfork) => hardfork,
275            _ => Self::default(),
276        }
277    }
278}
279
280#[cfg(feature = "monad")]
281impl From<MonadHardfork> for FoundryHardfork {
282    fn from(value: MonadHardfork) -> Self {
283        Self::Monad(value)
284    }
285}
286
287#[cfg(feature = "monad")]
288impl From<FoundryHardfork> for MonadHardfork {
289    fn from(fork: FoundryHardfork) -> Self {
290        match fork {
291            FoundryHardfork::Monad(hardfork) => hardfork,
292            _ => Self::default(),
293        }
294    }
295}
296
297impl From<FoundryHardfork> for SpecId {
298    fn from(fork: FoundryHardfork) -> Self {
299        match fork {
300            FoundryHardfork::Ethereum(hardfork) => spec_id_from_ethereum_hardfork(hardfork),
301            #[cfg(feature = "optimism")]
302            FoundryHardfork::Optimism(hardfork) => eth_spec_id_from_optimism_hardfork(hardfork),
303            #[cfg(feature = "base")]
304            FoundryHardfork::Base(hardfork) => BaseSpecId::new(hardfork).into_eth_spec(),
305            FoundryHardfork::Tempo(hardfork) => spec_id_from_tempo_hardfork(hardfork),
306            #[cfg(feature = "monad")]
307            FoundryHardfork::Monad(hardfork) => hardfork.into(),
308        }
309    }
310}
311
312#[cfg(feature = "optimism")]
313impl From<FoundryHardfork> for OpSpecId {
314    fn from(fork: FoundryHardfork) -> Self {
315        match fork {
316            FoundryHardfork::Optimism(hardfork) => spec_id_from_optimism_hardfork(hardfork),
317            _ => Self::default(),
318        }
319    }
320}
321
322/// Map an `EthereumHardfork` enum into its corresponding `SpecId`.
323pub fn spec_id_from_ethereum_hardfork(hardfork: EthereumHardfork) -> SpecId {
324    match hardfork {
325        EthereumHardfork::Frontier => SpecId::FRONTIER,
326        EthereumHardfork::Homestead => SpecId::HOMESTEAD,
327        EthereumHardfork::Dao => SpecId::HOMESTEAD,
328        EthereumHardfork::Tangerine => SpecId::TANGERINE,
329        EthereumHardfork::SpuriousDragon => SpecId::SPURIOUS_DRAGON,
330        EthereumHardfork::Byzantium => SpecId::BYZANTIUM,
331        EthereumHardfork::Constantinople => SpecId::PETERSBURG,
332        EthereumHardfork::Petersburg => SpecId::PETERSBURG,
333        EthereumHardfork::Istanbul => SpecId::ISTANBUL,
334        EthereumHardfork::MuirGlacier => SpecId::ISTANBUL,
335        EthereumHardfork::Berlin => SpecId::BERLIN,
336        EthereumHardfork::London => SpecId::LONDON,
337        EthereumHardfork::ArrowGlacier => SpecId::LONDON,
338        EthereumHardfork::GrayGlacier => SpecId::LONDON,
339        EthereumHardfork::Paris => SpecId::MERGE,
340        EthereumHardfork::Shanghai => SpecId::SHANGHAI,
341        EthereumHardfork::Cancun => SpecId::CANCUN,
342        EthereumHardfork::Prague => SpecId::PRAGUE,
343        EthereumHardfork::Osaka => SpecId::OSAKA,
344        EthereumHardfork::Bpo1
345        | EthereumHardfork::Bpo2
346        | EthereumHardfork::Bpo3
347        | EthereumHardfork::Bpo4
348        | EthereumHardfork::Bpo5 => SpecId::OSAKA,
349        EthereumHardfork::Amsterdam => SpecId::AMSTERDAM,
350        f => unreachable!("unimplemented {}", f),
351    }
352}
353
354/// Map an `OptimismHardfork` enum into its corresponding `OpSpecId`.
355#[cfg(feature = "optimism")]
356pub fn spec_id_from_optimism_hardfork(hardfork: OpHardfork) -> OpSpecId {
357    match hardfork {
358        OpHardfork::Bedrock => OpSpecId::BEDROCK,
359        OpHardfork::Regolith => OpSpecId::REGOLITH,
360        OpHardfork::Canyon => OpSpecId::CANYON,
361        OpHardfork::Ecotone => OpSpecId::ECOTONE,
362        OpHardfork::Fjord => OpSpecId::FJORD,
363        OpHardfork::Granite => OpSpecId::GRANITE,
364        OpHardfork::Holocene => OpSpecId::HOLOCENE,
365        OpHardfork::Isthmus => OpSpecId::ISTHMUS,
366        OpHardfork::Jovian => OpSpecId::JOVIAN,
367        OpHardfork::Karst => OpSpecId::KARST,
368        OpHardfork::Lagoon => OpSpecId::LAGOON,
369        f => unreachable!("unimplemented {}", f),
370    }
371}
372
373/// Map an `OptimismHardfork` enum into its corresponding Ethereum `SpecId`.
374#[cfg(feature = "optimism")]
375pub fn eth_spec_id_from_optimism_hardfork(hardfork: OpHardfork) -> SpecId {
376    match hardfork {
377        OpHardfork::Bedrock | OpHardfork::Regolith => SpecId::MERGE,
378        OpHardfork::Canyon => SpecId::SHANGHAI,
379        OpHardfork::Ecotone | OpHardfork::Fjord | OpHardfork::Granite | OpHardfork::Holocene => {
380            SpecId::CANCUN
381        }
382        OpHardfork::Isthmus | OpHardfork::Jovian => SpecId::PRAGUE,
383        OpHardfork::Karst | OpHardfork::Lagoon => SpecId::OSAKA,
384        f => unreachable!("unimplemented {}", f),
385    }
386}
387
388/// Map a `TempoHardfork` enum into its corresponding Ethereum `SpecId`.
389pub const fn spec_id_from_tempo_hardfork(_: TempoHardfork) -> SpecId {
390    SpecId::OSAKA
391}
392
393/// Trait for converting an [`EvmVersion`] into a network-specific spec type.
394pub trait FromEvmVersion: From<FoundryHardfork> {
395    fn from_evm_version(version: EvmVersion) -> Self;
396}
397
398/// Conversion, hardfork scheduling, and trace metadata for a concrete execution spec.
399pub trait ExecutionSpec: FromEvmVersion {
400    // Returns the user-facing name for the active execution spec.
401    fn evm_version_name(&self) -> String;
402
403    // Parses an unnamespaced hardfork name for the active network.
404    fn from_network_hardfork(_: &str) -> Option<Self> {
405        None
406    }
407
408    // Converts a namespaced Foundry hardfork if it belongs to this spec family.
409    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self>;
410
411    /// Returns this family's hardfork at a historical source-chain timestamp.
412    /// Unknown source chains return `None`; this never discovers another execution family.
413    fn historical_hardfork(chain_id: u64, timestamp: u64) -> Option<FoundryHardfork> {
414        FoundryHardfork::from_chain_and_timestamp(chain_id, timestamp)
415            .filter(|&hardfork| Self::from_foundry_hardfork(hardfork).is_some())
416    }
417
418    /// Resolves a local fork's hardfork from compatible endpoint metadata or its source schedule.
419    /// Ethereum overrides this to retain the configured EVM version for local forks.
420    fn fork_hardfork(
421        chain_id: u64,
422        timestamp: u64,
423        endpoint_hardfork: Option<FoundryHardfork>,
424    ) -> Option<FoundryHardfork> {
425        endpoint_hardfork
426            .filter(|&hardfork| Self::from_foundry_hardfork(hardfork).is_some())
427            .or_else(|| Self::historical_hardfork(chain_id, timestamp))
428    }
429
430    /// Returns exact metadata for a directly selected spec, when the family requires it.
431    /// Ethereum and Optimism retain their existing absence of metadata for direct spec overrides.
432    fn reported_hardfork(self) -> Option<FoundryHardfork> {
433        None
434    }
435}
436
437impl FromEvmVersion for SpecId {
438    fn from_evm_version(version: EvmVersion) -> Self {
439        match version {
440            EvmVersion::Homestead => Self::HOMESTEAD,
441            EvmVersion::TangerineWhistle => Self::TANGERINE,
442            EvmVersion::SpuriousDragon => Self::SPURIOUS_DRAGON,
443            EvmVersion::Byzantium => Self::BYZANTIUM,
444            EvmVersion::Constantinople => Self::PETERSBURG,
445            EvmVersion::Petersburg => Self::PETERSBURG,
446            EvmVersion::Istanbul => Self::ISTANBUL,
447            EvmVersion::Berlin => Self::BERLIN,
448            EvmVersion::London => Self::LONDON,
449            EvmVersion::Paris => Self::MERGE,
450            EvmVersion::Shanghai => Self::SHANGHAI,
451            EvmVersion::Cancun => Self::CANCUN,
452            EvmVersion::Prague => Self::PRAGUE,
453            EvmVersion::Osaka => Self::OSAKA,
454            EvmVersion::Amsterdam => Self::AMSTERDAM,
455        }
456    }
457}
458
459impl ExecutionSpec for SpecId {
460    // Returns the user-facing name for the active execution spec.
461    fn evm_version_name(&self) -> String {
462        self.to_string()
463    }
464
465    // Parses an unnamespaced Ethereum hardfork name.
466    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
467        EthereumHardfork::from_str(hardfork).ok().map(spec_id_from_ethereum_hardfork)
468    }
469
470    // Converts only Ethereum namespaced hardforks to an Ethereum spec.
471    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
472        match hardfork {
473            FoundryHardfork::Ethereum(hardfork) => Some(spec_id_from_ethereum_hardfork(hardfork)),
474            _ => None,
475        }
476    }
477
478    fn fork_hardfork(
479        _chain_id: u64,
480        _timestamp: u64,
481        _endpoint_hardfork: Option<FoundryHardfork>,
482    ) -> Option<FoundryHardfork> {
483        None
484    }
485}
486
487#[cfg(feature = "optimism")]
488impl FromEvmVersion for OpSpecId {
489    fn from_evm_version(version: EvmVersion) -> Self {
490        match version {
491            EvmVersion::Homestead
492            | EvmVersion::TangerineWhistle
493            | EvmVersion::SpuriousDragon
494            | EvmVersion::Byzantium
495            | EvmVersion::Constantinople
496            | EvmVersion::Petersburg
497            | EvmVersion::Istanbul
498            | EvmVersion::Berlin
499            | EvmVersion::London
500            | EvmVersion::Paris => Self::BEDROCK,
501            EvmVersion::Shanghai => Self::CANYON,
502            EvmVersion::Cancun => Self::ECOTONE,
503            EvmVersion::Prague => Self::ISTHMUS,
504            EvmVersion::Osaka | EvmVersion::Amsterdam => Self::KARST,
505        }
506    }
507}
508
509#[cfg(feature = "optimism")]
510impl ExecutionSpec for OpSpecId {
511    // Returns the user-facing name for the active execution spec.
512    fn evm_version_name(&self) -> String {
513        let name: &'static str = (*self).into();
514        name.to_string()
515    }
516
517    // Parses an unnamespaced Optimism hardfork name.
518    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
519        OpHardfork::from_str(hardfork).ok().map(spec_id_from_optimism_hardfork)
520    }
521
522    // Converts only Optimism namespaced hardforks to an Optimism spec.
523    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
524        match hardfork {
525            FoundryHardfork::Optimism(hardfork) => Some(spec_id_from_optimism_hardfork(hardfork)),
526            _ => None,
527        }
528    }
529
530    fn fork_hardfork(
531        chain_id: u64,
532        timestamp: u64,
533        endpoint_hardfork: Option<FoundryHardfork>,
534    ) -> Option<FoundryHardfork> {
535        let endpoint_hardfork =
536            endpoint_hardfork.filter(|&hardfork| Self::from_foundry_hardfork(hardfork).is_some());
537        if endpoint_hardfork.is_some() {
538            return endpoint_hardfork;
539        }
540
541        // Base uses its own upgrade schedule. Without native Base support, retain the configured
542        // EVM version instead of applying the incompatible OP schedule.
543        #[cfg(not(feature = "base"))]
544        if matches!(
545            Chain::from_id(chain_id).named(),
546            Some(NamedChain::Base | NamedChain::BaseSepolia)
547        ) {
548            return None;
549        }
550
551        Self::historical_hardfork(chain_id, timestamp)
552    }
553
554    fn historical_hardfork(chain_id: u64, timestamp: u64) -> Option<FoundryHardfork> {
555        OpHardfork::from_chain_and_timestamp(Chain::from_id(chain_id), timestamp)
556            .map(FoundryHardfork::Optimism)
557    }
558}
559
560impl FromEvmVersion for TempoHardfork {
561    fn from_evm_version(_: EvmVersion) -> Self {
562        latest_active_tempo_hardfork()
563    }
564}
565
566impl ExecutionSpec for TempoHardfork {
567    // Returns the user-facing name for the active execution spec.
568    fn evm_version_name(&self) -> String {
569        self.to_string()
570    }
571
572    // Parses an unnamespaced Tempo hardfork name.
573    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
574        Self::from_str(hardfork).ok()
575    }
576
577    // Converts only Tempo namespaced hardforks to a Tempo spec.
578    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
579        match hardfork {
580            FoundryHardfork::Tempo(hardfork) => Some(hardfork),
581            _ => None,
582        }
583    }
584
585    fn reported_hardfork(self) -> Option<FoundryHardfork> {
586        Some(self.into())
587    }
588}
589
590#[cfg(feature = "monad")]
591impl FromEvmVersion for MonadHardfork {
592    fn from_evm_version(_: EvmVersion) -> Self {
593        Self::default()
594    }
595}
596
597#[cfg(feature = "monad")]
598impl ExecutionSpec for MonadHardfork {
599    // Returns the user-facing name for the active execution spec.
600    fn evm_version_name(&self) -> String {
601        self.to_string()
602    }
603
604    // Parses an unnamespaced Monad hardfork name.
605    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
606        Self::from_str(hardfork).ok()
607    }
608
609    // Converts only Monad namespaced hardforks to a Monad spec.
610    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
611        match hardfork {
612            #[cfg(feature = "monad")]
613            FoundryHardfork::Monad(hardfork) => Some(hardfork),
614            _ => None,
615        }
616    }
617
618    fn reported_hardfork(self) -> Option<FoundryHardfork> {
619        Some(self.into())
620    }
621}
622
623#[cfg(feature = "base")]
624impl FromEvmVersion for BaseSpecId {
625    fn from_evm_version(version: EvmVersion) -> Self {
626        let upgrade = match version {
627            EvmVersion::Homestead
628            | EvmVersion::TangerineWhistle
629            | EvmVersion::SpuriousDragon
630            | EvmVersion::Byzantium
631            | EvmVersion::Constantinople
632            | EvmVersion::Petersburg
633            | EvmVersion::Istanbul
634            | EvmVersion::Berlin
635            | EvmVersion::London
636            | EvmVersion::Paris => BaseUpgrade::Bedrock,
637            EvmVersion::Shanghai => BaseUpgrade::Canyon,
638            EvmVersion::Cancun => BaseUpgrade::Ecotone,
639            EvmVersion::Prague => BaseUpgrade::Isthmus,
640            EvmVersion::Osaka | EvmVersion::Amsterdam => BaseUpgrade::Azul,
641        };
642        Self::new(upgrade)
643    }
644}
645
646#[cfg(feature = "base")]
647impl ExecutionSpec for BaseSpecId {
648    // Returns the user-facing name for the active execution spec.
649    fn evm_version_name(&self) -> String {
650        self.to_string()
651    }
652
653    // Parses an unnamespaced Base hardfork name.
654    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
655        Self::from_str(hardfork).ok()
656    }
657
658    // Converts only Base namespaced hardforks to a Base spec.
659    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
660        match hardfork {
661            FoundryHardfork::Base(hardfork) => Some(Self::new(hardfork)),
662            _ => None,
663        }
664    }
665
666    fn reported_hardfork(self) -> Option<FoundryHardfork> {
667        Some(FoundryHardfork::Base(self.upgrade()))
668    }
669}
670
671/// Returns the spec id derived from [`EvmVersion`] for a given spec type.
672pub fn evm_spec_id<SPEC: FromEvmVersion>(evm_version: EvmVersion) -> SPEC {
673    SPEC::from_evm_version(evm_version)
674}
675
676/// Returns the latest Tempo hardfork that has an activation on a known Tempo network.
677pub fn latest_active_tempo_hardfork() -> TempoHardfork {
678    // Tempo currently publishes activation timestamps through chain-aware hardfork resolution.
679    let now = SystemTime::now()
680        .duration_since(UNIX_EPOCH)
681        .map(|duration| duration.as_secs())
682        .unwrap_or(u64::MAX);
683    TempoHardfork::from_chain_and_timestamp(4217, now)
684        .or_else(|| TempoHardfork::from_chain_and_timestamp(42431, now))
685        .unwrap_or_default()
686}
687
688// Parses an EVM version or network-specific hardfork into the given spec type.
689pub fn evm_spec_id_from_str<SPEC: ExecutionSpec>(evm_version: &str) -> Option<SPEC> {
690    let evm_version = evm_version.trim();
691
692    if let Ok(version) = EvmVersion::from_str(evm_version) {
693        return Some(evm_spec_id(version));
694    }
695
696    if let Some(spec) = SPEC::from_network_hardfork(evm_version) {
697        return Some(spec);
698    }
699
700    FoundryHardfork::from_str(evm_version).ok().and_then(SPEC::from_foundry_hardfork)
701}
702
703/// Convert a `BlockNumberOrTag` into an `EthereumHardfork`.
704pub fn ethereum_hardfork_from_block_tag(block: impl Into<BlockNumberOrTag>) -> EthereumHardfork {
705    let num = match block.into() {
706        BlockNumberOrTag::Earliest => 0,
707        BlockNumberOrTag::Number(num) => num,
708        _ => u64::MAX,
709    };
710
711    EthereumHardfork::from_mainnet_block_number(num)
712}
713
714#[cfg(test)]
715mod tests {
716    use super::*;
717    use alloy_hardforks::ethereum::mainnet::*;
718    use tempo_hardfork::constants::{mainnet::*, moderato::*};
719
720    #[test]
721    fn test_ethereum_spec_id_mapping() {
722        for (hardfork, expected) in [
723            (EthereumHardfork::Frontier, SpecId::FRONTIER),
724            (EthereumHardfork::Homestead, SpecId::HOMESTEAD),
725            (EthereumHardfork::Dao, SpecId::HOMESTEAD),
726            (EthereumHardfork::Tangerine, SpecId::TANGERINE),
727            (EthereumHardfork::SpuriousDragon, SpecId::SPURIOUS_DRAGON),
728            (EthereumHardfork::Byzantium, SpecId::BYZANTIUM),
729            (EthereumHardfork::Constantinople, SpecId::PETERSBURG),
730            (EthereumHardfork::Petersburg, SpecId::PETERSBURG),
731            (EthereumHardfork::Istanbul, SpecId::ISTANBUL),
732            (EthereumHardfork::MuirGlacier, SpecId::ISTANBUL),
733            (EthereumHardfork::Berlin, SpecId::BERLIN),
734            (EthereumHardfork::London, SpecId::LONDON),
735            (EthereumHardfork::ArrowGlacier, SpecId::LONDON),
736            (EthereumHardfork::GrayGlacier, SpecId::LONDON),
737            (EthereumHardfork::Paris, SpecId::MERGE),
738            (EthereumHardfork::Shanghai, SpecId::SHANGHAI),
739            (EthereumHardfork::Cancun, SpecId::CANCUN),
740            (EthereumHardfork::Prague, SpecId::PRAGUE),
741            (EthereumHardfork::Osaka, SpecId::OSAKA),
742            (EthereumHardfork::Bpo1, SpecId::OSAKA),
743            (EthereumHardfork::Bpo2, SpecId::OSAKA),
744            (EthereumHardfork::Bpo3, SpecId::OSAKA),
745            (EthereumHardfork::Bpo4, SpecId::OSAKA),
746            (EthereumHardfork::Bpo5, SpecId::OSAKA),
747            (EthereumHardfork::Amsterdam, SpecId::AMSTERDAM),
748        ] {
749            assert_eq!(spec_id_from_ethereum_hardfork(hardfork), expected, "{hardfork}");
750        }
751    }
752
753    #[test]
754    fn test_tempo_spec_id_mapping() {
755        assert_eq!(spec_id_from_tempo_hardfork(TempoHardfork::Genesis), SpecId::OSAKA);
756        assert_eq!(spec_id_from_tempo_hardfork(TempoHardfork::T8), SpecId::OSAKA);
757    }
758
759    #[test]
760    fn test_tempo_evm_version_defaults_to_latest_active_hardfork() {
761        let latest = latest_active_tempo_hardfork();
762        assert_eq!(evm_spec_id::<TempoHardfork>(EvmVersion::Osaka), latest);
763    }
764
765    #[test]
766    #[cfg(feature = "monad")]
767    fn test_monad_hardfork_parsing() {
768        assert_eq!(
769            "monad:MonadNine".parse::<FoundryHardfork>().unwrap(),
770            FoundryHardfork::Monad(MonadHardfork::MonadNine)
771        );
772        assert_eq!(
773            "monad:MonadTen".parse::<FoundryHardfork>().unwrap(),
774            FoundryHardfork::Monad(MonadHardfork::MonadTen)
775        );
776        assert_eq!(
777            "m:MonadNext".parse::<FoundryHardfork>().unwrap(),
778            FoundryHardfork::Monad(MonadHardfork::MonadNext)
779        );
780    }
781
782    #[test]
783    fn test_hardfork_namespace_parsing() {
784        let cases = [
785            ("prague", Ok(FoundryHardfork::Ethereum(EthereumHardfork::Prague))),
786            ("eth:prague", Ok(FoundryHardfork::Ethereum(EthereumHardfork::Prague))),
787            ("ethereum:Osaka", Ok(FoundryHardfork::Ethereum(EthereumHardfork::Osaka))),
788            (" ETH : Prague ", Ok(FoundryHardfork::Ethereum(EthereumHardfork::Prague))),
789            ("t:T5", Ok(FoundryHardfork::Tempo(TempoHardfork::T5))),
790            ("tempo:t5", Ok(FoundryHardfork::Tempo(TempoHardfork::T5))),
791            #[cfg(feature = "optimism")]
792            ("op:Isthmus", Ok(FoundryHardfork::Optimism(OpHardfork::Isthmus))),
793            #[cfg(feature = "optimism")]
794            ("optimism:jovian", Ok(FoundryHardfork::Optimism(OpHardfork::Jovian))),
795            #[cfg(not(feature = "optimism"))]
796            ("op:prague", Err("hardfork namespace 'op' requires the `optimism` feature")),
797            #[cfg(feature = "base")]
798            ("base:azul", Ok(FoundryHardfork::Base(BaseUpgrade::Azul))),
799            #[cfg(not(feature = "base"))]
800            ("base:prague", Err("hardfork namespace 'base' requires the `base` feature")),
801            #[cfg(feature = "monad")]
802            ("m:MonadTen", Ok(FoundryHardfork::Monad(MonadHardfork::MonadTen))),
803            #[cfg(feature = "monad")]
804            ("monad:monadnine", Ok(FoundryHardfork::Monad(MonadHardfork::MonadNine))),
805            #[cfg(not(feature = "monad"))]
806            ("monad:prague", Err("hardfork namespace 'monad' requires the `monad` feature")),
807            ("foo:prague", Err("unknown hardfork namespace 'foo'")),
808            ("Foo:Prague", Err("unknown hardfork namespace 'foo'")),
809        ];
810
811        for (input, expected) in cases {
812            assert_eq!(
813                input.parse::<FoundryHardfork>(),
814                expected.map_err(str::to_string),
815                "{input}"
816            );
817        }
818    }
819
820    #[test]
821    #[cfg(feature = "monad")]
822    fn test_monad_hardfork_serialization() {
823        assert_eq!(
824            String::from(FoundryHardfork::Monad(MonadHardfork::MonadEight)),
825            "monad:MonadEight"
826        );
827        assert_eq!(FoundryHardfork::Monad(MonadHardfork::MonadEight).namespace(), Some("monad"));
828        assert_eq!(FoundryHardfork::Monad(MonadHardfork::MonadEight).name(), "MonadEight");
829    }
830
831    #[test]
832    #[cfg(feature = "monad")]
833    fn test_monad_hardfork_spec_id_mapping() {
834        assert_eq!(SpecId::from(FoundryHardfork::Monad(MonadHardfork::MonadEight)), SpecId::PRAGUE);
835        assert_eq!(SpecId::from(FoundryHardfork::Monad(MonadHardfork::MonadNine)), SpecId::OSAKA);
836        assert_eq!(SpecId::from(FoundryHardfork::Monad(MonadHardfork::MonadTen)), SpecId::OSAKA);
837        assert_eq!(
838            MonadHardfork::from(FoundryHardfork::Monad(MonadHardfork::MonadNext)),
839            MonadHardfork::MonadNext
840        );
841    }
842
843    #[test]
844    fn test_tempo_hardfork_from_chain_and_timestamp() {
845        assert_eq!(
846            FoundryHardfork::from_chain_and_timestamp(4217, u64::MAX),
847            Some(FoundryHardfork::Tempo(TempoHardfork::T12))
848        );
849        assert_eq!(
850            FoundryHardfork::from_chain_and_timestamp(42431, u64::MAX),
851            Some(FoundryHardfork::Tempo(TempoHardfork::T12))
852        );
853
854        assert_eq!(
855            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T6_TIMESTAMP - 1),
856            Some(FoundryHardfork::Tempo(TempoHardfork::T5))
857        );
858        assert_eq!(
859            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T6_TIMESTAMP),
860            Some(FoundryHardfork::Tempo(TempoHardfork::T6))
861        );
862        assert_eq!(
863            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T7_TIMESTAMP - 1),
864            Some(FoundryHardfork::Tempo(TempoHardfork::T6))
865        );
866        assert_eq!(
867            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T7_TIMESTAMP),
868            Some(FoundryHardfork::Tempo(TempoHardfork::T7))
869        );
870        assert_eq!(
871            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T8_TIMESTAMP - 1),
872            Some(FoundryHardfork::Tempo(TempoHardfork::T7))
873        );
874        assert_eq!(
875            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T8_TIMESTAMP),
876            Some(FoundryHardfork::Tempo(TempoHardfork::T8))
877        );
878        assert_eq!(
879            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T9_TIMESTAMP - 1),
880            Some(FoundryHardfork::Tempo(TempoHardfork::T8))
881        );
882        assert_eq!(
883            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T9_TIMESTAMP),
884            Some(FoundryHardfork::Tempo(TempoHardfork::T9))
885        );
886        assert_eq!(
887            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T6_TIMESTAMP - 1),
888            Some(FoundryHardfork::Tempo(TempoHardfork::T5))
889        );
890        assert_eq!(
891            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T6_TIMESTAMP),
892            Some(FoundryHardfork::Tempo(TempoHardfork::T6))
893        );
894        assert_eq!(
895            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T7_TIMESTAMP - 1),
896            Some(FoundryHardfork::Tempo(TempoHardfork::T6))
897        );
898        assert_eq!(
899            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T7_TIMESTAMP),
900            Some(FoundryHardfork::Tempo(TempoHardfork::T7))
901        );
902        assert_eq!(
903            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T8_TIMESTAMP - 1),
904            Some(FoundryHardfork::Tempo(TempoHardfork::T7))
905        );
906        assert_eq!(
907            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T8_TIMESTAMP),
908            Some(FoundryHardfork::Tempo(TempoHardfork::T8))
909        );
910        assert_eq!(
911            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T9_TIMESTAMP - 1),
912            Some(FoundryHardfork::Tempo(TempoHardfork::T8))
913        );
914        assert_eq!(
915            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T9_TIMESTAMP),
916            Some(FoundryHardfork::Tempo(TempoHardfork::T9))
917        );
918    }
919
920    #[test]
921    #[cfg(feature = "monad")]
922    fn test_monad_hardfork_from_chain_and_timestamp() {
923        let mainnet_ten = MonadHardfork::MonadTen.mainnet_activation_timestamp().unwrap();
924        let testnet_ten = MonadHardfork::MonadTen.testnet_activation_timestamp().unwrap();
925
926        assert_eq!(
927            FoundryHardfork::from_chain_and_timestamp(
928                monad_revm::MONAD_MAINNET_CHAIN_ID,
929                1_763_648_999,
930            ),
931            Some(FoundryHardfork::Monad(MonadHardfork::MonadEight))
932        );
933        assert_eq!(
934            FoundryHardfork::from_chain_and_timestamp(
935                monad_revm::MONAD_MAINNET_CHAIN_ID,
936                1_773_930_600,
937            ),
938            Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
939        );
940        assert_eq!(
941            FoundryHardfork::from_chain_and_timestamp(
942                monad_revm::MONAD_MAINNET_CHAIN_ID,
943                mainnet_ten - 1,
944            ),
945            Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
946        );
947        assert_eq!(
948            FoundryHardfork::from_chain_and_timestamp(
949                monad_revm::MONAD_MAINNET_CHAIN_ID,
950                mainnet_ten,
951            ),
952            Some(FoundryHardfork::Monad(MonadHardfork::MonadTen))
953        );
954        assert_eq!(
955            FoundryHardfork::from_chain_and_timestamp(
956                monad_revm::MONAD_TESTNET_CHAIN_ID,
957                1_773_152_999,
958            ),
959            Some(FoundryHardfork::Monad(MonadHardfork::MonadEight))
960        );
961        assert_eq!(
962            FoundryHardfork::from_chain_and_timestamp(
963                monad_revm::MONAD_TESTNET_CHAIN_ID,
964                1_773_153_000,
965            ),
966            Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
967        );
968        assert_eq!(
969            FoundryHardfork::from_chain_and_timestamp(
970                monad_revm::MONAD_TESTNET_CHAIN_ID,
971                testnet_ten - 1,
972            ),
973            Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
974        );
975        assert_eq!(
976            FoundryHardfork::from_chain_and_timestamp(
977                monad_revm::MONAD_TESTNET_CHAIN_ID,
978                testnet_ten,
979            ),
980            Some(FoundryHardfork::Monad(MonadHardfork::MonadTen))
981        );
982    }
983
984    #[test]
985    fn test_evm_spec_id_from_str_parses_network_hardforks() {
986        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("T3"), Some(TempoHardfork::T3));
987        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T2"), Some(TempoHardfork::T2));
988        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T7"), Some(TempoHardfork::T7));
989        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T8"), Some(TempoHardfork::T8));
990        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T13"), Some(TempoHardfork::T13));
991        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("T13"), Some(TempoHardfork::T13));
992        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T14"), Some(TempoHardfork::T14));
993        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("T14"), Some(TempoHardfork::T14));
994        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("ethereum:prague"), None);
995
996        #[cfg(feature = "monad")]
997        {
998            assert_eq!(
999                evm_spec_id_from_str::<MonadHardfork>("MonadNine"),
1000                Some(MonadHardfork::MonadNine)
1001            );
1002            assert_eq!(
1003                evm_spec_id_from_str::<MonadHardfork>("MonadTen"),
1004                Some(MonadHardfork::MonadTen)
1005            );
1006            assert_eq!(
1007                evm_spec_id_from_str::<MonadHardfork>("monad:MonadEight"),
1008                Some(MonadHardfork::MonadEight)
1009            );
1010            assert_eq!(evm_spec_id_from_str::<MonadHardfork>("tempo:T3"), None);
1011        }
1012    }
1013
1014    #[test]
1015    fn test_hardfork_from_block_tag_numbers() {
1016        assert_eq!(
1017            ethereum_hardfork_from_block_tag(MAINNET_HOMESTEAD_BLOCK - 1),
1018            EthereumHardfork::Frontier
1019        );
1020        assert_eq!(
1021            ethereum_hardfork_from_block_tag(MAINNET_LONDON_BLOCK + 1),
1022            EthereumHardfork::London
1023        );
1024    }
1025
1026    #[test]
1027    fn test_from_chain_and_timestamp_ethereum_mainnet() {
1028        assert_eq!(
1029            FoundryHardfork::from_chain_and_timestamp(1, 0),
1030            Some(FoundryHardfork::Ethereum(EthereumHardfork::Frontier))
1031        );
1032        // Shanghai activated at timestamp 1681338455 on mainnet
1033        assert_eq!(
1034            FoundryHardfork::from_chain_and_timestamp(1, 1_681_338_455),
1035            Some(FoundryHardfork::Ethereum(EthereumHardfork::Shanghai))
1036        );
1037    }
1038
1039    #[test]
1040    fn test_from_chain_and_timestamp_sepolia() {
1041        let sepolia_chain_id = 11155111;
1042        assert!(FoundryHardfork::from_chain_and_timestamp(sepolia_chain_id, u64::MAX).is_some());
1043    }
1044
1045    #[test]
1046    fn test_from_chain_and_timestamp_unknown_chain() {
1047        assert_eq!(FoundryHardfork::from_chain_and_timestamp(999999, 0), None);
1048    }
1049
1050    #[cfg(feature = "base")]
1051    mod base {
1052        use super::*;
1053        use base_common_genesis::UpgradeConfig;
1054
1055        #[test]
1056        fn test_base_hardfork_serialization() {
1057            assert_eq!(String::from(FoundryHardfork::Base(BaseUpgrade::Azul)), "base:Azul");
1058            assert_eq!(FoundryHardfork::Base(BaseUpgrade::Azul).namespace(), Some("base"));
1059            assert_eq!(FoundryHardfork::Base(BaseUpgrade::Azul).name(), "Azul");
1060        }
1061
1062        #[test]
1063        fn test_base_hardfork_spec_id_mapping() {
1064            assert_eq!(SpecId::from(FoundryHardfork::Base(BaseUpgrade::Azul)), SpecId::OSAKA);
1065            assert_eq!(SpecId::from(FoundryHardfork::Base(BaseUpgrade::Jovian)), SpecId::PRAGUE);
1066            assert_eq!(
1067                BaseSpecId::from(FoundryHardfork::Base(BaseUpgrade::Azul)),
1068                BaseSpecId::new(BaseUpgrade::Azul)
1069            );
1070        }
1071
1072        #[test]
1073        fn test_base_hardfork_parsing() {
1074            assert_eq!(
1075                "base:Azul".parse::<FoundryHardfork>().unwrap(),
1076                FoundryHardfork::Base(BaseUpgrade::Azul)
1077            );
1078            assert_eq!(
1079                "base:Beryl".parse::<FoundryHardfork>().unwrap(),
1080                FoundryHardfork::Base(BaseUpgrade::Beryl)
1081            );
1082        }
1083
1084        #[test]
1085        fn test_base_hardfork_from_chain_and_timestamp() {
1086            let mainnet_config = UpgradeConfig::BASE_MAINNET;
1087            let sepolia_config = UpgradeConfig::BASE_SEPOLIA;
1088            assert_eq!(
1089                FoundryHardfork::from_chain_and_timestamp(8453, u64::MAX),
1090                Some(FoundryHardfork::Base(BaseUpgrade::Beryl))
1091            );
1092            assert_eq!(
1093                FoundryHardfork::from_chain_and_timestamp(84532, u64::MAX),
1094                Some(FoundryHardfork::Base(BaseUpgrade::Beryl))
1095            );
1096            assert_eq!(
1097                FoundryHardfork::from_chain_and_timestamp(8453, mainnet_config.base.azul.unwrap()),
1098                Some(FoundryHardfork::Base(BaseUpgrade::Azul))
1099            );
1100            assert_eq!(
1101                FoundryHardfork::from_chain_and_timestamp(84532, sepolia_config.base.azul.unwrap()),
1102                Some(FoundryHardfork::Base(BaseUpgrade::Azul))
1103            );
1104            assert_eq!(
1105                FoundryHardfork::from_chain_and_timestamp(
1106                    8453,
1107                    mainnet_config.base.azul.unwrap() - 1
1108                ),
1109                Some(FoundryHardfork::Base(BaseUpgrade::Jovian))
1110            );
1111            assert_eq!(
1112                FoundryHardfork::from_chain_and_timestamp(
1113                    84532,
1114                    sepolia_config.base.azul.unwrap() - 1
1115                ),
1116                Some(FoundryHardfork::Base(BaseUpgrade::Jovian))
1117            );
1118        }
1119
1120        #[test]
1121        fn test_evm_spec_id_from_str_parses_base_upgrades() {
1122            assert_eq!(
1123                evm_spec_id_from_str::<BaseSpecId>("Azul"),
1124                Some(BaseSpecId::new(BaseUpgrade::Azul))
1125            );
1126            assert_eq!(
1127                evm_spec_id_from_str::<BaseSpecId>("base:Beryl"),
1128                Some(BaseSpecId::new(BaseUpgrade::Beryl))
1129            );
1130            assert_eq!(evm_spec_id_from_str::<BaseSpecId>("tempo:T3"), None);
1131        }
1132    }
1133
1134    #[cfg(feature = "optimism")]
1135    mod optimism {
1136        use super::*;
1137
1138        #[cfg(feature = "base")]
1139        use base_common_genesis::UpgradeConfig;
1140
1141        #[test]
1142        fn test_optimism_spec_id_mapping() {
1143            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Bedrock), OpSpecId::BEDROCK);
1144            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Regolith), OpSpecId::REGOLITH);
1145
1146            // Test latest hardforks
1147            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Holocene), OpSpecId::HOLOCENE);
1148            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Karst), OpSpecId::KARST);
1149            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Lagoon), OpSpecId::LAGOON);
1150            assert_eq!(eth_spec_id_from_optimism_hardfork(OpHardfork::Jovian), SpecId::PRAGUE);
1151            assert_eq!(eth_spec_id_from_optimism_hardfork(OpHardfork::Karst), SpecId::OSAKA);
1152            assert_eq!(eth_spec_id_from_optimism_hardfork(OpHardfork::Lagoon), SpecId::OSAKA);
1153            assert_eq!(evm_spec_id::<OpSpecId>(EvmVersion::Osaka), OpSpecId::KARST);
1154        }
1155
1156        #[test]
1157        fn test_from_chain_and_timestamp_op_mainnet() {
1158            let op_chain_id = 10;
1159            assert!(matches!(
1160                FoundryHardfork::from_chain_and_timestamp(op_chain_id, u64::MAX),
1161                Some(FoundryHardfork::Optimism(_))
1162            ));
1163        }
1164
1165        #[test]
1166        #[cfg(feature = "base")]
1167        fn test_base_chain_historical_hardfork_is_execution_family_specific() {
1168            let chains = [
1169                (8453, UpgradeConfig::BASE_MAINNET.ecotone_time.unwrap()),
1170                (84532, UpgradeConfig::BASE_SEPOLIA.ecotone_time.unwrap()),
1171            ];
1172
1173            for (chain_id, timestamp) in chains {
1174                assert_eq!(
1175                    FoundryHardfork::from_chain_and_timestamp(chain_id, timestamp),
1176                    Some(FoundryHardfork::Base(BaseUpgrade::Ecotone))
1177                );
1178                assert_eq!(
1179                    BaseSpecId::historical_hardfork(chain_id, timestamp),
1180                    Some(FoundryHardfork::Base(BaseUpgrade::Ecotone))
1181                );
1182                assert_eq!(
1183                    OpSpecId::historical_hardfork(chain_id, timestamp),
1184                    Some(FoundryHardfork::Optimism(OpHardfork::Ecotone))
1185                );
1186                assert_eq!(
1187                    OpSpecId::fork_hardfork(chain_id, timestamp, None),
1188                    Some(FoundryHardfork::Optimism(OpHardfork::Ecotone))
1189                );
1190            }
1191        }
1192
1193        /// Base is an OP-stack chain, so without the `base` feature its chain IDs must still map
1194        /// to an Optimism hardfork rather than resolving to nothing.
1195        #[test]
1196        #[cfg(not(feature = "base"))]
1197        fn test_from_chain_and_timestamp_base() {
1198            let base_chain_id = 8453;
1199            assert!(matches!(
1200                FoundryHardfork::from_chain_and_timestamp(base_chain_id, u64::MAX),
1201                Some(FoundryHardfork::Optimism(_))
1202            ));
1203        }
1204    }
1205}