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