1use 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 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 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 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
184pub 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
214pub 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
231pub 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
284pub fn evm_spec_id<SPEC: FromEvmVersion>(evm_version: EvmVersion) -> SPEC {
286 SPEC::from_evm_version(evm_version)
287}
288
289pub 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 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 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 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}