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, and Tempo hardforks
4//! with `FromStr`/`Serialize`/`Deserialize` support for CLI and config usage.
5
6use std::str::FromStr;
7
8use alloy_chains::Chain;
9use alloy_rpc_types::BlockNumberOrTag;
10use foundry_compilers::artifacts::EvmVersion;
11use op_revm::OpSpecId;
12use revm::primitives::hardfork::SpecId;
13use serde::{Deserialize, Serialize};
14
15pub use alloy_hardforks::EthereumHardfork;
16pub use alloy_op_hardforks::OpHardfork;
17pub use tempo_chainspec::hardfork::TempoHardfork;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
20#[serde(into = "String")]
21pub enum FoundryHardfork {
22    Ethereum(EthereumHardfork),
23    Optimism(OpHardfork),
24    Tempo(TempoHardfork),
25}
26
27impl From<FoundryHardfork> for String {
28    fn from(fork: FoundryHardfork) -> Self {
29        match fork {
30            FoundryHardfork::Ethereum(h) => format!("{h}"),
31            FoundryHardfork::Optimism(h) => format!("optimism:{h}"),
32            FoundryHardfork::Tempo(h) => format!("tempo:{h}"),
33        }
34    }
35}
36
37impl<'de> Deserialize<'de> for FoundryHardfork {
38    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
39    where
40        D: serde::Deserializer<'de>,
41    {
42        let s = String::deserialize(deserializer)?;
43        Self::from_str(&s).map_err(serde::de::Error::custom)
44    }
45}
46
47impl FromStr for FoundryHardfork {
48    type Err = String;
49
50    fn from_str(s: &str) -> Result<Self, Self::Err> {
51        let raw = s.trim();
52
53        let Some((ns, fork_raw)) = raw.split_once(':') else {
54            return EthereumHardfork::from_str(raw)
55                .map(Self::Ethereum)
56                .map_err(|_| format!("unknown ethereum hardfork '{raw}'"));
57        };
58
59        let ns = ns.trim().to_ascii_lowercase();
60        let fork = fork_raw.trim().to_ascii_lowercase().replace(['-', ' '], "_");
61
62        match ns.as_str() {
63            "eth" | "ethereum" => EthereumHardfork::from_str(&fork)
64                .map(Self::Ethereum)
65                .map_err(|_| format!("unknown ethereum hardfork '{fork_raw}'")),
66
67            "op" | "optimism" => OpHardfork::from_str(&fork)
68                .map(Self::Optimism)
69                .map_err(|_| format!("unknown optimism hardfork '{fork_raw}'")),
70
71            "t" | "tempo" => TempoHardfork::from_str(&fork)
72                .map(Self::Tempo)
73                .map_err(|_| format!("unknown tempo hardfork '{fork_raw}'")),
74            _ => EthereumHardfork::from_str(&fork)
75                .map(Self::Ethereum)
76                .map_err(|_| format!("unknown hardfork '{raw}'")),
77        }
78    }
79}
80
81impl FoundryHardfork {
82    pub const fn ethereum(h: EthereumHardfork) -> Self {
83        Self::Ethereum(h)
84    }
85
86    pub const fn optimism(h: OpHardfork) -> Self {
87        Self::Optimism(h)
88    }
89
90    pub const fn tempo(h: TempoHardfork) -> Self {
91        Self::Tempo(h)
92    }
93
94    /// Returns the hardfork name without a network namespace prefix.
95    pub fn name(&self) -> String {
96        match self {
97            Self::Ethereum(h) => format!("{h}"),
98            Self::Optimism(h) => format!("{h}"),
99            Self::Tempo(h) => format!("{h}"),
100        }
101    }
102
103    /// Returns the network namespace for this hardfork, or `None` for plain Ethereum.
104    ///
105    /// Mirrors the namespace prefix used in the `"network:hardfork"` serialization format.
106    pub const fn namespace(&self) -> Option<&'static str> {
107        match self {
108            Self::Ethereum(_) => None,
109            Self::Optimism(_) => Some("optimism"),
110            Self::Tempo(_) => Some("tempo"),
111        }
112    }
113
114    /// Auto-detect the active hardfork for a given chain at a specific timestamp.
115    ///
116    /// Tries Ethereum, then Optimism. Returns `None` for unknown chains.
117    pub fn from_chain_and_timestamp(chain_id: u64, timestamp: u64) -> Option<Self> {
118        let chain = Chain::from_id(chain_id);
119        if let Some(fork) = EthereumHardfork::from_chain_and_timestamp(chain, timestamp) {
120            return Some(Self::Ethereum(fork));
121        }
122        if let Some(fork) = OpHardfork::from_chain_and_timestamp(chain, timestamp) {
123            return Some(Self::Optimism(fork));
124        }
125        // TODO: add tempo support after https://github.com/tempoxyz/tempo/pull/3514 release
126        // providing TempoHardfork::from_chain_and_timestamp
127        None
128    }
129}
130
131impl From<EthereumHardfork> for FoundryHardfork {
132    fn from(value: EthereumHardfork) -> Self {
133        Self::Ethereum(value)
134    }
135}
136
137impl From<FoundryHardfork> for EthereumHardfork {
138    fn from(fork: FoundryHardfork) -> Self {
139        match fork {
140            FoundryHardfork::Ethereum(hardfork) => hardfork,
141            _ => Self::default(),
142        }
143    }
144}
145
146impl From<OpHardfork> for FoundryHardfork {
147    fn from(value: OpHardfork) -> Self {
148        Self::Optimism(value)
149    }
150}
151
152impl From<FoundryHardfork> for OpHardfork {
153    fn from(fork: FoundryHardfork) -> Self {
154        match fork {
155            FoundryHardfork::Optimism(hardfork) => hardfork,
156            _ => Self::default(),
157        }
158    }
159}
160
161impl From<TempoHardfork> for FoundryHardfork {
162    fn from(value: TempoHardfork) -> Self {
163        Self::Tempo(value)
164    }
165}
166
167impl From<FoundryHardfork> for TempoHardfork {
168    fn from(fork: FoundryHardfork) -> Self {
169        match fork {
170            FoundryHardfork::Tempo(hardfork) => hardfork,
171            _ => Self::default(),
172        }
173    }
174}
175
176impl From<FoundryHardfork> for SpecId {
177    fn from(fork: FoundryHardfork) -> Self {
178        match fork {
179            FoundryHardfork::Ethereum(hardfork) => spec_id_from_ethereum_hardfork(hardfork),
180            FoundryHardfork::Optimism(hardfork) => spec_id_from_optimism_hardfork(hardfork).into(),
181            FoundryHardfork::Tempo(hardfork) => hardfork.into(),
182        }
183    }
184}
185
186impl From<FoundryHardfork> for OpSpecId {
187    fn from(fork: FoundryHardfork) -> Self {
188        match fork {
189            FoundryHardfork::Optimism(hardfork) => spec_id_from_optimism_hardfork(hardfork),
190            _ => Self::default(),
191        }
192    }
193}
194
195/// Map an `EthereumHardfork` enum into its corresponding `SpecId`.
196pub fn spec_id_from_ethereum_hardfork(hardfork: EthereumHardfork) -> SpecId {
197    match hardfork {
198        EthereumHardfork::Frontier => SpecId::FRONTIER,
199        EthereumHardfork::Homestead => SpecId::HOMESTEAD,
200        EthereumHardfork::Dao => SpecId::DAO_FORK,
201        EthereumHardfork::Tangerine => SpecId::TANGERINE,
202        EthereumHardfork::SpuriousDragon => SpecId::SPURIOUS_DRAGON,
203        EthereumHardfork::Byzantium => SpecId::BYZANTIUM,
204        EthereumHardfork::Constantinople => SpecId::CONSTANTINOPLE,
205        EthereumHardfork::Petersburg => SpecId::PETERSBURG,
206        EthereumHardfork::Istanbul => SpecId::ISTANBUL,
207        EthereumHardfork::MuirGlacier => SpecId::MUIR_GLACIER,
208        EthereumHardfork::Berlin => SpecId::BERLIN,
209        EthereumHardfork::London => SpecId::LONDON,
210        EthereumHardfork::ArrowGlacier => SpecId::ARROW_GLACIER,
211        EthereumHardfork::GrayGlacier => SpecId::GRAY_GLACIER,
212        EthereumHardfork::Paris => SpecId::MERGE,
213        EthereumHardfork::Shanghai => SpecId::SHANGHAI,
214        EthereumHardfork::Cancun => SpecId::CANCUN,
215        EthereumHardfork::Prague => SpecId::PRAGUE,
216        EthereumHardfork::Osaka => SpecId::OSAKA,
217        EthereumHardfork::Bpo1 | EthereumHardfork::Bpo2 => SpecId::OSAKA,
218        EthereumHardfork::Bpo3 | EthereumHardfork::Bpo4 | EthereumHardfork::Bpo5 => {
219            unimplemented!()
220        }
221        f => unreachable!("unimplemented {}", f),
222    }
223}
224
225/// Map an `OptimismHardfork` enum into its corresponding `OpSpecId`.
226pub fn spec_id_from_optimism_hardfork(hardfork: OpHardfork) -> OpSpecId {
227    match hardfork {
228        OpHardfork::Bedrock => OpSpecId::BEDROCK,
229        OpHardfork::Regolith => OpSpecId::REGOLITH,
230        OpHardfork::Canyon => OpSpecId::CANYON,
231        OpHardfork::Ecotone => OpSpecId::ECOTONE,
232        OpHardfork::Fjord => OpSpecId::FJORD,
233        OpHardfork::Granite => OpSpecId::GRANITE,
234        OpHardfork::Holocene => OpSpecId::HOLOCENE,
235        OpHardfork::Isthmus => OpSpecId::ISTHMUS,
236        OpHardfork::Interop => OpSpecId::INTEROP,
237        OpHardfork::Jovian => OpSpecId::JOVIAN,
238        f => unreachable!("unimplemented {}", f),
239    }
240}
241
242/// Trait for converting an [`EvmVersion`] into a network-specific spec type.
243pub trait FromEvmVersion: From<FoundryHardfork> {
244    fn from_evm_version(version: EvmVersion) -> Self;
245}
246
247impl FromEvmVersion for SpecId {
248    fn from_evm_version(version: EvmVersion) -> Self {
249        match version {
250            EvmVersion::Homestead => Self::HOMESTEAD,
251            EvmVersion::TangerineWhistle => Self::TANGERINE,
252            EvmVersion::SpuriousDragon => Self::SPURIOUS_DRAGON,
253            EvmVersion::Byzantium => Self::BYZANTIUM,
254            EvmVersion::Constantinople => Self::CONSTANTINOPLE,
255            EvmVersion::Petersburg => Self::PETERSBURG,
256            EvmVersion::Istanbul => Self::ISTANBUL,
257            EvmVersion::Berlin => Self::BERLIN,
258            EvmVersion::London => Self::LONDON,
259            EvmVersion::Paris => Self::MERGE,
260            EvmVersion::Shanghai => Self::SHANGHAI,
261            EvmVersion::Cancun => Self::CANCUN,
262            EvmVersion::Prague => Self::PRAGUE,
263            EvmVersion::Osaka => Self::OSAKA,
264        }
265    }
266}
267
268impl FromEvmVersion for OpSpecId {
269    fn from_evm_version(version: EvmVersion) -> Self {
270        match version {
271            EvmVersion::Homestead
272            | EvmVersion::TangerineWhistle
273            | EvmVersion::SpuriousDragon
274            | EvmVersion::Byzantium
275            | EvmVersion::Constantinople
276            | EvmVersion::Petersburg
277            | EvmVersion::Istanbul
278            | EvmVersion::Berlin
279            | EvmVersion::London
280            | EvmVersion::Paris => Self::BEDROCK,
281            EvmVersion::Shanghai => Self::CANYON,
282            EvmVersion::Cancun => Self::ECOTONE,
283            EvmVersion::Prague => Self::ISTHMUS,
284            EvmVersion::Osaka => Self::JOVIAN,
285        }
286    }
287}
288
289impl FromEvmVersion for TempoHardfork {
290    fn from_evm_version(_: EvmVersion) -> Self {
291        Self::default()
292    }
293}
294
295/// Returns the spec id derived from [`EvmVersion`] for a given spec type.
296pub fn evm_spec_id<SPEC: FromEvmVersion>(evm_version: EvmVersion) -> SPEC {
297    SPEC::from_evm_version(evm_version)
298}
299
300/// Convert a `BlockNumberOrTag` into an `EthereumHardfork`.
301pub fn ethereum_hardfork_from_block_tag(block: impl Into<BlockNumberOrTag>) -> EthereumHardfork {
302    let num = match block.into() {
303        BlockNumberOrTag::Earliest => 0,
304        BlockNumberOrTag::Number(num) => num,
305        _ => u64::MAX,
306    };
307
308    EthereumHardfork::from_mainnet_block_number(num)
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use alloy_hardforks::ethereum::mainnet::*;
315
316    #[test]
317    fn test_ethereum_spec_id_mapping() {
318        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Frontier), SpecId::FRONTIER);
319        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Homestead), SpecId::HOMESTEAD);
320
321        // Test latest hardforks
322        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Cancun), SpecId::CANCUN);
323        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Prague), SpecId::PRAGUE);
324        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Osaka), SpecId::OSAKA);
325    }
326
327    #[test]
328    fn test_optimism_spec_id_mapping() {
329        assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Bedrock), OpSpecId::BEDROCK);
330        assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Regolith), OpSpecId::REGOLITH);
331
332        // Test latest hardforks
333        assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Holocene), OpSpecId::HOLOCENE);
334        assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Interop), OpSpecId::INTEROP);
335    }
336
337    #[test]
338    fn test_tempo_spec_id_mapping() {
339        assert_eq!(SpecId::from(TempoHardfork::Genesis), SpecId::OSAKA);
340    }
341
342    #[test]
343    fn test_hardfork_from_block_tag_numbers() {
344        assert_eq!(
345            ethereum_hardfork_from_block_tag(MAINNET_HOMESTEAD_BLOCK - 1),
346            EthereumHardfork::Frontier
347        );
348        assert_eq!(
349            ethereum_hardfork_from_block_tag(MAINNET_LONDON_BLOCK + 1),
350            EthereumHardfork::London
351        );
352    }
353
354    #[test]
355    fn test_from_chain_and_timestamp_ethereum_mainnet() {
356        assert_eq!(
357            FoundryHardfork::from_chain_and_timestamp(1, 0),
358            Some(FoundryHardfork::Ethereum(EthereumHardfork::Frontier))
359        );
360        // Shanghai activated at timestamp 1681338455 on mainnet
361        assert_eq!(
362            FoundryHardfork::from_chain_and_timestamp(1, 1_681_338_455),
363            Some(FoundryHardfork::Ethereum(EthereumHardfork::Shanghai))
364        );
365    }
366
367    #[test]
368    fn test_from_chain_and_timestamp_sepolia() {
369        let sepolia_chain_id = 11155111;
370        assert!(FoundryHardfork::from_chain_and_timestamp(sepolia_chain_id, u64::MAX).is_some());
371    }
372
373    #[test]
374    fn test_from_chain_and_timestamp_op_mainnet() {
375        let op_chain_id = 10;
376        assert!(matches!(
377            FoundryHardfork::from_chain_and_timestamp(op_chain_id, u64::MAX),
378            Some(FoundryHardfork::Optimism(_))
379        ));
380    }
381
382    #[test]
383    fn test_from_chain_and_timestamp_base() {
384        let base_chain_id = 8453;
385        assert!(matches!(
386            FoundryHardfork::from_chain_and_timestamp(base_chain_id, u64::MAX),
387            Some(FoundryHardfork::Optimism(_))
388        ));
389    }
390
391    #[test]
392    fn test_from_chain_and_timestamp_unknown_chain() {
393        assert_eq!(FoundryHardfork::from_chain_and_timestamp(999999, 0), None);
394    }
395}