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    /// Resolves the hardfork of a script fork, whose transactions are sent to the source chain.
431    /// Ethereum overrides this to follow the source chain when it is ahead of `configured`.
432    fn script_fork_hardfork(
433        chain_id: u64,
434        timestamp: u64,
435        endpoint_hardfork: Option<FoundryHardfork>,
436        _configured: Self,
437    ) -> Option<FoundryHardfork> {
438        Self::fork_hardfork(chain_id, timestamp, endpoint_hardfork)
439    }
440
441    /// Returns exact metadata for a directly selected spec, when the family requires it.
442    /// Ethereum and Optimism retain their existing absence of metadata for direct spec overrides.
443    fn reported_hardfork(self) -> Option<FoundryHardfork> {
444        None
445    }
446}
447
448impl FromEvmVersion for SpecId {
449    fn from_evm_version(version: EvmVersion) -> Self {
450        match version {
451            EvmVersion::Homestead => Self::HOMESTEAD,
452            EvmVersion::TangerineWhistle => Self::TANGERINE,
453            EvmVersion::SpuriousDragon => Self::SPURIOUS_DRAGON,
454            EvmVersion::Byzantium => Self::BYZANTIUM,
455            EvmVersion::Constantinople => Self::PETERSBURG,
456            EvmVersion::Petersburg => Self::PETERSBURG,
457            EvmVersion::Istanbul => Self::ISTANBUL,
458            EvmVersion::Berlin => Self::BERLIN,
459            EvmVersion::London => Self::LONDON,
460            EvmVersion::Paris => Self::MERGE,
461            EvmVersion::Shanghai => Self::SHANGHAI,
462            EvmVersion::Cancun => Self::CANCUN,
463            EvmVersion::Prague => Self::PRAGUE,
464            EvmVersion::Osaka => Self::OSAKA,
465            EvmVersion::Amsterdam => Self::AMSTERDAM,
466        }
467    }
468}
469
470impl ExecutionSpec for SpecId {
471    // Returns the user-facing name for the active execution spec.
472    fn evm_version_name(&self) -> String {
473        self.to_string()
474    }
475
476    // Parses an unnamespaced Ethereum hardfork name.
477    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
478        EthereumHardfork::from_str(hardfork).ok().map(spec_id_from_ethereum_hardfork)
479    }
480
481    // Converts only Ethereum namespaced hardforks to an Ethereum spec.
482    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
483        match hardfork {
484            FoundryHardfork::Ethereum(hardfork) => Some(spec_id_from_ethereum_hardfork(hardfork)),
485            _ => None,
486        }
487    }
488
489    fn fork_hardfork(
490        _chain_id: u64,
491        _timestamp: u64,
492        _endpoint_hardfork: Option<FoundryHardfork>,
493    ) -> Option<FoundryHardfork> {
494        None
495    }
496
497    // Gas limits come from the script simulation, so it must not price below the source chain.
498    fn script_fork_hardfork(
499        chain_id: u64,
500        timestamp: u64,
501        endpoint_hardfork: Option<FoundryHardfork>,
502        configured: Self,
503    ) -> Option<FoundryHardfork> {
504        endpoint_hardfork
505            .filter(|&hardfork| Self::from_foundry_hardfork(hardfork).is_some())
506            .or_else(|| Self::historical_hardfork(chain_id, timestamp))
507            .filter(|&hardfork| {
508                Self::from_foundry_hardfork(hardfork).is_some_and(|spec| spec > configured)
509            })
510    }
511}
512
513#[cfg(feature = "optimism")]
514impl FromEvmVersion for OpSpecId {
515    fn from_evm_version(version: EvmVersion) -> Self {
516        match version {
517            EvmVersion::Homestead
518            | EvmVersion::TangerineWhistle
519            | EvmVersion::SpuriousDragon
520            | EvmVersion::Byzantium
521            | EvmVersion::Constantinople
522            | EvmVersion::Petersburg
523            | EvmVersion::Istanbul
524            | EvmVersion::Berlin
525            | EvmVersion::London
526            | EvmVersion::Paris => Self::BEDROCK,
527            EvmVersion::Shanghai => Self::CANYON,
528            EvmVersion::Cancun => Self::ECOTONE,
529            EvmVersion::Prague => Self::ISTHMUS,
530            EvmVersion::Osaka | EvmVersion::Amsterdam => Self::KARST,
531        }
532    }
533}
534
535#[cfg(feature = "optimism")]
536impl ExecutionSpec for OpSpecId {
537    // Returns the user-facing name for the active execution spec.
538    fn evm_version_name(&self) -> String {
539        let name: &'static str = (*self).into();
540        name.to_string()
541    }
542
543    // Parses an unnamespaced Optimism hardfork name.
544    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
545        OpHardfork::from_str(hardfork).ok().map(spec_id_from_optimism_hardfork)
546    }
547
548    // Converts only Optimism namespaced hardforks to an Optimism spec.
549    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
550        match hardfork {
551            FoundryHardfork::Optimism(hardfork) => Some(spec_id_from_optimism_hardfork(hardfork)),
552            _ => None,
553        }
554    }
555
556    fn fork_hardfork(
557        chain_id: u64,
558        timestamp: u64,
559        endpoint_hardfork: Option<FoundryHardfork>,
560    ) -> Option<FoundryHardfork> {
561        let endpoint_hardfork =
562            endpoint_hardfork.filter(|&hardfork| Self::from_foundry_hardfork(hardfork).is_some());
563        if endpoint_hardfork.is_some() {
564            return endpoint_hardfork;
565        }
566
567        // Base uses its own upgrade schedule. Without native Base support, retain the configured
568        // EVM version instead of applying the incompatible OP schedule.
569        #[cfg(not(feature = "base"))]
570        if matches!(
571            Chain::from_id(chain_id).named(),
572            Some(NamedChain::Base | NamedChain::BaseSepolia)
573        ) {
574            return None;
575        }
576
577        Self::historical_hardfork(chain_id, timestamp)
578    }
579
580    fn historical_hardfork(chain_id: u64, timestamp: u64) -> Option<FoundryHardfork> {
581        OpHardfork::from_chain_and_timestamp(Chain::from_id(chain_id), timestamp)
582            .map(FoundryHardfork::Optimism)
583    }
584}
585
586impl FromEvmVersion for TempoHardfork {
587    fn from_evm_version(_: EvmVersion) -> Self {
588        latest_active_tempo_hardfork()
589    }
590}
591
592impl ExecutionSpec for TempoHardfork {
593    // Returns the user-facing name for the active execution spec.
594    fn evm_version_name(&self) -> String {
595        self.to_string()
596    }
597
598    // Parses an unnamespaced Tempo hardfork name.
599    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
600        Self::from_str(hardfork).ok()
601    }
602
603    // Converts only Tempo namespaced hardforks to a Tempo spec.
604    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
605        match hardfork {
606            FoundryHardfork::Tempo(hardfork) => Some(hardfork),
607            _ => None,
608        }
609    }
610
611    fn reported_hardfork(self) -> Option<FoundryHardfork> {
612        Some(self.into())
613    }
614}
615
616#[cfg(feature = "monad")]
617impl FromEvmVersion for MonadHardfork {
618    fn from_evm_version(_: EvmVersion) -> Self {
619        Self::default()
620    }
621}
622
623#[cfg(feature = "monad")]
624impl ExecutionSpec for MonadHardfork {
625    // Returns the user-facing name for the active execution spec.
626    fn evm_version_name(&self) -> String {
627        self.to_string()
628    }
629
630    // Parses an unnamespaced Monad hardfork name.
631    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
632        Self::from_str(hardfork).ok()
633    }
634
635    // Converts only Monad namespaced hardforks to a Monad spec.
636    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
637        match hardfork {
638            #[cfg(feature = "monad")]
639            FoundryHardfork::Monad(hardfork) => Some(hardfork),
640            _ => None,
641        }
642    }
643
644    fn reported_hardfork(self) -> Option<FoundryHardfork> {
645        Some(self.into())
646    }
647}
648
649#[cfg(feature = "base")]
650impl FromEvmVersion for BaseSpecId {
651    fn from_evm_version(version: EvmVersion) -> Self {
652        let upgrade = match version {
653            EvmVersion::Homestead
654            | EvmVersion::TangerineWhistle
655            | EvmVersion::SpuriousDragon
656            | EvmVersion::Byzantium
657            | EvmVersion::Constantinople
658            | EvmVersion::Petersburg
659            | EvmVersion::Istanbul
660            | EvmVersion::Berlin
661            | EvmVersion::London
662            | EvmVersion::Paris => BaseUpgrade::Bedrock,
663            EvmVersion::Shanghai => BaseUpgrade::Canyon,
664            EvmVersion::Cancun => BaseUpgrade::Ecotone,
665            EvmVersion::Prague => BaseUpgrade::Isthmus,
666            EvmVersion::Osaka | EvmVersion::Amsterdam => BaseUpgrade::Azul,
667        };
668        Self::new(upgrade)
669    }
670}
671
672#[cfg(feature = "base")]
673impl ExecutionSpec for BaseSpecId {
674    // Returns the user-facing name for the active execution spec.
675    fn evm_version_name(&self) -> String {
676        self.to_string()
677    }
678
679    // Parses an unnamespaced Base hardfork name.
680    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
681        Self::from_str(hardfork).ok()
682    }
683
684    // Converts only Base namespaced hardforks to a Base spec.
685    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
686        match hardfork {
687            FoundryHardfork::Base(hardfork) => Some(Self::new(hardfork)),
688            _ => None,
689        }
690    }
691
692    fn reported_hardfork(self) -> Option<FoundryHardfork> {
693        Some(FoundryHardfork::Base(self.upgrade()))
694    }
695}
696
697/// Returns the spec id derived from [`EvmVersion`] for a given spec type.
698pub fn evm_spec_id<SPEC: FromEvmVersion>(evm_version: EvmVersion) -> SPEC {
699    SPEC::from_evm_version(evm_version)
700}
701
702/// Returns the latest Tempo hardfork that has an activation on a known Tempo network.
703pub fn latest_active_tempo_hardfork() -> TempoHardfork {
704    // Tempo currently publishes activation timestamps through chain-aware hardfork resolution.
705    let now = SystemTime::now()
706        .duration_since(UNIX_EPOCH)
707        .map(|duration| duration.as_secs())
708        .unwrap_or(u64::MAX);
709    TempoHardfork::from_chain_and_timestamp(4217, now)
710        .or_else(|| TempoHardfork::from_chain_and_timestamp(42431, now))
711        .unwrap_or_default()
712}
713
714// Parses an EVM version or network-specific hardfork into the given spec type.
715pub fn evm_spec_id_from_str<SPEC: ExecutionSpec>(evm_version: &str) -> Option<SPEC> {
716    let evm_version = evm_version.trim();
717
718    if let Ok(version) = EvmVersion::from_str(evm_version) {
719        return Some(evm_spec_id(version));
720    }
721
722    if let Some(spec) = SPEC::from_network_hardfork(evm_version) {
723        return Some(spec);
724    }
725
726    FoundryHardfork::from_str(evm_version).ok().and_then(SPEC::from_foundry_hardfork)
727}
728
729/// Convert a `BlockNumberOrTag` into an `EthereumHardfork`.
730pub fn ethereum_hardfork_from_block_tag(block: impl Into<BlockNumberOrTag>) -> EthereumHardfork {
731    let num = match block.into() {
732        BlockNumberOrTag::Earliest => 0,
733        BlockNumberOrTag::Number(num) => num,
734        _ => u64::MAX,
735    };
736
737    EthereumHardfork::from_mainnet_block_number(num)
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743    use alloy_hardforks::ethereum::mainnet::*;
744    use tempo_hardfork::constants::{mainnet::*, moderato::*};
745
746    #[test]
747    fn test_ethereum_spec_id_mapping() {
748        for (hardfork, expected) in [
749            (EthereumHardfork::Frontier, SpecId::FRONTIER),
750            (EthereumHardfork::Homestead, SpecId::HOMESTEAD),
751            (EthereumHardfork::Dao, SpecId::HOMESTEAD),
752            (EthereumHardfork::Tangerine, SpecId::TANGERINE),
753            (EthereumHardfork::SpuriousDragon, SpecId::SPURIOUS_DRAGON),
754            (EthereumHardfork::Byzantium, SpecId::BYZANTIUM),
755            (EthereumHardfork::Constantinople, SpecId::PETERSBURG),
756            (EthereumHardfork::Petersburg, SpecId::PETERSBURG),
757            (EthereumHardfork::Istanbul, SpecId::ISTANBUL),
758            (EthereumHardfork::MuirGlacier, SpecId::ISTANBUL),
759            (EthereumHardfork::Berlin, SpecId::BERLIN),
760            (EthereumHardfork::London, SpecId::LONDON),
761            (EthereumHardfork::ArrowGlacier, SpecId::LONDON),
762            (EthereumHardfork::GrayGlacier, SpecId::LONDON),
763            (EthereumHardfork::Paris, SpecId::MERGE),
764            (EthereumHardfork::Shanghai, SpecId::SHANGHAI),
765            (EthereumHardfork::Cancun, SpecId::CANCUN),
766            (EthereumHardfork::Prague, SpecId::PRAGUE),
767            (EthereumHardfork::Osaka, SpecId::OSAKA),
768            (EthereumHardfork::Bpo1, SpecId::OSAKA),
769            (EthereumHardfork::Bpo2, SpecId::OSAKA),
770            (EthereumHardfork::Bpo3, SpecId::OSAKA),
771            (EthereumHardfork::Bpo4, SpecId::OSAKA),
772            (EthereumHardfork::Bpo5, SpecId::OSAKA),
773            (EthereumHardfork::Amsterdam, SpecId::AMSTERDAM),
774        ] {
775            assert_eq!(spec_id_from_ethereum_hardfork(hardfork), expected, "{hardfork}");
776        }
777    }
778
779    #[test]
780    fn test_tempo_spec_id_mapping() {
781        assert_eq!(spec_id_from_tempo_hardfork(TempoHardfork::Genesis), SpecId::OSAKA);
782        assert_eq!(spec_id_from_tempo_hardfork(TempoHardfork::T8), SpecId::OSAKA);
783    }
784
785    #[test]
786    fn test_tempo_evm_version_defaults_to_latest_active_hardfork() {
787        let latest = latest_active_tempo_hardfork();
788        assert_eq!(evm_spec_id::<TempoHardfork>(EvmVersion::Osaka), latest);
789    }
790
791    #[test]
792    #[cfg(feature = "monad")]
793    fn test_monad_hardfork_parsing() {
794        assert_eq!(
795            "monad:MonadNine".parse::<FoundryHardfork>().unwrap(),
796            FoundryHardfork::Monad(MonadHardfork::MonadNine)
797        );
798        assert_eq!(
799            "monad:MonadTen".parse::<FoundryHardfork>().unwrap(),
800            FoundryHardfork::Monad(MonadHardfork::MonadTen)
801        );
802        assert_eq!(
803            "m:MonadNext".parse::<FoundryHardfork>().unwrap(),
804            FoundryHardfork::Monad(MonadHardfork::MonadNext)
805        );
806    }
807
808    #[test]
809    fn test_hardfork_namespace_parsing() {
810        let cases = [
811            ("prague", Ok(FoundryHardfork::Ethereum(EthereumHardfork::Prague))),
812            ("eth:prague", Ok(FoundryHardfork::Ethereum(EthereumHardfork::Prague))),
813            ("ethereum:Osaka", Ok(FoundryHardfork::Ethereum(EthereumHardfork::Osaka))),
814            (" ETH : Prague ", Ok(FoundryHardfork::Ethereum(EthereumHardfork::Prague))),
815            ("t:T5", Ok(FoundryHardfork::Tempo(TempoHardfork::T5))),
816            ("tempo:t5", Ok(FoundryHardfork::Tempo(TempoHardfork::T5))),
817            #[cfg(feature = "optimism")]
818            ("op:Isthmus", Ok(FoundryHardfork::Optimism(OpHardfork::Isthmus))),
819            #[cfg(feature = "optimism")]
820            ("optimism:jovian", Ok(FoundryHardfork::Optimism(OpHardfork::Jovian))),
821            #[cfg(not(feature = "optimism"))]
822            ("op:prague", Err("hardfork namespace 'op' requires the `optimism` feature")),
823            #[cfg(feature = "base")]
824            ("base:azul", Ok(FoundryHardfork::Base(BaseUpgrade::Azul))),
825            #[cfg(not(feature = "base"))]
826            ("base:prague", Err("hardfork namespace 'base' requires the `base` feature")),
827            #[cfg(feature = "monad")]
828            ("m:MonadTen", Ok(FoundryHardfork::Monad(MonadHardfork::MonadTen))),
829            #[cfg(feature = "monad")]
830            ("monad:monadnine", Ok(FoundryHardfork::Monad(MonadHardfork::MonadNine))),
831            #[cfg(not(feature = "monad"))]
832            ("monad:prague", Err("hardfork namespace 'monad' requires the `monad` feature")),
833            ("foo:prague", Err("unknown hardfork namespace 'foo'")),
834            ("Foo:Prague", Err("unknown hardfork namespace 'foo'")),
835        ];
836
837        for (input, expected) in cases {
838            assert_eq!(
839                input.parse::<FoundryHardfork>(),
840                expected.map_err(str::to_string),
841                "{input}"
842            );
843        }
844    }
845
846    #[test]
847    #[cfg(feature = "monad")]
848    fn test_monad_hardfork_serialization() {
849        assert_eq!(
850            String::from(FoundryHardfork::Monad(MonadHardfork::MonadEight)),
851            "monad:MonadEight"
852        );
853        assert_eq!(FoundryHardfork::Monad(MonadHardfork::MonadEight).namespace(), Some("monad"));
854        assert_eq!(FoundryHardfork::Monad(MonadHardfork::MonadEight).name(), "MonadEight");
855    }
856
857    #[test]
858    #[cfg(feature = "monad")]
859    fn test_monad_hardfork_spec_id_mapping() {
860        assert_eq!(SpecId::from(FoundryHardfork::Monad(MonadHardfork::MonadEight)), SpecId::PRAGUE);
861        assert_eq!(SpecId::from(FoundryHardfork::Monad(MonadHardfork::MonadNine)), SpecId::OSAKA);
862        assert_eq!(SpecId::from(FoundryHardfork::Monad(MonadHardfork::MonadTen)), SpecId::OSAKA);
863        assert_eq!(
864            MonadHardfork::from(FoundryHardfork::Monad(MonadHardfork::MonadNext)),
865            MonadHardfork::MonadNext
866        );
867    }
868
869    #[test]
870    fn test_tempo_hardfork_from_chain_and_timestamp() {
871        assert_eq!(
872            FoundryHardfork::from_chain_and_timestamp(4217, u64::MAX),
873            Some(FoundryHardfork::Tempo(TempoHardfork::T12))
874        );
875        assert_eq!(
876            FoundryHardfork::from_chain_and_timestamp(42431, u64::MAX),
877            Some(FoundryHardfork::Tempo(TempoHardfork::T12))
878        );
879
880        assert_eq!(
881            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T6_TIMESTAMP - 1),
882            Some(FoundryHardfork::Tempo(TempoHardfork::T5))
883        );
884        assert_eq!(
885            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T6_TIMESTAMP),
886            Some(FoundryHardfork::Tempo(TempoHardfork::T6))
887        );
888        assert_eq!(
889            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T7_TIMESTAMP - 1),
890            Some(FoundryHardfork::Tempo(TempoHardfork::T6))
891        );
892        assert_eq!(
893            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T7_TIMESTAMP),
894            Some(FoundryHardfork::Tempo(TempoHardfork::T7))
895        );
896        assert_eq!(
897            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T8_TIMESTAMP - 1),
898            Some(FoundryHardfork::Tempo(TempoHardfork::T7))
899        );
900        assert_eq!(
901            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T8_TIMESTAMP),
902            Some(FoundryHardfork::Tempo(TempoHardfork::T8))
903        );
904        assert_eq!(
905            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T9_TIMESTAMP - 1),
906            Some(FoundryHardfork::Tempo(TempoHardfork::T8))
907        );
908        assert_eq!(
909            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T9_TIMESTAMP),
910            Some(FoundryHardfork::Tempo(TempoHardfork::T9))
911        );
912        assert_eq!(
913            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T6_TIMESTAMP - 1),
914            Some(FoundryHardfork::Tempo(TempoHardfork::T5))
915        );
916        assert_eq!(
917            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T6_TIMESTAMP),
918            Some(FoundryHardfork::Tempo(TempoHardfork::T6))
919        );
920        assert_eq!(
921            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T7_TIMESTAMP - 1),
922            Some(FoundryHardfork::Tempo(TempoHardfork::T6))
923        );
924        assert_eq!(
925            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T7_TIMESTAMP),
926            Some(FoundryHardfork::Tempo(TempoHardfork::T7))
927        );
928        assert_eq!(
929            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T8_TIMESTAMP - 1),
930            Some(FoundryHardfork::Tempo(TempoHardfork::T7))
931        );
932        assert_eq!(
933            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T8_TIMESTAMP),
934            Some(FoundryHardfork::Tempo(TempoHardfork::T8))
935        );
936        assert_eq!(
937            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T9_TIMESTAMP - 1),
938            Some(FoundryHardfork::Tempo(TempoHardfork::T8))
939        );
940        assert_eq!(
941            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T9_TIMESTAMP),
942            Some(FoundryHardfork::Tempo(TempoHardfork::T9))
943        );
944    }
945
946    #[test]
947    #[cfg(feature = "monad")]
948    fn test_monad_hardfork_from_chain_and_timestamp() {
949        let mainnet_ten = MonadHardfork::MonadTen.mainnet_activation_timestamp().unwrap();
950        let testnet_ten = MonadHardfork::MonadTen.testnet_activation_timestamp().unwrap();
951
952        assert_eq!(
953            FoundryHardfork::from_chain_and_timestamp(
954                monad_revm::MONAD_MAINNET_CHAIN_ID,
955                1_763_648_999,
956            ),
957            Some(FoundryHardfork::Monad(MonadHardfork::MonadEight))
958        );
959        assert_eq!(
960            FoundryHardfork::from_chain_and_timestamp(
961                monad_revm::MONAD_MAINNET_CHAIN_ID,
962                1_773_930_600,
963            ),
964            Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
965        );
966        assert_eq!(
967            FoundryHardfork::from_chain_and_timestamp(
968                monad_revm::MONAD_MAINNET_CHAIN_ID,
969                mainnet_ten - 1,
970            ),
971            Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
972        );
973        assert_eq!(
974            FoundryHardfork::from_chain_and_timestamp(
975                monad_revm::MONAD_MAINNET_CHAIN_ID,
976                mainnet_ten,
977            ),
978            Some(FoundryHardfork::Monad(MonadHardfork::MonadTen))
979        );
980        assert_eq!(
981            FoundryHardfork::from_chain_and_timestamp(
982                monad_revm::MONAD_TESTNET_CHAIN_ID,
983                1_773_152_999,
984            ),
985            Some(FoundryHardfork::Monad(MonadHardfork::MonadEight))
986        );
987        assert_eq!(
988            FoundryHardfork::from_chain_and_timestamp(
989                monad_revm::MONAD_TESTNET_CHAIN_ID,
990                1_773_153_000,
991            ),
992            Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
993        );
994        assert_eq!(
995            FoundryHardfork::from_chain_and_timestamp(
996                monad_revm::MONAD_TESTNET_CHAIN_ID,
997                testnet_ten - 1,
998            ),
999            Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
1000        );
1001        assert_eq!(
1002            FoundryHardfork::from_chain_and_timestamp(
1003                monad_revm::MONAD_TESTNET_CHAIN_ID,
1004                testnet_ten,
1005            ),
1006            Some(FoundryHardfork::Monad(MonadHardfork::MonadTen))
1007        );
1008    }
1009
1010    #[test]
1011    fn test_evm_spec_id_from_str_parses_network_hardforks() {
1012        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("T3"), Some(TempoHardfork::T3));
1013        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T2"), Some(TempoHardfork::T2));
1014        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T7"), Some(TempoHardfork::T7));
1015        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T8"), Some(TempoHardfork::T8));
1016        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T13"), Some(TempoHardfork::T13));
1017        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("T13"), Some(TempoHardfork::T13));
1018        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T14"), Some(TempoHardfork::T14));
1019        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("T14"), Some(TempoHardfork::T14));
1020        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("ethereum:prague"), None);
1021
1022        #[cfg(feature = "monad")]
1023        {
1024            assert_eq!(
1025                evm_spec_id_from_str::<MonadHardfork>("MonadNine"),
1026                Some(MonadHardfork::MonadNine)
1027            );
1028            assert_eq!(
1029                evm_spec_id_from_str::<MonadHardfork>("MonadTen"),
1030                Some(MonadHardfork::MonadTen)
1031            );
1032            assert_eq!(
1033                evm_spec_id_from_str::<MonadHardfork>("monad:MonadEight"),
1034                Some(MonadHardfork::MonadEight)
1035            );
1036            assert_eq!(evm_spec_id_from_str::<MonadHardfork>("tempo:T3"), None);
1037        }
1038    }
1039
1040    #[test]
1041    fn test_hardfork_from_block_tag_numbers() {
1042        assert_eq!(
1043            ethereum_hardfork_from_block_tag(MAINNET_HOMESTEAD_BLOCK - 1),
1044            EthereumHardfork::Frontier
1045        );
1046        assert_eq!(
1047            ethereum_hardfork_from_block_tag(MAINNET_LONDON_BLOCK + 1),
1048            EthereumHardfork::London
1049        );
1050    }
1051
1052    #[test]
1053    fn test_from_chain_and_timestamp_ethereum_mainnet() {
1054        assert_eq!(
1055            FoundryHardfork::from_chain_and_timestamp(1, 0),
1056            Some(FoundryHardfork::Ethereum(EthereumHardfork::Frontier))
1057        );
1058        // Shanghai activated at timestamp 1681338455 on mainnet
1059        assert_eq!(
1060            FoundryHardfork::from_chain_and_timestamp(1, 1_681_338_455),
1061            Some(FoundryHardfork::Ethereum(EthereumHardfork::Shanghai))
1062        );
1063    }
1064
1065    #[test]
1066    fn test_from_chain_and_timestamp_sepolia() {
1067        let sepolia_chain_id = 11155111;
1068        assert!(FoundryHardfork::from_chain_and_timestamp(sepolia_chain_id, u64::MAX).is_some());
1069    }
1070
1071    #[test]
1072    fn test_from_chain_and_timestamp_unknown_chain() {
1073        assert_eq!(FoundryHardfork::from_chain_and_timestamp(999999, 0), None);
1074    }
1075
1076    #[cfg(feature = "base")]
1077    mod base {
1078        use super::*;
1079        use base_common_genesis::UpgradeConfig;
1080
1081        #[test]
1082        fn test_base_hardfork_serialization() {
1083            assert_eq!(String::from(FoundryHardfork::Base(BaseUpgrade::Azul)), "base:Azul");
1084            assert_eq!(FoundryHardfork::Base(BaseUpgrade::Azul).namespace(), Some("base"));
1085            assert_eq!(FoundryHardfork::Base(BaseUpgrade::Azul).name(), "Azul");
1086        }
1087
1088        #[test]
1089        fn test_base_hardfork_spec_id_mapping() {
1090            assert_eq!(SpecId::from(FoundryHardfork::Base(BaseUpgrade::Azul)), SpecId::OSAKA);
1091            assert_eq!(SpecId::from(FoundryHardfork::Base(BaseUpgrade::Jovian)), SpecId::PRAGUE);
1092            assert_eq!(
1093                BaseSpecId::from(FoundryHardfork::Base(BaseUpgrade::Azul)),
1094                BaseSpecId::new(BaseUpgrade::Azul)
1095            );
1096        }
1097
1098        #[test]
1099        fn test_base_hardfork_parsing() {
1100            assert_eq!(
1101                "base:Azul".parse::<FoundryHardfork>().unwrap(),
1102                FoundryHardfork::Base(BaseUpgrade::Azul)
1103            );
1104            assert_eq!(
1105                "base:Beryl".parse::<FoundryHardfork>().unwrap(),
1106                FoundryHardfork::Base(BaseUpgrade::Beryl)
1107            );
1108        }
1109
1110        #[test]
1111        fn test_base_hardfork_from_chain_and_timestamp() {
1112            let mainnet_config = UpgradeConfig::BASE_MAINNET;
1113            let sepolia_config = UpgradeConfig::BASE_SEPOLIA;
1114            assert_eq!(
1115                FoundryHardfork::from_chain_and_timestamp(8453, u64::MAX),
1116                Some(FoundryHardfork::Base(BaseUpgrade::Beryl))
1117            );
1118            assert_eq!(
1119                FoundryHardfork::from_chain_and_timestamp(84532, u64::MAX),
1120                Some(FoundryHardfork::Base(BaseUpgrade::Beryl))
1121            );
1122            assert_eq!(
1123                FoundryHardfork::from_chain_and_timestamp(8453, mainnet_config.base.azul.unwrap()),
1124                Some(FoundryHardfork::Base(BaseUpgrade::Azul))
1125            );
1126            assert_eq!(
1127                FoundryHardfork::from_chain_and_timestamp(84532, sepolia_config.base.azul.unwrap()),
1128                Some(FoundryHardfork::Base(BaseUpgrade::Azul))
1129            );
1130            assert_eq!(
1131                FoundryHardfork::from_chain_and_timestamp(
1132                    8453,
1133                    mainnet_config.base.azul.unwrap() - 1
1134                ),
1135                Some(FoundryHardfork::Base(BaseUpgrade::Jovian))
1136            );
1137            assert_eq!(
1138                FoundryHardfork::from_chain_and_timestamp(
1139                    84532,
1140                    sepolia_config.base.azul.unwrap() - 1
1141                ),
1142                Some(FoundryHardfork::Base(BaseUpgrade::Jovian))
1143            );
1144        }
1145
1146        #[test]
1147        fn test_evm_spec_id_from_str_parses_base_upgrades() {
1148            assert_eq!(
1149                evm_spec_id_from_str::<BaseSpecId>("Azul"),
1150                Some(BaseSpecId::new(BaseUpgrade::Azul))
1151            );
1152            assert_eq!(
1153                evm_spec_id_from_str::<BaseSpecId>("base:Beryl"),
1154                Some(BaseSpecId::new(BaseUpgrade::Beryl))
1155            );
1156            assert_eq!(evm_spec_id_from_str::<BaseSpecId>("tempo:T3"), None);
1157        }
1158    }
1159
1160    #[cfg(feature = "optimism")]
1161    mod optimism {
1162        use super::*;
1163
1164        #[cfg(feature = "base")]
1165        use base_common_genesis::UpgradeConfig;
1166
1167        #[test]
1168        fn test_optimism_spec_id_mapping() {
1169            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Bedrock), OpSpecId::BEDROCK);
1170            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Regolith), OpSpecId::REGOLITH);
1171
1172            // Test latest hardforks
1173            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Holocene), OpSpecId::HOLOCENE);
1174            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Karst), OpSpecId::KARST);
1175            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Lagoon), OpSpecId::LAGOON);
1176            assert_eq!(eth_spec_id_from_optimism_hardfork(OpHardfork::Jovian), SpecId::PRAGUE);
1177            assert_eq!(eth_spec_id_from_optimism_hardfork(OpHardfork::Karst), SpecId::OSAKA);
1178            assert_eq!(eth_spec_id_from_optimism_hardfork(OpHardfork::Lagoon), SpecId::OSAKA);
1179            assert_eq!(evm_spec_id::<OpSpecId>(EvmVersion::Osaka), OpSpecId::KARST);
1180        }
1181
1182        #[test]
1183        fn test_from_chain_and_timestamp_op_mainnet() {
1184            let op_chain_id = 10;
1185            assert!(matches!(
1186                FoundryHardfork::from_chain_and_timestamp(op_chain_id, u64::MAX),
1187                Some(FoundryHardfork::Optimism(_))
1188            ));
1189        }
1190
1191        #[test]
1192        #[cfg(feature = "base")]
1193        fn test_base_chain_historical_hardfork_is_execution_family_specific() {
1194            let chains = [
1195                (8453, UpgradeConfig::BASE_MAINNET.ecotone_time.unwrap()),
1196                (84532, UpgradeConfig::BASE_SEPOLIA.ecotone_time.unwrap()),
1197            ];
1198
1199            for (chain_id, timestamp) in chains {
1200                assert_eq!(
1201                    FoundryHardfork::from_chain_and_timestamp(chain_id, timestamp),
1202                    Some(FoundryHardfork::Base(BaseUpgrade::Ecotone))
1203                );
1204                assert_eq!(
1205                    BaseSpecId::historical_hardfork(chain_id, timestamp),
1206                    Some(FoundryHardfork::Base(BaseUpgrade::Ecotone))
1207                );
1208                assert_eq!(
1209                    OpSpecId::historical_hardfork(chain_id, timestamp),
1210                    Some(FoundryHardfork::Optimism(OpHardfork::Ecotone))
1211                );
1212                assert_eq!(
1213                    OpSpecId::fork_hardfork(chain_id, timestamp, None),
1214                    Some(FoundryHardfork::Optimism(OpHardfork::Ecotone))
1215                );
1216            }
1217        }
1218
1219        /// Base is an OP-stack chain, so without the `base` feature its chain IDs must still map
1220        /// to an Optimism hardfork rather than resolving to nothing.
1221        #[test]
1222        #[cfg(not(feature = "base"))]
1223        fn test_from_chain_and_timestamp_base() {
1224            let base_chain_id = 8453;
1225            assert!(matches!(
1226                FoundryHardfork::from_chain_and_timestamp(base_chain_id, u64::MAX),
1227                Some(FoundryHardfork::Optimism(_))
1228            ));
1229        }
1230    }
1231}