1#![doc = include_str!("../README.md")]
2#![doc(
3 html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg",
4 html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico"
5)]
6#![cfg_attr(not(test), warn(unused_crate_dependencies))]
7#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
8#![no_std]
9
10extern crate alloc;
11use alloc::vec::Vec;
12use alloy_chains::{Chain, NamedChain};
13use alloy_hardforks::{EthereumHardfork, hardfork};
14pub use alloy_hardforks::{EthereumHardforks, ForkCondition};
15use core::ops::Index;
16
17pub mod optimism;
18pub use optimism::{mainnet as op_mainnet, mainnet::*, sepolia as op_sepolia, sepolia::*};
19
20pub mod base;
21pub use base::{mainnet as base_mainnet, mainnet::*, sepolia as base_sepolia, sepolia::*};
22
23hardfork!(
24 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29 #[derive(Default)]
30 OpHardfork {
31 Bedrock,
33 Regolith,
35 Canyon,
37 Ecotone,
39 Fjord,
41 Granite,
43 Holocene,
45 #[default]
47 Isthmus,
48 Jovian,
50 Interop,
52 }
53);
54
55impl OpHardfork {
56 pub fn from_chain_and_timestamp(chain: Chain, timestamp: u64) -> Option<Self> {
59 let named = chain.named()?;
60
61 match named {
62 NamedChain::Optimism => Some(match timestamp {
63 _i if timestamp < OP_MAINNET_CANYON_TIMESTAMP => Self::Regolith,
64 _i if timestamp < OP_MAINNET_ECOTONE_TIMESTAMP => Self::Canyon,
65 _i if timestamp < OP_MAINNET_FJORD_TIMESTAMP => Self::Ecotone,
66 _i if timestamp < OP_MAINNET_GRANITE_TIMESTAMP => Self::Fjord,
67 _i if timestamp < OP_MAINNET_HOLOCENE_TIMESTAMP => Self::Granite,
68 _i if timestamp < OP_MAINNET_ISTHMUS_TIMESTAMP => Self::Holocene,
69 _ => Self::Isthmus,
70 }),
71 NamedChain::OptimismSepolia => Some(match timestamp {
72 _i if timestamp < OP_SEPOLIA_CANYON_TIMESTAMP => Self::Regolith,
73 _i if timestamp < OP_SEPOLIA_ECOTONE_TIMESTAMP => Self::Canyon,
74 _i if timestamp < OP_SEPOLIA_FJORD_TIMESTAMP => Self::Ecotone,
75 _i if timestamp < OP_SEPOLIA_GRANITE_TIMESTAMP => Self::Fjord,
76 _i if timestamp < OP_SEPOLIA_HOLOCENE_TIMESTAMP => Self::Granite,
77 _i if timestamp < OP_SEPOLIA_ISTHMUS_TIMESTAMP => Self::Holocene,
78 _ => Self::Isthmus,
79 }),
80 NamedChain::Base => Some(match timestamp {
81 _i if timestamp < BASE_MAINNET_CANYON_TIMESTAMP => Self::Regolith,
82 _i if timestamp < BASE_MAINNET_ECOTONE_TIMESTAMP => Self::Canyon,
83 _i if timestamp < BASE_MAINNET_FJORD_TIMESTAMP => Self::Ecotone,
84 _i if timestamp < BASE_MAINNET_GRANITE_TIMESTAMP => Self::Fjord,
85 _i if timestamp < BASE_MAINNET_HOLOCENE_TIMESTAMP => Self::Granite,
86 _i if timestamp < BASE_MAINNET_ISTHMUS_TIMESTAMP => Self::Holocene,
87 _ => Self::Isthmus,
88 }),
89 NamedChain::BaseSepolia => Some(match timestamp {
90 _i if timestamp < BASE_SEPOLIA_CANYON_TIMESTAMP => Self::Regolith,
91 _i if timestamp < BASE_SEPOLIA_ECOTONE_TIMESTAMP => Self::Canyon,
92 _i if timestamp < BASE_SEPOLIA_FJORD_TIMESTAMP => Self::Ecotone,
93 _i if timestamp < BASE_SEPOLIA_GRANITE_TIMESTAMP => Self::Fjord,
94 _i if timestamp < BASE_SEPOLIA_HOLOCENE_TIMESTAMP => Self::Granite,
95 _i if timestamp < BASE_SEPOLIA_ISTHMUS_TIMESTAMP => Self::Holocene,
96 _ => Self::Isthmus,
97 }),
98 _ => None,
99 }
100 }
101
102 pub const fn op_mainnet() -> [(Self, ForkCondition); 8] {
104 [
105 (Self::Bedrock, ForkCondition::Block(OP_MAINNET_BEDROCK_BLOCK)),
106 (Self::Regolith, ForkCondition::Timestamp(OP_MAINNET_REGOLITH_TIMESTAMP)),
107 (Self::Canyon, ForkCondition::Timestamp(OP_MAINNET_CANYON_TIMESTAMP)),
108 (Self::Ecotone, ForkCondition::Timestamp(OP_MAINNET_ECOTONE_TIMESTAMP)),
109 (Self::Fjord, ForkCondition::Timestamp(OP_MAINNET_FJORD_TIMESTAMP)),
110 (Self::Granite, ForkCondition::Timestamp(OP_MAINNET_GRANITE_TIMESTAMP)),
111 (Self::Holocene, ForkCondition::Timestamp(OP_MAINNET_HOLOCENE_TIMESTAMP)),
112 (Self::Isthmus, ForkCondition::Timestamp(OP_MAINNET_ISTHMUS_TIMESTAMP)),
113 ]
114 }
115
116 pub const fn op_sepolia() -> [(Self, ForkCondition); 8] {
118 [
119 (Self::Bedrock, ForkCondition::Block(OP_SEPOLIA_BEDROCK_BLOCK)),
120 (Self::Regolith, ForkCondition::Timestamp(OP_SEPOLIA_REGOLITH_TIMESTAMP)),
121 (Self::Canyon, ForkCondition::Timestamp(OP_SEPOLIA_CANYON_TIMESTAMP)),
122 (Self::Ecotone, ForkCondition::Timestamp(OP_SEPOLIA_ECOTONE_TIMESTAMP)),
123 (Self::Fjord, ForkCondition::Timestamp(OP_SEPOLIA_FJORD_TIMESTAMP)),
124 (Self::Granite, ForkCondition::Timestamp(OP_SEPOLIA_GRANITE_TIMESTAMP)),
125 (Self::Holocene, ForkCondition::Timestamp(OP_SEPOLIA_HOLOCENE_TIMESTAMP)),
126 (Self::Isthmus, ForkCondition::Timestamp(OP_SEPOLIA_ISTHMUS_TIMESTAMP)),
127 ]
128 }
129
130 pub const fn base_mainnet() -> [(Self, ForkCondition); 8] {
132 [
133 (Self::Bedrock, ForkCondition::Block(0)),
134 (Self::Regolith, ForkCondition::Timestamp(0)),
135 (Self::Canyon, ForkCondition::Timestamp(BASE_MAINNET_CANYON_TIMESTAMP)),
136 (Self::Ecotone, ForkCondition::Timestamp(BASE_MAINNET_ECOTONE_TIMESTAMP)),
137 (Self::Fjord, ForkCondition::Timestamp(BASE_MAINNET_FJORD_TIMESTAMP)),
138 (Self::Granite, ForkCondition::Timestamp(BASE_MAINNET_GRANITE_TIMESTAMP)),
139 (Self::Holocene, ForkCondition::Timestamp(BASE_MAINNET_HOLOCENE_TIMESTAMP)),
140 (Self::Isthmus, ForkCondition::Timestamp(BASE_MAINNET_ISTHMUS_TIMESTAMP)),
141 ]
142 }
143
144 pub const fn base_sepolia() -> [(Self, ForkCondition); 8] {
146 [
147 (Self::Bedrock, ForkCondition::Block(0)),
148 (Self::Regolith, ForkCondition::Timestamp(0)),
149 (Self::Canyon, ForkCondition::Timestamp(BASE_SEPOLIA_CANYON_TIMESTAMP)),
150 (Self::Ecotone, ForkCondition::Timestamp(BASE_SEPOLIA_ECOTONE_TIMESTAMP)),
151 (Self::Fjord, ForkCondition::Timestamp(BASE_SEPOLIA_FJORD_TIMESTAMP)),
152 (Self::Granite, ForkCondition::Timestamp(BASE_SEPOLIA_GRANITE_TIMESTAMP)),
153 (Self::Holocene, ForkCondition::Timestamp(BASE_SEPOLIA_HOLOCENE_TIMESTAMP)),
154 (Self::Isthmus, ForkCondition::Timestamp(BASE_SEPOLIA_ISTHMUS_TIMESTAMP)),
155 ]
156 }
157
158 pub const fn idx(&self) -> usize {
160 *self as usize
161 }
162}
163
164#[auto_impl::auto_impl(&, Arc)]
166pub trait OpHardforks: EthereumHardforks {
167 fn op_fork_activation(&self, fork: OpHardfork) -> ForkCondition;
170
171 fn is_bedrock_active_at_block(&self, block_number: u64) -> bool {
174 self.op_fork_activation(OpHardfork::Bedrock).active_at_block(block_number)
175 }
176
177 fn is_regolith_active_at_timestamp(&self, timestamp: u64) -> bool {
180 self.op_fork_activation(OpHardfork::Regolith).active_at_timestamp(timestamp)
181 }
182
183 fn is_canyon_active_at_timestamp(&self, timestamp: u64) -> bool {
185 self.op_fork_activation(OpHardfork::Canyon).active_at_timestamp(timestamp)
186 }
187
188 fn is_ecotone_active_at_timestamp(&self, timestamp: u64) -> bool {
190 self.op_fork_activation(OpHardfork::Ecotone).active_at_timestamp(timestamp)
191 }
192
193 fn is_fjord_active_at_timestamp(&self, timestamp: u64) -> bool {
195 self.op_fork_activation(OpHardfork::Fjord).active_at_timestamp(timestamp)
196 }
197
198 fn is_granite_active_at_timestamp(&self, timestamp: u64) -> bool {
200 self.op_fork_activation(OpHardfork::Granite).active_at_timestamp(timestamp)
201 }
202
203 fn is_holocene_active_at_timestamp(&self, timestamp: u64) -> bool {
206 self.op_fork_activation(OpHardfork::Holocene).active_at_timestamp(timestamp)
207 }
208
209 fn is_isthmus_active_at_timestamp(&self, timestamp: u64) -> bool {
212 self.op_fork_activation(OpHardfork::Isthmus).active_at_timestamp(timestamp)
213 }
214
215 fn is_jovian_active_at_block(&self, timestamp: u64) -> bool {
218 self.op_fork_activation(OpHardfork::Jovian).active_at_timestamp(timestamp)
219 }
220
221 fn is_interop_active_at_timestamp(&self, timestamp: u64) -> bool {
224 self.op_fork_activation(OpHardfork::Interop).active_at_timestamp(timestamp)
225 }
226}
227
228#[derive(Debug, Clone)]
240pub struct OpChainHardforks {
241 forks: Vec<(OpHardfork, ForkCondition)>,
243}
244
245impl OpChainHardforks {
246 pub fn new(forks: impl IntoIterator<Item = (OpHardfork, ForkCondition)>) -> Self {
249 let mut forks = forks.into_iter().collect::<Vec<_>>();
250 forks.sort();
251 Self { forks }
252 }
253
254 pub fn op_mainnet() -> Self {
256 Self::new(OpHardfork::op_mainnet())
257 }
258
259 pub fn op_sepolia() -> Self {
261 Self::new(OpHardfork::op_sepolia())
262 }
263
264 pub fn base_mainnet() -> Self {
266 Self::new(OpHardfork::base_mainnet())
267 }
268
269 pub fn base_sepolia() -> Self {
271 Self::new(OpHardfork::base_sepolia())
272 }
273
274 pub fn is_op_mainnet(&self) -> bool {
276 self[OpHardfork::Bedrock] == ForkCondition::Block(OP_MAINNET_BEDROCK_BLOCK)
277 }
278}
279
280impl EthereumHardforks for OpChainHardforks {
281 fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition {
282 use EthereumHardfork::{Cancun, Osaka, Prague, Shanghai};
283 use OpHardfork::{Canyon, Ecotone, Isthmus};
284
285 if self.forks.is_empty() {
286 return ForkCondition::Never;
287 }
288
289 let forks_len = self.forks.len();
290 match fork {
292 Shanghai if forks_len <= Canyon.idx() => ForkCondition::Never,
293 Cancun if forks_len <= Ecotone.idx() => ForkCondition::Never,
294 Prague if forks_len <= Isthmus.idx() => ForkCondition::Never,
295 Osaka => ForkCondition::Never,
296 _ => self[fork],
297 }
298 }
299}
300
301impl OpHardforks for OpChainHardforks {
302 fn op_fork_activation(&self, fork: OpHardfork) -> ForkCondition {
303 if self.forks.len() <= fork.idx() {
305 return ForkCondition::Never;
306 }
307 self[fork]
308 }
309}
310
311impl Index<OpHardfork> for OpChainHardforks {
312 type Output = ForkCondition;
313
314 fn index(&self, hf: OpHardfork) -> &Self::Output {
315 use OpHardfork::*;
316
317 match hf {
318 Bedrock => &self.forks[Bedrock.idx()].1,
319 Regolith => &self.forks[Regolith.idx()].1,
320 Canyon => &self.forks[Canyon.idx()].1,
321 Ecotone => &self.forks[Ecotone.idx()].1,
322 Fjord => &self.forks[Fjord.idx()].1,
323 Granite => &self.forks[Granite.idx()].1,
324 Holocene => &self.forks[Holocene.idx()].1,
325 Isthmus => &self.forks[Isthmus.idx()].1,
326 Jovian => &self.forks[Jovian.idx()].1,
327 Interop => &self.forks[Interop.idx()].1,
328 }
329 }
330}
331
332impl Index<EthereumHardfork> for OpChainHardforks {
333 type Output = ForkCondition;
334
335 fn index(&self, hf: EthereumHardfork) -> &Self::Output {
336 use EthereumHardfork::*;
337 use OpHardfork::*;
338
339 match hf {
340 Frontier | Homestead | Dao | Tangerine | SpuriousDragon | Byzantium
341 | Constantinople | Petersburg | Istanbul | MuirGlacier => &ForkCondition::ZERO_BLOCK,
342 Berlin if self.is_op_mainnet() => &ForkCondition::Block(OP_MAINNET_BERLIN_BLOCK),
343 Berlin => &ForkCondition::ZERO_BLOCK,
344 London | ArrowGlacier | GrayGlacier | Paris => &self[Bedrock],
345 Shanghai => &self[Canyon],
346 Cancun => &self[Ecotone],
347 Prague => &self[Isthmus],
348 Osaka => panic!("index out of bounds"),
349 }
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use core::str::FromStr;
357
358 extern crate alloc;
359
360 #[test]
361 fn check_op_hardfork_from_str() {
362 let hardfork_str = [
363 "beDrOck", "rEgOlITH", "cAnYoN", "eCoToNe", "FJorD", "GRaNiTe", "hOlOcEnE", "isthMUS",
364 "jOvIaN", "inTerOP",
365 ];
366 let expected_hardforks = [
367 OpHardfork::Bedrock,
368 OpHardfork::Regolith,
369 OpHardfork::Canyon,
370 OpHardfork::Ecotone,
371 OpHardfork::Fjord,
372 OpHardfork::Granite,
373 OpHardfork::Holocene,
374 OpHardfork::Isthmus,
375 OpHardfork::Jovian,
376 OpHardfork::Interop,
377 ];
378
379 let hardforks: alloc::vec::Vec<OpHardfork> =
380 hardfork_str.iter().map(|h| OpHardfork::from_str(h).unwrap()).collect();
381
382 assert_eq!(hardforks, expected_hardforks);
383 }
384
385 #[test]
386 fn check_nonexistent_hardfork_from_str() {
387 assert!(OpHardfork::from_str("not a hardfork").is_err());
388 }
389
390 #[test]
391 fn op_mainnet_fork_conditions() {
392 use OpHardfork::*;
393
394 let op_mainnet_forks = OpChainHardforks::op_mainnet();
395 assert_eq!(op_mainnet_forks[Bedrock], ForkCondition::Block(OP_MAINNET_BEDROCK_BLOCK));
396 assert_eq!(
397 op_mainnet_forks[Regolith],
398 ForkCondition::Timestamp(OP_MAINNET_REGOLITH_TIMESTAMP)
399 );
400 assert_eq!(op_mainnet_forks[Canyon], ForkCondition::Timestamp(OP_MAINNET_CANYON_TIMESTAMP));
401 assert_eq!(
402 op_mainnet_forks[Ecotone],
403 ForkCondition::Timestamp(OP_MAINNET_ECOTONE_TIMESTAMP)
404 );
405 assert_eq!(op_mainnet_forks[Fjord], ForkCondition::Timestamp(OP_MAINNET_FJORD_TIMESTAMP));
406 assert_eq!(
407 op_mainnet_forks[Granite],
408 ForkCondition::Timestamp(OP_MAINNET_GRANITE_TIMESTAMP)
409 );
410 assert_eq!(
411 op_mainnet_forks[Holocene],
412 ForkCondition::Timestamp(OP_MAINNET_HOLOCENE_TIMESTAMP)
413 );
414 assert_eq!(
415 op_mainnet_forks[Isthmus],
416 ForkCondition::Timestamp(OP_MAINNET_ISTHMUS_TIMESTAMP)
417 );
418 assert_eq!(op_mainnet_forks.op_fork_activation(Jovian), ForkCondition::Never);
419 assert_eq!(op_mainnet_forks.op_fork_activation(Interop), ForkCondition::Never);
420 }
421
422 #[test]
423 fn op_sepolia_fork_conditions() {
424 use OpHardfork::*;
425
426 let op_sepolia_forks = OpChainHardforks::op_sepolia();
427 assert_eq!(op_sepolia_forks[Bedrock], ForkCondition::Block(OP_SEPOLIA_BEDROCK_BLOCK));
428 assert_eq!(
429 op_sepolia_forks[Regolith],
430 ForkCondition::Timestamp(OP_SEPOLIA_REGOLITH_TIMESTAMP)
431 );
432 assert_eq!(op_sepolia_forks[Canyon], ForkCondition::Timestamp(OP_SEPOLIA_CANYON_TIMESTAMP));
433 assert_eq!(
434 op_sepolia_forks[Ecotone],
435 ForkCondition::Timestamp(OP_SEPOLIA_ECOTONE_TIMESTAMP)
436 );
437 assert_eq!(op_sepolia_forks[Fjord], ForkCondition::Timestamp(OP_SEPOLIA_FJORD_TIMESTAMP));
438 assert_eq!(
439 op_sepolia_forks[Granite],
440 ForkCondition::Timestamp(OP_SEPOLIA_GRANITE_TIMESTAMP)
441 );
442 assert_eq!(
443 op_sepolia_forks[Holocene],
444 ForkCondition::Timestamp(OP_SEPOLIA_HOLOCENE_TIMESTAMP)
445 );
446 assert_eq!(
447 op_sepolia_forks[Isthmus],
448 ForkCondition::Timestamp(OP_SEPOLIA_ISTHMUS_TIMESTAMP)
449 );
450 assert_eq!(op_sepolia_forks.op_fork_activation(Jovian), ForkCondition::Never);
451 assert_eq!(op_sepolia_forks.op_fork_activation(Interop), ForkCondition::Never);
452 }
453
454 #[test]
455 fn base_mainnet_fork_conditions() {
456 use OpHardfork::*;
457
458 let base_mainnet_forks = OpChainHardforks::base_mainnet();
459 assert_eq!(base_mainnet_forks[Bedrock], ForkCondition::Block(BASE_MAINNET_BEDROCK_BLOCK));
460 assert_eq!(
461 base_mainnet_forks[Regolith],
462 ForkCondition::Timestamp(BASE_MAINNET_REGOLITH_TIMESTAMP)
463 );
464 assert_eq!(
465 base_mainnet_forks[Canyon],
466 ForkCondition::Timestamp(BASE_MAINNET_CANYON_TIMESTAMP)
467 );
468 assert_eq!(
469 base_mainnet_forks[Ecotone],
470 ForkCondition::Timestamp(BASE_MAINNET_ECOTONE_TIMESTAMP)
471 );
472 assert_eq!(
473 base_mainnet_forks[Fjord],
474 ForkCondition::Timestamp(BASE_MAINNET_FJORD_TIMESTAMP)
475 );
476 assert_eq!(
477 base_mainnet_forks[Granite],
478 ForkCondition::Timestamp(BASE_MAINNET_GRANITE_TIMESTAMP)
479 );
480 assert_eq!(
481 base_mainnet_forks[Holocene],
482 ForkCondition::Timestamp(BASE_MAINNET_HOLOCENE_TIMESTAMP)
483 );
484 assert_eq!(
485 base_mainnet_forks[Isthmus],
486 ForkCondition::Timestamp(BASE_MAINNET_ISTHMUS_TIMESTAMP)
487 );
488 assert_eq!(base_mainnet_forks.op_fork_activation(Jovian), ForkCondition::Never);
489 assert_eq!(base_mainnet_forks.op_fork_activation(Interop), ForkCondition::Never);
490 }
491
492 #[test]
493 fn base_sepolia_fork_conditions() {
494 use OpHardfork::*;
495
496 let base_sepolia_forks = OpChainHardforks::base_sepolia();
497 assert_eq!(base_sepolia_forks[Bedrock], ForkCondition::Block(BASE_SEPOLIA_BEDROCK_BLOCK));
498 assert_eq!(
499 base_sepolia_forks[Regolith],
500 ForkCondition::Timestamp(BASE_SEPOLIA_REGOLITH_TIMESTAMP)
501 );
502 assert_eq!(
503 base_sepolia_forks[Canyon],
504 ForkCondition::Timestamp(BASE_SEPOLIA_CANYON_TIMESTAMP)
505 );
506 assert_eq!(
507 base_sepolia_forks[Ecotone],
508 ForkCondition::Timestamp(BASE_SEPOLIA_ECOTONE_TIMESTAMP)
509 );
510 assert_eq!(
511 base_sepolia_forks[Fjord],
512 ForkCondition::Timestamp(BASE_SEPOLIA_FJORD_TIMESTAMP)
513 );
514 assert_eq!(
515 base_sepolia_forks[Granite],
516 ForkCondition::Timestamp(BASE_SEPOLIA_GRANITE_TIMESTAMP)
517 );
518 assert_eq!(
519 base_sepolia_forks[Holocene],
520 ForkCondition::Timestamp(BASE_SEPOLIA_HOLOCENE_TIMESTAMP)
521 );
522 assert_eq!(
523 base_sepolia_forks[Isthmus],
524 ForkCondition::Timestamp(BASE_SEPOLIA_ISTHMUS_TIMESTAMP)
525 );
526 assert_eq!(base_sepolia_forks.op_fork_activation(Jovian), ForkCondition::Never);
527 assert_eq!(base_sepolia_forks.op_fork_activation(Interop), ForkCondition::Never);
528 }
529
530 #[test]
531 fn test_reverse_lookup_op_chains() {
532 let test_cases = [
534 (Chain::optimism_mainnet(), OP_MAINNET_CANYON_TIMESTAMP, OpHardfork::Canyon),
537 (Chain::optimism_mainnet(), OP_MAINNET_ECOTONE_TIMESTAMP, OpHardfork::Ecotone),
538 (Chain::optimism_mainnet(), OP_MAINNET_GRANITE_TIMESTAMP, OpHardfork::Granite),
539 (Chain::optimism_mainnet(), OP_MAINNET_CANYON_TIMESTAMP - 1, OpHardfork::Regolith),
540 (Chain::optimism_mainnet(), OP_MAINNET_ISTHMUS_TIMESTAMP + 1000, OpHardfork::Isthmus),
541 (Chain::optimism_sepolia(), OP_SEPOLIA_CANYON_TIMESTAMP, OpHardfork::Canyon),
543 (Chain::optimism_sepolia(), OP_SEPOLIA_ECOTONE_TIMESTAMP, OpHardfork::Ecotone),
544 (Chain::optimism_sepolia(), OP_SEPOLIA_CANYON_TIMESTAMP - 1, OpHardfork::Regolith),
545 (Chain::base_mainnet(), BASE_MAINNET_CANYON_TIMESTAMP, OpHardfork::Canyon),
547 (Chain::base_mainnet(), BASE_MAINNET_ECOTONE_TIMESTAMP, OpHardfork::Ecotone),
548 (Chain::base_sepolia(), BASE_SEPOLIA_CANYON_TIMESTAMP, OpHardfork::Canyon),
550 (Chain::base_sepolia(), BASE_SEPOLIA_ECOTONE_TIMESTAMP, OpHardfork::Ecotone),
551 ];
552
553 for (chain_id, timestamp, expected) in test_cases {
554 assert_eq!(
555 OpHardfork::from_chain_and_timestamp(chain_id, timestamp),
556 Some(expected),
557 "chain {chain_id} at timestamp {timestamp}"
558 );
559 }
560
561 assert_eq!(OpHardfork::from_chain_and_timestamp(Chain::from_id(999999), 1000000), None);
563 }
564}