tempo_precompiles/stablecoin_exchange/
orderbook.rs

1//! Orderbook and tick level management for the stablecoin DEX.
2
3use 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
13/// Constants from Solidity implementation
14pub const MIN_TICK: i16 = -2000;
15pub const MAX_TICK: i16 = 2000;
16pub const PRICE_SCALE: u32 = 100_000;
17
18// Pre-moderato: MIN_PRICE and MAX_PRICE covered full i16 range
19//
20// i16::MIN as price
21pub(crate) const MIN_PRICE_PRE_MODERATO: u32 = 67_232;
22// i16::MAX as price
23pub(crate) const MAX_PRICE_PRE_MODERATO: u32 = 132_767;
24
25// Post-moderato: MIN_PRICE and MAX_PRICE match MIN_TICK and MAX_TICK
26//
27// PRICE_SCALE + MIN_TICK = 100_000 - 2000
28pub(crate) const MIN_PRICE_POST_MODERATO: u32 = 98_000;
29// PRICE_SCALE + MAX_TICK = 100_000 + 2000
30pub(crate) const MAX_PRICE_POST_MODERATO: u32 = 102_000;
31
32/// Represents a price level in the orderbook with a doubly-linked list of orders
33/// Orders are maintained in FIFO order at each tick level
34#[derive(Debug, Storable, Default, Clone, Copy, PartialEq, Eq)]
35pub struct TickLevel {
36    /// Order ID of the first order at this tick (0 if empty)
37    pub head: u128,
38    /// Order ID of the last order at this tick (0 if empty)
39    pub tail: u128,
40    /// Total liquidity available at this tick level
41    pub total_liquidity: u128,
42}
43
44impl TickLevel {
45    /// Creates a new empty tick level
46    pub fn new() -> Self {
47        Self {
48            head: 0,
49            tail: 0,
50            total_liquidity: 0,
51        }
52    }
53
54    /// Creates a tick level with specific values
55    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    /// Returns true if this tick level has no orders
64    pub fn is_empty(&self) -> bool {
65        self.head == 0 && self.tail == 0
66    }
67
68    /// Returns true if this tick level has orders
69    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/// Orderbook for token pair with price-time priority
85/// Uses tick-based pricing with bitmaps for price discovery
86#[derive(Storable, Default)]
87pub struct Orderbook {
88    /// Base token address
89    pub base: Address,
90    /// Quote token address
91    pub quote: Address,
92    /// Bid orders by tick
93    #[allow(dead_code)]
94    bids: Mapping<i16, TickLevel>,
95    /// Ask orders by tick
96    #[allow(dead_code)]
97    asks: Mapping<i16, TickLevel>,
98    /// Best bid tick for highest bid price
99    pub best_bid_tick: i16,
100    /// Best ask tick for lowest ask price
101    pub best_ask_tick: i16,
102    #[allow(dead_code)]
103    /// Mapping of tick index to bid bitmap for price discovery
104    bid_bitmap: Mapping<i16, U256>,
105    /// Mapping of tick index to ask bitmap for price discovery
106    #[allow(dead_code)]
107    ask_bitmap: Mapping<i16, U256>,
108}
109
110// Helper type to easily access storage for orderbook tokens (base, quote)
111type Tokens = Slot<Address>;
112// Helper type to easily access storage for best orderbook orders (best_bid, best_ask)
113type BestOrders = Slot<i16>;
114// Helper type to easile access storage for orders (bids, asks)
115type Orders = Mapping<i16, TickLevel>;
116// Helper type to easily access storage for bitmaps (bid_bitmap, ask_bitmap)
117type BitMaps = Mapping<i16, U256>;
118
119impl Orderbook {
120    /// Creates a new orderbook for a token pair
121    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    /// Returns true if this orderbook is initialized
132    pub fn is_initialized(&self) -> bool {
133        self.base != Address::ZERO
134    }
135
136    /// Returns true if the base and quote tokens match the provided base and quote token options.
137    pub fn matches_tokens(
138        &self,
139        base_token: Option<Address>,
140        quote_token: Option<Address>,
141    ) -> bool {
142        // Check base token filter
143        if let Some(base) = base_token
144            && base != self.base
145        {
146            return false;
147        }
148
149        // Check quote token filter
150        if let Some(quote) = quote_token
151            && quote != self.quote
152        {
153            return false;
154        }
155
156        true
157    }
158
159    /// Update only the best bid tick
160    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    /// Update only the best ask tick
172    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    /// Check if this orderbook exists in storage
184    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    /// Read a `TickLevel` at a specific tick
199    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    /// Write a `TickLevel` at a specific tick
224    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    /// Delete a `TickLevel` at a specific tick
250    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    /// Set bit in bitmap to mark tick as active
275    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        // Use bitwise AND to get lower 8 bits correctly for both positive and negative ticks
287        let bit_index = (tick & 0xFF) as usize;
288        let mask = U256::from(1u8) << bit_index;
289
290        // Read current bitmap word
291        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        // Set the bit
301        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    /// Clear bit in bitmap to mark tick as inactive
309    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        // Use bitwise AND to get lower 8 bits correctly for both positive and negative ticks
321        let bit_index = (tick & 0xFF) as usize;
322        let mask = !(U256::from(1u8) << bit_index);
323
324        // Read current bitmap word
325        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        // Clear the bit
335        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    /// Check if a tick is initialized (has orders)
343    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        // Use bitwise AND to get lower 8 bits correctly for both positive and negative ticks
355        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    /// Find next initialized ask tick higher than current tick
371    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    /// Find next initialized ask tick higher than current tick
386    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        // Guard against overflow when tick is at or above MAX_TICK
393        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    /// Find next initialized bid tick lower than current tick
407    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        // Guard against underflow when tick is at or below MIN_TICK
414        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
439/// Compute deterministic book key from base, quote token pair
440pub fn compute_book_key(token_a: Address, token_b: Address) -> B256 {
441    // Sort tokens to ensure deterministic key
442    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    // Compute keccak256(abi.encodePacked(tokenA, tokenB))
449    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
455/// Convert relative tick to scaled price
456pub fn tick_to_price(tick: i16) -> u32 {
457    (PRICE_SCALE as i32 + tick as i32) as u32
458}
459
460/// Convert scaled price to relative tick pre moderato hardfork
461pub fn price_to_tick_pre_moderato(price: u32) -> Result<i16, TempoPrecompileError> {
462    // Pre-Moderato: legacy behavior without validation
463    Ok((price as i32 - PRICE_SCALE as i32) as i16)
464}
465
466/// Convert scaled price to relative tick post moderato hardfork
467pub 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        // Test at peg price (tick 0)
506        assert_eq!(tick_to_price(0), PRICE_SCALE);
507        assert_eq!(price_to_tick_post_moderato(PRICE_SCALE).unwrap(), 0);
508
509        // Test above peg
510        assert_eq!(tick_to_price(100), PRICE_SCALE + 100);
511        assert_eq!(price_to_tick_post_moderato(PRICE_SCALE + 100).unwrap(), 100);
512
513        // Test below peg
514        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        // Price below MIN_PRICE should return an error
524        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        // Price above MAX_PRICE should return an error
535        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        // MIN_PRICE should be valid and return i16::MIN (the minimum representable tick)
546        let result = price_to_tick_pre_moderato(MIN_PRICE_PRE_MODERATO);
547        assert!(result.is_ok());
548        assert_eq!(result.unwrap(), i16::MIN);
549        // Verify MIN_PRICE = PRICE_SCALE + i16::MIN
550        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        // MAX_PRICE should be valid and return i16::MAX (the maximum representable tick)
559        let result = price_to_tick_pre_moderato(MAX_PRICE_PRE_MODERATO);
560        assert!(result.is_ok());
561        assert_eq!(result.unwrap(), i16::MAX);
562        // Verify MAX_PRICE = PRICE_SCALE + i16::MAX
563        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        // Test boundary values
597        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        // Test wrapper that implements ContractStorage for HashMapStorageProvider
631        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            // Test full lifecycle (set, check, clear, check) for positive and negative ticks
657            // Include boundary cases, word boundaries, and various representative values
658            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                // Initially not set
665                assert!(
666                    !Orderbook::is_tick_initialized(&mut storage, book_key, tick, true).unwrap(),
667                    "Tick {tick} should not be initialized initially"
668                );
669
670                // Set the bit
671                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                // Clear the bit
679                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            // Test MIN_TICK
694            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            // Test MAX_TICK (use different storage for ask side)
702            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            // Clear MIN_TICK
710            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            // Set as bid
725            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            // Set as ask
737            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            // Ticks that span word boundary at 256
755            Orderbook::set_tick_bit(&mut storage, book_key, 255, true).unwrap(); // word_index = 0, bit_index = 255
756            Orderbook::set_tick_bit(&mut storage, book_key, 256, true).unwrap(); // word_index = 1, bit_index = 0
757
758            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            // Test ticks in different words (both positive and negative)
768
769            // Negative ticks in different words
770            Orderbook::set_tick_bit(&mut storage, book_key, -1, true).unwrap(); // word_index = -1, bit_index = 255
771            Orderbook::set_tick_bit(&mut storage, book_key, -100, true).unwrap(); // word_index = -1, bit_index = 156
772            Orderbook::set_tick_bit(&mut storage, book_key, -256, true).unwrap(); // word_index = -1, bit_index = 0
773            Orderbook::set_tick_bit(&mut storage, book_key, -257, true).unwrap(); // word_index = -2, bit_index = 255
774
775            // Positive ticks in different words
776            Orderbook::set_tick_bit(&mut storage, book_key, 1, true).unwrap(); // word_index = 0, bit_index = 1
777            Orderbook::set_tick_bit(&mut storage, book_key, 100, true).unwrap(); // word_index = 0, bit_index = 100
778            Orderbook::set_tick_bit(&mut storage, book_key, 256, true).unwrap(); // word_index = 1, bit_index = 0
779            Orderbook::set_tick_bit(&mut storage, book_key, 512, true).unwrap(); // word_index = 2, bit_index = 0
780
781            // Verify negative ticks
782            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            // Verify positive ticks
788            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            // Verify unset ticks
794            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            // Test tick above MAX_TICK
810            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            // Test tick below MIN_TICK
818            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            // Test tick above MAX_TICK
832            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            // Test tick below MIN_TICK
840            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            // Test tick above MAX_TICK
854            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            // Test tick below MIN_TICK
862            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}