1use crate::{
4 error::TempoPrecompileError,
5 stablecoin_exchange::IStablecoinExchange,
6 storage::{Mapping, Slot, StorageOps, slots::mapping_slot},
7};
8use alloy::primitives::{Address, B256, U256, keccak256};
9use tempo_chainspec::hardfork::TempoHardfork;
10use tempo_contracts::precompiles::StablecoinExchangeError;
11use tempo_precompiles_macros::Storable;
12
13pub const MIN_TICK: i16 = -2000;
15pub const MAX_TICK: i16 = 2000;
16pub const PRICE_SCALE: u32 = 100_000;
17
18pub(crate) const MIN_PRICE_PRE_MODERATO: u32 = 67_232;
22pub(crate) const MAX_PRICE_PRE_MODERATO: u32 = 132_767;
24
25pub(crate) const MIN_PRICE_POST_MODERATO: u32 = 98_000;
29pub(crate) const MAX_PRICE_POST_MODERATO: u32 = 102_000;
31
32#[derive(Debug, Storable, Default, Clone, Copy, PartialEq, Eq)]
35pub struct TickLevel {
36 pub head: u128,
38 pub tail: u128,
40 pub total_liquidity: u128,
42}
43
44impl TickLevel {
45 pub fn new() -> Self {
47 Self {
48 head: 0,
49 tail: 0,
50 total_liquidity: 0,
51 }
52 }
53
54 pub fn with_values(head: u128, tail: u128, total_liquidity: u128) -> Self {
56 Self {
57 head,
58 tail,
59 total_liquidity,
60 }
61 }
62
63 pub fn is_empty(&self) -> bool {
65 self.head == 0 && self.tail == 0
66 }
67
68 pub fn has_liquidity(&self) -> bool {
70 !self.is_empty()
71 }
72}
73
74impl From<TickLevel> for IStablecoinExchange::PriceLevel {
75 fn from(value: TickLevel) -> Self {
76 Self {
77 head: value.head,
78 tail: value.tail,
79 totalLiquidity: value.total_liquidity,
80 }
81 }
82}
83
84#[derive(Storable, Default)]
87pub struct Orderbook {
88 pub base: Address,
90 pub quote: Address,
92 #[allow(dead_code)]
94 bids: Mapping<i16, TickLevel>,
95 #[allow(dead_code)]
97 asks: Mapping<i16, TickLevel>,
98 pub best_bid_tick: i16,
100 pub best_ask_tick: i16,
102 #[allow(dead_code)]
103 bid_bitmap: Mapping<i16, U256>,
105 #[allow(dead_code)]
107 ask_bitmap: Mapping<i16, U256>,
108}
109
110type Tokens = Slot<Address>;
112type BestOrders = Slot<i16>;
114type Orders = Mapping<i16, TickLevel>;
116type BitMaps = Mapping<i16, U256>;
118
119impl Orderbook {
120 pub fn new(base: Address, quote: Address) -> Self {
122 Self {
123 base,
124 quote,
125 best_bid_tick: i16::MIN,
126 best_ask_tick: i16::MAX,
127 ..Default::default()
128 }
129 }
130
131 pub fn is_initialized(&self) -> bool {
133 self.base != Address::ZERO
134 }
135
136 pub fn matches_tokens(
138 &self,
139 base_token: Option<Address>,
140 quote_token: Option<Address>,
141 ) -> bool {
142 if let Some(base) = base_token
144 && base != self.base
145 {
146 return false;
147 }
148
149 if let Some(quote) = quote_token
151 && quote != self.quote
152 {
153 return false;
154 }
155
156 true
157 }
158
159 pub fn update_best_bid_tick<S: StorageOps>(
161 contract: &mut S,
162 book_key: B256,
163 new_best_bid: i16,
164 ) -> Result<(), TempoPrecompileError> {
165 let orderbook_base_slot = mapping_slot(book_key.as_slice(), super::slots::BOOKS);
166 BestOrders::new_at_loc(orderbook_base_slot, __packing_orderbook::BEST_BID_TICK_LOC)
167 .write(contract, new_best_bid)?;
168 Ok(())
169 }
170
171 pub fn update_best_ask_tick<S: StorageOps>(
173 contract: &mut S,
174 book_key: B256,
175 new_best_ask: i16,
176 ) -> Result<(), TempoPrecompileError> {
177 let orderbook_base_slot = mapping_slot(book_key.as_slice(), super::slots::BOOKS);
178 BestOrders::new_at_loc(orderbook_base_slot, __packing_orderbook::BEST_ASK_TICK_LOC)
179 .write(contract, new_best_ask)?;
180 Ok(())
181 }
182
183 pub fn exists<S: StorageOps>(
185 book_key: B256,
186 contract: &mut S,
187 ) -> Result<bool, TempoPrecompileError> {
188 let orderbook_base_slot = mapping_slot(book_key.as_slice(), super::slots::BOOKS);
189 let base = Tokens::new_at_offset(
190 orderbook_base_slot,
191 __packing_orderbook::BASE_LOC.offset_slots,
192 )
193 .read(contract)?;
194
195 Ok(base != Address::ZERO)
196 }
197
198 pub fn read_tick_level<S: StorageOps>(
200 storage: &mut S,
201 book_key: B256,
202 is_bid: bool,
203 tick: i16,
204 ) -> Result<TickLevel, TempoPrecompileError> {
205 let orderbook_base_slot = mapping_slot(book_key.as_slice(), super::slots::BOOKS);
206 if is_bid {
207 Orders::at_offset(
208 orderbook_base_slot,
209 __packing_orderbook::BIDS_LOC.offset_slots,
210 tick,
211 )
212 .read(storage)
213 } else {
214 Orders::at_offset(
215 orderbook_base_slot,
216 __packing_orderbook::ASKS_LOC.offset_slots,
217 tick,
218 )
219 .read(storage)
220 }
221 }
222
223 pub fn write_tick_level<S: StorageOps>(
225 storage: &mut S,
226 book_key: B256,
227 is_bid: bool,
228 tick: i16,
229 tick_level: TickLevel,
230 ) -> Result<(), TempoPrecompileError> {
231 let orderbook_base_slot = mapping_slot(book_key.as_slice(), super::slots::BOOKS);
232 if is_bid {
233 Orders::at_offset(
234 orderbook_base_slot,
235 __packing_orderbook::BIDS_LOC.offset_slots,
236 tick,
237 )
238 .write(storage, tick_level)
239 } else {
240 Orders::at_offset(
241 orderbook_base_slot,
242 __packing_orderbook::ASKS_LOC.offset_slots,
243 tick,
244 )
245 .write(storage, tick_level)
246 }
247 }
248
249 pub fn delete_tick_level<S: StorageOps>(
251 storage: &mut S,
252 book_key: B256,
253 is_bid: bool,
254 tick: i16,
255 ) -> Result<(), TempoPrecompileError> {
256 let orderbook_base_slot = mapping_slot(book_key.as_slice(), super::slots::BOOKS);
257 if is_bid {
258 Orders::at_offset(
259 orderbook_base_slot,
260 __packing_orderbook::BIDS_LOC.offset_slots,
261 tick,
262 )
263 .delete(storage)
264 } else {
265 Orders::at_offset(
266 orderbook_base_slot,
267 __packing_orderbook::ASKS_LOC.offset_slots,
268 tick,
269 )
270 .delete(storage)
271 }
272 }
273
274 pub fn set_tick_bit<S: StorageOps>(
276 storage: &mut S,
277 book_key: B256,
278 tick: i16,
279 is_bid: bool,
280 ) -> Result<(), TempoPrecompileError> {
281 if !(MIN_TICK..=MAX_TICK).contains(&tick) {
282 return Err(StablecoinExchangeError::invalid_tick().into());
283 }
284
285 let word_index = tick >> 8;
286 let bit_index = (tick & 0xFF) as usize;
288 let mask = U256::from(1u8) << bit_index;
289
290 let orderbook_base_slot = mapping_slot(book_key.as_slice(), super::slots::BOOKS);
292 let bitmap_slot = if is_bid {
293 __packing_orderbook::BID_BITMAP_LOC.offset_slots
294 } else {
295 __packing_orderbook::ASK_BITMAP_LOC.offset_slots
296 };
297 let current_word =
298 BitMaps::at_offset(orderbook_base_slot, bitmap_slot, word_index).read(storage)?;
299
300 let new_word = current_word | mask;
302 BitMaps::at_offset(orderbook_base_slot, bitmap_slot, word_index)
303 .write(storage, new_word)?;
304
305 Ok(())
306 }
307
308 pub fn clear_tick_bit<S: StorageOps>(
310 storage: &mut S,
311 book_key: B256,
312 tick: i16,
313 is_bid: bool,
314 ) -> Result<(), TempoPrecompileError> {
315 if !(MIN_TICK..=MAX_TICK).contains(&tick) {
316 return Err(StablecoinExchangeError::invalid_tick().into());
317 }
318
319 let word_index = tick >> 8;
320 let bit_index = (tick & 0xFF) as usize;
322 let mask = !(U256::from(1u8) << bit_index);
323
324 let orderbook_base_slot = mapping_slot(book_key.as_slice(), super::slots::BOOKS);
326 let bitmap_slot = if is_bid {
327 __packing_orderbook::BID_BITMAP_LOC.offset_slots
328 } else {
329 __packing_orderbook::ASK_BITMAP_LOC.offset_slots
330 };
331 let current_word =
332 BitMaps::at_offset(orderbook_base_slot, bitmap_slot, word_index).read(storage)?;
333
334 let new_word = current_word & mask;
336 BitMaps::at_offset(orderbook_base_slot, bitmap_slot, word_index)
337 .write(storage, new_word)?;
338
339 Ok(())
340 }
341
342 pub fn is_tick_initialized<S: StorageOps>(
344 storage: &mut S,
345 book_key: B256,
346 tick: i16,
347 is_bid: bool,
348 ) -> Result<bool, TempoPrecompileError> {
349 if !(MIN_TICK..=MAX_TICK).contains(&tick) {
350 return Err(StablecoinExchangeError::invalid_tick().into());
351 }
352
353 let word_index = tick >> 8;
354 let bit_index = (tick & 0xFF) as usize;
356 let mask = U256::from(1u8) << bit_index;
357
358 let orderbook_base_slot = mapping_slot(book_key.as_slice(), super::slots::BOOKS);
359 let bitmap_slot = if is_bid {
360 __packing_orderbook::BID_BITMAP_LOC.offset_slots
361 } else {
362 __packing_orderbook::ASK_BITMAP_LOC.offset_slots
363 };
364 let word =
365 BitMaps::at_offset(orderbook_base_slot, bitmap_slot, word_index).read(storage)?;
366
367 Ok((word & mask) != U256::ZERO)
368 }
369
370 pub fn next_initialized_tick<S: StorageOps>(
372 storage: &mut S,
373 book_key: B256,
374 is_bid: bool,
375 tick: i16,
376 spec: TempoHardfork,
377 ) -> (i16, bool) {
378 if is_bid {
379 Self::next_initialized_bid_tick(storage, book_key, tick, spec)
380 } else {
381 Self::next_initialized_ask_tick(storage, book_key, tick, spec)
382 }
383 }
384
385 fn next_initialized_ask_tick<S: StorageOps>(
387 storage: &mut S,
388 book_key: B256,
389 tick: i16,
390 spec: TempoHardfork,
391 ) -> (i16, bool) {
392 if spec.is_allegretto() && tick >= MAX_TICK {
394 return (MAX_TICK, false);
395 }
396 let mut next_tick = tick + 1;
397 while next_tick <= MAX_TICK {
398 if Self::is_tick_initialized(storage, book_key, next_tick, false).unwrap_or(false) {
399 return (next_tick, true);
400 }
401 next_tick += 1;
402 }
403 (next_tick, false)
404 }
405
406 fn next_initialized_bid_tick<S: StorageOps>(
408 storage: &mut S,
409 book_key: B256,
410 tick: i16,
411 spec: TempoHardfork,
412 ) -> (i16, bool) {
413 if spec.is_allegretto() && tick <= MIN_TICK {
415 return (MIN_TICK, false);
416 }
417 let mut next_tick = tick - 1;
418 while next_tick >= MIN_TICK {
419 if Self::is_tick_initialized(storage, book_key, next_tick, true).unwrap_or(false) {
420 return (next_tick, true);
421 }
422 next_tick -= 1;
423 }
424 (next_tick, false)
425 }
426}
427
428impl From<Orderbook> for IStablecoinExchange::Orderbook {
429 fn from(value: Orderbook) -> Self {
430 Self {
431 base: value.base,
432 quote: value.quote,
433 bestBidTick: value.best_bid_tick,
434 bestAskTick: value.best_ask_tick,
435 }
436 }
437}
438
439pub fn compute_book_key(token_a: Address, token_b: Address) -> B256 {
441 let (token_a, token_b) = if token_a < token_b {
443 (token_a, token_b)
444 } else {
445 (token_b, token_a)
446 };
447
448 let mut buf = [0u8; 40];
450 buf[..20].copy_from_slice(token_a.as_slice());
451 buf[20..].copy_from_slice(token_b.as_slice());
452 keccak256(buf)
453}
454
455pub fn tick_to_price(tick: i16) -> u32 {
457 (PRICE_SCALE as i32 + tick as i32) as u32
458}
459
460pub fn price_to_tick_pre_moderato(price: u32) -> Result<i16, TempoPrecompileError> {
462 Ok((price as i32 - PRICE_SCALE as i32) as i16)
464}
465
466pub fn price_to_tick_post_moderato(price: u32) -> Result<i16, TempoPrecompileError> {
468 if !(MIN_PRICE_POST_MODERATO..=MAX_PRICE_POST_MODERATO).contains(&price) {
469 let invalid_tick = (price as i32 - PRICE_SCALE as i32) as i16;
470 return Err(StablecoinExchangeError::tick_out_of_bounds(invalid_tick).into());
471 }
472 Ok((price as i32 - PRICE_SCALE as i32) as i16)
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use alloy::primitives::address;
479
480 #[test]
481 fn test_tick_level_creation() {
482 let level = TickLevel::new();
483 assert_eq!(level.head, 0);
484 assert_eq!(level.tail, 0);
485 assert_eq!(level.total_liquidity, 0);
486 assert!(level.is_empty());
487 assert!(!level.has_liquidity());
488 }
489
490 #[test]
491 fn test_orderbook_creation() {
492 let base = address!("0x1111111111111111111111111111111111111111");
493 let quote = address!("0x2222222222222222222222222222222222222222");
494 let book = Orderbook::new(base, quote);
495
496 assert_eq!(book.base, base);
497 assert_eq!(book.quote, quote);
498 assert_eq!(book.best_bid_tick, i16::MIN);
499 assert_eq!(book.best_ask_tick, i16::MAX);
500 assert!(book.is_initialized());
501 }
502
503 #[test]
504 fn test_tick_price_conversion() {
505 assert_eq!(tick_to_price(0), PRICE_SCALE);
507 assert_eq!(price_to_tick_post_moderato(PRICE_SCALE).unwrap(), 0);
508
509 assert_eq!(tick_to_price(100), PRICE_SCALE + 100);
511 assert_eq!(price_to_tick_post_moderato(PRICE_SCALE + 100).unwrap(), 100);
512
513 assert_eq!(tick_to_price(-100), PRICE_SCALE - 100);
515 assert_eq!(
516 price_to_tick_post_moderato(PRICE_SCALE - 100).unwrap(),
517 -100
518 );
519 }
520
521 #[test]
522 fn test_price_to_tick_below_min() {
523 let result = price_to_tick_post_moderato(MIN_PRICE_POST_MODERATO - 1);
525 assert!(result.is_err());
526 assert!(matches!(
527 result.unwrap_err(),
528 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::TickOutOfBounds(_))
529 ));
530 }
531
532 #[test]
533 fn test_price_to_tick_above_max() {
534 let result = price_to_tick_post_moderato(MAX_PRICE_POST_MODERATO + 1);
536 assert!(result.is_err());
537 assert!(matches!(
538 result.unwrap_err(),
539 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::TickOutOfBounds(_))
540 ));
541 }
542
543 #[test]
544 fn test_price_to_tick_at_min_boundary_pre_moderato() {
545 let result = price_to_tick_pre_moderato(MIN_PRICE_PRE_MODERATO);
547 assert!(result.is_ok());
548 assert_eq!(result.unwrap(), i16::MIN);
549 assert_eq!(
551 MIN_PRICE_PRE_MODERATO,
552 (PRICE_SCALE as i32 + i16::MIN as i32) as u32
553 );
554 }
555
556 #[test]
557 fn test_price_to_tick_at_max_boundary_pre_moderato() {
558 let result = price_to_tick_pre_moderato(MAX_PRICE_PRE_MODERATO);
560 assert!(result.is_ok());
561 assert_eq!(result.unwrap(), i16::MAX);
562 assert_eq!(
564 MAX_PRICE_PRE_MODERATO,
565 (PRICE_SCALE as i32 + i16::MAX as i32) as u32
566 );
567 }
568
569 #[test]
570 fn test_price_to_tick_at_min_boundary_post_moderato() {
571 let result = price_to_tick_post_moderato(MIN_PRICE_POST_MODERATO);
572 assert!(result.is_ok());
573 assert_eq!(result.unwrap(), MIN_TICK);
574 assert_eq!(
575 MIN_PRICE_POST_MODERATO,
576 (PRICE_SCALE as i32 + MIN_TICK as i32) as u32
577 );
578 }
579
580 #[test]
581 fn test_price_to_tick_at_max_boundary_post_moderato() {
582 let result = price_to_tick_post_moderato(MAX_PRICE_POST_MODERATO);
583 assert!(result.is_ok());
584 assert_eq!(result.unwrap(), MAX_TICK);
585 assert_eq!(
586 MAX_PRICE_POST_MODERATO,
587 (PRICE_SCALE as i32 + MAX_TICK as i32) as u32
588 );
589 }
590
591 #[test]
592 fn test_tick_bounds() {
593 assert_eq!(MIN_TICK, -2000);
594 assert_eq!(MAX_TICK, 2000);
595
596 assert_eq!(tick_to_price(MIN_TICK), PRICE_SCALE - 2000);
598 assert_eq!(tick_to_price(MAX_TICK), PRICE_SCALE + 2000);
599 }
600
601 #[test]
602 fn test_compute_book_key() {
603 let token_a = address!("0x1111111111111111111111111111111111111111");
604 let token_b = address!("0x2222222222222222222222222222222222222222");
605
606 let key_ab = compute_book_key(token_a, token_b);
607 let key_ba = compute_book_key(token_b, token_a);
608 assert_eq!(key_ab, key_ba);
609
610 assert_eq!(
611 key_ab, key_ba,
612 "Book key should be the same regardless of address order"
613 );
614
615 let mut buf = [0u8; 40];
616 buf[..20].copy_from_slice(token_a.as_slice());
617 buf[20..].copy_from_slice(token_b.as_slice());
618 let expected_hash = keccak256(buf);
619
620 assert_eq!(
621 key_ab, expected_hash,
622 "Book key should match manual keccak256 computation"
623 );
624 }
625
626 mod bitmap_tests {
627 use super::*;
628 use crate::storage::{ContractStorage, hashmap::HashMapStorageProvider};
629
630 struct TestStorage(HashMapStorageProvider);
632
633 impl TestStorage {
634 fn new(chain_id: u64) -> Self {
635 Self(HashMapStorageProvider::new(chain_id))
636 }
637 }
638
639 impl ContractStorage for TestStorage {
640 type Storage = HashMapStorageProvider;
641
642 fn address(&self) -> Address {
643 Address::ZERO
644 }
645
646 fn storage(&mut self) -> &mut Self::Storage {
647 &mut self.0
648 }
649 }
650
651 #[test]
652 fn test_tick_lifecycle() {
653 let mut storage = TestStorage::new(1);
654 let book_key = B256::ZERO;
655
656 let test_ticks = [
659 MIN_TICK, -1000, -500, -257, -256, -100, -1, 0, 1, 100, 255, 256, 500, 1000,
660 MAX_TICK,
661 ];
662
663 for &tick in &test_ticks {
664 assert!(
666 !Orderbook::is_tick_initialized(&mut storage, book_key, tick, true).unwrap(),
667 "Tick {tick} should not be initialized initially"
668 );
669
670 Orderbook::set_tick_bit(&mut storage, book_key, tick, true).unwrap();
672
673 assert!(
674 Orderbook::is_tick_initialized(&mut storage, book_key, tick, true).unwrap(),
675 "Tick {tick} should be initialized after set"
676 );
677
678 Orderbook::clear_tick_bit(&mut storage, book_key, tick, true).unwrap();
680
681 assert!(
682 !Orderbook::is_tick_initialized(&mut storage, book_key, tick, true).unwrap(),
683 "Tick {tick} should not be initialized after clear"
684 );
685 }
686 }
687
688 #[test]
689 fn test_boundary_ticks() {
690 let mut storage = TestStorage::new(1);
691 let book_key = B256::ZERO;
692
693 Orderbook::set_tick_bit(&mut storage, book_key, MIN_TICK, true).unwrap();
695
696 assert!(
697 Orderbook::is_tick_initialized(&mut storage, book_key, MIN_TICK, true).unwrap(),
698 "MIN_TICK should be settable"
699 );
700
701 Orderbook::set_tick_bit(&mut storage, book_key, MAX_TICK, false).unwrap();
703
704 assert!(
705 Orderbook::is_tick_initialized(&mut storage, book_key, MAX_TICK, false).unwrap(),
706 "MAX_TICK should be settable"
707 );
708
709 Orderbook::clear_tick_bit(&mut storage, book_key, MIN_TICK, true).unwrap();
711
712 assert!(
713 !Orderbook::is_tick_initialized(&mut storage, book_key, MIN_TICK, true).unwrap(),
714 "MIN_TICK should be clearable"
715 );
716 }
717
718 #[test]
719 fn test_bid_and_ask_separate() {
720 let mut storage = TestStorage::new(1);
721 let book_key = B256::ZERO;
722 let tick = 100;
723
724 Orderbook::set_tick_bit(&mut storage, book_key, tick, true).unwrap();
726
727 assert!(
728 Orderbook::is_tick_initialized(&mut storage, book_key, tick, true).unwrap(),
729 "Tick should be initialized for bids"
730 );
731 assert!(
732 !Orderbook::is_tick_initialized(&mut storage, book_key, tick, false).unwrap(),
733 "Tick should not be initialized for asks"
734 );
735
736 Orderbook::set_tick_bit(&mut storage, book_key, tick, false).unwrap();
738
739 assert!(
740 Orderbook::is_tick_initialized(&mut storage, book_key, tick, true).unwrap(),
741 "Tick should still be initialized for bids"
742 );
743 assert!(
744 Orderbook::is_tick_initialized(&mut storage, book_key, tick, false).unwrap(),
745 "Tick should now be initialized for asks"
746 );
747 }
748
749 #[test]
750 fn test_ticks_across_word_boundary() {
751 let mut storage = TestStorage::new(1);
752 let book_key = B256::ZERO;
753
754 Orderbook::set_tick_bit(&mut storage, book_key, 255, true).unwrap(); Orderbook::set_tick_bit(&mut storage, book_key, 256, true).unwrap(); assert!(Orderbook::is_tick_initialized(&mut storage, book_key, 255, true).unwrap());
759 assert!(Orderbook::is_tick_initialized(&mut storage, book_key, 256, true).unwrap());
760 }
761
762 #[test]
763 fn test_ticks_different_words() {
764 let mut storage = TestStorage::new(1);
765 let book_key = B256::ZERO;
766
767 Orderbook::set_tick_bit(&mut storage, book_key, -1, true).unwrap(); Orderbook::set_tick_bit(&mut storage, book_key, -100, true).unwrap(); Orderbook::set_tick_bit(&mut storage, book_key, -256, true).unwrap(); Orderbook::set_tick_bit(&mut storage, book_key, -257, true).unwrap(); Orderbook::set_tick_bit(&mut storage, book_key, 1, true).unwrap(); Orderbook::set_tick_bit(&mut storage, book_key, 100, true).unwrap(); Orderbook::set_tick_bit(&mut storage, book_key, 256, true).unwrap(); Orderbook::set_tick_bit(&mut storage, book_key, 512, true).unwrap(); assert!(Orderbook::is_tick_initialized(&mut storage, book_key, -1, true).unwrap());
783 assert!(Orderbook::is_tick_initialized(&mut storage, book_key, -100, true).unwrap());
784 assert!(Orderbook::is_tick_initialized(&mut storage, book_key, -256, true).unwrap());
785 assert!(Orderbook::is_tick_initialized(&mut storage, book_key, -257, true).unwrap());
786
787 assert!(Orderbook::is_tick_initialized(&mut storage, book_key, 1, true).unwrap());
789 assert!(Orderbook::is_tick_initialized(&mut storage, book_key, 100, true).unwrap());
790 assert!(Orderbook::is_tick_initialized(&mut storage, book_key, 256, true).unwrap());
791 assert!(Orderbook::is_tick_initialized(&mut storage, book_key, 512, true).unwrap());
792
793 assert!(
795 !Orderbook::is_tick_initialized(&mut storage, book_key, -50, true).unwrap(),
796 "Unset negative tick should not be initialized"
797 );
798 assert!(
799 !Orderbook::is_tick_initialized(&mut storage, book_key, 50, true).unwrap(),
800 "Unset positive tick should not be initialized"
801 );
802 }
803
804 #[test]
805 fn test_set_tick_bit_out_of_bounds() {
806 let mut storage = TestStorage::new(1);
807 let book_key = B256::ZERO;
808
809 let result = Orderbook::set_tick_bit(&mut storage, book_key, MAX_TICK + 1, true);
811 assert!(result.is_err());
812 assert!(matches!(
813 result.unwrap_err(),
814 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::InvalidTick(_))
815 ));
816
817 let result = Orderbook::set_tick_bit(&mut storage, book_key, MIN_TICK - 1, true);
819 assert!(result.is_err());
820 assert!(matches!(
821 result.unwrap_err(),
822 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::InvalidTick(_))
823 ));
824 }
825
826 #[test]
827 fn test_clear_tick_bit_out_of_bounds() {
828 let mut storage = TestStorage::new(1);
829 let book_key = B256::ZERO;
830
831 let result = Orderbook::clear_tick_bit(&mut storage, book_key, MAX_TICK + 1, true);
833 assert!(result.is_err());
834 assert!(matches!(
835 result.unwrap_err(),
836 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::InvalidTick(_))
837 ));
838
839 let result = Orderbook::clear_tick_bit(&mut storage, book_key, MIN_TICK - 1, true);
841 assert!(result.is_err());
842 assert!(matches!(
843 result.unwrap_err(),
844 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::InvalidTick(_))
845 ));
846 }
847
848 #[test]
849 fn test_is_tick_initialized_out_of_bounds() {
850 let mut storage = TestStorage::new(1);
851 let book_key = B256::ZERO;
852
853 let result = Orderbook::is_tick_initialized(&mut storage, book_key, MAX_TICK + 1, true);
855 assert!(result.is_err());
856 assert!(matches!(
857 result.unwrap_err(),
858 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::InvalidTick(_))
859 ));
860
861 let result = Orderbook::is_tick_initialized(&mut storage, book_key, MIN_TICK - 1, true);
863 assert!(result.is_err());
864 assert!(matches!(
865 result.unwrap_err(),
866 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::InvalidTick(_))
867 ));
868 }
869 }
870}