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 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 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 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 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 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
195pub 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
225pub 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
242pub 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
295pub fn evm_spec_id<SPEC: FromEvmVersion>(evm_version: EvmVersion) -> SPEC {
297 SPEC::from_evm_version(evm_version)
298}
299
300pub 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 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 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 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}