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 std::{
7    str::FromStr,
8    time::{SystemTime, UNIX_EPOCH},
9};
10
11use alloy_chains::Chain;
12use alloy_rpc_types::BlockNumberOrTag;
13use foundry_compilers::artifacts::EvmVersion;
14#[cfg(feature = "optimism")]
15use op_revm::OpSpecId;
16use revm::primitives::hardfork::SpecId;
17use serde::{Deserialize, Serialize};
18
19pub use alloy_hardforks::EthereumHardfork;
20#[cfg(feature = "optimism")]
21pub use alloy_op_hardforks::OpHardfork;
22#[cfg(feature = "monad")]
23pub use monad_revm::MonadHardfork;
24pub use tempo_hardfork::TempoHardfork;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
27#[serde(into = "String")]
28pub enum FoundryHardfork {
29    Ethereum(EthereumHardfork),
30    #[cfg(feature = "optimism")]
31    Optimism(OpHardfork),
32    Tempo(TempoHardfork),
33    #[cfg(feature = "monad")]
34    Monad(MonadHardfork),
35}
36
37impl From<FoundryHardfork> for String {
38    fn from(fork: FoundryHardfork) -> Self {
39        match fork {
40            FoundryHardfork::Ethereum(h) => format!("{h}"),
41            #[cfg(feature = "optimism")]
42            FoundryHardfork::Optimism(h) => format!("optimism:{h}"),
43            FoundryHardfork::Tempo(h) => format!("tempo:{h}"),
44            #[cfg(feature = "monad")]
45            FoundryHardfork::Monad(h) => format!("monad:{h}"),
46        }
47    }
48}
49
50impl<'de> Deserialize<'de> for FoundryHardfork {
51    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
52    where
53        D: serde::Deserializer<'de>,
54    {
55        let s = String::deserialize(deserializer)?;
56        Self::from_str(&s).map_err(serde::de::Error::custom)
57    }
58}
59
60impl FromStr for FoundryHardfork {
61    type Err = String;
62
63    fn from_str(s: &str) -> Result<Self, Self::Err> {
64        let raw = s.trim();
65
66        let Some((ns, fork_raw)) = raw.split_once(':') else {
67            return EthereumHardfork::from_str(raw)
68                .map(Self::Ethereum)
69                .map_err(|_| format!("unknown ethereum hardfork '{raw}'"));
70        };
71
72        let ns = ns.trim().to_ascii_lowercase();
73        let fork = fork_raw.trim().to_ascii_lowercase().replace(['-', ' '], "_");
74
75        match ns.as_str() {
76            "eth" | "ethereum" => EthereumHardfork::from_str(&fork)
77                .map(Self::Ethereum)
78                .map_err(|_| format!("unknown ethereum hardfork '{fork_raw}'")),
79
80            #[cfg(feature = "optimism")]
81            "op" | "optimism" => OpHardfork::from_str(&fork)
82                .map(Self::Optimism)
83                .map_err(|_| format!("unknown optimism hardfork '{fork_raw}'")),
84
85            "t" | "tempo" => TempoHardfork::from_str(&fork)
86                .map(Self::Tempo)
87                .map_err(|_| format!("unknown tempo hardfork '{fork_raw}'")),
88
89            #[cfg(feature = "monad")]
90            "m" | "monad" => MonadHardfork::from_str(&fork)
91                .map(Self::Monad)
92                .map_err(|_| format!("unknown monad hardfork '{fork_raw}'")),
93            _ => EthereumHardfork::from_str(&fork)
94                .map(Self::Ethereum)
95                .map_err(|_| format!("unknown hardfork '{raw}'")),
96        }
97    }
98}
99
100impl FoundryHardfork {
101    pub const fn ethereum(h: EthereumHardfork) -> Self {
102        Self::Ethereum(h)
103    }
104
105    #[cfg(feature = "optimism")]
106    pub const fn optimism(h: OpHardfork) -> Self {
107        Self::Optimism(h)
108    }
109
110    pub const fn tempo(h: TempoHardfork) -> Self {
111        Self::Tempo(h)
112    }
113
114    #[cfg(feature = "monad")]
115    pub const fn monad(h: MonadHardfork) -> Self {
116        Self::Monad(h)
117    }
118
119    /// Returns the hardfork name without a network namespace prefix.
120    pub fn name(&self) -> String {
121        match self {
122            Self::Ethereum(h) => format!("{h}"),
123            #[cfg(feature = "optimism")]
124            Self::Optimism(h) => format!("{h}"),
125            Self::Tempo(h) => format!("{h}"),
126            #[cfg(feature = "monad")]
127            Self::Monad(h) => format!("{h}"),
128        }
129    }
130
131    /// Returns the network namespace for this hardfork, or `None` for plain Ethereum.
132    ///
133    /// Mirrors the namespace prefix used in the `"network:hardfork"` serialization format.
134    pub const fn namespace(&self) -> Option<&'static str> {
135        match self {
136            Self::Ethereum(_) => None,
137            #[cfg(feature = "optimism")]
138            Self::Optimism(_) => Some("optimism"),
139            Self::Tempo(_) => Some("tempo"),
140            #[cfg(feature = "monad")]
141            Self::Monad(_) => Some("monad"),
142        }
143    }
144
145    /// Auto-detect the active hardfork for a given chain at a specific timestamp.
146    ///
147    /// Tries Ethereum, then Optimism, Tempo, and Monad. Returns `None` for unknown chains.
148    pub fn from_chain_and_timestamp(chain_id: u64, timestamp: u64) -> Option<Self> {
149        let chain = Chain::from_id(chain_id);
150        if let Some(fork) = EthereumHardfork::from_chain_and_timestamp(chain, timestamp) {
151            return Some(Self::Ethereum(fork));
152        }
153        #[cfg(feature = "optimism")]
154        if let Some(fork) = OpHardfork::from_chain_and_timestamp(chain, timestamp) {
155            return Some(Self::Optimism(fork));
156        }
157        if let Some(fork) = TempoHardfork::from_chain_and_timestamp(chain_id, timestamp) {
158            return Some(Self::Tempo(fork));
159        }
160        #[cfg(feature = "monad")]
161        if let Some(fork) = MonadHardfork::from_chain_and_timestamp(chain_id, timestamp) {
162            return Some(Self::Monad(fork));
163        }
164        None
165    }
166}
167
168impl From<EthereumHardfork> for FoundryHardfork {
169    fn from(value: EthereumHardfork) -> Self {
170        Self::Ethereum(value)
171    }
172}
173
174impl From<FoundryHardfork> for EthereumHardfork {
175    fn from(fork: FoundryHardfork) -> Self {
176        match fork {
177            FoundryHardfork::Ethereum(hardfork) => hardfork,
178            _ => Self::default(),
179        }
180    }
181}
182
183#[cfg(feature = "optimism")]
184impl From<OpHardfork> for FoundryHardfork {
185    fn from(value: OpHardfork) -> Self {
186        Self::Optimism(value)
187    }
188}
189
190#[cfg(feature = "optimism")]
191impl From<FoundryHardfork> for OpHardfork {
192    fn from(fork: FoundryHardfork) -> Self {
193        match fork {
194            FoundryHardfork::Optimism(hardfork) => hardfork,
195            _ => Self::default(),
196        }
197    }
198}
199
200impl From<TempoHardfork> for FoundryHardfork {
201    fn from(value: TempoHardfork) -> Self {
202        Self::Tempo(value)
203    }
204}
205
206impl From<FoundryHardfork> for TempoHardfork {
207    fn from(fork: FoundryHardfork) -> Self {
208        match fork {
209            FoundryHardfork::Tempo(hardfork) => hardfork,
210            _ => Self::default(),
211        }
212    }
213}
214
215#[cfg(feature = "monad")]
216impl From<MonadHardfork> for FoundryHardfork {
217    fn from(value: MonadHardfork) -> Self {
218        Self::Monad(value)
219    }
220}
221
222#[cfg(feature = "monad")]
223impl From<FoundryHardfork> for MonadHardfork {
224    fn from(fork: FoundryHardfork) -> Self {
225        match fork {
226            FoundryHardfork::Monad(hardfork) => hardfork,
227            _ => Self::default(),
228        }
229    }
230}
231
232impl From<FoundryHardfork> for SpecId {
233    fn from(fork: FoundryHardfork) -> Self {
234        match fork {
235            FoundryHardfork::Ethereum(hardfork) => spec_id_from_ethereum_hardfork(hardfork),
236            #[cfg(feature = "optimism")]
237            FoundryHardfork::Optimism(hardfork) => eth_spec_id_from_optimism_hardfork(hardfork),
238            FoundryHardfork::Tempo(hardfork) => spec_id_from_tempo_hardfork(hardfork),
239            #[cfg(feature = "monad")]
240            FoundryHardfork::Monad(hardfork) => hardfork.into(),
241        }
242    }
243}
244
245#[cfg(feature = "optimism")]
246impl From<FoundryHardfork> for OpSpecId {
247    fn from(fork: FoundryHardfork) -> Self {
248        match fork {
249            FoundryHardfork::Optimism(hardfork) => spec_id_from_optimism_hardfork(hardfork),
250            _ => Self::default(),
251        }
252    }
253}
254
255/// Map an `EthereumHardfork` enum into its corresponding `SpecId`.
256pub fn spec_id_from_ethereum_hardfork(hardfork: EthereumHardfork) -> SpecId {
257    match hardfork {
258        EthereumHardfork::Frontier => SpecId::FRONTIER,
259        EthereumHardfork::Homestead => SpecId::HOMESTEAD,
260        EthereumHardfork::Dao => SpecId::HOMESTEAD,
261        EthereumHardfork::Tangerine => SpecId::TANGERINE,
262        EthereumHardfork::SpuriousDragon => SpecId::SPURIOUS_DRAGON,
263        EthereumHardfork::Byzantium => SpecId::BYZANTIUM,
264        EthereumHardfork::Constantinople => SpecId::PETERSBURG,
265        EthereumHardfork::Petersburg => SpecId::PETERSBURG,
266        EthereumHardfork::Istanbul => SpecId::ISTANBUL,
267        EthereumHardfork::MuirGlacier => SpecId::ISTANBUL,
268        EthereumHardfork::Berlin => SpecId::BERLIN,
269        EthereumHardfork::London => SpecId::LONDON,
270        EthereumHardfork::ArrowGlacier => SpecId::LONDON,
271        EthereumHardfork::GrayGlacier => SpecId::LONDON,
272        EthereumHardfork::Paris => SpecId::MERGE,
273        EthereumHardfork::Shanghai => SpecId::SHANGHAI,
274        EthereumHardfork::Cancun => SpecId::CANCUN,
275        EthereumHardfork::Prague => SpecId::PRAGUE,
276        EthereumHardfork::Osaka => SpecId::OSAKA,
277        EthereumHardfork::Bpo1 | EthereumHardfork::Bpo2 => SpecId::OSAKA,
278        EthereumHardfork::Bpo3 | EthereumHardfork::Bpo4 | EthereumHardfork::Bpo5 => {
279            unimplemented!()
280        }
281        EthereumHardfork::Amsterdam => SpecId::AMSTERDAM,
282        f => unreachable!("unimplemented {}", f),
283    }
284}
285
286/// Map an `OptimismHardfork` enum into its corresponding `OpSpecId`.
287#[cfg(feature = "optimism")]
288pub fn spec_id_from_optimism_hardfork(hardfork: OpHardfork) -> OpSpecId {
289    match hardfork {
290        OpHardfork::Bedrock => OpSpecId::BEDROCK,
291        OpHardfork::Regolith => OpSpecId::REGOLITH,
292        OpHardfork::Canyon => OpSpecId::CANYON,
293        OpHardfork::Ecotone => OpSpecId::ECOTONE,
294        OpHardfork::Fjord => OpSpecId::FJORD,
295        OpHardfork::Granite => OpSpecId::GRANITE,
296        OpHardfork::Holocene => OpSpecId::HOLOCENE,
297        OpHardfork::Isthmus => OpSpecId::ISTHMUS,
298        OpHardfork::Jovian => OpSpecId::JOVIAN,
299        OpHardfork::Karst => OpSpecId::KARST,
300        OpHardfork::Lagoon => OpSpecId::LAGOON,
301        f => unreachable!("unimplemented {}", f),
302    }
303}
304
305/// Map an `OptimismHardfork` enum into its corresponding Ethereum `SpecId`.
306#[cfg(feature = "optimism")]
307pub fn eth_spec_id_from_optimism_hardfork(hardfork: OpHardfork) -> SpecId {
308    match hardfork {
309        OpHardfork::Bedrock | OpHardfork::Regolith => SpecId::MERGE,
310        OpHardfork::Canyon => SpecId::SHANGHAI,
311        OpHardfork::Ecotone | OpHardfork::Fjord | OpHardfork::Granite | OpHardfork::Holocene => {
312            SpecId::CANCUN
313        }
314        OpHardfork::Isthmus | OpHardfork::Jovian => SpecId::PRAGUE,
315        OpHardfork::Karst | OpHardfork::Lagoon => SpecId::OSAKA,
316        f => unreachable!("unimplemented {}", f),
317    }
318}
319
320/// Map a `TempoHardfork` enum into its corresponding Ethereum `SpecId`.
321pub const fn spec_id_from_tempo_hardfork(_: TempoHardfork) -> SpecId {
322    SpecId::OSAKA
323}
324
325/// Trait for converting an [`EvmVersion`] into a network-specific spec type.
326pub trait FromEvmVersion: From<FoundryHardfork> {
327    fn from_evm_version(version: EvmVersion) -> Self;
328}
329
330/// Trait for parsing and displaying a network-specific execution spec.
331pub trait ExecutionSpec: FromEvmVersion {
332    // Returns the user-facing name for the active execution spec.
333    fn evm_version_name(&self) -> String;
334
335    // Parses an unnamespaced hardfork name for the active network.
336    fn from_network_hardfork(_: &str) -> Option<Self> {
337        None
338    }
339
340    // Converts a namespaced Foundry hardfork if it belongs to this spec family.
341    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self>;
342}
343
344impl FromEvmVersion for SpecId {
345    fn from_evm_version(version: EvmVersion) -> Self {
346        match version {
347            EvmVersion::Homestead => Self::HOMESTEAD,
348            EvmVersion::TangerineWhistle => Self::TANGERINE,
349            EvmVersion::SpuriousDragon => Self::SPURIOUS_DRAGON,
350            EvmVersion::Byzantium => Self::BYZANTIUM,
351            EvmVersion::Constantinople => Self::PETERSBURG,
352            EvmVersion::Petersburg => Self::PETERSBURG,
353            EvmVersion::Istanbul => Self::ISTANBUL,
354            EvmVersion::Berlin => Self::BERLIN,
355            EvmVersion::London => Self::LONDON,
356            EvmVersion::Paris => Self::MERGE,
357            EvmVersion::Shanghai => Self::SHANGHAI,
358            EvmVersion::Cancun => Self::CANCUN,
359            EvmVersion::Prague => Self::PRAGUE,
360            EvmVersion::Osaka => Self::OSAKA,
361            EvmVersion::Amsterdam => Self::AMSTERDAM,
362        }
363    }
364}
365
366impl ExecutionSpec for SpecId {
367    // Returns the user-facing name for the active execution spec.
368    fn evm_version_name(&self) -> String {
369        self.to_string()
370    }
371
372    // Parses an unnamespaced Ethereum hardfork name.
373    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
374        EthereumHardfork::from_str(hardfork).ok().map(spec_id_from_ethereum_hardfork)
375    }
376
377    // Converts only Ethereum namespaced hardforks to an Ethereum spec.
378    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
379        match hardfork {
380            FoundryHardfork::Ethereum(hardfork) => Some(spec_id_from_ethereum_hardfork(hardfork)),
381            _ => None,
382        }
383    }
384}
385
386#[cfg(feature = "optimism")]
387impl FromEvmVersion for OpSpecId {
388    fn from_evm_version(version: EvmVersion) -> Self {
389        match version {
390            EvmVersion::Homestead
391            | EvmVersion::TangerineWhistle
392            | EvmVersion::SpuriousDragon
393            | EvmVersion::Byzantium
394            | EvmVersion::Constantinople
395            | EvmVersion::Petersburg
396            | EvmVersion::Istanbul
397            | EvmVersion::Berlin
398            | EvmVersion::London
399            | EvmVersion::Paris => Self::BEDROCK,
400            EvmVersion::Shanghai => Self::CANYON,
401            EvmVersion::Cancun => Self::ECOTONE,
402            EvmVersion::Prague => Self::ISTHMUS,
403            EvmVersion::Osaka | EvmVersion::Amsterdam => Self::KARST,
404        }
405    }
406}
407
408#[cfg(feature = "optimism")]
409impl ExecutionSpec for OpSpecId {
410    // Returns the user-facing name for the active execution spec.
411    fn evm_version_name(&self) -> String {
412        let name: &'static str = (*self).into();
413        name.to_string()
414    }
415
416    // Parses an unnamespaced Optimism hardfork name.
417    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
418        OpHardfork::from_str(hardfork).ok().map(spec_id_from_optimism_hardfork)
419    }
420
421    // Converts only Optimism namespaced hardforks to an Optimism spec.
422    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
423        match hardfork {
424            FoundryHardfork::Optimism(hardfork) => Some(spec_id_from_optimism_hardfork(hardfork)),
425            _ => None,
426        }
427    }
428}
429
430impl FromEvmVersion for TempoHardfork {
431    fn from_evm_version(_: EvmVersion) -> Self {
432        latest_active_tempo_hardfork()
433    }
434}
435
436impl ExecutionSpec for TempoHardfork {
437    // Returns the user-facing name for the active execution spec.
438    fn evm_version_name(&self) -> String {
439        self.to_string()
440    }
441
442    // Parses an unnamespaced Tempo hardfork name.
443    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
444        Self::from_str(hardfork).ok()
445    }
446
447    // Converts only Tempo namespaced hardforks to a Tempo spec.
448    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
449        match hardfork {
450            FoundryHardfork::Tempo(hardfork) => Some(hardfork),
451            _ => None,
452        }
453    }
454}
455
456#[cfg(feature = "monad")]
457impl FromEvmVersion for MonadHardfork {
458    fn from_evm_version(_: EvmVersion) -> Self {
459        Self::default()
460    }
461}
462
463#[cfg(feature = "monad")]
464impl ExecutionSpec for MonadHardfork {
465    // Returns the user-facing name for the active execution spec.
466    fn evm_version_name(&self) -> String {
467        self.to_string()
468    }
469
470    // Parses an unnamespaced Monad hardfork name.
471    fn from_network_hardfork(hardfork: &str) -> Option<Self> {
472        Self::from_str(hardfork).ok()
473    }
474
475    // Converts only Monad namespaced hardforks to a Monad spec.
476    fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
477        match hardfork {
478            #[cfg(feature = "monad")]
479            FoundryHardfork::Monad(hardfork) => Some(hardfork),
480            _ => None,
481        }
482    }
483}
484
485/// Returns the spec id derived from [`EvmVersion`] for a given spec type.
486pub fn evm_spec_id<SPEC: FromEvmVersion>(evm_version: EvmVersion) -> SPEC {
487    SPEC::from_evm_version(evm_version)
488}
489
490/// Returns the latest Tempo hardfork that has an activation on a known Tempo network.
491pub fn latest_active_tempo_hardfork() -> TempoHardfork {
492    // Tempo currently publishes activation timestamps through chain-aware hardfork resolution.
493    let now = SystemTime::now()
494        .duration_since(UNIX_EPOCH)
495        .map(|duration| duration.as_secs())
496        .unwrap_or(u64::MAX);
497    TempoHardfork::from_chain_and_timestamp(4217, now)
498        .or_else(|| TempoHardfork::from_chain_and_timestamp(42431, now))
499        .unwrap_or_default()
500}
501
502// Parses an EVM version or network-specific hardfork into the given spec type.
503pub fn evm_spec_id_from_str<SPEC: ExecutionSpec>(evm_version: &str) -> Option<SPEC> {
504    let evm_version = evm_version.trim();
505
506    if let Ok(version) = EvmVersion::from_str(evm_version) {
507        return Some(evm_spec_id(version));
508    }
509
510    if let Some(spec) = SPEC::from_network_hardfork(evm_version) {
511        return Some(spec);
512    }
513
514    FoundryHardfork::from_str(evm_version).ok().and_then(SPEC::from_foundry_hardfork)
515}
516
517/// Convert a `BlockNumberOrTag` into an `EthereumHardfork`.
518pub fn ethereum_hardfork_from_block_tag(block: impl Into<BlockNumberOrTag>) -> EthereumHardfork {
519    let num = match block.into() {
520        BlockNumberOrTag::Earliest => 0,
521        BlockNumberOrTag::Number(num) => num,
522        _ => u64::MAX,
523    };
524
525    EthereumHardfork::from_mainnet_block_number(num)
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531    use alloy_hardforks::ethereum::mainnet::*;
532    use tempo_hardfork::constants::{mainnet::*, moderato::*};
533
534    #[test]
535    fn test_ethereum_spec_id_mapping() {
536        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Frontier), SpecId::FRONTIER);
537        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Homestead), SpecId::HOMESTEAD);
538
539        // Test latest hardforks
540        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Cancun), SpecId::CANCUN);
541        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Prague), SpecId::PRAGUE);
542        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Osaka), SpecId::OSAKA);
543        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Amsterdam), SpecId::AMSTERDAM);
544    }
545
546    #[test]
547    fn test_tempo_spec_id_mapping() {
548        assert_eq!(spec_id_from_tempo_hardfork(TempoHardfork::Genesis), SpecId::OSAKA);
549        assert_eq!(spec_id_from_tempo_hardfork(TempoHardfork::T8), SpecId::OSAKA);
550    }
551
552    #[test]
553    fn test_tempo_evm_version_defaults_to_latest_active_hardfork() {
554        let latest = latest_active_tempo_hardfork();
555        assert_eq!(evm_spec_id::<TempoHardfork>(EvmVersion::Osaka), latest);
556    }
557
558    #[test]
559    #[cfg(feature = "monad")]
560    fn test_monad_hardfork_parsing() {
561        assert_eq!(
562            "monad:MonadNine".parse::<FoundryHardfork>().unwrap(),
563            FoundryHardfork::Monad(MonadHardfork::MonadNine)
564        );
565        assert_eq!(
566            "monad:MonadTen".parse::<FoundryHardfork>().unwrap(),
567            FoundryHardfork::Monad(MonadHardfork::MonadTen)
568        );
569        assert_eq!(
570            "m:MonadNext".parse::<FoundryHardfork>().unwrap(),
571            FoundryHardfork::Monad(MonadHardfork::MonadNext)
572        );
573    }
574
575    #[test]
576    #[cfg(feature = "monad")]
577    fn test_monad_hardfork_serialization() {
578        assert_eq!(
579            String::from(FoundryHardfork::Monad(MonadHardfork::MonadEight)),
580            "monad:MonadEight"
581        );
582        assert_eq!(FoundryHardfork::Monad(MonadHardfork::MonadEight).namespace(), Some("monad"));
583        assert_eq!(FoundryHardfork::Monad(MonadHardfork::MonadEight).name(), "MonadEight");
584    }
585
586    #[test]
587    #[cfg(feature = "monad")]
588    fn test_monad_hardfork_spec_id_mapping() {
589        assert_eq!(SpecId::from(FoundryHardfork::Monad(MonadHardfork::MonadEight)), SpecId::PRAGUE);
590        assert_eq!(SpecId::from(FoundryHardfork::Monad(MonadHardfork::MonadNine)), SpecId::OSAKA);
591        assert_eq!(SpecId::from(FoundryHardfork::Monad(MonadHardfork::MonadTen)), SpecId::OSAKA);
592        assert_eq!(
593            MonadHardfork::from(FoundryHardfork::Monad(MonadHardfork::MonadNext)),
594            MonadHardfork::MonadNext
595        );
596    }
597
598    #[test]
599    fn test_tempo_hardfork_from_chain_and_timestamp() {
600        assert_eq!(
601            FoundryHardfork::from_chain_and_timestamp(4217, u64::MAX),
602            Some(FoundryHardfork::Tempo(TempoHardfork::T10))
603        );
604        assert_eq!(
605            FoundryHardfork::from_chain_and_timestamp(42431, u64::MAX),
606            Some(FoundryHardfork::Tempo(TempoHardfork::T10))
607        );
608
609        assert_eq!(
610            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T6_TIMESTAMP - 1),
611            Some(FoundryHardfork::Tempo(TempoHardfork::T5))
612        );
613        assert_eq!(
614            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T6_TIMESTAMP),
615            Some(FoundryHardfork::Tempo(TempoHardfork::T6))
616        );
617        assert_eq!(
618            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T7_TIMESTAMP - 1),
619            Some(FoundryHardfork::Tempo(TempoHardfork::T6))
620        );
621        assert_eq!(
622            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T7_TIMESTAMP),
623            Some(FoundryHardfork::Tempo(TempoHardfork::T7))
624        );
625        assert_eq!(
626            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T8_TIMESTAMP - 1),
627            Some(FoundryHardfork::Tempo(TempoHardfork::T7))
628        );
629        assert_eq!(
630            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T8_TIMESTAMP),
631            Some(FoundryHardfork::Tempo(TempoHardfork::T8))
632        );
633        assert_eq!(
634            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T9_TIMESTAMP - 1),
635            Some(FoundryHardfork::Tempo(TempoHardfork::T8))
636        );
637        assert_eq!(
638            FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T9_TIMESTAMP),
639            Some(FoundryHardfork::Tempo(TempoHardfork::T9))
640        );
641        assert_eq!(
642            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T6_TIMESTAMP - 1),
643            Some(FoundryHardfork::Tempo(TempoHardfork::T5))
644        );
645        assert_eq!(
646            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T6_TIMESTAMP),
647            Some(FoundryHardfork::Tempo(TempoHardfork::T6))
648        );
649        assert_eq!(
650            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T7_TIMESTAMP - 1),
651            Some(FoundryHardfork::Tempo(TempoHardfork::T6))
652        );
653        assert_eq!(
654            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T7_TIMESTAMP),
655            Some(FoundryHardfork::Tempo(TempoHardfork::T7))
656        );
657        assert_eq!(
658            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T8_TIMESTAMP - 1),
659            Some(FoundryHardfork::Tempo(TempoHardfork::T7))
660        );
661        assert_eq!(
662            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T8_TIMESTAMP),
663            Some(FoundryHardfork::Tempo(TempoHardfork::T8))
664        );
665        assert_eq!(
666            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T9_TIMESTAMP - 1),
667            Some(FoundryHardfork::Tempo(TempoHardfork::T8))
668        );
669        assert_eq!(
670            FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T9_TIMESTAMP),
671            Some(FoundryHardfork::Tempo(TempoHardfork::T9))
672        );
673    }
674
675    #[test]
676    #[cfg(feature = "monad")]
677    fn test_monad_hardfork_from_chain_and_timestamp() {
678        let mainnet_ten = MonadHardfork::MonadTen.mainnet_activation_timestamp().unwrap();
679        let testnet_ten = MonadHardfork::MonadTen.testnet_activation_timestamp().unwrap();
680
681        assert_eq!(
682            FoundryHardfork::from_chain_and_timestamp(
683                monad_revm::MONAD_MAINNET_CHAIN_ID,
684                1_763_648_999,
685            ),
686            Some(FoundryHardfork::Monad(MonadHardfork::MonadEight))
687        );
688        assert_eq!(
689            FoundryHardfork::from_chain_and_timestamp(
690                monad_revm::MONAD_MAINNET_CHAIN_ID,
691                1_773_930_600,
692            ),
693            Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
694        );
695        assert_eq!(
696            FoundryHardfork::from_chain_and_timestamp(
697                monad_revm::MONAD_MAINNET_CHAIN_ID,
698                mainnet_ten - 1,
699            ),
700            Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
701        );
702        assert_eq!(
703            FoundryHardfork::from_chain_and_timestamp(
704                monad_revm::MONAD_MAINNET_CHAIN_ID,
705                mainnet_ten,
706            ),
707            Some(FoundryHardfork::Monad(MonadHardfork::MonadTen))
708        );
709        assert_eq!(
710            FoundryHardfork::from_chain_and_timestamp(
711                monad_revm::MONAD_TESTNET_CHAIN_ID,
712                1_773_152_999,
713            ),
714            Some(FoundryHardfork::Monad(MonadHardfork::MonadEight))
715        );
716        assert_eq!(
717            FoundryHardfork::from_chain_and_timestamp(
718                monad_revm::MONAD_TESTNET_CHAIN_ID,
719                1_773_153_000,
720            ),
721            Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
722        );
723        assert_eq!(
724            FoundryHardfork::from_chain_and_timestamp(
725                monad_revm::MONAD_TESTNET_CHAIN_ID,
726                testnet_ten - 1,
727            ),
728            Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
729        );
730        assert_eq!(
731            FoundryHardfork::from_chain_and_timestamp(
732                monad_revm::MONAD_TESTNET_CHAIN_ID,
733                testnet_ten,
734            ),
735            Some(FoundryHardfork::Monad(MonadHardfork::MonadTen))
736        );
737    }
738
739    #[test]
740    fn test_evm_spec_id_from_str_parses_network_hardforks() {
741        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("T3"), Some(TempoHardfork::T3));
742        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T2"), Some(TempoHardfork::T2));
743        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T7"), Some(TempoHardfork::T7));
744        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T8"), Some(TempoHardfork::T8));
745        assert_eq!(evm_spec_id_from_str::<TempoHardfork>("ethereum:prague"), None);
746
747        #[cfg(feature = "monad")]
748        {
749            assert_eq!(
750                evm_spec_id_from_str::<MonadHardfork>("MonadNine"),
751                Some(MonadHardfork::MonadNine)
752            );
753            assert_eq!(
754                evm_spec_id_from_str::<MonadHardfork>("MonadTen"),
755                Some(MonadHardfork::MonadTen)
756            );
757            assert_eq!(
758                evm_spec_id_from_str::<MonadHardfork>("monad:MonadEight"),
759                Some(MonadHardfork::MonadEight)
760            );
761            assert_eq!(evm_spec_id_from_str::<MonadHardfork>("tempo:T3"), None);
762        }
763    }
764
765    #[test]
766    fn test_hardfork_from_block_tag_numbers() {
767        assert_eq!(
768            ethereum_hardfork_from_block_tag(MAINNET_HOMESTEAD_BLOCK - 1),
769            EthereumHardfork::Frontier
770        );
771        assert_eq!(
772            ethereum_hardfork_from_block_tag(MAINNET_LONDON_BLOCK + 1),
773            EthereumHardfork::London
774        );
775    }
776
777    #[test]
778    fn test_from_chain_and_timestamp_ethereum_mainnet() {
779        assert_eq!(
780            FoundryHardfork::from_chain_and_timestamp(1, 0),
781            Some(FoundryHardfork::Ethereum(EthereumHardfork::Frontier))
782        );
783        // Shanghai activated at timestamp 1681338455 on mainnet
784        assert_eq!(
785            FoundryHardfork::from_chain_and_timestamp(1, 1_681_338_455),
786            Some(FoundryHardfork::Ethereum(EthereumHardfork::Shanghai))
787        );
788    }
789
790    #[test]
791    fn test_from_chain_and_timestamp_sepolia() {
792        let sepolia_chain_id = 11155111;
793        assert!(FoundryHardfork::from_chain_and_timestamp(sepolia_chain_id, u64::MAX).is_some());
794    }
795
796    #[test]
797    fn test_from_chain_and_timestamp_unknown_chain() {
798        assert_eq!(FoundryHardfork::from_chain_and_timestamp(999999, 0), None);
799    }
800
801    #[cfg(feature = "optimism")]
802    mod optimism {
803        use super::*;
804
805        #[test]
806        fn test_optimism_spec_id_mapping() {
807            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Bedrock), OpSpecId::BEDROCK);
808            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Regolith), OpSpecId::REGOLITH);
809
810            // Test latest hardforks
811            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Holocene), OpSpecId::HOLOCENE);
812            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Karst), OpSpecId::KARST);
813            assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Lagoon), OpSpecId::LAGOON);
814            assert_eq!(eth_spec_id_from_optimism_hardfork(OpHardfork::Jovian), SpecId::PRAGUE);
815            assert_eq!(eth_spec_id_from_optimism_hardfork(OpHardfork::Karst), SpecId::OSAKA);
816            assert_eq!(eth_spec_id_from_optimism_hardfork(OpHardfork::Lagoon), SpecId::OSAKA);
817            assert_eq!(evm_spec_id::<OpSpecId>(EvmVersion::Osaka), OpSpecId::KARST);
818        }
819
820        #[test]
821        fn test_from_chain_and_timestamp_op_mainnet() {
822            let op_chain_id = 10;
823            assert!(matches!(
824                FoundryHardfork::from_chain_and_timestamp(op_chain_id, u64::MAX),
825                Some(FoundryHardfork::Optimism(_))
826            ));
827        }
828
829        #[test]
830        fn test_from_chain_and_timestamp_base() {
831            let base_chain_id = 8453;
832            assert!(matches!(
833                FoundryHardfork::from_chain_and_timestamp(base_chain_id, u64::MAX),
834                Some(FoundryHardfork::Optimism(_))
835            ));
836        }
837    }
838}