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 fn ethereum(h: EthereumHardfork) -> Self {
83        Self::Ethereum(h)
84    }
85
86    pub fn optimism(h: OpHardfork) -> Self {
87        Self::Optimism(h)
88    }
89
90    pub 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    /// Auto-detect the active hardfork for a given chain at a specific timestamp.
104    ///
105    /// Tries Ethereum, then Optimism. Returns `None` for unknown chains.
106    pub fn from_chain_and_timestamp(chain_id: u64, timestamp: u64) -> Option<Self> {
107        let chain = Chain::from_id(chain_id);
108        if let Some(fork) = EthereumHardfork::from_chain_and_timestamp(chain, timestamp) {
109            return Some(Self::Ethereum(fork));
110        }
111        if let Some(fork) = OpHardfork::from_chain_and_timestamp(chain, timestamp) {
112            return Some(Self::Optimism(fork));
113        }
114        // TODO: add tempo support after https://github.com/tempoxyz/tempo/pull/3514 release
115        // providing TempoHardfork::from_chain_and_timestamp
116        None
117    }
118}
119
120impl From<EthereumHardfork> for FoundryHardfork {
121    fn from(value: EthereumHardfork) -> Self {
122        Self::Ethereum(value)
123    }
124}
125
126impl From<FoundryHardfork> for EthereumHardfork {
127    fn from(fork: FoundryHardfork) -> Self {
128        match fork {
129            FoundryHardfork::Ethereum(hardfork) => hardfork,
130            _ => Self::default(),
131        }
132    }
133}
134
135impl From<OpHardfork> for FoundryHardfork {
136    fn from(value: OpHardfork) -> Self {
137        Self::Optimism(value)
138    }
139}
140
141impl From<FoundryHardfork> for OpHardfork {
142    fn from(fork: FoundryHardfork) -> Self {
143        match fork {
144            FoundryHardfork::Optimism(hardfork) => hardfork,
145            _ => Self::default(),
146        }
147    }
148}
149
150impl From<TempoHardfork> for FoundryHardfork {
151    fn from(value: TempoHardfork) -> Self {
152        Self::Tempo(value)
153    }
154}
155
156impl From<FoundryHardfork> for TempoHardfork {
157    fn from(fork: FoundryHardfork) -> Self {
158        match fork {
159            FoundryHardfork::Tempo(hardfork) => hardfork,
160            _ => Self::default(),
161        }
162    }
163}
164
165impl From<FoundryHardfork> for SpecId {
166    fn from(fork: FoundryHardfork) -> Self {
167        match fork {
168            FoundryHardfork::Ethereum(hardfork) => spec_id_from_ethereum_hardfork(hardfork),
169            FoundryHardfork::Optimism(hardfork) => spec_id_from_optimism_hardfork(hardfork).into(),
170            FoundryHardfork::Tempo(hardfork) => hardfork.into(),
171        }
172    }
173}
174
175impl From<FoundryHardfork> for OpSpecId {
176    fn from(fork: FoundryHardfork) -> Self {
177        match fork {
178            FoundryHardfork::Optimism(hardfork) => spec_id_from_optimism_hardfork(hardfork),
179            _ => Self::default(),
180        }
181    }
182}
183
184/// Map an `EthereumHardfork` enum into its corresponding `SpecId`.
185pub fn spec_id_from_ethereum_hardfork(hardfork: EthereumHardfork) -> SpecId {
186    match hardfork {
187        EthereumHardfork::Frontier => SpecId::FRONTIER,
188        EthereumHardfork::Homestead => SpecId::HOMESTEAD,
189        EthereumHardfork::Dao => SpecId::DAO_FORK,
190        EthereumHardfork::Tangerine => SpecId::TANGERINE,
191        EthereumHardfork::SpuriousDragon => SpecId::SPURIOUS_DRAGON,
192        EthereumHardfork::Byzantium => SpecId::BYZANTIUM,
193        EthereumHardfork::Constantinople => SpecId::CONSTANTINOPLE,
194        EthereumHardfork::Petersburg => SpecId::PETERSBURG,
195        EthereumHardfork::Istanbul => SpecId::ISTANBUL,
196        EthereumHardfork::MuirGlacier => SpecId::MUIR_GLACIER,
197        EthereumHardfork::Berlin => SpecId::BERLIN,
198        EthereumHardfork::London => SpecId::LONDON,
199        EthereumHardfork::ArrowGlacier => SpecId::ARROW_GLACIER,
200        EthereumHardfork::GrayGlacier => SpecId::GRAY_GLACIER,
201        EthereumHardfork::Paris => SpecId::MERGE,
202        EthereumHardfork::Shanghai => SpecId::SHANGHAI,
203        EthereumHardfork::Cancun => SpecId::CANCUN,
204        EthereumHardfork::Prague => SpecId::PRAGUE,
205        EthereumHardfork::Osaka => SpecId::OSAKA,
206        EthereumHardfork::Bpo1 | EthereumHardfork::Bpo2 => SpecId::OSAKA,
207        EthereumHardfork::Bpo3 | EthereumHardfork::Bpo4 | EthereumHardfork::Bpo5 => {
208            unimplemented!()
209        }
210        f => unreachable!("unimplemented {}", f),
211    }
212}
213
214/// Map an `OptimismHardfork` enum into its corresponding `OpSpecId`.
215pub fn spec_id_from_optimism_hardfork(hardfork: OpHardfork) -> OpSpecId {
216    match hardfork {
217        OpHardfork::Bedrock => OpSpecId::BEDROCK,
218        OpHardfork::Regolith => OpSpecId::REGOLITH,
219        OpHardfork::Canyon => OpSpecId::CANYON,
220        OpHardfork::Ecotone => OpSpecId::ECOTONE,
221        OpHardfork::Fjord => OpSpecId::FJORD,
222        OpHardfork::Granite => OpSpecId::GRANITE,
223        OpHardfork::Holocene => OpSpecId::HOLOCENE,
224        OpHardfork::Isthmus => OpSpecId::ISTHMUS,
225        OpHardfork::Interop => OpSpecId::INTEROP,
226        OpHardfork::Jovian => OpSpecId::JOVIAN,
227        f => unreachable!("unimplemented {}", f),
228    }
229}
230
231/// Trait for converting an [`EvmVersion`] into a network-specific spec type.
232pub trait FromEvmVersion: From<FoundryHardfork> {
233    fn from_evm_version(version: EvmVersion) -> Self;
234}
235
236impl FromEvmVersion for SpecId {
237    fn from_evm_version(version: EvmVersion) -> Self {
238        match version {
239            EvmVersion::Homestead => Self::HOMESTEAD,
240            EvmVersion::TangerineWhistle => Self::TANGERINE,
241            EvmVersion::SpuriousDragon => Self::SPURIOUS_DRAGON,
242            EvmVersion::Byzantium => Self::BYZANTIUM,
243            EvmVersion::Constantinople => Self::CONSTANTINOPLE,
244            EvmVersion::Petersburg => Self::PETERSBURG,
245            EvmVersion::Istanbul => Self::ISTANBUL,
246            EvmVersion::Berlin => Self::BERLIN,
247            EvmVersion::London => Self::LONDON,
248            EvmVersion::Paris => Self::MERGE,
249            EvmVersion::Shanghai => Self::SHANGHAI,
250            EvmVersion::Cancun => Self::CANCUN,
251            EvmVersion::Prague => Self::PRAGUE,
252            EvmVersion::Osaka => Self::OSAKA,
253        }
254    }
255}
256
257impl FromEvmVersion for OpSpecId {
258    fn from_evm_version(version: EvmVersion) -> Self {
259        match version {
260            EvmVersion::Homestead
261            | EvmVersion::TangerineWhistle
262            | EvmVersion::SpuriousDragon
263            | EvmVersion::Byzantium
264            | EvmVersion::Constantinople
265            | EvmVersion::Petersburg
266            | EvmVersion::Istanbul
267            | EvmVersion::Berlin
268            | EvmVersion::London
269            | EvmVersion::Paris => Self::BEDROCK,
270            EvmVersion::Shanghai => Self::CANYON,
271            EvmVersion::Cancun => Self::ECOTONE,
272            EvmVersion::Prague => Self::ISTHMUS,
273            EvmVersion::Osaka => Self::JOVIAN,
274        }
275    }
276}
277
278impl FromEvmVersion for TempoHardfork {
279    fn from_evm_version(_: EvmVersion) -> Self {
280        Self::default()
281    }
282}
283
284/// Returns the spec id derived from [`EvmVersion`] for a given spec type.
285pub fn evm_spec_id<SPEC: FromEvmVersion>(evm_version: EvmVersion) -> SPEC {
286    SPEC::from_evm_version(evm_version)
287}
288
289/// Convert a `BlockNumberOrTag` into an `EthereumHardfork`.
290pub fn ethereum_hardfork_from_block_tag(block: impl Into<BlockNumberOrTag>) -> EthereumHardfork {
291    let num = match block.into() {
292        BlockNumberOrTag::Earliest => 0,
293        BlockNumberOrTag::Number(num) => num,
294        _ => u64::MAX,
295    };
296
297    EthereumHardfork::from_mainnet_block_number(num)
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use alloy_hardforks::ethereum::mainnet::*;
304
305    #[test]
306    fn test_ethereum_spec_id_mapping() {
307        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Frontier), SpecId::FRONTIER);
308        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Homestead), SpecId::HOMESTEAD);
309
310        // Test latest hardforks
311        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Cancun), SpecId::CANCUN);
312        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Prague), SpecId::PRAGUE);
313        assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Osaka), SpecId::OSAKA);
314    }
315
316    #[test]
317    fn test_optimism_spec_id_mapping() {
318        assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Bedrock), OpSpecId::BEDROCK);
319        assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Regolith), OpSpecId::REGOLITH);
320
321        // Test latest hardforks
322        assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Holocene), OpSpecId::HOLOCENE);
323        assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Interop), OpSpecId::INTEROP);
324    }
325
326    #[test]
327    fn test_tempo_spec_id_mapping() {
328        assert_eq!(SpecId::from(TempoHardfork::Genesis), SpecId::OSAKA);
329    }
330
331    #[test]
332    fn test_hardfork_from_block_tag_numbers() {
333        assert_eq!(
334            ethereum_hardfork_from_block_tag(MAINNET_HOMESTEAD_BLOCK - 1),
335            EthereumHardfork::Frontier
336        );
337        assert_eq!(
338            ethereum_hardfork_from_block_tag(MAINNET_LONDON_BLOCK + 1),
339            EthereumHardfork::London
340        );
341    }
342
343    #[test]
344    fn test_from_chain_and_timestamp_ethereum_mainnet() {
345        assert_eq!(
346            FoundryHardfork::from_chain_and_timestamp(1, 0),
347            Some(FoundryHardfork::Ethereum(EthereumHardfork::Frontier))
348        );
349        // Shanghai activated at timestamp 1681338455 on mainnet
350        assert_eq!(
351            FoundryHardfork::from_chain_and_timestamp(1, 1_681_338_455),
352            Some(FoundryHardfork::Ethereum(EthereumHardfork::Shanghai))
353        );
354    }
355
356    #[test]
357    fn test_from_chain_and_timestamp_sepolia() {
358        let sepolia_chain_id = 11155111;
359        assert!(FoundryHardfork::from_chain_and_timestamp(sepolia_chain_id, u64::MAX).is_some());
360    }
361
362    #[test]
363    fn test_from_chain_and_timestamp_op_mainnet() {
364        let op_chain_id = 10;
365        assert!(matches!(
366            FoundryHardfork::from_chain_and_timestamp(op_chain_id, u64::MAX),
367            Some(FoundryHardfork::Optimism(_))
368        ));
369    }
370
371    #[test]
372    fn test_from_chain_and_timestamp_base() {
373        let base_chain_id = 8453;
374        assert!(matches!(
375            FoundryHardfork::from_chain_and_timestamp(base_chain_id, u64::MAX),
376            Some(FoundryHardfork::Optimism(_))
377        ));
378    }
379
380    #[test]
381    fn test_from_chain_and_timestamp_unknown_chain() {
382        assert_eq!(FoundryHardfork::from_chain_and_timestamp(999999, 0), None);
383    }
384}