Skip to main content

tempo_precompiles/stablecoin_dex/
orderbook.rs

1//! Orderbook and tick level management for the stablecoin DEX.
2
3use 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
12/// Minimum allowed tick value (corresponds to `MIN_PRICE`).
13pub const MIN_TICK: i16 = -2000;
14/// Maximum allowed tick value (corresponds to `MAX_PRICE`).
15pub const MAX_TICK: i16 = 2000;
16/// Scaling factor for tick-to-price conversion. A tick of 0 maps to `PRICE_SCALE` (peg).
17pub const PRICE_SCALE: u32 = 100_000;
18
19/// Rounding direction for price conversions.
20///
21/// Rounding should always favor the protocol to prevent insolvency:
22/// - When users deposit funds (escrow) → round UP (user pays more)
23/// - When users receive funds (settlement/refunds) → round DOWN (user receives less)
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum RoundingDirection {
26    /// Round down (floor division) - favors protocol when user receives funds
27    Down,
28    /// Round up (ceiling division) - favors protocol when user deposits funds
29    Up,
30}
31
32/// Convert base token amount to quote token amount at a given tick.
33///
34/// Formula: quote_amount = (base_amount * price) / PRICE_SCALE
35///
36/// Uses U256 for intermediate multiplication to prevent overflow.
37///
38/// # Arguments
39/// * `base_amount` - Amount of base tokens
40/// * `tick` - Price tick
41/// * `rounding` - Rounding direction
42///
43/// # Returns
44/// Quote token amount, or None if result exceeds u128
45pub 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
60/// Convert quote token amount to base token amount at a given tick.
61///
62/// Formula: base_amount = (quote_amount * PRICE_SCALE) / price
63///
64/// Uses U256 for intermediate multiplication to prevent overflow.
65///
66/// # Arguments
67/// * `quote_amount` - Amount of quote tokens
68/// * `tick` - Price tick
69/// * `rounding` - Rounding direction
70///
71/// # Returns
72/// Base token amount, or None if result exceeds u128
73pub 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
88/// Lowest representable scaled price (`PRICE_SCALE + MIN_TICK`).
89pub(crate) const MIN_PRICE: u32 = 98_000;
90/// Highest representable scaled price (`PRICE_SCALE + MAX_TICK`).
91pub(crate) const MAX_PRICE: u32 = 102_000;
92
93/// Represents a price level in the orderbook with a doubly-linked list of orders
94/// Orders are maintained in FIFO order at each tick level
95#[derive(Debug, Storable, Default, Clone, Copy, PartialEq, Eq)]
96pub struct TickLevel {
97    /// Order ID of the first order at this tick (0 if empty)
98    pub head: u128,
99    /// Order ID of the last order at this tick (0 if empty)
100    pub tail: u128,
101    /// Total liquidity available at this tick level
102    pub total_liquidity: u128,
103}
104
105impl TickLevel {
106    /// Creates a new empty tick level
107    pub fn new() -> Self {
108        Self {
109            head: 0,
110            tail: 0,
111            total_liquidity: 0,
112        }
113    }
114
115    /// Creates a tick level with specific values
116    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    /// Returns true if this tick level has no orders
125    pub fn is_empty(&self) -> bool {
126        self.head == 0 && self.tail == 0
127    }
128
129    /// Returns true if this tick level has orders
130    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/// Orderbook for token pair with price-time priority
146/// Uses tick-based pricing with bitmaps for price discovery
147#[derive(Storable, Default)]
148pub struct Orderbook {
149    /// Base token address
150    pub base: Address,
151    /// Quote token address
152    pub quote: Address,
153    /// Bid orders by tick
154    #[allow(dead_code)]
155    bids: Mapping<i16, TickLevel>,
156    /// Ask orders by tick
157    #[allow(dead_code)]
158    asks: Mapping<i16, TickLevel>,
159    /// Best bid tick for highest bid price
160    pub best_bid_tick: i16,
161    /// Best ask tick for lowest ask price
162    pub best_ask_tick: i16,
163    #[allow(dead_code)]
164    /// Mapping of tick index to bid bitmap for price discovery
165    bid_bitmap: Mapping<i16, U256>,
166    /// Mapping of tick index to ask bitmap for price discovery
167    #[allow(dead_code)]
168    ask_bitmap: Mapping<i16, U256>,
169}
170
171impl Orderbook {
172    /// Creates a new orderbook for a token pair
173    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    /// Returns true if this orderbook is initialized
184    pub fn is_initialized(&self) -> bool {
185        self.base != Address::ZERO
186    }
187
188    /// Returns true if the base and quote tokens match the provided base and quote token options.
189    pub fn matches_tokens(
190        &self,
191        base_token: Option<Address>,
192        quote_token: Option<Address>,
193    ) -> bool {
194        // Check base token filter
195        if let Some(base) = base_token
196            && base != self.base
197        {
198            return false;
199        }
200
201        // Check quote token filter
202        if let Some(quote) = quote_token
203            && quote != self.quote
204        {
205            return false;
206        }
207
208        true
209    }
210}
211
212impl OrderbookHandler {
213    /// Returns a reference to the tick level handler for the given tick and side.
214    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    /// Returns a mutable reference to the tick level handler for the given tick and side.
223    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    /// Sets the bitmap bit for `tick` to mark it as active on the given side.
240    ///
241    /// # Errors
242    /// - `InvalidTick` — tick is outside `[MIN_TICK, MAX_TICK]`
243    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        // Read current bitmap word
252        let current_word = bitmap.read()?;
253
254        // Use bitwise AND to get lower 8 bits correctly for both positive and negative ticks
255        let bit_index = (tick & 0xFF) as usize;
256        let mask = U256::from(1u8) << bit_index;
257
258        // Set the bit
259        bitmap.write(current_word | mask)
260    }
261
262    /// Clears the bitmap bit for `tick` to mark it as inactive on the given side.
263    ///
264    /// # Errors
265    /// - `InvalidTick` — tick is outside `[MIN_TICK, MAX_TICK]`
266    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        // Read current bitmap word
275        let current_word = bitmap.read()?;
276
277        // Use bitwise AND to get lower 8 bits correctly for both positive and negative ticks
278        let bit_index = (tick & 0xFF) as usize;
279        let mask = !(U256::from(1u8) << bit_index);
280
281        // Set the bit
282        bitmap.write(current_word & mask)
283    }
284
285    /// Returns `true` if the given `tick` has active orders on the specified side.
286    ///
287    /// # Errors
288    /// - `InvalidTick` — tick is outside `[MIN_TICK, MAX_TICK]`
289    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        // Read current bitmap word
298        let word = bitmap.read()?;
299
300        // Use bitwise AND to get lower 8 bits correctly for both positive and negative ticks
301        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    /// Finds the next initialized tick with liquidity. Searches downward for bids, upward for asks.
308    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    /// Find next initialized ask tick higher than current tick.
317    ///
318    /// Uses efficient bitmap word traversal: reads entire 256-bit words and uses
319    /// bit manipulation to find set bits, minimizing storage reads.
320    fn next_initialized_ask_tick(&self, tick: i16) -> Result<(i16, bool)> {
321        // Guard against overflow when tick is at or above MAX_TICK
322        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            // Mask off bits below bit_index to only consider ticks >= next_tick
340            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                // Find the lowest set bit position using trailing_zeros
349                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            // No set bits in this word, move to next word
358            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; // First tick of next word
363        }
364    }
365
366    /// Find next initialized bid tick lower than current tick.
367    ///
368    /// Uses efficient bitmap word traversal: reads entire 256-bit words and uses
369    /// bit manipulation to find set bits, minimizing storage reads.
370    fn next_initialized_bid_tick(&self, tick: i16) -> Result<(i16, bool)> {
371        // Guard against underflow when tick is at or below MIN_TICK
372        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            // Mask off bits above bit_index to only consider ticks <= next_tick
390            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                // Find the highest set bit position using leading_zeros
399                // U256 is 256 bits, so highest bit index = 255 - leading_zeros
400                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            // No set bits in this word, move to previous word
410            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; // Last tick of previous word
415        }
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
430/// Compute deterministic book key from ordered (base, quote) token pair
431pub fn compute_book_key(base: Address, quote: Address) -> B256 {
432    // Compute keccak256(abi.encodePacked(base, quote))
433    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
439/// Convert relative tick to scaled price
440pub fn tick_to_price(tick: i16) -> u32 {
441    (PRICE_SCALE as i32 + tick as i32) as u32
442}
443
444/// Converts a scaled price back to a relative tick.
445///
446/// # Errors
447/// - `TickOutOfBounds` — price is outside `[MIN_PRICE, MAX_PRICE]`
448pub 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
456/// Validates that a tick is aligned to [`TICK_SPACING`].
457///
458/// # Errors
459/// - `InvalidTick` — tick is not a multiple of [`TICK_SPACING`]
460pub 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        // Test at peg price (tick 0)
501        assert_eq!(tick_to_price(0), PRICE_SCALE);
502        assert_eq!(price_to_tick(PRICE_SCALE)?, 0);
503
504        // Test above peg
505        assert_eq!(tick_to_price(100), PRICE_SCALE + 100);
506        assert_eq!(price_to_tick(PRICE_SCALE + 100)?, 100);
507
508        // Test below peg
509        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        // Price below MIN_PRICE should return an error
518        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        // Price above MAX_PRICE should return an error
529        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        // Test boundary values
559        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                // Test full lifecycle (set, check, clear, check) for positive and negative ticks
622                // Include boundary cases, word boundaries, and various representative values
623                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                    // Initially not set
630                    assert!(
631                        !book_handler.is_tick_initialized(tick, true)?,
632                        "Tick {tick} should not be initialized initially"
633                    );
634
635                    // Set the bit
636                    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                    // Clear the bit
644                    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                // Test MIN_TICK
665                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                // Test MAX_TICK (use different storage for ask side)
673                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                // Clear MIN_TICK
681                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                // Set as bid
702                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                // Set as ask
714                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                // Ticks that span word boundary at 256
737                book_handler.set_tick_bit(255, true)?; // word_index = 0, bit_index = 255
738                book_handler.set_tick_bit(256, true)?; // word_index = 1, bit_index = 0
739
740                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                // Test ticks in different words (both positive and negative)
755
756                // Negative ticks in different words
757                book_handler.set_tick_bit(-1, true)?; // word_index = -1, bit_index = 255
758                book_handler.set_tick_bit(-100, true)?; // word_index = -1, bit_index = 156
759                book_handler.set_tick_bit(-256, true)?; // word_index = -1, bit_index = 0
760                book_handler.set_tick_bit(-257, true)?; // word_index = -2, bit_index = 255
761
762                // Positive ticks in different words
763                book_handler.set_tick_bit(1, true)?; // word_index = 0, bit_index = 1
764                book_handler.set_tick_bit(100, true)?; // word_index = 0, bit_index = 100
765                book_handler.set_tick_bit(256, true)?; // word_index = 1, bit_index = 0
766                book_handler.set_tick_bit(512, true)?; // word_index = 2, bit_index = 0
767
768                // Verify negative ticks
769                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                // Verify positive ticks
775                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                // Verify unset ticks
781                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                // Test tick above MAX_TICK
802                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                // Test tick below MIN_TICK
810                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                // Test tick above MAX_TICK
829                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                // Test tick below MIN_TICK
837                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                // Test tick above MAX_TICK
855                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                // Test tick below MIN_TICK
863                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                // Set ticks 10 and 50 (both in word 0)
882                book_handler.set_tick_bit(10, false)?;
883                book_handler.set_tick_bit(50, false)?;
884
885                // From tick 0, should find tick 10
886                let (next, found) = book_handler.next_initialized_tick(0, false)?;
887                assert!(found);
888                assert_eq!(next, 10);
889
890                // From tick 10, should find tick 50
891                let (next, found) = book_handler.next_initialized_tick(10, false)?;
892                assert!(found);
893                assert_eq!(next, 50);
894
895                // From tick 50, should find nothing in bounds
896                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                // Set ticks in different words: 100 (word 0), 300 (word 1), 600 (word 2)
913                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                // From tick 0, should find tick 100 (same word)
918                let (next, found) = book_handler.next_initialized_tick(0, false)?;
919                assert!(found);
920                assert_eq!(next, 100);
921
922                // From tick 100, should find tick 300 (cross word boundary)
923                let (next, found) = book_handler.next_initialized_tick(100, false)?;
924                assert!(found);
925                assert_eq!(next, 300);
926
927                // From tick 300, should find tick 600 (cross word boundary)
928                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                // Set ticks 10 and 50 (both in word 0) for bids
945                book_handler.set_tick_bit(10, true)?;
946                book_handler.set_tick_bit(50, true)?;
947
948                // From tick 100, should find tick 50
949                let (next, found) = book_handler.next_initialized_tick(100, true)?;
950                assert!(found);
951                assert_eq!(next, 50);
952
953                // From tick 50, should find tick 10
954                let (next, found) = book_handler.next_initialized_tick(50, true)?;
955                assert!(found);
956                assert_eq!(next, 10);
957
958                // From tick 10, should find nothing in bounds
959                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                // Set ticks in different words for bids: 600 (word 2), 300 (word 1), 100 (word 0)
976                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                // From tick 700, should find tick 600 (same word)
981                let (next, found) = book_handler.next_initialized_tick(700, true)?;
982                assert!(found);
983                assert_eq!(next, 600);
984
985                // From tick 600, should find tick 300 (cross word boundary)
986                let (next, found) = book_handler.next_initialized_tick(600, true)?;
987                assert!(found);
988                assert_eq!(next, 300);
989
990                // From tick 300, should find tick 100 (cross word boundary)
991                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                // Set negative ticks for asks
1008                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                // From -600, should find -500
1013                let (next, found) = book_handler.next_initialized_tick(-600, false)?;
1014                assert!(found);
1015                assert_eq!(next, -500);
1016
1017                // From -500, should find -100
1018                let (next, found) = book_handler.next_initialized_tick(-500, false)?;
1019                assert!(found);
1020                assert_eq!(next, -100);
1021
1022                // From -100, should find 50
1023                let (next, found) = book_handler.next_initialized_tick(-100, false)?;
1024                assert!(found);
1025                assert_eq!(next, 50);
1026
1027                // Set negative ticks for bids
1028                book_handler.set_tick_bit(-100, true)?;
1029                book_handler.set_tick_bit(-500, true)?;
1030
1031                // From 0, should find -100
1032                let (next, found) = book_handler.next_initialized_tick(0, true)?;
1033                assert!(found);
1034                assert_eq!(next, -100);
1035
1036                // From -100, should find -500
1037                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                // Test exact word boundaries (256, 512, -256, -512)
1054                book_handler.set_tick_bit(255, false)?; // Last bit of word 0
1055                book_handler.set_tick_bit(256, false)?; // First bit of word 1
1056
1057                // From 254, should find 255
1058                let (next, found) = book_handler.next_initialized_tick(254, false)?;
1059                assert!(found);
1060                assert_eq!(next, 255);
1061
1062                // From 255, should find 256 (cross word)
1063                let (next, found) = book_handler.next_initialized_tick(255, false)?;
1064                assert!(found);
1065                assert_eq!(next, 256);
1066
1067                // Test bid direction at word boundary
1068                book_handler.set_tick_bit(256, true)?;
1069                book_handler.set_tick_bit(255, true)?;
1070
1071                // From 257, should find 256
1072                let (next, found) = book_handler.next_initialized_tick(257, true)?;
1073                assert!(found);
1074                assert_eq!(next, 256);
1075
1076                // From 256, should find 255 (cross word going down)
1077                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}