Skip to main content

tempo_precompiles/stablecoin_dex/
order.rs

1//! Limit order type for the stablecoin DEX.
2//!
3//! This module defines the core `Order` type used in the stablecoin DEX orderbook.
4//! Orders support price-time priority matching, partial fills, and flip orders that
5//! automatically place opposite-side orders when filled.
6
7use crate::stablecoin_dex::{IStablecoinDEX, error::OrderError};
8use alloy::primitives::{Address, B256};
9use tempo_precompiles_macros::Storable;
10
11/// Represents an order in the stablecoin DEX orderbook.
12///
13/// This struct matches the Solidity reference implementation in StablecoinDEX.sol.
14///
15/// # Order Types
16/// - **Regular orders**: Orders with `is_flip = false`
17/// - **Flip orders**: Orders with `is_flip = true` that automatically create
18///   a new order on the opposite side when fully filled
19///
20/// # Order Lifecycle
21/// 1. Order is placed via `place()` or `placeFlip()` and immediately added to the orderbook
22/// 2. Orders can be filled (fully or partially) by swaps
23/// 3. Flip orders automatically create a new order on the opposite side when fully filled
24/// 4. Orders can be cancelled, removing them from the book and refunding escrow
25///
26/// # Price-Time Priority
27/// Orders are sorted by price (tick), then by insertion time.
28/// The doubly linked list maintains insertion order - orders are added at the tail,
29/// so traversing from head to tail gives price-time priority.
30///
31/// # Onchain Storage
32/// Orders are stored onchain in doubly linked lists organized by tick.
33/// Each tick maintains a FIFO queue of orders using `prev` and `next` pointers.
34#[derive(Debug, Clone, PartialEq, Eq, Storable)]
35pub struct Order {
36    /// Unique identifier for this order
37    pub order_id: u128,
38    /// Address of the user who placed this order
39    pub maker: Address,
40    /// Orderbook key (identifies the trading pair)
41    pub book_key: B256,
42    /// Whether this is a bid (true) or ask (false) order
43    pub is_bid: bool,
44    /// Price tick
45    pub tick: i16,
46    /// Original order amount
47    pub amount: u128,
48    /// Remaining amount to be filled
49    pub remaining: u128,
50    /// Previous order ID in the doubly linked list (0 if head)
51    pub prev: u128,
52    /// Next order ID in the doubly linked list (0 if tail)
53    pub next: u128,
54    /// Whether this is a flip order
55    pub is_flip: bool,
56    /// Tick to flip to when fully filled (for flip orders, 0 for regular orders)
57    /// For bid flips: flip_tick must be > tick
58    /// For ask flips: flip_tick must be < tick
59    pub flip_tick: i16,
60}
61
62impl Order {
63    /// Creates a new [`Order`] with `prev` and `next` initialized to 0.
64    #[allow(clippy::too_many_arguments)]
65    pub fn new(
66        order_id: u128,
67        maker: Address,
68        book_key: B256,
69        amount: u128,
70        tick: i16,
71        is_bid: bool,
72        is_flip: bool,
73        flip_tick: i16,
74    ) -> Self {
75        Self {
76            order_id,
77            maker,
78            book_key,
79            is_bid,
80            tick,
81            amount,
82            remaining: amount,
83            prev: 0,
84            next: 0,
85            is_flip,
86            flip_tick,
87        }
88    }
89
90    /// Creates a new bid order
91    pub fn new_bid(
92        order_id: u128,
93        maker: Address,
94        book_key: B256,
95        amount: u128,
96        tick: i16,
97    ) -> Self {
98        Self::new(order_id, maker, book_key, amount, tick, true, false, 0)
99    }
100
101    /// Creates a new ask order
102    pub fn new_ask(
103        order_id: u128,
104        maker: Address,
105        book_key: B256,
106        amount: u128,
107        tick: i16,
108    ) -> Self {
109        Self::new(order_id, maker, book_key, amount, tick, false, false, 0)
110    }
111
112    /// Creates a new flip order with `prev` and `next` initialized to 0.
113    /// The orderbook sets linked-list pointers when inserting.
114    ///
115    /// # Errors
116    /// - `InvalidBidFlipTick` — `is_bid` is true and `flip_tick <= tick`
117    /// - `InvalidAskFlipTick` — `is_bid` is false and `flip_tick >= tick`
118    pub fn new_flip(
119        order_id: u128,
120        maker: Address,
121        book_key: B256,
122        amount: u128,
123        tick: i16,
124        is_bid: bool,
125        flip_tick: i16,
126    ) -> Result<Self, OrderError> {
127        // Validate flip tick constraint
128        if is_bid {
129            if flip_tick <= tick {
130                return Err(OrderError::InvalidBidFlipTick { tick, flip_tick });
131            }
132        } else if flip_tick >= tick {
133            return Err(OrderError::InvalidAskFlipTick { tick, flip_tick });
134        }
135
136        Ok(Self::new(
137            order_id, maker, book_key, amount, tick, is_bid, true, flip_tick,
138        ))
139    }
140
141    /// Returns the order ID.
142    pub fn order_id(&self) -> u128 {
143        self.order_id
144    }
145
146    /// Returns the maker address.
147    pub fn maker(&self) -> Address {
148        self.maker
149    }
150
151    /// Returns the orderbook key.
152    pub fn book_key(&self) -> B256 {
153        self.book_key
154    }
155
156    /// Returns whether this is a bid order.
157    pub fn is_bid(&self) -> bool {
158        self.is_bid
159    }
160
161    /// Returns the original amount.
162    pub fn amount(&self) -> u128 {
163        self.amount
164    }
165
166    /// Returns the remaining amount.
167    pub fn remaining(&self) -> u128 {
168        self.remaining
169    }
170
171    /// Returns a mutable reference to the remaining amount.
172    fn remaining_mut(&mut self) -> &mut u128 {
173        &mut self.remaining
174    }
175
176    /// Returns the tick price.
177    pub fn tick(&self) -> i16 {
178        self.tick
179    }
180
181    /// Returns true if this is an ask order (selling base token).
182    pub fn is_ask(&self) -> bool {
183        !self.is_bid
184    }
185
186    /// Returns true if this is a flip order.
187    pub fn is_flip(&self) -> bool {
188        self.is_flip
189    }
190
191    /// Returns the flip tick.
192    ///
193    /// For non-flip orders, this is always 0.
194    /// For flip orders, this can be any valid tick value including 0 (peg price).
195    pub fn flip_tick(&self) -> i16 {
196        self.flip_tick
197    }
198
199    /// Returns the previous order ID in the doubly linked list (0 if head).
200    pub fn prev(&self) -> u128 {
201        self.prev
202    }
203
204    /// Returns the next order ID in the doubly linked list (0 if tail).
205    pub fn next(&self) -> u128 {
206        self.next
207    }
208
209    /// Sets the previous order ID in the doubly linked list.
210    pub fn set_prev(&mut self, prev_id: u128) {
211        self.prev = prev_id;
212    }
213
214    /// Sets the next order ID in the doubly linked list.
215    pub fn set_next(&mut self, next_id: u128) {
216        self.next = next_id;
217    }
218
219    /// Returns true if the order is completely filled (no remaining amount).
220    pub fn is_fully_filled(&self) -> bool {
221        self.remaining == 0
222    }
223
224    /// Fills the order by the specified amount, reducing `remaining` accordingly.
225    ///
226    /// # Errors
227    /// - `FillAmountExceedsRemaining` — `fill_amount` is greater than `remaining`
228    pub fn fill(&mut self, fill_amount: u128) -> Result<(), OrderError> {
229        if fill_amount > self.remaining {
230            return Err(OrderError::FillAmountExceedsRemaining {
231                requested: fill_amount,
232                available: self.remaining,
233            });
234        }
235        *self.remaining_mut() = self.remaining.saturating_sub(fill_amount);
236        Ok(())
237    }
238
239    /// Creates a flipped order from a fully filled flip order.
240    ///
241    /// When a flip order is completely filled, it creates a new order on the opposite side:
242    /// - Sides are swapped (bid -> ask, ask -> bid)
243    /// - New price = original flip_tick
244    /// - New flip_tick = original tick
245    /// - Amount is the same as original
246    /// - Linked list pointers are reset to 0 (will be set by orderbook on insertion)
247    ///
248    /// # Errors
249    /// - `NotAFlipOrder` — called on a non-flip order
250    /// - `OrderNotFullyFilled` — `remaining` is not zero
251    pub fn create_flipped_order(&self, new_order_id: u128) -> Result<Self, OrderError> {
252        // Check if this is a flip order
253        if !self.is_flip {
254            return Err(OrderError::NotAFlipOrder);
255        }
256
257        // Check if fully filled
258        if self.remaining != 0 {
259            return Err(OrderError::OrderNotFullyFilled {
260                remaining: self.remaining,
261            });
262        }
263
264        // Create flipped order
265        Ok(Self {
266            order_id: new_order_id,
267            maker: self.maker,
268            book_key: self.book_key,
269            is_bid: !self.is_bid,   // Flip the side
270            tick: self.flip_tick,   // Old flip_tick becomes new tick
271            amount: self.amount,    // Same as original
272            remaining: self.amount, // Reset remaining to original amount
273            prev: 0,                // Reset linked list pointers
274            next: 0,
275            is_flip: true,        // Keep as flip order
276            flip_tick: self.tick, // Old tick becomes new flip_tick
277        })
278    }
279}
280
281impl From<Order> for IStablecoinDEX::Order {
282    fn from(value: Order) -> Self {
283        Self {
284            orderId: value.order_id,
285            maker: value.maker,
286            bookKey: value.book_key,
287            isBid: value.is_bid,
288            tick: value.tick,
289            amount: value.amount,
290            remaining: value.remaining,
291            prev: value.prev,
292            next: value.next,
293            isFlip: value.is_flip,
294            flipTick: value.flip_tick,
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use crate::{
302        stablecoin_dex::StablecoinDEX,
303        storage::{Handler, StorageCtx, hashmap::HashMapStorageProvider},
304    };
305
306    use super::*;
307    use alloy::primitives::{address, b256};
308
309    const TEST_MAKER: Address = address!("0x1111111111111111111111111111111111111111");
310    const TEST_BOOK_KEY: B256 =
311        b256!("0x0000000000000000000000000000000000000000000000000000000000000001");
312
313    #[test]
314    fn test_new_bid_order() {
315        let order = Order::new_bid(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5);
316
317        assert_eq!(order.order_id(), 1);
318        assert_eq!(order.maker(), TEST_MAKER);
319        assert_eq!(order.book_key(), TEST_BOOK_KEY);
320        assert!(order.is_bid());
321        assert_eq!(order.amount(), 1000);
322        assert_eq!(order.remaining(), 1000);
323        assert!(order.is_bid());
324        assert!(!order.is_ask());
325        assert_eq!(order.tick(), 5);
326        assert!(!order.is_flip());
327        assert_eq!(order.flip_tick(), 0);
328    }
329
330    #[test]
331    fn test_new_ask_order() {
332        let order = Order::new_ask(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5);
333
334        assert_eq!(order.order_id(), 1);
335        assert!(!order.is_bid());
336        assert!(!order.is_bid());
337        assert!(order.is_ask());
338        assert!(!order.is_flip());
339    }
340
341    #[test]
342    fn test_new_flip_order_bid() {
343        let order = Order::new_flip(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5, true, 10).unwrap();
344
345        assert!(order.is_flip());
346        assert_eq!(order.flip_tick(), 10);
347        assert_eq!(order.tick(), 5);
348        assert!(order.is_bid());
349    }
350
351    #[test]
352    fn test_new_flip_order_ask() {
353        let order = Order::new_flip(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5, false, 2).unwrap();
354
355        assert!(order.is_flip());
356        assert_eq!(order.flip_tick(), 2);
357        assert_eq!(order.tick(), 5);
358        assert!(!order.is_bid());
359        assert!(order.is_ask());
360    }
361
362    #[test]
363    fn test_new_flip_order_bid_invalid_flip_tick() {
364        let result = Order::new_flip(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5, true, 3);
365
366        assert!(matches!(result, Err(OrderError::InvalidBidFlipTick { .. })));
367    }
368
369    #[test]
370    fn test_new_flip_order_ask_invalid_flip_tick() {
371        let result = Order::new_flip(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5, false, 7);
372
373        assert!(matches!(result, Err(OrderError::InvalidAskFlipTick { .. })));
374    }
375
376    #[test]
377    fn test_fill_bid_order_partial() {
378        let mut order = Order::new_bid(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5);
379
380        assert!(!order.is_fully_filled());
381
382        order.fill(400).unwrap();
383
384        assert_eq!(order.remaining(), 600);
385        assert_eq!(order.amount(), 1000);
386        assert!(!order.is_fully_filled());
387    }
388
389    #[test]
390    fn test_fill_ask_order_complete() {
391        let mut order = Order::new_ask(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5);
392
393        order.fill(1000).unwrap();
394
395        assert_eq!(order.remaining(), 0);
396        assert_eq!(order.amount(), 1000);
397        assert!(order.is_fully_filled());
398    }
399
400    #[test]
401    fn test_fill_order_overfill() {
402        let mut order = Order::new_bid(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5);
403
404        let result = order.fill(1001);
405        assert!(matches!(
406            result,
407            Err(OrderError::FillAmountExceedsRemaining { .. })
408        ));
409    }
410
411    #[test]
412    fn test_create_flipped_order_bid_to_ask() {
413        let mut order = Order::new_flip(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5, true, 10).unwrap();
414
415        // Fully fill the order
416        order.fill(1000).unwrap();
417        assert!(order.is_fully_filled());
418
419        // Create flipped order
420        let flipped = order.create_flipped_order(2).unwrap();
421
422        assert_eq!(flipped.order_id(), 2);
423        assert_eq!(flipped.maker(), order.maker());
424        assert_eq!(flipped.book_key(), order.book_key());
425        assert_eq!(flipped.amount(), 1000); // Same as original
426        assert_eq!(flipped.remaining(), 1000); // Reset to full amount
427        assert!(!flipped.is_bid()); // Flipped from bid to ask
428        assert!(flipped.is_ask());
429        assert_eq!(flipped.tick(), 10); // Old flip_tick
430        assert_eq!(flipped.flip_tick(), 5); // Old tick
431        assert!(flipped.is_flip());
432    }
433
434    #[test]
435    fn test_create_flipped_order_ask_to_bid() {
436        let mut order = Order::new_flip(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 10, false, 5).unwrap();
437
438        order.fill(1000).unwrap();
439        let flipped = order.create_flipped_order(2).unwrap();
440
441        assert!(flipped.is_bid()); // Flipped from ask to bid
442        assert!(!flipped.is_ask());
443        assert_eq!(flipped.tick(), 5); // Old flip_tick
444        assert_eq!(flipped.flip_tick(), 10); // Old tick
445    }
446
447    #[test]
448    fn test_create_flipped_order_non_flip() {
449        let mut order = Order::new_bid(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5);
450
451        order.fill(1000).unwrap();
452        let result = order.create_flipped_order(2);
453        assert!(matches!(result, Err(OrderError::NotAFlipOrder)));
454    }
455
456    #[test]
457    fn test_create_flipped_order_not_filled() {
458        let order = Order::new_flip(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5, true, 10).unwrap();
459
460        let result = order.create_flipped_order(2);
461        assert!(matches!(
462            result,
463            Err(OrderError::OrderNotFullyFilled { .. })
464        ));
465    }
466
467    #[test]
468    fn test_multiple_fills() {
469        let mut order = Order::new_bid(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5);
470
471        // Multiple partial fills
472        order.fill(300).unwrap();
473        assert_eq!(order.remaining(), 700);
474
475        order.fill(200).unwrap();
476        assert_eq!(order.remaining(), 500);
477
478        order.fill(500).unwrap();
479        assert_eq!(order.remaining(), 0);
480        assert!(order.is_fully_filled());
481    }
482
483    #[test]
484    fn test_multiple_flips() {
485        // Test that an order can flip multiple times
486        let mut order = Order::new_flip(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5, true, 10).unwrap();
487
488        // First flip: bid -> ask
489        order.fill(1000).unwrap();
490        let mut flipped1 = order.create_flipped_order(2).unwrap();
491
492        assert!(!flipped1.is_bid());
493        assert!(flipped1.is_ask());
494        assert_eq!(flipped1.tick(), 10);
495        assert_eq!(flipped1.flip_tick(), 5);
496
497        // Second flip: ask -> bid
498        flipped1.fill(1000).unwrap();
499        let flipped2 = flipped1.create_flipped_order(3).unwrap();
500
501        assert!(flipped2.is_bid());
502        assert!(!flipped2.is_ask());
503        assert_eq!(flipped2.tick(), 5);
504        assert_eq!(flipped2.flip_tick(), 10);
505    }
506
507    #[test]
508    fn test_tick_price_encoding() {
509        // Tick represents price offset from peg
510
511        let order_above = Order::new_bid(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 2);
512        assert_eq!(order_above.tick(), 2);
513
514        let order_below = Order::new_ask(2, TEST_MAKER, TEST_BOOK_KEY, 1000, -2);
515        assert_eq!(order_below.tick(), -2);
516
517        let order_par = Order::new_bid(3, TEST_MAKER, TEST_BOOK_KEY, 1000, 0);
518        assert_eq!(order_par.tick(), 0);
519    }
520
521    #[test]
522    fn test_linked_list_pointers_initialization() {
523        let order = Order::new_bid(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5);
524        // Linked list pointers should be initialized to 0
525        assert_eq!(order.prev(), 0);
526        assert_eq!(order.next(), 0);
527    }
528
529    #[test]
530    fn test_set_linked_list_pointers() {
531        let mut order = Order::new_bid(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5);
532
533        // Set prev and next pointers
534        order.set_prev(42);
535        order.set_next(43);
536
537        assert_eq!(order.prev(), 42);
538        assert_eq!(order.next(), 43);
539    }
540
541    #[test]
542    fn test_flipped_order_resets_linked_list_pointers() {
543        let mut order = Order::new_flip(1, TEST_MAKER, TEST_BOOK_KEY, 1000, 5, true, 10).unwrap();
544
545        // Set linked list pointers on original order
546        order.set_prev(100);
547        order.set_next(200);
548
549        // Fill the order
550        order.fill(1000).unwrap();
551
552        // Create flipped order
553        let flipped = order.create_flipped_order(2).unwrap();
554
555        // Flipped order should have reset pointers
556        assert_eq!(flipped.prev(), 0);
557        assert_eq!(flipped.next(), 0);
558    }
559
560    #[test]
561    fn test_store_order() -> eyre::Result<()> {
562        let mut storage = HashMapStorageProvider::new(1);
563        StorageCtx::enter(&mut storage, || {
564            let mut exchange = StablecoinDEX::new();
565
566            let id = 42;
567            let order = Order::new_flip(id, TEST_MAKER, TEST_BOOK_KEY, 1000, 5, true, 10).unwrap();
568            exchange.orders[id].write(order)?;
569
570            let loaded_order = exchange.orders[id].read()?;
571            assert_eq!(loaded_order.order_id(), 42);
572            assert_eq!(loaded_order.maker(), TEST_MAKER);
573            assert_eq!(loaded_order.book_key(), TEST_BOOK_KEY);
574            assert_eq!(loaded_order.amount(), 1000);
575            assert_eq!(loaded_order.remaining(), 1000);
576            assert_eq!(loaded_order.tick(), 5);
577            assert!(loaded_order.is_bid());
578            assert!(loaded_order.is_flip());
579            assert_eq!(loaded_order.flip_tick(), 10);
580            assert_eq!(loaded_order.prev(), 0);
581            assert_eq!(loaded_order.next(), 0);
582
583            Ok(())
584        })
585    }
586
587    #[test]
588    fn test_delete_order() -> eyre::Result<()> {
589        let mut storage = HashMapStorageProvider::new(1);
590        StorageCtx::enter(&mut storage, || {
591            let mut exchange = StablecoinDEX::new();
592
593            let id = 42;
594            let order = Order::new_flip(id, TEST_MAKER, TEST_BOOK_KEY, 1000, 5, true, 10).unwrap();
595            exchange.orders[id].write(order)?;
596            exchange.orders[id].delete()?;
597
598            let deleted_order = exchange.orders[id].read()?;
599            assert_eq!(deleted_order.order_id(), 0);
600            assert_eq!(deleted_order.maker(), Address::ZERO);
601            assert_eq!(deleted_order.book_key(), B256::ZERO);
602            assert_eq!(deleted_order.amount(), 0);
603            assert_eq!(deleted_order.remaining(), 0);
604            assert_eq!(deleted_order.tick(), 0);
605            assert!(!deleted_order.is_bid());
606            assert!(!deleted_order.is_flip());
607            assert_eq!(deleted_order.flip_tick(), 0);
608            assert_eq!(deleted_order.prev(), 0);
609            assert_eq!(deleted_order.next(), 0);
610
611            Ok(())
612        })
613    }
614}