1use crate::{
4 error::Result,
5 stablecoin_dex::{IStablecoinDEX, TICK_SPACING},
6 storage::{Handler, Mapping},
7};
8use alloy::primitives::{Address, B256, U256, keccak256};
9use tempo_contracts::precompiles::StablecoinDEXError;
10use tempo_precompiles_macros::Storable;
11
12pub const MIN_TICK: i16 = -2000;
14pub const MAX_TICK: i16 = 2000;
16pub const PRICE_SCALE: u32 = 100_000;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum RoundingDirection {
26 Down,
28 Up,
30}
31
32pub fn base_to_quote(base_amount: u128, tick: i16, rounding: RoundingDirection) -> Option<u128> {
46 let price = U256::from(tick_to_price(tick));
47 let base = U256::from(base_amount);
48 let scale = U256::from(PRICE_SCALE);
49
50 let numerator = base * price;
51
52 let result = match rounding {
53 RoundingDirection::Down => numerator / scale,
54 RoundingDirection::Up => numerator.div_ceil(scale),
55 };
56
57 result.try_into().ok()
58}
59
60pub fn quote_to_base(quote_amount: u128, tick: i16, rounding: RoundingDirection) -> Option<u128> {
74 let price = U256::from(tick_to_price(tick));
75 let quote = U256::from(quote_amount);
76 let scale = U256::from(PRICE_SCALE);
77
78 let numerator = quote * scale;
79
80 let result = match rounding {
81 RoundingDirection::Down => numerator / price,
82 RoundingDirection::Up => numerator.div_ceil(price),
83 };
84
85 result.try_into().ok()
86}
87
88pub(crate) const MIN_PRICE: u32 = 98_000;
90pub(crate) const MAX_PRICE: u32 = 102_000;
92
93#[derive(Debug, Storable, Default, Clone, Copy, PartialEq, Eq)]
96pub struct TickLevel {
97 pub head: u128,
99 pub tail: u128,
101 pub total_liquidity: u128,
103}
104
105impl TickLevel {
106 pub fn new() -> Self {
108 Self {
109 head: 0,
110 tail: 0,
111 total_liquidity: 0,
112 }
113 }
114
115 pub fn with_values(head: u128, tail: u128, total_liquidity: u128) -> Self {
117 Self {
118 head,
119 tail,
120 total_liquidity,
121 }
122 }
123
124 pub fn is_empty(&self) -> bool {
126 self.head == 0 && self.tail == 0
127 }
128
129 pub fn has_liquidity(&self) -> bool {
131 !self.is_empty()
132 }
133}
134
135impl From<TickLevel> for IStablecoinDEX::PriceLevel {
136 fn from(value: TickLevel) -> Self {
137 Self {
138 head: value.head,
139 tail: value.tail,
140 totalLiquidity: value.total_liquidity,
141 }
142 }
143}
144
145#[derive(Storable, Default)]
148pub struct Orderbook {
149 pub base: Address,
151 pub quote: Address,
153 #[allow(dead_code)]
155 bids: Mapping<i16, TickLevel>,
156 #[allow(dead_code)]
158 asks: Mapping<i16, TickLevel>,
159 pub best_bid_tick: i16,
161 pub best_ask_tick: i16,
163 #[allow(dead_code)]
164 bid_bitmap: Mapping<i16, U256>,
166 #[allow(dead_code)]
168 ask_bitmap: Mapping<i16, U256>,
169}
170
171impl Orderbook {
172 pub fn new(base: Address, quote: Address) -> Self {
174 Self {
175 base,
176 quote,
177 best_bid_tick: i16::MIN,
178 best_ask_tick: i16::MAX,
179 ..Default::default()
180 }
181 }
182
183 pub fn is_initialized(&self) -> bool {
185 self.base != Address::ZERO
186 }
187
188 pub fn matches_tokens(
190 &self,
191 base_token: Option<Address>,
192 quote_token: Option<Address>,
193 ) -> bool {
194 if let Some(base) = base_token
196 && base != self.base
197 {
198 return false;
199 }
200
201 if let Some(quote) = quote_token
203 && quote != self.quote
204 {
205 return false;
206 }
207
208 true
209 }
210}
211
212impl OrderbookHandler {
213 pub fn tick_level_handler(&self, tick: i16, is_bid: bool) -> &TickLevelHandler {
215 if is_bid {
216 &self.bids[tick]
217 } else {
218 &self.asks[tick]
219 }
220 }
221
222 pub fn tick_level_handler_mut(&mut self, tick: i16, is_bid: bool) -> &mut TickLevelHandler {
224 if is_bid {
225 &mut self.bids[tick]
226 } else {
227 &mut self.asks[tick]
228 }
229 }
230
231 fn calc_tick_word_idx(&self, tick: i16) -> Result<i16> {
232 if !(MIN_TICK..=MAX_TICK).contains(&tick) {
233 return Err(StablecoinDEXError::invalid_tick().into());
234 }
235
236 Ok(tick >> 8)
237 }
238
239 pub fn set_tick_bit(&mut self, tick: i16, is_bid: bool) -> Result<()> {
244 let word_index = self.calc_tick_word_idx(tick)?;
245 let bitmap = if is_bid {
246 &mut self.bid_bitmap[word_index]
247 } else {
248 &mut self.ask_bitmap[word_index]
249 };
250
251 let current_word = bitmap.read()?;
253
254 let bit_index = (tick & 0xFF) as usize;
256 let mask = U256::from(1u8) << bit_index;
257
258 bitmap.write(current_word | mask)
260 }
261
262 pub fn delete_tick_bit(&mut self, tick: i16, is_bid: bool) -> Result<()> {
267 let word_index = self.calc_tick_word_idx(tick)?;
268 let bitmap = if is_bid {
269 &mut self.bid_bitmap[word_index]
270 } else {
271 &mut self.ask_bitmap[word_index]
272 };
273
274 let current_word = bitmap.read()?;
276
277 let bit_index = (tick & 0xFF) as usize;
279 let mask = !(U256::from(1u8) << bit_index);
280
281 bitmap.write(current_word & mask)
283 }
284
285 pub fn is_tick_initialized(&self, tick: i16, is_bid: bool) -> Result<bool> {
290 let word_index = self.calc_tick_word_idx(tick)?;
291 let bitmap = if is_bid {
292 &self.bid_bitmap[word_index]
293 } else {
294 &self.ask_bitmap[word_index]
295 };
296
297 let word = bitmap.read()?;
299
300 let bit_index = (tick & 0xFF) as usize;
302 let mask = U256::from(1u8) << bit_index;
303
304 Ok((word & mask) != U256::ZERO)
305 }
306
307 pub fn next_initialized_tick(&self, tick: i16, is_bid: bool) -> Result<(i16, bool)> {
309 if is_bid {
310 self.next_initialized_bid_tick(tick)
311 } else {
312 self.next_initialized_ask_tick(tick)
313 }
314 }
315
316 fn next_initialized_ask_tick(&self, tick: i16) -> Result<(i16, bool)> {
321 if tick >= MAX_TICK {
323 return Ok((MAX_TICK, false));
324 }
325
326 let mut next_tick = tick + 1;
327 let max_word_index = MAX_TICK >> 8;
328
329 loop {
330 let word_index = next_tick >> 8;
331 if word_index > max_word_index {
332 return Ok((next_tick, false));
333 }
334
335 let bit_index = (next_tick & 0xFF) as usize;
336
337 let word = self.ask_bitmap[word_index].read()?;
338
339 let mask = if bit_index == 0 {
341 U256::MAX
342 } else {
343 U256::MAX << bit_index
344 };
345 let masked_word = word & mask;
346
347 if masked_word != U256::ZERO {
348 let lowest_bit = masked_word.trailing_zeros();
350 let found_tick = (word_index << 8) | (lowest_bit as i16);
351 if found_tick <= MAX_TICK {
352 return Ok((found_tick, true));
353 }
354 return Ok((found_tick, false));
355 }
356
357 let next_word_index = word_index + 1;
359 if next_word_index > max_word_index {
360 return Ok((next_word_index << 8, false));
361 }
362 next_tick = next_word_index << 8; }
364 }
365
366 fn next_initialized_bid_tick(&self, tick: i16) -> Result<(i16, bool)> {
371 if tick <= MIN_TICK {
373 return Ok((MIN_TICK, false));
374 }
375
376 let mut next_tick = tick - 1;
377 let min_word_index = MIN_TICK >> 8;
378
379 loop {
380 let word_index = next_tick >> 8;
381 if word_index < min_word_index {
382 return Ok((next_tick, false));
383 }
384
385 let bit_index = (next_tick & 0xFF) as usize;
386
387 let word = self.bid_bitmap[word_index].read()?;
388
389 let mask = if bit_index == 255 {
391 U256::MAX
392 } else {
393 U256::MAX >> (255 - bit_index)
394 };
395 let masked_word = word & mask;
396
397 if masked_word != U256::ZERO {
398 let leading = masked_word.leading_zeros();
401 let highest_bit = 255 - leading;
402 let found_tick = (word_index << 8) | (highest_bit as i16);
403 if found_tick >= MIN_TICK {
404 return Ok((found_tick, true));
405 }
406 return Ok((found_tick, false));
407 }
408
409 let prev_word_index = word_index - 1;
411 if prev_word_index < min_word_index {
412 return Ok(((prev_word_index << 8) | 0xFF, false));
413 }
414 next_tick = (prev_word_index << 8) | 0xFF; }
416 }
417}
418
419impl From<Orderbook> for IStablecoinDEX::Orderbook {
420 fn from(value: Orderbook) -> Self {
421 Self {
422 base: value.base,
423 quote: value.quote,
424 bestBidTick: value.best_bid_tick,
425 bestAskTick: value.best_ask_tick,
426 }
427 }
428}
429
430pub fn compute_book_key(base: Address, quote: Address) -> B256 {
432 let mut buf = [0u8; 40];
434 buf[..20].copy_from_slice(base.as_slice());
435 buf[20..].copy_from_slice(quote.as_slice());
436 keccak256(buf)
437}
438
439pub fn tick_to_price(tick: i16) -> u32 {
441 (PRICE_SCALE as i32 + tick as i32) as u32
442}
443
444pub fn price_to_tick(price: u32) -> Result<i16> {
449 if !(MIN_PRICE..=MAX_PRICE).contains(&price) {
450 let invalid_tick = (price as i32 - PRICE_SCALE as i32) as i16;
451 return Err(StablecoinDEXError::tick_out_of_bounds(invalid_tick).into());
452 }
453 Ok((price as i32 - PRICE_SCALE as i32) as i16)
454}
455
456pub fn validate_tick_spacing(tick: i16) -> Result<()> {
461 if tick % TICK_SPACING != 0 {
462 return Err(StablecoinDEXError::invalid_tick().into());
463 }
464 Ok(())
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use crate::error::TempoPrecompileError;
471 use rand_08::Rng;
472
473 use alloy::primitives::address;
474
475 #[test]
476 fn test_tick_level_creation() {
477 let level = TickLevel::new();
478 assert_eq!(level.head, 0);
479 assert_eq!(level.tail, 0);
480 assert_eq!(level.total_liquidity, 0);
481 assert!(level.is_empty());
482 assert!(!level.has_liquidity());
483 }
484
485 #[test]
486 fn test_orderbook_creation() {
487 let base = address!("0x1111111111111111111111111111111111111111");
488 let quote = address!("0x2222222222222222222222222222222222222222");
489 let book = Orderbook::new(base, quote);
490
491 assert_eq!(book.base, base);
492 assert_eq!(book.quote, quote);
493 assert_eq!(book.best_bid_tick, i16::MIN);
494 assert_eq!(book.best_ask_tick, i16::MAX);
495 assert!(book.is_initialized());
496 }
497
498 #[test]
499 fn test_tick_price_conversion() -> eyre::Result<()> {
500 assert_eq!(tick_to_price(0), PRICE_SCALE);
502 assert_eq!(price_to_tick(PRICE_SCALE)?, 0);
503
504 assert_eq!(tick_to_price(100), PRICE_SCALE + 100);
506 assert_eq!(price_to_tick(PRICE_SCALE + 100)?, 100);
507
508 assert_eq!(tick_to_price(-100), PRICE_SCALE - 100);
510 assert_eq!(price_to_tick(PRICE_SCALE - 100)?, -100);
511
512 Ok(())
513 }
514
515 #[test]
516 fn test_price_to_tick_below_min() {
517 let result = price_to_tick(MIN_PRICE - 1);
519 assert!(result.is_err());
520 assert!(matches!(
521 result.unwrap_err(),
522 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::TickOutOfBounds(_))
523 ));
524 }
525
526 #[test]
527 fn test_price_to_tick_above_max() {
528 let result = price_to_tick(MAX_PRICE + 1);
530 assert!(result.is_err());
531 assert!(matches!(
532 result.unwrap_err(),
533 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::TickOutOfBounds(_))
534 ));
535 }
536
537 #[test]
538 fn test_price_to_tick_at_min_boundary() {
539 let result = price_to_tick(MIN_PRICE);
540 assert!(result.is_ok());
541 assert_eq!(result.unwrap(), MIN_TICK);
542 assert_eq!(MIN_PRICE, (PRICE_SCALE as i32 + MIN_TICK as i32) as u32);
543 }
544
545 #[test]
546 fn test_price_to_tick_at_max_boundary() {
547 let result = price_to_tick(MAX_PRICE);
548 assert!(result.is_ok());
549 assert_eq!(result.unwrap(), MAX_TICK);
550 assert_eq!(MAX_PRICE, (PRICE_SCALE as i32 + MAX_TICK as i32) as u32);
551 }
552
553 #[test]
554 fn test_tick_bounds() {
555 assert_eq!(MIN_TICK, -2000);
556 assert_eq!(MAX_TICK, 2000);
557
558 assert_eq!(tick_to_price(MIN_TICK), PRICE_SCALE - 2000);
560 assert_eq!(tick_to_price(MAX_TICK), PRICE_SCALE + 2000);
561 }
562
563 #[test]
564 fn test_validate_tick_spacing() {
565 let mut rng = rand_08::thread_rng();
566
567 assert!(validate_tick_spacing(0).is_ok());
568 assert!(validate_tick_spacing(10).is_ok());
569 assert!(validate_tick_spacing(-10).is_ok());
570 assert!(validate_tick_spacing(100).is_ok());
571 assert!(validate_tick_spacing(MIN_TICK).is_ok());
572 assert!(validate_tick_spacing(MAX_TICK).is_ok());
573
574 for _ in 0..100 {
575 let tick = rng.gen_range(MIN_TICK..=MAX_TICK) * TICK_SPACING;
576 assert!(validate_tick_spacing(tick).is_ok());
577 }
578
579 for _ in 0..100 {
580 let offset = rng.gen_range(1..TICK_SPACING);
581 let base = rng.gen_range(MIN_TICK..=MAX_TICK) * TICK_SPACING;
582 let tick = base + offset;
583 assert!(validate_tick_spacing(tick).is_err());
584 }
585 }
586
587 #[test]
588 fn test_compute_book_key() {
589 let base = address!("0x1111111111111111111111111111111111111111");
590 let quote = address!("0x2222222222222222222222222222222222222222");
591
592 let key_bq = compute_book_key(base, quote);
593 let key_qb = compute_book_key(quote, base);
594
595 assert_ne!(key_bq, key_qb);
596
597 let mut buf = [0u8; 40];
598 buf[..20].copy_from_slice(base.as_slice());
599 buf[20..].copy_from_slice(quote.as_slice());
600 let expected_hash = keccak256(buf);
601
602 assert_eq!(key_bq, expected_hash,);
603 }
604
605 mod bitmap_tests {
606 use super::*;
607 use crate::{
608 stablecoin_dex::StablecoinDEX,
609 storage::{StorageCtx, hashmap::HashMapStorageProvider},
610 };
611 const BOOK_KEY: B256 = B256::ZERO;
612
613 #[test]
614 fn test_tick_lifecycle() -> eyre::Result<()> {
615 let mut storage = HashMapStorageProvider::new(1);
616 StorageCtx::enter(&mut storage, || {
617 let mut exchange = StablecoinDEX::new();
618 exchange.initialize()?;
619 let book_handler = &mut exchange.books[BOOK_KEY];
620
621 let test_ticks = [
624 MIN_TICK, -1000, -500, -257, -256, -100, -1, 0, 1, 100, 255, 256, 500, 1000,
625 MAX_TICK,
626 ];
627
628 for &tick in &test_ticks {
629 assert!(
631 !book_handler.is_tick_initialized(tick, true)?,
632 "Tick {tick} should not be initialized initially"
633 );
634
635 book_handler.set_tick_bit(tick, true)?;
637
638 assert!(
639 book_handler.is_tick_initialized(tick, true)?,
640 "Tick {tick} should be initialized after set"
641 );
642
643 book_handler.delete_tick_bit(tick, true)?;
645
646 assert!(
647 !book_handler.is_tick_initialized(tick, true)?,
648 "Tick {tick} should not be initialized after clear"
649 );
650 }
651
652 Ok(())
653 })
654 }
655
656 #[test]
657 fn test_boundary_ticks() -> eyre::Result<()> {
658 let mut storage = HashMapStorageProvider::new(1);
659 StorageCtx::enter(&mut storage, || {
660 let mut exchange = StablecoinDEX::new();
661 exchange.initialize()?;
662 let book_handler = &mut exchange.books[BOOK_KEY];
663
664 book_handler.set_tick_bit(MIN_TICK, true)?;
666
667 assert!(
668 book_handler.is_tick_initialized(MIN_TICK, true)?,
669 "MIN_TICK should be settable"
670 );
671
672 book_handler.set_tick_bit(MAX_TICK, false)?;
674
675 assert!(
676 book_handler.is_tick_initialized(MAX_TICK, false)?,
677 "MAX_TICK should be settable"
678 );
679
680 book_handler.delete_tick_bit(MIN_TICK, true)?;
682
683 assert!(
684 !book_handler.is_tick_initialized(MIN_TICK, true)?,
685 "MIN_TICK should be clearable"
686 );
687 Ok(())
688 })
689 }
690
691 #[test]
692 fn test_bid_and_ask_separate() -> eyre::Result<()> {
693 let mut storage = HashMapStorageProvider::new(1);
694 StorageCtx::enter(&mut storage, || {
695 let mut exchange = StablecoinDEX::new();
696 exchange.initialize()?;
697 let book_handler = &mut exchange.books[BOOK_KEY];
698
699 let tick = 100;
700
701 book_handler.set_tick_bit(tick, true)?;
703
704 assert!(
705 book_handler.is_tick_initialized(tick, true)?,
706 "Tick should be initialized for bids"
707 );
708 assert!(
709 !book_handler.is_tick_initialized(tick, false)?,
710 "Tick should not be initialized for asks"
711 );
712
713 book_handler.set_tick_bit(tick, false)?;
715
716 assert!(
717 book_handler.is_tick_initialized(tick, true)?,
718 "Tick should still be initialized for bids"
719 );
720 assert!(
721 book_handler.is_tick_initialized(tick, false)?,
722 "Tick should now be initialized for asks"
723 );
724 Ok(())
725 })
726 }
727
728 #[test]
729 fn test_ticks_across_word_boundary() -> eyre::Result<()> {
730 let mut storage = HashMapStorageProvider::new(1);
731 StorageCtx::enter(&mut storage, || {
732 let mut exchange = StablecoinDEX::new();
733 exchange.initialize()?;
734 let book_handler = &mut exchange.books[BOOK_KEY];
735
736 book_handler.set_tick_bit(255, true)?; book_handler.set_tick_bit(256, true)?; assert!(book_handler.is_tick_initialized(255, true)?);
741 assert!(book_handler.is_tick_initialized(256, true)?);
742 Ok(())
743 })
744 }
745
746 #[test]
747 fn test_ticks_different_words() -> eyre::Result<()> {
748 let mut storage = HashMapStorageProvider::new(1);
749 StorageCtx::enter(&mut storage, || {
750 let mut exchange = StablecoinDEX::new();
751 exchange.initialize()?;
752 let book_handler = &mut exchange.books[BOOK_KEY];
753
754 book_handler.set_tick_bit(-1, true)?; book_handler.set_tick_bit(-100, true)?; book_handler.set_tick_bit(-256, true)?; book_handler.set_tick_bit(-257, true)?; book_handler.set_tick_bit(1, true)?; book_handler.set_tick_bit(100, true)?; book_handler.set_tick_bit(256, true)?; book_handler.set_tick_bit(512, true)?; assert!(book_handler.is_tick_initialized(-1, true)?);
770 assert!(book_handler.is_tick_initialized(-100, true)?);
771 assert!(book_handler.is_tick_initialized(-256, true)?);
772 assert!(book_handler.is_tick_initialized(-257, true)?);
773
774 assert!(book_handler.is_tick_initialized(1, true)?);
776 assert!(book_handler.is_tick_initialized(100, true)?);
777 assert!(book_handler.is_tick_initialized(256, true)?);
778 assert!(book_handler.is_tick_initialized(512, true)?);
779
780 assert!(
782 !book_handler.is_tick_initialized(-50, true)?,
783 "Unset negative tick should not be initialized"
784 );
785 assert!(
786 !book_handler.is_tick_initialized(50, true)?,
787 "Unset positive tick should not be initialized"
788 );
789 Ok(())
790 })
791 }
792
793 #[test]
794 fn test_set_tick_bit_out_of_bounds() -> eyre::Result<()> {
795 let mut storage = HashMapStorageProvider::new(1);
796 StorageCtx::enter(&mut storage, || {
797 let mut exchange = StablecoinDEX::new();
798 exchange.initialize()?;
799 let book_handler = &mut exchange.books[BOOK_KEY];
800
801 let result = book_handler.set_tick_bit(MAX_TICK + 1, true);
803 assert!(result.is_err());
804 assert!(matches!(
805 result.unwrap_err(),
806 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidTick(_))
807 ));
808
809 let result = book_handler.set_tick_bit(MIN_TICK - 1, true);
811 assert!(result.is_err());
812 assert!(matches!(
813 result.unwrap_err(),
814 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidTick(_))
815 ));
816 Ok(())
817 })
818 }
819
820 #[test]
821 fn test_clear_tick_bit_out_of_bounds() -> eyre::Result<()> {
822 let mut storage = HashMapStorageProvider::new(1);
823 StorageCtx::enter(&mut storage, || {
824 let mut exchange = StablecoinDEX::new();
825 exchange.initialize()?;
826 let book_handler = &mut exchange.books[BOOK_KEY];
827
828 let result = book_handler.delete_tick_bit(MAX_TICK + 1, true);
830 assert!(result.is_err());
831 assert!(matches!(
832 result.unwrap_err(),
833 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidTick(_))
834 ));
835
836 let result = book_handler.delete_tick_bit(MIN_TICK - 1, true);
838 assert!(result.is_err());
839 assert!(matches!(
840 result.unwrap_err(),
841 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidTick(_))
842 ));
843 Ok(())
844 })
845 }
846
847 #[test]
848 fn test_is_tick_initialized_out_of_bounds() -> eyre::Result<()> {
849 let mut storage = HashMapStorageProvider::new(1);
850 StorageCtx::enter(&mut storage, || {
851 let exchange = StablecoinDEX::new();
852 let book_handler = &exchange.books[BOOK_KEY];
853
854 let result = book_handler.is_tick_initialized(MAX_TICK + 1, true);
856 assert!(result.is_err());
857 assert!(matches!(
858 result.unwrap_err(),
859 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidTick(_))
860 ));
861
862 let result = book_handler.is_tick_initialized(MIN_TICK - 1, true);
864 assert!(result.is_err());
865 assert!(matches!(
866 result.unwrap_err(),
867 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidTick(_))
868 ));
869 Ok(())
870 })
871 }
872
873 #[test]
874 fn test_next_initialized_ask_tick_same_word() -> eyre::Result<()> {
875 let mut storage = HashMapStorageProvider::new(1);
876 StorageCtx::enter(&mut storage, || {
877 let mut exchange = StablecoinDEX::new();
878 exchange.initialize()?;
879 let book_handler = &mut exchange.books[BOOK_KEY];
880
881 book_handler.set_tick_bit(10, false)?;
883 book_handler.set_tick_bit(50, false)?;
884
885 let (next, found) = book_handler.next_initialized_tick(0, false)?;
887 assert!(found);
888 assert_eq!(next, 10);
889
890 let (next, found) = book_handler.next_initialized_tick(10, false)?;
892 assert!(found);
893 assert_eq!(next, 50);
894
895 let (next, found) = book_handler.next_initialized_tick(50, false)?;
897 assert!(!found);
898 assert!(next > MAX_TICK);
899
900 Ok(())
901 })
902 }
903
904 #[test]
905 fn test_next_initialized_ask_tick_cross_word() -> eyre::Result<()> {
906 let mut storage = HashMapStorageProvider::new(1);
907 StorageCtx::enter(&mut storage, || {
908 let mut exchange = StablecoinDEX::new();
909 exchange.initialize()?;
910 let book_handler = &mut exchange.books[BOOK_KEY];
911
912 book_handler.set_tick_bit(100, false)?;
914 book_handler.set_tick_bit(300, false)?;
915 book_handler.set_tick_bit(600, false)?;
916
917 let (next, found) = book_handler.next_initialized_tick(0, false)?;
919 assert!(found);
920 assert_eq!(next, 100);
921
922 let (next, found) = book_handler.next_initialized_tick(100, false)?;
924 assert!(found);
925 assert_eq!(next, 300);
926
927 let (next, found) = book_handler.next_initialized_tick(300, false)?;
929 assert!(found);
930 assert_eq!(next, 600);
931
932 Ok(())
933 })
934 }
935
936 #[test]
937 fn test_next_initialized_bid_tick_same_word() -> eyre::Result<()> {
938 let mut storage = HashMapStorageProvider::new(1);
939 StorageCtx::enter(&mut storage, || {
940 let mut exchange = StablecoinDEX::new();
941 exchange.initialize()?;
942 let book_handler = &mut exchange.books[BOOK_KEY];
943
944 book_handler.set_tick_bit(10, true)?;
946 book_handler.set_tick_bit(50, true)?;
947
948 let (next, found) = book_handler.next_initialized_tick(100, true)?;
950 assert!(found);
951 assert_eq!(next, 50);
952
953 let (next, found) = book_handler.next_initialized_tick(50, true)?;
955 assert!(found);
956 assert_eq!(next, 10);
957
958 let (next, found) = book_handler.next_initialized_tick(10, true)?;
960 assert!(!found);
961 assert!(next < MIN_TICK);
962
963 Ok(())
964 })
965 }
966
967 #[test]
968 fn test_next_initialized_bid_tick_cross_word() -> eyre::Result<()> {
969 let mut storage = HashMapStorageProvider::new(1);
970 StorageCtx::enter(&mut storage, || {
971 let mut exchange = StablecoinDEX::new();
972 exchange.initialize()?;
973 let book_handler = &mut exchange.books[BOOK_KEY];
974
975 book_handler.set_tick_bit(600, true)?;
977 book_handler.set_tick_bit(300, true)?;
978 book_handler.set_tick_bit(100, true)?;
979
980 let (next, found) = book_handler.next_initialized_tick(700, true)?;
982 assert!(found);
983 assert_eq!(next, 600);
984
985 let (next, found) = book_handler.next_initialized_tick(600, true)?;
987 assert!(found);
988 assert_eq!(next, 300);
989
990 let (next, found) = book_handler.next_initialized_tick(300, true)?;
992 assert!(found);
993 assert_eq!(next, 100);
994
995 Ok(())
996 })
997 }
998
999 #[test]
1000 fn test_next_initialized_tick_negative_ticks() -> eyre::Result<()> {
1001 let mut storage = HashMapStorageProvider::new(1);
1002 StorageCtx::enter(&mut storage, || {
1003 let mut exchange = StablecoinDEX::new();
1004 exchange.initialize()?;
1005 let book_handler = &mut exchange.books[BOOK_KEY];
1006
1007 book_handler.set_tick_bit(-500, false)?;
1009 book_handler.set_tick_bit(-100, false)?;
1010 book_handler.set_tick_bit(50, false)?;
1011
1012 let (next, found) = book_handler.next_initialized_tick(-600, false)?;
1014 assert!(found);
1015 assert_eq!(next, -500);
1016
1017 let (next, found) = book_handler.next_initialized_tick(-500, false)?;
1019 assert!(found);
1020 assert_eq!(next, -100);
1021
1022 let (next, found) = book_handler.next_initialized_tick(-100, false)?;
1024 assert!(found);
1025 assert_eq!(next, 50);
1026
1027 book_handler.set_tick_bit(-100, true)?;
1029 book_handler.set_tick_bit(-500, true)?;
1030
1031 let (next, found) = book_handler.next_initialized_tick(0, true)?;
1033 assert!(found);
1034 assert_eq!(next, -100);
1035
1036 let (next, found) = book_handler.next_initialized_tick(-100, true)?;
1038 assert!(found);
1039 assert_eq!(next, -500);
1040
1041 Ok(())
1042 })
1043 }
1044
1045 #[test]
1046 fn test_next_initialized_tick_at_word_boundary() -> eyre::Result<()> {
1047 let mut storage = HashMapStorageProvider::new(1);
1048 StorageCtx::enter(&mut storage, || {
1049 let mut exchange = StablecoinDEX::new();
1050 exchange.initialize()?;
1051 let book_handler = &mut exchange.books[BOOK_KEY];
1052
1053 book_handler.set_tick_bit(255, false)?; book_handler.set_tick_bit(256, false)?; let (next, found) = book_handler.next_initialized_tick(254, false)?;
1059 assert!(found);
1060 assert_eq!(next, 255);
1061
1062 let (next, found) = book_handler.next_initialized_tick(255, false)?;
1064 assert!(found);
1065 assert_eq!(next, 256);
1066
1067 book_handler.set_tick_bit(256, true)?;
1069 book_handler.set_tick_bit(255, true)?;
1070
1071 let (next, found) = book_handler.next_initialized_tick(257, true)?;
1073 assert!(found);
1074 assert_eq!(next, 256);
1075
1076 let (next, found) = book_handler.next_initialized_tick(256, true)?;
1078 assert!(found);
1079 assert_eq!(next, 255);
1080
1081 Ok(())
1082 })
1083 }
1084 }
1085
1086 mod rounding_tests {
1087 use super::*;
1088
1089 #[test]
1090 fn test_base_to_quote_rounds_down_correctly() {
1091 let base_amount = 1_000_003u128;
1092 let tick = 0i16;
1093
1094 let quote_down = base_to_quote(base_amount, tick, RoundingDirection::Down).unwrap();
1095 let quote_up = base_to_quote(base_amount, tick, RoundingDirection::Up).unwrap();
1096
1097 assert_eq!(quote_down, 1_000_003);
1098 assert_eq!(quote_up, 1_000_003);
1099 }
1100
1101 #[test]
1102 fn test_base_to_quote_rounds_up_when_remainder_exists() {
1103 let base_amount = 33u128;
1104 let tick = 100i16;
1105
1106 let price = tick_to_price(tick) as u128;
1107 let numerator = base_amount * price;
1108 let has_remainder = !numerator.is_multiple_of(PRICE_SCALE as u128);
1109
1110 let quote_down = base_to_quote(base_amount, tick, RoundingDirection::Down).unwrap();
1111 let quote_up = base_to_quote(base_amount, tick, RoundingDirection::Up).unwrap();
1112
1113 if has_remainder {
1114 assert_eq!(
1115 quote_up,
1116 quote_down + 1,
1117 "Round up should be 1 more than round down when there's a remainder"
1118 );
1119 } else {
1120 assert_eq!(
1121 quote_up, quote_down,
1122 "Round up and down should be equal when there's no remainder"
1123 );
1124 }
1125 }
1126
1127 #[test]
1128 fn test_quote_to_base_rounds_down_correctly() {
1129 let quote_amount = 1_000_003u128;
1130 let tick = 0i16;
1131
1132 let base_down = quote_to_base(quote_amount, tick, RoundingDirection::Down).unwrap();
1133 let base_up = quote_to_base(quote_amount, tick, RoundingDirection::Up).unwrap();
1134
1135 assert_eq!(base_down, 1_000_003);
1136 assert_eq!(base_up, 1_000_003);
1137 }
1138
1139 #[test]
1140 fn test_quote_to_base_rounds_up_when_remainder_exists() {
1141 let quote_amount = 33u128;
1142 let tick = 100i16;
1143
1144 let price = tick_to_price(tick) as u128;
1145 let numerator = quote_amount * PRICE_SCALE as u128;
1146 let has_remainder = !numerator.is_multiple_of(price);
1147
1148 let base_down = quote_to_base(quote_amount, tick, RoundingDirection::Down).unwrap();
1149 let base_up = quote_to_base(quote_amount, tick, RoundingDirection::Up).unwrap();
1150
1151 if has_remainder {
1152 assert_eq!(
1153 base_up,
1154 base_down + 1,
1155 "Round up should be 1 more than round down when there's a remainder"
1156 );
1157 } else {
1158 assert_eq!(
1159 base_up, base_down,
1160 "Round up and down should be equal when there's no remainder"
1161 );
1162 }
1163 }
1164
1165 #[test]
1166 fn test_rounding_favors_protocol_for_bid_escrow() {
1167 let base_amount = 10_000_001u128;
1168 let tick = 100i16;
1169
1170 let escrow_floor = base_to_quote(base_amount, tick, RoundingDirection::Down).unwrap();
1171 let escrow_ceil = base_to_quote(base_amount, tick, RoundingDirection::Up).unwrap();
1172
1173 assert!(
1174 escrow_ceil >= escrow_floor,
1175 "Ceiling should never be less than floor"
1176 );
1177 }
1178
1179 #[test]
1180 fn test_rounding_favors_protocol_for_settlement() {
1181 let base_amount = 10_000_001u128;
1182 let tick = 100i16;
1183
1184 let payout_floor = base_to_quote(base_amount, tick, RoundingDirection::Down).unwrap();
1185 let payout_ceil = base_to_quote(base_amount, tick, RoundingDirection::Up).unwrap();
1186
1187 assert!(
1188 payout_floor <= payout_ceil,
1189 "Floor should never be more than ceiling"
1190 );
1191 }
1192 }
1193
1194 mod u256_upcast_tests {
1195 use super::*;
1196
1197 #[test]
1198 fn test_base_to_quote_large_amount_no_overflow() {
1199 let large_base_amount: u128 = u128::MAX / 100_000;
1200 let result = base_to_quote(large_base_amount, MAX_TICK, RoundingDirection::Down);
1201
1202 assert!(
1203 result.is_some(),
1204 "base_to_quote should handle large amounts without overflow using U256"
1205 );
1206
1207 let expected = large_base_amount
1208 .checked_mul(102)
1209 .and_then(|v| v.checked_div(100));
1210 assert_eq!(result, expected);
1211 }
1212
1213 #[test]
1214 fn test_quote_to_base_large_amount_no_overflow() {
1215 let large_quote_amount: u128 = (u128::MAX / PRICE_SCALE as u128) + 1;
1216
1217 assert!(
1218 large_quote_amount
1219 .checked_mul(PRICE_SCALE as u128)
1220 .is_none(),
1221 "Test setup: this value should overflow u128 multiplication"
1222 );
1223
1224 let result = quote_to_base(large_quote_amount, MAX_TICK, RoundingDirection::Down);
1225
1226 assert!(
1227 result.is_some(),
1228 "quote_to_base should handle large amounts without overflow using U256"
1229 );
1230 }
1231 }
1232}