1pub mod dispatch;
9pub mod error;
10pub mod order;
11pub mod orderbook;
12
13pub use order::Order;
14pub use orderbook::{
15 MAX_TICK, MIN_TICK, Orderbook, PRICE_SCALE, RoundingDirection, TickLevel, base_to_quote,
16 quote_to_base, tick_to_price, validate_tick_spacing,
17};
18use tempo_contracts::precompiles::PATH_USD_ADDRESS;
19pub use tempo_contracts::precompiles::{IStablecoinDEX, StablecoinDEXError, StablecoinDEXEvents};
20
21use crate::{
22 STABLECOIN_DEX_ADDRESS,
23 error::{Result, TempoPrecompileError},
24 stablecoin_dex::orderbook::{MAX_PRICE, MIN_PRICE, compute_book_key},
25 storage::{Handler, Mapping},
26 tip20::{ITIP20, TIP20Token, is_tip20_prefix, validate_usd_currency},
27 tip20_factory::TIP20Factory,
28 tip403_registry::{AuthRole, TIP403Registry, is_policy_lookup_error},
29};
30use alloy::primitives::{Address, B256, U256};
31use tempo_precompiles_macros::contract;
32
33pub const MIN_ORDER_AMOUNT: u128 = 100_000_000;
35
36pub const TICK_SPACING: i16 = 10;
38
39#[contract(addr = STABLECOIN_DEX_ADDRESS)]
47pub struct StablecoinDEX {
48 books: Mapping<B256, Orderbook>,
49 orders: Mapping<u128, Order>,
50 balances: Mapping<Address, Mapping<Address, u128>>,
51 next_order_id: u128,
52 book_keys: Vec<B256>,
53}
54
55impl StablecoinDEX {
56 pub fn address(&self) -> Address {
58 self.address
59 }
60
61 pub fn initialize(&mut self) -> Result<()> {
63 self.__initialize()
65 }
66
67 fn next_order_id(&self) -> Result<u128> {
69 Ok(self.next_order_id.read()?.max(1))
70 }
71
72 fn increment_next_order_id(&mut self) -> Result<()> {
74 let next_order_id = self.next_order_id()?;
75 self.next_order_id.write(next_order_id + 1)
76 }
77
78 pub fn balance_of(&self, user: Address, token: Address) -> Result<u128> {
80 self.balances[user][token].read()
81 }
82
83 pub fn min_price(&self) -> u32 {
85 MIN_PRICE
86 }
87
88 pub fn max_price(&self) -> u32 {
90 MAX_PRICE
91 }
92
93 fn validate_or_create_pair(&mut self, book: &Orderbook, token: Address) -> Result<()> {
95 if book.base.is_zero() {
96 self.create_pair(token)?;
97 }
98 Ok(())
99 }
100
101 pub fn get_order(&self, order_id: u128) -> Result<Order> {
107 let order = self.orders[order_id].read()?;
108
109 if !order.maker().is_zero() && order.order_id() < self.next_order_id()? {
111 Ok(order)
112 } else {
113 Err(StablecoinDEXError::order_does_not_exist().into())
114 }
115 }
116
117 fn set_balance(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
119 self.balances[user][token].write(amount)
120 }
121
122 fn increment_balance(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
124 let current = self.balance_of(user, token)?;
125 self.set_balance(
126 user,
127 token,
128 current
129 .checked_add(amount)
130 .ok_or(TempoPrecompileError::under_overflow())?,
131 )
132 }
133
134 fn sub_balance(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
136 let current = self.balance_of(user, token)?;
137 self.set_balance(
138 user,
139 token,
140 current
141 .checked_sub(amount)
142 .ok_or(TempoPrecompileError::under_overflow())?,
143 )
144 }
145
146 fn emit_order_filled(
148 &mut self,
149 order_id: u128,
150 maker: Address,
151 taker: Address,
152 amount_filled: u128,
153 partial_fill: bool,
154 ) -> Result<()> {
155 self.emit_event(StablecoinDEXEvents::OrderFilled(
156 IStablecoinDEX::OrderFilled {
157 orderId: order_id,
158 maker,
159 taker,
160 amountFilled: amount_filled,
161 partialFill: partial_fill,
162 },
163 ))?;
164
165 Ok(())
166 }
167
168 fn transfer(&mut self, token: Address, to: Address, amount: u128) -> Result<()> {
170 TIP20Token::from_address(token)?.transfer(
171 self.address,
172 ITIP20::transferCall {
173 to,
174 amount: U256::from(amount),
175 },
176 )?;
177 Ok(())
178 }
179
180 fn transfer_from(&mut self, token: Address, from: Address, amount: u128) -> Result<()> {
182 TIP20Token::from_address(token)?.transfer_from(
183 self.address,
184 ITIP20::transferFromCall {
185 from,
186 to: self.address,
187 amount: U256::from(amount),
188 },
189 )?;
190 Ok(())
191 }
192
193 fn decrement_balance_or_transfer_from(
195 &mut self,
196 user: Address,
197 token: Address,
198 amount: u128,
199 ) -> Result<()> {
200 TIP20Token::from_address(token)?.ensure_transfer_authorized(user, self.address)?;
201
202 let user_balance = self.balance_of(user, token)?;
203 if user_balance >= amount {
204 self.sub_balance(user, token, amount)
205 } else {
206 let remaining = amount
207 .checked_sub(user_balance)
208 .ok_or(TempoPrecompileError::under_overflow())?;
209
210 self.transfer_from(token, user, remaining)?;
211 self.set_balance(user, token, 0)
212 }
213 }
214
215 pub fn quote_swap_exact_amount_out(
224 &self,
225 token_in: Address,
226 token_out: Address,
227 amount_out: u128,
228 ) -> Result<u128> {
229 let route = self.find_trade_path(token_in, token_out)?;
231
232 let mut current_amount = amount_out;
234 for (book_key, base_for_quote) in route.iter().rev() {
235 current_amount = self.quote_exact_out(*book_key, current_amount, *base_for_quote)?;
236 }
237
238 Ok(current_amount)
239 }
240
241 pub fn quote_swap_exact_amount_in(
250 &self,
251 token_in: Address,
252 token_out: Address,
253 amount_in: u128,
254 ) -> Result<u128> {
255 let route = self.find_trade_path(token_in, token_out)?;
257
258 let mut current_amount = amount_in;
260 for (book_key, base_for_quote) in route {
261 current_amount = self.quote_exact_in(book_key, current_amount, base_for_quote)?;
262 }
263
264 Ok(current_amount)
265 }
266
267 pub fn swap_exact_amount_in(
277 &mut self,
278 sender: Address,
279 token_in: Address,
280 token_out: Address,
281 amount_in: u128,
282 min_amount_out: u128,
283 ) -> Result<u128> {
284 let route = self.find_trade_path(token_in, token_out)?;
286
287 self.decrement_balance_or_transfer_from(sender, token_in, amount_in)?;
289
290 let mut amount = amount_in;
292 for (book_key, base_for_quote) in route {
293 amount = self.fill_orders_exact_in(book_key, base_for_quote, amount, sender)?;
295 }
296
297 if amount < min_amount_out {
299 return Err(StablecoinDEXError::insufficient_output().into());
300 }
301
302 self.transfer(token_out, sender, amount)?;
303
304 Ok(amount)
305 }
306
307 pub fn swap_exact_amount_out(
317 &mut self,
318 sender: Address,
319 token_in: Address,
320 token_out: Address,
321 amount_out: u128,
322 max_amount_in: u128,
323 ) -> Result<u128> {
324 let route = self.find_trade_path(token_in, token_out)?;
326
327 let mut amount = amount_out;
329 for (book_key, base_for_quote) in route.iter().rev() {
330 amount = self.fill_orders_exact_out(*book_key, *base_for_quote, amount, sender)?;
331 }
332
333 if amount > max_amount_in {
334 return Err(StablecoinDEXError::max_input_exceeded().into());
335 }
336
337 self.decrement_balance_or_transfer_from(sender, token_in, amount)?;
339
340 self.transfer(token_out, sender, amount_out)?;
342
343 Ok(amount)
344 }
345
346 pub fn get_price_level(&self, base: Address, tick: i16, is_bid: bool) -> Result<TickLevel> {
352 let quote = TIP20Token::from_address(base)?.quote_token()?;
353 let book_key = compute_book_key(base, quote);
354 if is_bid {
355 self.books[book_key].bids[tick].read()
356 } else {
357 self.books[book_key].asks[tick].read()
358 }
359 }
360
361 pub fn books(&self, pair_key: B256) -> Result<Orderbook> {
363 self.books[pair_key].read()
364 }
365
366 pub fn get_book_keys(&self) -> Result<Vec<B256>> {
368 self.book_keys.read()
369 }
370
371 pub fn tick_to_price(&self, tick: i16) -> Result<u32> {
376 if self.storage.spec().is_t2() {
377 orderbook::validate_tick_spacing(tick)?;
378 }
379
380 Ok(orderbook::tick_to_price(tick))
381 }
382
383 pub fn price_to_tick(&self, price: u32) -> Result<i16> {
389 let tick = orderbook::price_to_tick(price)?;
390
391 if self.storage.spec().is_t2() {
392 orderbook::validate_tick_spacing(tick)?;
393 }
394
395 Ok(tick)
396 }
397
398 pub fn create_pair(&mut self, base: Address) -> Result<B256> {
407 if !TIP20Factory::new().is_tip20(base)? {
409 return Err(StablecoinDEXError::invalid_base_token().into());
410 }
411
412 let quote = TIP20Token::from_address(base)?.quote_token()?;
413 validate_usd_currency(base)?;
414 validate_usd_currency(quote)?;
415
416 let book_key = compute_book_key(base, quote);
417
418 if self.books[book_key].read()?.is_initialized() {
419 return Err(StablecoinDEXError::pair_already_exists().into());
420 }
421
422 let book = Orderbook::new(base, quote);
423 self.books[book_key].write(book)?;
424 self.book_keys.push(book_key)?;
425
426 self.emit_event(StablecoinDEXEvents::PairCreated(
428 IStablecoinDEX::PairCreated {
429 key: book_key,
430 base,
431 quote,
432 },
433 ))?;
434
435 Ok(book_key)
436 }
437
438 pub fn place(
453 &mut self,
454 sender: Address,
455 token: Address,
456 amount: u128,
457 is_bid: bool,
458 tick: i16,
459 ) -> Result<u128> {
460 let quote_token = TIP20Token::from_address(token)?.quote_token()?;
461
462 let book_key = compute_book_key(token, quote_token);
464
465 let book = self.books[book_key].read()?;
466 self.validate_or_create_pair(&book, token)?;
467
468 if !(MIN_TICK..=MAX_TICK).contains(&tick) {
470 return Err(StablecoinDEXError::tick_out_of_bounds(tick).into());
471 }
472
473 if tick % TICK_SPACING != 0 {
475 return Err(StablecoinDEXError::invalid_tick().into());
476 }
477
478 if amount < MIN_ORDER_AMOUNT {
480 return Err(StablecoinDEXError::below_minimum_order_size(amount).into());
481 }
482
483 let (escrow_token, escrow_amount, non_escrow_token) = if is_bid {
485 let quote_amount = base_to_quote(amount, tick, RoundingDirection::Up)
487 .ok_or(StablecoinDEXError::insufficient_balance())?;
488 (quote_token, quote_amount, token)
489 } else {
490 (token, amount, quote_token)
492 };
493
494 TIP20Token::from_address(non_escrow_token)?
497 .ensure_transfer_authorized(self.address, sender)?;
498
499 self.decrement_balance_or_transfer_from(sender, escrow_token, escrow_amount)?;
501
502 let order_id = self.next_order_id()?;
504 self.increment_next_order_id()?;
505 let order = if is_bid {
506 Order::new_bid(order_id, sender, book_key, amount, tick)
507 } else {
508 Order::new_ask(order_id, sender, book_key, amount, tick)
509 };
510 self.commit_order_to_book(order)?;
511
512 self.emit_event(StablecoinDEXEvents::OrderPlaced(
514 IStablecoinDEX::OrderPlaced {
515 orderId: order_id,
516 maker: sender,
517 token,
518 amount,
519 isBid: is_bid,
520 tick,
521 isFlipOrder: false,
522 flipTick: 0,
523 },
524 ))?;
525
526 Ok(order_id)
527 }
528
529 fn commit_order_to_book(&mut self, mut order: Order) -> Result<()> {
531 let orderbook = self.books[order.book_key()].read()?;
532 let mut level = self.books[order.book_key()]
533 .tick_level_handler(order.tick(), order.is_bid())
534 .read()?;
535
536 let prev_tail = level.tail;
537 if prev_tail == 0 {
538 level.head = order.order_id();
539 level.tail = order.order_id();
540
541 self.books[order.book_key()].set_tick_bit(order.tick(), order.is_bid())?;
542
543 if order.is_bid() {
544 if order.tick() > orderbook.best_bid_tick {
545 self.books[order.book_key()]
546 .best_bid_tick
547 .write(order.tick())?;
548 }
549 } else if order.tick() < orderbook.best_ask_tick {
550 self.books[order.book_key()]
551 .best_ask_tick
552 .write(order.tick())?;
553 }
554 } else {
555 let mut prev_order = self.orders[prev_tail].read()?;
557 prev_order.next = order.order_id();
558 self.orders[prev_tail].write(prev_order)?;
559
560 order.prev = prev_tail;
562 level.tail = order.order_id();
563 }
564
565 let new_liquidity = level
566 .total_liquidity
567 .checked_add(order.remaining())
568 .ok_or(TempoPrecompileError::under_overflow())?;
569 level.total_liquidity = new_liquidity;
570
571 self.books[order.book_key()]
572 .tick_level_handler_mut(order.tick(), order.is_bid())
573 .write(level)?;
574
575 self.orders[order.order_id()].write(order)
576 }
577
578 #[allow(clippy::too_many_arguments)]
592 pub fn place_flip(
593 &mut self,
594 sender: Address,
595 token: Address,
596 amount: u128,
597 is_bid: bool,
598 tick: i16,
599 flip_tick: i16,
600 internal_balance_only: bool,
601 ) -> Result<u128> {
602 let quote_token = TIP20Token::from_address(token)?.quote_token()?;
603
604 let book_key = compute_book_key(token, quote_token);
606
607 let batch = self.storage.checkpoint();
610
611 let book = self.books[book_key].read()?;
613 self.validate_or_create_pair(&book, token)?;
614
615 if !(MIN_TICK..=MAX_TICK).contains(&tick) {
617 return Err(StablecoinDEXError::tick_out_of_bounds(tick).into());
618 }
619
620 if tick % TICK_SPACING != 0 {
622 return Err(StablecoinDEXError::invalid_tick().into());
623 }
624
625 if !(MIN_TICK..=MAX_TICK).contains(&flip_tick) {
626 return Err(StablecoinDEXError::tick_out_of_bounds(flip_tick).into());
627 }
628
629 if flip_tick % TICK_SPACING != 0 {
631 return Err(StablecoinDEXError::invalid_flip_tick().into());
632 }
633
634 if (is_bid && flip_tick <= tick) || (!is_bid && flip_tick >= tick) {
636 return Err(StablecoinDEXError::invalid_flip_tick().into());
637 }
638
639 if amount < MIN_ORDER_AMOUNT {
641 return Err(StablecoinDEXError::below_minimum_order_size(amount).into());
642 }
643
644 let (escrow_token, escrow_amount, non_escrow_token) = if is_bid {
646 let quote_amount = base_to_quote(amount, tick, RoundingDirection::Up)
648 .ok_or(StablecoinDEXError::insufficient_balance())?;
649 (quote_token, quote_amount, token)
650 } else {
651 (token, amount, quote_token)
653 };
654
655 TIP20Token::from_address(non_escrow_token)?
658 .ensure_transfer_authorized(self.address, sender)?;
659
660 if internal_balance_only {
663 TIP20Token::from_address(escrow_token)?
664 .ensure_transfer_authorized(sender, self.address)?;
665 let user_balance = self.balance_of(sender, escrow_token)?;
666 if user_balance < escrow_amount {
667 return Err(StablecoinDEXError::insufficient_balance().into());
668 }
669 self.sub_balance(sender, escrow_token, escrow_amount)?;
670 } else {
671 self.decrement_balance_or_transfer_from(sender, escrow_token, escrow_amount)?;
672 }
673
674 let order_id = self.next_order_id()?;
676 let order = Order::new_flip(order_id, sender, book_key, amount, tick, is_bid, flip_tick)
677 .map_err(|_| StablecoinDEXError::invalid_flip_tick())?;
678
679 if self.storage.spec().is_t1c() {
681 self.next_order_id.write(order_id + 1)?;
683 } else {
684 self.increment_next_order_id()?;
685 }
686 self.commit_order_to_book(order)?;
687
688 self.emit_event(StablecoinDEXEvents::OrderPlaced(
690 IStablecoinDEX::OrderPlaced {
691 orderId: order_id,
692 maker: sender,
693 token,
694 amount,
695 isBid: is_bid,
696 tick,
697 isFlipOrder: true,
698 flipTick: flip_tick,
699 },
700 ))?;
701
702 batch.commit();
704
705 Ok(order_id)
706 }
707
708 fn partial_fill_order(
710 &mut self,
711 order: &mut Order,
712 level: &mut TickLevel,
713 fill_amount: u128,
714 taker: Address,
715 ) -> Result<u128> {
716 let orderbook = self.books[order.book_key()].read()?;
717
718 let new_remaining = order.remaining() - fill_amount;
720 self.orders[order.order_id()]
721 .remaining
722 .write(new_remaining)?;
723
724 let quote_amount = base_to_quote(
726 fill_amount,
727 order.tick(),
728 if order.is_bid() {
729 RoundingDirection::Down } else {
731 RoundingDirection::Up },
733 )
734 .ok_or(TempoPrecompileError::under_overflow())?;
735
736 if order.is_bid() {
737 self.increment_balance(order.maker(), orderbook.base, fill_amount)?;
739 } else {
740 self.increment_balance(order.maker(), orderbook.quote, quote_amount)?;
742 }
743
744 let amount_out = if order.is_bid() {
746 quote_amount
747 } else {
748 fill_amount
749 };
750
751 let new_liquidity = level
753 .total_liquidity
754 .checked_sub(fill_amount)
755 .ok_or(TempoPrecompileError::under_overflow())?;
756 level.total_liquidity = new_liquidity;
757
758 self.books[order.book_key()]
759 .tick_level_handler_mut(order.tick(), order.is_bid())
760 .write(*level)?;
761
762 self.emit_order_filled(order.order_id(), order.maker(), taker, fill_amount, true)?;
764
765 Ok(amount_out)
766 }
767
768 fn fill_order(
770 &mut self,
771 book_key: B256,
772 order: &mut Order,
773 mut level: TickLevel,
774 taker: Address,
775 ) -> Result<(u128, Option<(TickLevel, Order)>)> {
776 debug_assert_eq!(order.book_key(), book_key);
777
778 let orderbook = self.books[book_key].read()?;
779 let fill_amount = order.remaining();
780
781 let amount_out = if order.is_bid() {
783 self.increment_balance(order.maker(), orderbook.base, fill_amount)?;
785 base_to_quote(fill_amount, order.tick(), RoundingDirection::Down)
787 .ok_or(TempoPrecompileError::under_overflow())?
788 } else {
789 let quote_amount = base_to_quote(fill_amount, order.tick(), RoundingDirection::Up)
791 .ok_or(TempoPrecompileError::under_overflow())?;
792
793 self.increment_balance(order.maker(), orderbook.quote, quote_amount)?;
794
795 fill_amount
797 };
798
799 self.emit_order_filled(order.order_id(), order.maker(), taker, fill_amount, false)?;
801
802 if order.is_flip() {
803 if let Err(e) = self.place_flip(
811 order.maker(),
812 orderbook.base,
813 order.amount(),
814 !order.is_bid(),
815 order.flip_tick(),
816 order.tick(),
817 true,
818 ) && e.is_system_error()
819 && self.storage.spec().is_t1a()
820 {
821 return Err(e);
822 }
823 }
824
825 self.orders[order.order_id()].delete()?;
827
828 let next_tick_info = if order.next() == 0 {
830 self.books[book_key]
831 .tick_level_handler_mut(order.tick(), order.is_bid())
832 .delete()?;
833 self.books[book_key].delete_tick_bit(order.tick(), order.is_bid())?;
834
835 let (tick, has_liquidity) =
836 self.books[book_key].next_initialized_tick(order.tick(), order.is_bid())?;
837
838 if order.is_bid() {
840 let new_best = if has_liquidity { tick } else { i16::MIN };
841 self.books[book_key].best_bid_tick.write(new_best)?;
842 } else {
843 let new_best = if has_liquidity { tick } else { i16::MAX };
844 self.books[book_key].best_ask_tick.write(new_best)?;
845 }
846
847 if !has_liquidity {
848 None
850 } else {
851 let new_level = self.books[book_key]
852 .tick_level_handler(tick, order.is_bid())
853 .read()?;
854 let new_order = self.orders[new_level.head].read()?;
855
856 Some((new_level, new_order))
857 }
858 } else {
859 level.head = order.next();
861 self.orders[order.next()].prev.delete()?;
862
863 let new_liquidity = level
864 .total_liquidity
865 .checked_sub(fill_amount)
866 .ok_or(TempoPrecompileError::under_overflow())?;
867 level.total_liquidity = new_liquidity;
868
869 self.books[book_key]
870 .tick_level_handler_mut(order.tick(), order.is_bid())
871 .write(level)?;
872
873 let new_order = self.orders[order.next()].read()?;
874 Some((level, new_order))
875 };
876
877 Ok((amount_out, next_tick_info))
878 }
879
880 fn fill_orders_exact_out(
882 &mut self,
883 book_key: B256,
884 bid: bool,
885 mut amount_out: u128,
886 taker: Address,
887 ) -> Result<u128> {
888 let mut level = self.get_best_price_level(book_key, bid)?;
889 let mut order = self.orders[level.head].read()?;
890
891 let mut total_amount_in: u128 = 0;
892
893 while amount_out > 0 {
894 let tick = order.tick();
895
896 let (fill_amount, amount_in) = if bid {
897 let base_needed = quote_to_base(amount_out, tick, RoundingDirection::Up)
900 .ok_or(TempoPrecompileError::under_overflow())?;
901 let fill_amount = base_needed.min(order.remaining());
902 (fill_amount, fill_amount)
903 } else {
904 let fill_amount = amount_out.min(order.remaining());
907 let amount_in = base_to_quote(fill_amount, tick, RoundingDirection::Up)
908 .ok_or(TempoPrecompileError::under_overflow())?;
909 (fill_amount, amount_in)
910 };
911
912 if fill_amount < order.remaining() {
913 self.partial_fill_order(&mut order, &mut level, fill_amount, taker)?;
914 total_amount_in = total_amount_in
915 .checked_add(amount_in)
916 .ok_or(TempoPrecompileError::under_overflow())?;
917 break;
918 } else {
919 let (amount_out_received, next_order_info) =
920 self.fill_order(book_key, &mut order, level, taker)?;
921 total_amount_in = total_amount_in
922 .checked_add(amount_in)
923 .ok_or(TempoPrecompileError::under_overflow())?;
924
925 if bid {
927 let base_needed = quote_to_base(amount_out, tick, RoundingDirection::Up)
929 .ok_or(TempoPrecompileError::under_overflow())?;
930 if base_needed > order.remaining() {
931 amount_out = amount_out
932 .checked_sub(amount_out_received)
933 .ok_or(TempoPrecompileError::under_overflow())?;
934 } else {
935 amount_out = 0;
936 }
937 } else if amount_out > order.remaining() {
938 amount_out = amount_out
939 .checked_sub(amount_out_received)
940 .ok_or(TempoPrecompileError::under_overflow())?;
941 } else {
942 amount_out = 0;
943 }
944
945 if let Some((new_level, new_order)) = next_order_info {
946 level = new_level;
947 order = new_order;
948 } else {
949 if amount_out > 0 {
950 return Err(StablecoinDEXError::insufficient_liquidity().into());
951 }
952 break;
953 }
954 }
955 }
956
957 Ok(total_amount_in)
958 }
959
960 fn fill_orders_exact_in(
962 &mut self,
963 book_key: B256,
964 bid: bool,
965 mut amount_in: u128,
966 taker: Address,
967 ) -> Result<u128> {
968 let mut level = self.get_best_price_level(book_key, bid)?;
969 let mut order = self.orders[level.head].read()?;
970
971 let mut total_amount_out: u128 = 0;
972
973 while amount_in > 0 {
974 let tick = order.tick();
975
976 let fill_amount = if bid {
977 amount_in.min(order.remaining())
979 } else {
980 let base_out = quote_to_base(amount_in, tick, RoundingDirection::Down)
983 .ok_or(TempoPrecompileError::under_overflow())?;
984 base_out.min(order.remaining())
985 };
986
987 if fill_amount < order.remaining() {
988 let amount_out =
989 self.partial_fill_order(&mut order, &mut level, fill_amount, taker)?;
990 total_amount_out = total_amount_out
991 .checked_add(amount_out)
992 .ok_or(TempoPrecompileError::under_overflow())?;
993 break;
994 } else {
995 let (amount_out, next_order_info) =
996 self.fill_order(book_key, &mut order, level, taker)?;
997 total_amount_out = total_amount_out
998 .checked_add(amount_out)
999 .ok_or(TempoPrecompileError::under_overflow())?;
1000
1001 if bid {
1003 if amount_in > order.remaining() {
1004 amount_in = amount_in
1005 .checked_sub(order.remaining())
1006 .ok_or(TempoPrecompileError::under_overflow())?;
1007 } else {
1008 amount_in = 0;
1009 }
1010 } else {
1011 let base_out = quote_to_base(amount_in, tick, RoundingDirection::Down)
1013 .ok_or(TempoPrecompileError::under_overflow())?;
1014 if base_out > order.remaining() {
1015 let quote_needed =
1017 base_to_quote(order.remaining(), tick, RoundingDirection::Up)
1018 .ok_or(TempoPrecompileError::under_overflow())?;
1019 amount_in = amount_in
1020 .checked_sub(quote_needed)
1021 .ok_or(TempoPrecompileError::under_overflow())?;
1022 } else {
1023 amount_in = 0;
1024 }
1025 }
1026
1027 if let Some((new_level, new_order)) = next_order_info {
1028 level = new_level;
1029 order = new_order;
1030 } else {
1031 if amount_in > 0 {
1032 return Err(StablecoinDEXError::insufficient_liquidity().into());
1033 }
1034 break;
1035 }
1036 }
1037 }
1038
1039 Ok(total_amount_out)
1040 }
1041
1042 fn get_best_price_level(&mut self, book_key: B256, is_bid: bool) -> Result<TickLevel> {
1044 let orderbook = self.books[book_key].read()?;
1045
1046 let current_tick = if is_bid {
1047 if orderbook.best_bid_tick == i16::MIN {
1048 return Err(StablecoinDEXError::insufficient_liquidity().into());
1049 }
1050 orderbook.best_bid_tick
1051 } else {
1052 if orderbook.best_ask_tick == i16::MAX {
1053 return Err(StablecoinDEXError::insufficient_liquidity().into());
1054 }
1055 orderbook.best_ask_tick
1056 };
1057
1058 self.books[book_key]
1059 .tick_level_handler(current_tick, is_bid)
1060 .read()
1061 }
1062
1063 pub fn cancel(&mut self, sender: Address, order_id: u128) -> Result<()> {
1070 let order = self.orders[order_id].read()?;
1071
1072 if order.maker().is_zero() {
1073 return Err(StablecoinDEXError::order_does_not_exist().into());
1074 }
1075
1076 if order.maker() != sender {
1077 return Err(StablecoinDEXError::unauthorized().into());
1078 }
1079
1080 if order.remaining() == 0 {
1081 return Err(StablecoinDEXError::order_does_not_exist().into());
1082 }
1083
1084 self.cancel_active_order(order)
1085 }
1086
1087 fn cancel_active_order(&mut self, order: Order) -> Result<()> {
1089 let mut level = self.books[order.book_key()]
1090 .tick_level_handler(order.tick(), order.is_bid())
1091 .read()?;
1092
1093 if order.prev() != 0 {
1095 self.orders[order.prev()].next.write(order.next())?;
1096 } else {
1097 level.head = order.next();
1098 }
1099
1100 if order.next() != 0 {
1101 self.orders[order.next()].prev.write(order.prev())?;
1102 } else {
1103 level.tail = order.prev();
1104 }
1105
1106 let new_liquidity = level
1108 .total_liquidity
1109 .checked_sub(order.remaining())
1110 .ok_or(TempoPrecompileError::under_overflow())?;
1111 level.total_liquidity = new_liquidity;
1112
1113 if level.head == 0 {
1115 self.books[order.book_key()].delete_tick_bit(order.tick(), order.is_bid())?;
1116
1117 let orderbook = self.books[order.book_key()].read()?;
1119 let best_tick = if order.is_bid() {
1120 orderbook.best_bid_tick
1121 } else {
1122 orderbook.best_ask_tick
1123 };
1124
1125 if best_tick == order.tick() {
1126 let (next_tick, has_liquidity) = self.books[order.book_key()]
1127 .next_initialized_tick(order.tick(), order.is_bid())?;
1128
1129 if order.is_bid() {
1130 let new_best = if has_liquidity { next_tick } else { i16::MIN };
1131 self.books[order.book_key()].best_bid_tick.write(new_best)?;
1132 } else {
1133 let new_best = if has_liquidity { next_tick } else { i16::MAX };
1134 self.books[order.book_key()].best_ask_tick.write(new_best)?;
1135 }
1136 }
1137 }
1138
1139 self.books[order.book_key()]
1140 .tick_level_handler_mut(order.tick(), order.is_bid())
1141 .write(level)?;
1142
1143 let orderbook = self.books[order.book_key()].read()?;
1145 if order.is_bid() {
1146 let quote_amount =
1149 base_to_quote(order.remaining(), order.tick(), RoundingDirection::Up)
1150 .ok_or(TempoPrecompileError::under_overflow())?;
1151
1152 self.increment_balance(order.maker(), orderbook.quote, quote_amount)?;
1153 } else {
1154 self.increment_balance(order.maker(), orderbook.base, order.remaining())?;
1156 }
1157
1158 self.orders[order.order_id()].delete()?;
1160
1161 self.emit_event(StablecoinDEXEvents::OrderCancelled(
1163 IStablecoinDEX::OrderCancelled {
1164 orderId: order.order_id(),
1165 },
1166 ))
1167 }
1168
1169 pub fn cancel_stale_order(&mut self, order_id: u128) -> Result<()> {
1180 let order = self.orders[order_id].read()?;
1181
1182 if order.maker().is_zero() {
1183 return Err(StablecoinDEXError::order_does_not_exist().into());
1184 }
1185
1186 let book = self.books[order.book_key()].read()?;
1187 let token = if order.is_bid() {
1188 book.quote
1189 } else {
1190 book.base
1191 };
1192
1193 let policy_id = TIP20Token::from_address(token)?.transfer_policy_id()?;
1194 match TIP403Registry::new().is_authorized_as(policy_id, order.maker(), AuthRole::sender()) {
1197 Ok(true) => Err(StablecoinDEXError::order_not_stale().into()),
1198 Ok(false) => self.cancel_active_order(order),
1199 Err(e) if is_policy_lookup_error(&e) => self.cancel_active_order(order),
1200 Err(e) => Err(e),
1201 }
1202 }
1203
1204 pub fn withdraw(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
1210 let current_balance = self.balance_of(user, token)?;
1211 if current_balance < amount {
1212 return Err(StablecoinDEXError::insufficient_balance().into());
1213 }
1214 self.sub_balance(user, token, amount)?;
1215 self.transfer(token, user, amount)?;
1216
1217 Ok(())
1218 }
1219
1220 fn quote_exact_out(&self, book_key: B256, amount_out: u128, is_bid: bool) -> Result<u128> {
1222 let mut remaining_out = amount_out;
1223 let mut amount_in = 0u128;
1224 let orderbook = self.books[book_key].read()?;
1225
1226 let mut current_tick = if is_bid {
1227 orderbook.best_bid_tick
1228 } else {
1229 orderbook.best_ask_tick
1230 };
1231 if current_tick == i16::MIN || current_tick == i16::MAX {
1233 return Err(StablecoinDEXError::insufficient_liquidity().into());
1234 }
1235
1236 while remaining_out > 0 {
1237 let level = self.books[book_key]
1238 .tick_level_handler(current_tick, is_bid)
1239 .read()?;
1240
1241 if level.total_liquidity == 0 {
1243 let (next_tick, initialized) =
1244 self.books[book_key].next_initialized_tick(current_tick, is_bid)?;
1245
1246 if !initialized {
1247 return Err(StablecoinDEXError::insufficient_liquidity().into());
1248 }
1249 current_tick = next_tick;
1250 continue;
1251 }
1252
1253 let (fill_amount, amount_in_tick) = if is_bid {
1254 let base_needed = quote_to_base(remaining_out, current_tick, RoundingDirection::Up)
1260 .ok_or(TempoPrecompileError::under_overflow())?;
1261 let fill_amount = if base_needed > level.total_liquidity {
1262 level.total_liquidity
1263 } else {
1264 base_needed
1265 };
1266 (fill_amount, fill_amount)
1267 } else {
1268 let fill_amount = if remaining_out > level.total_liquidity {
1271 level.total_liquidity
1272 } else {
1273 remaining_out
1274 };
1275 let quote_needed = base_to_quote(fill_amount, current_tick, RoundingDirection::Up)
1276 .ok_or(TempoPrecompileError::under_overflow())?;
1277 (fill_amount, quote_needed)
1278 };
1279
1280 let amount_out_tick = if is_bid {
1281 base_to_quote(fill_amount, current_tick, RoundingDirection::Down)
1285 .ok_or(TempoPrecompileError::under_overflow())?
1286 .min(remaining_out)
1287 } else {
1288 fill_amount
1289 };
1290
1291 remaining_out = remaining_out.saturating_sub(amount_out_tick);
1292 amount_in = amount_in
1293 .checked_add(amount_in_tick)
1294 .ok_or(TempoPrecompileError::under_overflow())?;
1295
1296 if fill_amount == level.total_liquidity {
1298 let (next_tick, initialized) =
1299 self.books[book_key].next_initialized_tick(current_tick, is_bid)?;
1300
1301 if !initialized && remaining_out > 0 {
1302 return Err(StablecoinDEXError::insufficient_liquidity().into());
1303 }
1304 current_tick = next_tick;
1305 } else {
1306 break;
1307 }
1308 }
1309
1310 Ok(amount_in)
1311 }
1312
1313 fn find_trade_path(&self, token_in: Address, token_out: Address) -> Result<Vec<(B256, bool)>> {
1317 if token_in == token_out {
1319 return Err(StablecoinDEXError::identical_tokens().into());
1320 }
1321
1322 if !is_tip20_prefix(token_in) || !is_tip20_prefix(token_out) {
1324 return Err(StablecoinDEXError::invalid_token().into());
1325 }
1326
1327 let in_quote = TIP20Token::from_address(token_in)?.quote_token()?;
1329 let out_quote = TIP20Token::from_address(token_out)?.quote_token()?;
1330
1331 if in_quote == token_out || out_quote == token_in {
1332 return self.validate_and_build_route(&[token_in, token_out]);
1333 }
1334
1335 let path_in = self.find_path_to_root(token_in)?;
1337 let path_out = self.find_path_to_root(token_out)?;
1338
1339 let path_out_set: std::collections::HashSet<Address> = path_out.iter().copied().collect();
1342 let mut lca = None;
1343 for token_a in &path_in {
1344 if path_out_set.contains(token_a) {
1345 lca = Some(*token_a);
1346 break;
1347 }
1348 }
1349
1350 let lca = lca.ok_or_else(StablecoinDEXError::pair_does_not_exist)?;
1351
1352 let mut trade_path = Vec::new();
1354
1355 for token in &path_in {
1357 trade_path.push(*token);
1358 if *token == lca {
1359 break;
1360 }
1361 }
1362
1363 let lca_to_out: Vec<Address> = path_out
1365 .iter()
1366 .take_while(|&&t| t != lca)
1367 .copied()
1368 .collect();
1369
1370 trade_path.extend(lca_to_out.iter().rev());
1372
1373 self.validate_and_build_route(&trade_path)
1374 }
1375
1376 fn validate_and_build_route(&self, path: &[Address]) -> Result<Vec<(B256, bool)>> {
1378 let mut route = Vec::new();
1379
1380 for i in 0..path.len() - 1 {
1381 let token_in = path[i];
1382 let token_out = path[i + 1];
1383
1384 let (base, quote) = {
1385 let token_in_tip20 = TIP20Token::from_address(token_in)?;
1386 if token_in_tip20.quote_token()? == token_out {
1387 (token_in, token_out)
1388 } else {
1389 let token_out_tip20 = TIP20Token::from_address(token_out)?;
1390 if token_out_tip20.quote_token()? == token_in {
1391 (token_out, token_in)
1392 } else {
1393 return Err(StablecoinDEXError::pair_does_not_exist().into());
1394 }
1395 }
1396 };
1397
1398 let book_key = compute_book_key(base, quote);
1399 let orderbook = self.books[book_key].read()?;
1400
1401 if orderbook.base.is_zero() {
1402 return Err(StablecoinDEXError::pair_does_not_exist().into());
1403 }
1404
1405 let is_base_for_quote = token_in == base;
1406 route.push((book_key, is_base_for_quote));
1407 }
1408
1409 Ok(route)
1410 }
1411
1412 fn find_path_to_root(&self, mut token: Address) -> Result<Vec<Address>> {
1415 let mut path = vec![token];
1416
1417 while token != PATH_USD_ADDRESS {
1418 token = TIP20Token::from_address(token)?.quote_token()?;
1419 path.push(token);
1420 }
1421
1422 Ok(path)
1423 }
1424
1425 fn quote_exact_in(&self, book_key: B256, amount_in: u128, is_bid: bool) -> Result<u128> {
1427 let mut remaining_in = amount_in;
1428 let mut amount_out = 0u128;
1429 let orderbook = self.books[book_key].read()?;
1430
1431 let mut current_tick = if is_bid {
1432 orderbook.best_bid_tick
1433 } else {
1434 orderbook.best_ask_tick
1435 };
1436
1437 if current_tick == i16::MIN || current_tick == i16::MAX {
1439 return Err(StablecoinDEXError::insufficient_liquidity().into());
1440 }
1441
1442 while remaining_in > 0 {
1443 let level = self.books[book_key]
1444 .tick_level_handler(current_tick, is_bid)
1445 .read()?;
1446
1447 if level.total_liquidity == 0 {
1449 let (next_tick, initialized) =
1450 self.books[book_key].next_initialized_tick(current_tick, is_bid)?;
1451
1452 if !initialized {
1453 return Err(StablecoinDEXError::insufficient_liquidity().into());
1454 }
1455 current_tick = next_tick;
1456 continue;
1457 }
1458
1459 let (fill_amount, amount_out_tick, amount_consumed) = if is_bid {
1461 let fill = remaining_in.min(level.total_liquidity);
1463 let quote_out = base_to_quote(fill, current_tick, RoundingDirection::Down)
1465 .ok_or(TempoPrecompileError::under_overflow())?;
1466 (fill, quote_out, fill)
1467 } else {
1468 let base_to_get =
1471 quote_to_base(remaining_in, current_tick, RoundingDirection::Down)
1472 .ok_or(TempoPrecompileError::under_overflow())?;
1473 let fill = base_to_get.min(level.total_liquidity);
1474 let quote_consumed = base_to_quote(fill, current_tick, RoundingDirection::Up)
1475 .ok_or(TempoPrecompileError::under_overflow())?;
1476 (fill, fill, quote_consumed)
1477 };
1478
1479 remaining_in = remaining_in
1480 .checked_sub(amount_consumed)
1481 .ok_or(TempoPrecompileError::under_overflow())?;
1482 amount_out = amount_out
1483 .checked_add(amount_out_tick)
1484 .ok_or(TempoPrecompileError::under_overflow())?;
1485
1486 if fill_amount == level.total_liquidity {
1488 let (next_tick, initialized) =
1489 self.books[book_key].next_initialized_tick(current_tick, is_bid)?;
1490
1491 if !initialized && remaining_in > 0 {
1492 return Err(StablecoinDEXError::insufficient_liquidity().into());
1493 }
1494 current_tick = next_tick;
1495 } else {
1496 break;
1497 }
1498 }
1499
1500 Ok(amount_out)
1501 }
1502}
1503
1504#[cfg(test)]
1505mod tests {
1506 use alloy::{primitives::IntoLogData, sol_types::SolEvent};
1507 use tempo_chainspec::hardfork::TempoHardfork;
1508 use tempo_contracts::precompiles::TIP20Error;
1509
1510 use crate::{
1511 error::TempoPrecompileError,
1512 storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
1513 test_util::TIP20Setup,
1514 tip403_registry::{ITIP403Registry, TIP403Registry},
1515 };
1516
1517 use super::*;
1518 use crate::STABLECOIN_DEX_ADDRESS;
1519
1520 fn setup_test_tokens(
1521 admin: Address,
1522 user: Address,
1523 exchange_address: Address,
1524 amount: u128,
1525 ) -> Result<(Address, Address)> {
1526 let quote = TIP20Setup::path_usd(admin)
1528 .with_issuer(admin)
1529 .with_mint(user, U256::from(amount))
1530 .with_approval(user, exchange_address, U256::from(amount))
1531 .apply()?;
1532
1533 let base = TIP20Setup::create("BASE", "BASE", admin)
1535 .with_issuer(admin)
1536 .with_mint(user, U256::from(amount))
1537 .with_approval(user, exchange_address, U256::from(amount))
1538 .apply()?;
1539
1540 Ok((base.address(), quote.address()))
1541 }
1542
1543 #[test]
1544 fn test_tick_to_price() {
1545 let test_ticks = [-2000i16, -1000, -100, -1, 0, 1, 100, 1000, 2000];
1546 for tick in test_ticks {
1547 let price = orderbook::tick_to_price(tick);
1548 let expected_price = (orderbook::PRICE_SCALE as i32 + tick as i32) as u32;
1549 assert_eq!(price, expected_price);
1550 }
1551 }
1552
1553 #[test]
1554 fn test_price_to_tick() -> eyre::Result<()> {
1555 let mut storage = HashMapStorageProvider::new(1);
1556 StorageCtx::enter(&mut storage, || {
1557 let exchange = StablecoinDEX::new();
1558
1559 assert_eq!(exchange.price_to_tick(orderbook::PRICE_SCALE)?, 0);
1561 assert_eq!(exchange.price_to_tick(orderbook::MIN_PRICE)?, MIN_TICK);
1562 assert_eq!(exchange.price_to_tick(orderbook::MAX_PRICE)?, MAX_TICK);
1563
1564 let result = exchange.price_to_tick(orderbook::MIN_PRICE - 1);
1566 assert!(result.is_err());
1567 assert!(matches!(
1568 result.unwrap_err(),
1569 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::TickOutOfBounds(_))
1570 ));
1571
1572 let result = exchange.price_to_tick(orderbook::MAX_PRICE + 1);
1573 assert!(result.is_err());
1574 assert!(matches!(
1575 result.unwrap_err(),
1576 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::TickOutOfBounds(_))
1577 ));
1578
1579 Ok(())
1580 })
1581 }
1582
1583 #[test]
1584 fn test_calculate_quote_amount_rounding() -> eyre::Result<()> {
1585 let amount = 100u128;
1590 let tick = 1i16;
1591 let result_floor = base_to_quote(amount, tick, RoundingDirection::Down).unwrap();
1592 assert_eq!(
1593 result_floor, 100,
1594 "Expected 100 (rounded down from 100.001)"
1595 );
1596
1597 let result_ceil = base_to_quote(amount, tick, RoundingDirection::Up).unwrap();
1599 assert_eq!(result_ceil, 101, "Expected 101 (rounded up from 100.001)");
1600
1601 let amount2 = 999u128;
1603 let tick2 = 5i16; let result2_floor = base_to_quote(amount2, tick2, RoundingDirection::Down).unwrap();
1605 assert_eq!(
1607 result2_floor, 999,
1608 "Expected 999 (rounded down from 999.04995)"
1609 );
1610
1611 let result2_ceil = base_to_quote(amount2, tick2, RoundingDirection::Up).unwrap();
1613 assert_eq!(
1614 result2_ceil, 1000,
1615 "Expected 1000 (rounded up from 999.04995)"
1616 );
1617
1618 let amount3 = 100000u128;
1620 let tick3 = 0i16; let result3_floor = base_to_quote(amount3, tick3, RoundingDirection::Down).unwrap();
1622 let result3_ceil = base_to_quote(amount3, tick3, RoundingDirection::Up).unwrap();
1623 assert_eq!(result3_floor, 100000, "Exact division should remain exact");
1625 assert_eq!(result3_ceil, 100000, "Exact division should remain exact");
1626
1627 Ok(())
1628 }
1629
1630 #[test]
1631 fn test_settlement_rounding_favors_protocol() -> eyre::Result<()> {
1632 let mut storage = HashMapStorageProvider::new(1);
1633 StorageCtx::enter(&mut storage, || {
1634 let mut exchange = StablecoinDEX::new();
1635 exchange.initialize()?;
1636
1637 let alice = Address::random();
1638 let bob = Address::random();
1639 let admin = Address::random();
1640
1641 let base_amount = 100_000_003u128;
1643 let tick = 100i16;
1644
1645 let price = orderbook::tick_to_price(tick) as u128;
1646 let expected_quote_floor = (base_amount * price) / orderbook::PRICE_SCALE as u128;
1647 let expected_quote_ceil =
1648 (base_amount * price).div_ceil(orderbook::PRICE_SCALE as u128);
1649
1650 let max_escrow = expected_quote_ceil * 2;
1651
1652 let base = TIP20Setup::create("BASE", "BASE", admin)
1653 .with_issuer(admin)
1654 .with_mint(alice, U256::from(base_amount * 2))
1655 .with_mint(bob, U256::from(base_amount * 2))
1656 .with_approval(alice, exchange.address, U256::MAX)
1657 .with_approval(bob, exchange.address, U256::MAX)
1658 .apply()?;
1659 let base_token = base.address();
1660 let quote_token = base.quote_token()?;
1661
1662 TIP20Setup::path_usd(admin)
1663 .with_issuer(admin)
1664 .with_mint(alice, U256::from(max_escrow))
1665 .with_mint(bob, U256::from(max_escrow))
1666 .with_approval(alice, exchange.address, U256::MAX)
1667 .with_approval(bob, exchange.address, U256::MAX)
1668 .apply()?;
1669
1670 exchange.create_pair(base_token)?;
1671
1672 exchange.place(alice, base_token, base_amount, false, tick)?;
1673
1674 let alice_quote_before = exchange.balance_of(alice, quote_token)?;
1675 assert_eq!(alice_quote_before, 0);
1676
1677 exchange.swap_exact_amount_in(bob, quote_token, base_token, expected_quote_ceil, 0)?;
1678
1679 let alice_quote_after = exchange.balance_of(alice, quote_token)?;
1680
1681 assert_eq!(
1683 alice_quote_after, expected_quote_ceil,
1684 "Ask order maker should receive quote rounded UP. Got {alice_quote_after}, expected ceil {expected_quote_ceil}"
1685 );
1686
1687 assert!(
1688 expected_quote_ceil > expected_quote_floor,
1689 "Test setup error: should have a non-zero remainder"
1690 );
1691
1692 Ok(())
1693 })
1694 }
1695
1696 #[test]
1697 fn test_cancellation_refund_equals_escrow_for_bid_orders() -> eyre::Result<()> {
1698 let mut storage = HashMapStorageProvider::new(1);
1699 StorageCtx::enter(&mut storage, || {
1700 let mut exchange = StablecoinDEX::new();
1701 exchange.initialize()?;
1702
1703 let alice = Address::random();
1704 let admin = Address::random();
1705
1706 let base_amount = 100_000_003u128;
1708 let tick = 100i16;
1709
1710 let price = orderbook::tick_to_price(tick) as u128;
1711 let escrow_ceil = (base_amount * price).div_ceil(orderbook::PRICE_SCALE as u128);
1712
1713 let base = TIP20Setup::create("BASE", "BASE", admin)
1714 .with_issuer(admin)
1715 .apply()?;
1716 let base_token = base.address();
1717 let quote_token = base.quote_token()?;
1718
1719 TIP20Setup::path_usd(admin)
1720 .with_issuer(admin)
1721 .with_mint(alice, U256::from(escrow_ceil))
1722 .with_approval(alice, exchange.address, U256::MAX)
1723 .apply()?;
1724
1725 exchange.create_pair(base_token)?;
1726
1727 let order_id = exchange.place(alice, base_token, base_amount, true, tick)?;
1728
1729 let alice_balance_after_place = exchange.balance_of(alice, quote_token)?;
1731 assert_eq!(
1732 alice_balance_after_place, 0,
1733 "All quote tokens should be escrowed"
1734 );
1735
1736 exchange.cancel(alice, order_id)?;
1737
1738 let alice_refund = exchange.balance_of(alice, quote_token)?;
1739
1740 assert_eq!(
1743 alice_refund, escrow_ceil,
1744 "Cancellation refund must equal escrow amount. User escrowed {escrow_ceil} but got back {alice_refund}"
1745 );
1746
1747 Ok(())
1748 })
1749 }
1750
1751 #[test]
1752 fn test_place_order_pair_auto_created() -> eyre::Result<()> {
1753 let mut storage = HashMapStorageProvider::new(1);
1754 StorageCtx::enter(&mut storage, || {
1755 let mut exchange = StablecoinDEX::new();
1756 exchange.initialize()?;
1757
1758 let alice = Address::random();
1759 let admin = Address::random();
1760 let min_order_amount = MIN_ORDER_AMOUNT;
1761 let tick = 100i16;
1762
1763 let price = orderbook::tick_to_price(tick);
1764 let expected_escrow =
1765 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
1766
1767 let (base_token, _quote_token) =
1768 setup_test_tokens(admin, alice, exchange.address, expected_escrow)?;
1769
1770 let result = exchange.place(alice, base_token, min_order_amount, true, tick);
1772 assert!(result.is_ok());
1773
1774 Ok(())
1775 })
1776 }
1777
1778 #[test]
1779 fn test_place_order_below_minimum_amount() -> eyre::Result<()> {
1780 let mut storage = HashMapStorageProvider::new(1);
1781 StorageCtx::enter(&mut storage, || {
1782 let mut exchange = StablecoinDEX::new();
1783 exchange.initialize()?;
1784
1785 let alice = Address::random();
1786 let admin = Address::random();
1787 let min_order_amount = MIN_ORDER_AMOUNT;
1788 let below_minimum = min_order_amount - 1;
1789 let tick = 100i16;
1790
1791 let price = orderbook::tick_to_price(tick);
1792 let escrow_amount = (below_minimum * price as u128) / orderbook::PRICE_SCALE as u128;
1793
1794 let (base_token, _quote_token) =
1795 setup_test_tokens(admin, alice, exchange.address, escrow_amount)?;
1796
1797 exchange
1799 .create_pair(base_token)
1800 .expect("Could not create pair");
1801
1802 let result = exchange.place(alice, base_token, below_minimum, true, tick);
1804 assert_eq!(
1805 result,
1806 Err(StablecoinDEXError::below_minimum_order_size(below_minimum).into())
1807 );
1808
1809 Ok(())
1810 })
1811 }
1812
1813 #[test]
1814 fn test_place_bid_order() -> eyre::Result<()> {
1815 let mut storage = HashMapStorageProvider::new(1);
1816 StorageCtx::enter(&mut storage, || {
1817 let mut exchange = StablecoinDEX::new();
1818 exchange.initialize()?;
1819
1820 let alice = Address::random();
1821 let admin = Address::random();
1822 let min_order_amount = MIN_ORDER_AMOUNT;
1823 let tick = 100i16;
1824
1825 let price = orderbook::tick_to_price(tick);
1826 let expected_escrow =
1827 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
1828
1829 let (base_token, quote_token) =
1831 setup_test_tokens(admin, alice, exchange.address, expected_escrow)?;
1832
1833 exchange
1835 .create_pair(base_token)
1836 .expect("Could not create pair");
1837
1838 let order_id = exchange
1840 .place(alice, base_token, min_order_amount, true, tick)
1841 .expect("Place bid order should succeed");
1842
1843 assert_eq!(order_id, 1);
1844 assert_eq!(exchange.next_order_id()?, 2);
1845
1846 let stored_order = exchange.orders[order_id].read()?;
1848 assert_eq!(stored_order.maker(), alice);
1849 assert_eq!(stored_order.amount(), min_order_amount);
1850 assert_eq!(stored_order.remaining(), min_order_amount);
1851 assert_eq!(stored_order.tick(), tick);
1852 assert!(stored_order.is_bid());
1853 assert!(!stored_order.is_flip());
1854
1855 let book_key = compute_book_key(base_token, quote_token);
1857 let book_handler = &exchange.books[book_key];
1858 let level = book_handler.tick_level_handler(tick, true).read()?;
1859 assert_eq!(level.head, order_id);
1860 assert_eq!(level.tail, order_id);
1861 assert_eq!(level.total_liquidity, min_order_amount);
1862
1863 let quote_tip20 = TIP20Token::from_address(quote_token)?;
1865 let remaining_balance =
1866 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?;
1867 assert_eq!(remaining_balance, U256::ZERO);
1868
1869 let exchange_balance = quote_tip20.balance_of(ITIP20::balanceOfCall {
1871 account: exchange.address,
1872 })?;
1873 assert_eq!(exchange_balance, U256::from(expected_escrow));
1874
1875 Ok(())
1876 })
1877 }
1878
1879 #[test]
1880 fn test_place_ask_order() -> eyre::Result<()> {
1881 let mut storage = HashMapStorageProvider::new(1);
1882 StorageCtx::enter(&mut storage, || {
1883 let mut exchange = StablecoinDEX::new();
1884 exchange.initialize()?;
1885
1886 let alice = Address::random();
1887 let admin = Address::random();
1888 let min_order_amount = MIN_ORDER_AMOUNT;
1889 let tick = 50i16; let (base_token, quote_token) =
1893 setup_test_tokens(admin, alice, exchange.address, min_order_amount)?;
1894 exchange
1896 .create_pair(base_token)
1897 .expect("Could not create pair");
1898
1899 let order_id = exchange
1900 .place(alice, base_token, min_order_amount, false, tick) .expect("Place ask order should succeed");
1902
1903 assert_eq!(order_id, 1);
1904 assert_eq!(exchange.next_order_id()?, 2);
1905
1906 let stored_order = exchange.orders[order_id].read()?;
1908 assert_eq!(stored_order.maker(), alice);
1909 assert_eq!(stored_order.amount(), min_order_amount);
1910 assert_eq!(stored_order.remaining(), min_order_amount);
1911 assert_eq!(stored_order.tick(), tick);
1912 assert!(!stored_order.is_bid());
1913 assert!(!stored_order.is_flip());
1914
1915 let book_key = compute_book_key(base_token, quote_token);
1917 let book_handler = &exchange.books[book_key];
1918 let level = book_handler.tick_level_handler(tick, false).read()?;
1919 assert_eq!(level.head, order_id);
1920 assert_eq!(level.tail, order_id);
1921 assert_eq!(level.total_liquidity, min_order_amount);
1922
1923 let base_tip20 = TIP20Token::from_address(base_token)?;
1925 let remaining_balance =
1926 base_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?;
1927 assert_eq!(remaining_balance, U256::ZERO); let exchange_balance = base_tip20.balance_of(ITIP20::balanceOfCall {
1931 account: exchange.address,
1932 })?;
1933 assert_eq!(exchange_balance, U256::from(min_order_amount));
1934
1935 Ok(())
1936 })
1937 }
1938
1939 #[test]
1940 fn test_place_flip_order_below_minimum_amount() -> eyre::Result<()> {
1941 let mut storage = HashMapStorageProvider::new(1);
1942 StorageCtx::enter(&mut storage, || {
1943 let mut exchange = StablecoinDEX::new();
1944 exchange.initialize()?;
1945
1946 let alice = Address::random();
1947 let admin = Address::random();
1948 let min_order_amount = MIN_ORDER_AMOUNT;
1949 let below_minimum = min_order_amount - 1;
1950 let tick = 100i16;
1951 let flip_tick = 200i16;
1952
1953 let price = orderbook::tick_to_price(tick);
1954 let escrow_amount = (below_minimum * price as u128) / orderbook::PRICE_SCALE as u128;
1955
1956 let (base_token, _quote_token) =
1957 setup_test_tokens(admin, alice, exchange.address, escrow_amount)?;
1958
1959 exchange
1961 .create_pair(base_token)
1962 .expect("Could not create pair");
1963
1964 let result = exchange.place_flip(
1966 alice,
1967 base_token,
1968 below_minimum,
1969 true,
1970 tick,
1971 flip_tick,
1972 false,
1973 );
1974 assert_eq!(
1975 result,
1976 Err(StablecoinDEXError::below_minimum_order_size(below_minimum).into())
1977 );
1978
1979 Ok(())
1980 })
1981 }
1982
1983 #[test]
1984 fn test_place_flip_auto_creates_pair() -> Result<()> {
1985 let mut storage = HashMapStorageProvider::new(1);
1986 StorageCtx::enter(&mut storage, || {
1987 let mut exchange = StablecoinDEX::new();
1988 exchange.initialize()?;
1989
1990 let admin = Address::random();
1991 let user = Address::random();
1992
1993 let (base_token, quote_token) =
1995 setup_test_tokens(admin, user, exchange.address, 100_000_000)?;
1996
1997 let book_key = compute_book_key(base_token, quote_token);
1999 let book_before = exchange.books[book_key].read()?;
2000 assert!(book_before.base.is_zero(),);
2001
2002 let mut base = TIP20Token::from_address(base_token)?;
2004 base.transfer(
2005 user,
2006 ITIP20::transferCall {
2007 to: exchange.address,
2008 amount: U256::from(MIN_ORDER_AMOUNT),
2009 },
2010 )
2011 .expect("Base token transfer failed");
2012
2013 exchange.place_flip(user, base_token, MIN_ORDER_AMOUNT, true, 0, 10, false)?;
2015
2016 let book_after = exchange.books[book_key].read()?;
2017 assert_eq!(book_after.base, base_token);
2018
2019 let events = exchange.emitted_events();
2021 assert_eq!(events.len(), 2);
2022 assert_eq!(
2023 events[0],
2024 StablecoinDEXEvents::PairCreated(IStablecoinDEX::PairCreated {
2025 key: book_key,
2026 base: base_token,
2027 quote: quote_token,
2028 })
2029 .into_log_data()
2030 );
2031
2032 Ok(())
2033 })
2034 }
2035
2036 #[test]
2037 fn test_place_flip_order() -> eyre::Result<()> {
2038 let mut storage = HashMapStorageProvider::new(1);
2039 StorageCtx::enter(&mut storage, || {
2040 let mut exchange = StablecoinDEX::new();
2041 exchange.initialize()?;
2042
2043 let alice = Address::random();
2044 let admin = Address::random();
2045 let min_order_amount = MIN_ORDER_AMOUNT;
2046 let tick = 100i16;
2047 let flip_tick = 200i16; let price = orderbook::tick_to_price(tick);
2051 let expected_escrow =
2052 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2053
2054 let (base_token, quote_token) =
2056 setup_test_tokens(admin, alice, exchange.address, expected_escrow)?;
2057 exchange
2058 .create_pair(base_token)
2059 .expect("Could not create pair");
2060
2061 let order_id = exchange
2062 .place_flip(
2063 alice,
2064 base_token,
2065 min_order_amount,
2066 true,
2067 tick,
2068 flip_tick,
2069 false,
2070 )
2071 .expect("Place flip bid order should succeed");
2072
2073 assert_eq!(order_id, 1);
2074 assert_eq!(exchange.next_order_id()?, 2);
2075
2076 let stored_order = exchange.orders[order_id].read()?;
2078 assert_eq!(stored_order.maker(), alice);
2079 assert_eq!(stored_order.amount(), min_order_amount);
2080 assert_eq!(stored_order.remaining(), min_order_amount);
2081 assert_eq!(stored_order.tick(), tick);
2082 assert!(stored_order.is_bid());
2083 assert!(stored_order.is_flip());
2084 assert_eq!(stored_order.flip_tick(), flip_tick);
2085
2086 let book_key = compute_book_key(base_token, quote_token);
2088 let book_handler = &exchange.books[book_key];
2089 let level = book_handler.tick_level_handler(tick, true).read()?;
2090 assert_eq!(level.head, order_id);
2091 assert_eq!(level.tail, order_id);
2092 assert_eq!(level.total_liquidity, min_order_amount);
2093
2094 let quote_tip20 = TIP20Token::from_address(quote_token)?;
2096 let remaining_balance =
2097 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?;
2098 assert_eq!(remaining_balance, U256::ZERO);
2099
2100 let exchange_balance = quote_tip20.balance_of(ITIP20::balanceOfCall {
2102 account: exchange.address,
2103 })?;
2104 assert_eq!(exchange_balance, U256::from(expected_escrow));
2105
2106 Ok(())
2107 })
2108 }
2109
2110 #[test]
2111 fn test_withdraw() -> eyre::Result<()> {
2112 let mut storage = HashMapStorageProvider::new(1);
2113 StorageCtx::enter(&mut storage, || {
2114 let mut exchange = StablecoinDEX::new();
2115 exchange.initialize()?;
2116
2117 let alice = Address::random();
2118 let admin = Address::random();
2119 let min_order_amount = MIN_ORDER_AMOUNT;
2120 let tick = 100i16;
2121 let price = orderbook::tick_to_price(tick);
2122 let expected_escrow =
2123 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2124
2125 let (base_token, quote_token) =
2127 setup_test_tokens(admin, alice, exchange.address, expected_escrow)?;
2128 exchange
2129 .create_pair(base_token)
2130 .expect("Could not create pair");
2131
2132 let order_id = exchange
2134 .place(alice, base_token, min_order_amount, true, tick)
2135 .expect("Place bid order should succeed");
2136
2137 exchange
2138 .cancel(alice, order_id)
2139 .expect("Cancel pending order should succeed");
2140
2141 assert_eq!(exchange.balance_of(alice, quote_token)?, expected_escrow);
2142
2143 exchange
2145 .withdraw(alice, quote_token, expected_escrow)
2146 .expect("Withdraw should succeed");
2147 assert_eq!(exchange.balance_of(alice, quote_token)?, 0);
2148
2149 let quote_tip20 = TIP20Token::from_address(quote_token)?;
2151 assert_eq!(
2152 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?,
2153 expected_escrow
2154 );
2155 assert_eq!(
2156 quote_tip20.balance_of(ITIP20::balanceOfCall {
2157 account: exchange.address
2158 })?,
2159 0
2160 );
2161
2162 Ok(())
2163 })
2164 }
2165
2166 #[test]
2167 fn test_withdraw_insufficient_balance() -> eyre::Result<()> {
2168 let mut storage = HashMapStorageProvider::new(1);
2169 StorageCtx::enter(&mut storage, || {
2170 let mut exchange = StablecoinDEX::new();
2171 exchange.initialize()?;
2172
2173 let alice = Address::random();
2174 let admin = Address::random();
2175
2176 let min_order_amount = MIN_ORDER_AMOUNT;
2177 let (_base_token, quote_token) =
2178 setup_test_tokens(admin, alice, exchange.address, min_order_amount)?;
2179
2180 assert_eq!(exchange.balance_of(alice, quote_token)?, 0);
2182
2183 let result = exchange.withdraw(alice, quote_token, 100u128);
2185
2186 assert_eq!(
2187 result,
2188 Err(StablecoinDEXError::insufficient_balance().into())
2189 );
2190
2191 Ok(())
2192 })
2193 }
2194
2195 #[test]
2196 fn test_quote_swap_exact_amount_out() -> eyre::Result<()> {
2197 let mut storage = HashMapStorageProvider::new(1);
2198 StorageCtx::enter(&mut storage, || {
2199 let mut exchange = StablecoinDEX::new();
2200 exchange.initialize()?;
2201
2202 let alice = Address::random();
2203 let admin = Address::random();
2204 let min_order_amount = MIN_ORDER_AMOUNT;
2205 let amount_out = 500_000u128;
2206 let tick = 10;
2207
2208 let (base_token, quote_token) =
2209 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2210 exchange
2211 .create_pair(base_token)
2212 .expect("Could not create pair");
2213
2214 let order_amount = min_order_amount;
2215 exchange
2216 .place(alice, base_token, order_amount, false, tick)
2217 .expect("Order should succeed");
2218
2219 let amount_in = exchange
2220 .quote_swap_exact_amount_out(quote_token, base_token, amount_out)
2221 .expect("Swap should succeed");
2222
2223 let price = orderbook::tick_to_price(tick);
2224 let expected_amount_in = (amount_out * price as u128) / orderbook::PRICE_SCALE as u128;
2225 assert_eq!(amount_in, expected_amount_in);
2226
2227 Ok(())
2228 })
2229 }
2230
2231 #[test]
2232 fn test_quote_swap_exact_amount_in() -> eyre::Result<()> {
2233 let mut storage = HashMapStorageProvider::new(1);
2234 StorageCtx::enter(&mut storage, || {
2235 let mut exchange = StablecoinDEX::new();
2236 exchange.initialize()?;
2237
2238 let alice = Address::random();
2239 let admin = Address::random();
2240 let min_order_amount = MIN_ORDER_AMOUNT;
2241 let amount_in = 500_000u128;
2242 let tick = 10;
2243
2244 let (base_token, quote_token) =
2245 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2246 exchange
2247 .create_pair(base_token)
2248 .expect("Could not create pair");
2249
2250 let order_amount = min_order_amount;
2251 exchange
2252 .place(alice, base_token, order_amount, true, tick)
2253 .expect("Place bid order should succeed");
2254
2255 let amount_out = exchange
2256 .quote_swap_exact_amount_in(base_token, quote_token, amount_in)
2257 .expect("Swap should succeed");
2258
2259 let price = orderbook::tick_to_price(tick);
2261 let expected_amount_out = (amount_in * price as u128) / orderbook::PRICE_SCALE as u128;
2262 assert_eq!(amount_out, expected_amount_out);
2263
2264 Ok(())
2265 })
2266 }
2267
2268 #[test]
2269 fn test_quote_swap_exact_amount_out_base_for_quote() -> eyre::Result<()> {
2270 let mut storage = HashMapStorageProvider::new(1);
2271 StorageCtx::enter(&mut storage, || {
2272 let mut exchange = StablecoinDEX::new();
2273 exchange.initialize()?;
2274
2275 let alice = Address::random();
2276 let admin = Address::random();
2277 let min_order_amount = MIN_ORDER_AMOUNT;
2278 let amount_out = 500_000u128;
2279 let tick = 0;
2280
2281 let (base_token, quote_token) =
2282 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2283 exchange
2284 .create_pair(base_token)
2285 .expect("Could not create pair");
2286
2287 let order_amount = min_order_amount;
2289 exchange
2290 .place(alice, base_token, order_amount, true, tick)
2291 .expect("Place bid order should succeed");
2292
2293 let amount_in = exchange
2296 .quote_swap_exact_amount_out(base_token, quote_token, amount_out)
2297 .expect("Quote should succeed");
2298
2299 let price = orderbook::tick_to_price(tick);
2300 let expected_amount_in =
2302 (amount_out * orderbook::PRICE_SCALE as u128).div_ceil(price as u128);
2303 assert_eq!(amount_in, expected_amount_in);
2304
2305 Ok(())
2306 })
2307 }
2308
2309 #[test]
2310 fn test_quote_exact_out_bid_positive_tick_no_underflow() -> eyre::Result<()> {
2311 let mut storage = HashMapStorageProvider::new(1);
2312 StorageCtx::enter(&mut storage, || {
2313 let mut exchange = StablecoinDEX::new();
2314 exchange.initialize()?;
2315
2316 let alice = Address::random();
2317 let admin = Address::random();
2318
2319 let (base_token, quote_token) =
2320 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2321 exchange.create_pair(base_token)?;
2322
2323 let tick = 10;
2324 let order_amount = MIN_ORDER_AMOUNT;
2325 exchange.place(alice, base_token, order_amount, true, tick)?;
2326
2327 for amount_out in [100_001u128, 100_003, 100_007, 100_009, 100_011] {
2328 let amount_in = exchange
2329 .quote_swap_exact_amount_out(base_token, quote_token, amount_out)
2330 .unwrap_or_else(|_| {
2331 panic!("quote_exact_out should not underflow for amount_out={amount_out}")
2332 });
2333
2334 let expected =
2335 orderbook::quote_to_base(amount_out, tick, RoundingDirection::Up).unwrap();
2336 assert_eq!(
2337 amount_in, expected,
2338 "amount_in should equal quote_to_base(amount_out, tick, Up) for amount_out={amount_out}"
2339 );
2340 }
2341
2342 Ok(())
2343 })
2344 }
2345
2346 #[test]
2347 fn test_swap_exact_amount_out() -> eyre::Result<()> {
2348 let mut storage = HashMapStorageProvider::new(1);
2349 StorageCtx::enter(&mut storage, || {
2350 let mut exchange = StablecoinDEX::new();
2351 exchange.initialize()?;
2352
2353 let alice = Address::random();
2354 let bob = Address::random();
2355 let admin = Address::random();
2356 let min_order_amount = MIN_ORDER_AMOUNT;
2357 let amount_out = 500_000u128;
2358 let tick = 10;
2359
2360 let (base_token, quote_token) =
2361 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2362 exchange
2363 .create_pair(base_token)
2364 .expect("Could not create pair");
2365
2366 let order_amount = min_order_amount;
2367 exchange
2368 .place(alice, base_token, order_amount, false, tick)
2369 .expect("Order should succeed");
2370
2371 exchange
2372 .set_balance(bob, quote_token, 200_000_000u128)
2373 .expect("Could not set balance");
2374
2375 let price = orderbook::tick_to_price(tick);
2376 let max_amount_in = (amount_out * price as u128) / orderbook::PRICE_SCALE as u128;
2377
2378 let amount_in = exchange
2379 .swap_exact_amount_out(bob, quote_token, base_token, amount_out, max_amount_in)
2380 .expect("Swap should succeed");
2381
2382 let base_tip20 = TIP20Token::from_address(base_token)?;
2383 let bob_base_balance = base_tip20.balance_of(ITIP20::balanceOfCall { account: bob })?;
2384 assert_eq!(bob_base_balance, U256::from(amount_out));
2385
2386 let alice_quote_exchange_balance = exchange.balance_of(alice, quote_token)?;
2387 assert_eq!(alice_quote_exchange_balance, amount_in);
2388
2389 Ok(())
2390 })
2391 }
2392
2393 #[test]
2394 fn test_swap_exact_amount_in() -> eyre::Result<()> {
2395 let mut storage = HashMapStorageProvider::new(1);
2396 StorageCtx::enter(&mut storage, || {
2397 let mut exchange = StablecoinDEX::new();
2398 exchange.initialize()?;
2399
2400 let alice = Address::random();
2401 let bob = Address::random();
2402 let admin = Address::random();
2403 let min_order_amount = MIN_ORDER_AMOUNT;
2404 let amount_in = 500_000u128;
2405 let tick = 10;
2406
2407 let (base_token, quote_token) =
2408 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2409 exchange
2410 .create_pair(base_token)
2411 .expect("Could not create pair");
2412
2413 let order_amount = min_order_amount;
2414 exchange
2415 .place(alice, base_token, order_amount, true, tick)
2416 .expect("Order should succeed");
2417
2418 exchange
2419 .set_balance(bob, base_token, 200_000_000u128)
2420 .expect("Could not set balance");
2421
2422 let price = orderbook::tick_to_price(tick);
2423 let min_amount_out = (amount_in * price as u128) / orderbook::PRICE_SCALE as u128;
2424
2425 let amount_out = exchange
2426 .swap_exact_amount_in(bob, base_token, quote_token, amount_in, min_amount_out)
2427 .expect("Swap should succeed");
2428
2429 let quote_tip20 = TIP20Token::from_address(quote_token)?;
2430 let bob_quote_balance =
2431 quote_tip20.balance_of(ITIP20::balanceOfCall { account: bob })?;
2432 assert_eq!(bob_quote_balance, U256::from(amount_out));
2433
2434 let alice_base_exchange_balance = exchange.balance_of(alice, base_token)?;
2435 assert_eq!(alice_base_exchange_balance, amount_in);
2436
2437 Ok(())
2438 })
2439 }
2440
2441 #[test]
2442 fn test_flip_order_execution() -> eyre::Result<()> {
2443 let mut storage = HashMapStorageProvider::new(1);
2444 StorageCtx::enter(&mut storage, || {
2445 let mut exchange = StablecoinDEX::new();
2446 exchange.initialize()?;
2447
2448 let alice = Address::random();
2449 let bob = Address::random();
2450 let admin = Address::random();
2451 let min_order_amount = MIN_ORDER_AMOUNT;
2452 let amount = min_order_amount;
2453 let tick = 100i16;
2454 let flip_tick = 200i16;
2455
2456 let price = orderbook::tick_to_price(tick);
2457 let expected_escrow = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
2458
2459 let (base_token, quote_token) =
2460 setup_test_tokens(admin, alice, exchange.address, expected_escrow * 2)?;
2461 exchange
2462 .create_pair(base_token)
2463 .expect("Could not create pair");
2464
2465 let flip_order_id = exchange
2467 .place_flip(alice, base_token, amount, true, tick, flip_tick, false)
2468 .expect("Place flip order should succeed");
2469
2470 exchange
2471 .set_balance(bob, base_token, amount)
2472 .expect("Could not set balance");
2473
2474 exchange
2475 .swap_exact_amount_in(bob, base_token, quote_token, amount, 0)
2476 .expect("Swap should succeed");
2477
2478 let filled_order = exchange.orders[flip_order_id].read()?;
2480 assert_eq!(filled_order.remaining(), 0);
2481
2482 let new_order_id = exchange.next_order_id()? - 1;
2484 assert_eq!(new_order_id, flip_order_id + 1);
2485
2486 let new_order = exchange.orders[new_order_id].read()?;
2487 assert_eq!(new_order.maker(), alice);
2488 assert_eq!(new_order.tick(), flip_tick);
2489 assert_eq!(new_order.flip_tick(), tick);
2490 assert!(new_order.is_ask());
2491 assert_eq!(new_order.amount(), amount);
2492 assert_eq!(new_order.remaining(), amount);
2493
2494 Ok(())
2495 })
2496 }
2497
2498 #[test]
2499 fn test_pair_created() -> eyre::Result<()> {
2500 let mut storage = HashMapStorageProvider::new(1);
2501 StorageCtx::enter(&mut storage, || {
2502 let mut exchange = StablecoinDEX::new();
2503 exchange.initialize()?;
2504
2505 let admin = Address::random();
2506 let alice = Address::random();
2507
2508 let min_order_amount = MIN_ORDER_AMOUNT;
2509 let (base_token, quote_token) =
2511 setup_test_tokens(admin, alice, exchange.address, min_order_amount)?;
2512
2513 let key = exchange
2515 .create_pair(base_token)
2516 .expect("Could not create pair");
2517
2518 exchange.assert_emitted_events(vec![StablecoinDEXEvents::PairCreated(
2520 IStablecoinDEX::PairCreated {
2521 key,
2522 base: base_token,
2523 quote: quote_token,
2524 },
2525 )]);
2526
2527 Ok(())
2528 })
2529 }
2530
2531 #[test]
2532 fn test_pair_already_created() -> eyre::Result<()> {
2533 let mut storage = HashMapStorageProvider::new(1);
2534 StorageCtx::enter(&mut storage, || {
2535 let mut exchange = StablecoinDEX::new();
2536 exchange.initialize()?;
2537
2538 let admin = Address::random();
2539 let alice = Address::random();
2540
2541 let min_order_amount = MIN_ORDER_AMOUNT;
2542 let (base_token, _) =
2544 setup_test_tokens(admin, alice, exchange.address, min_order_amount)?;
2545
2546 exchange
2547 .create_pair(base_token)
2548 .expect("Could not create pair");
2549
2550 let result = exchange.create_pair(base_token);
2551 assert_eq!(
2552 result,
2553 Err(StablecoinDEXError::pair_already_exists().into())
2554 );
2555
2556 Ok(())
2557 })
2558 }
2559
2560 fn verify_hop(hop: (B256, bool), token_in: Address) -> eyre::Result<()> {
2562 let (book_key, is_base_for_quote) = hop;
2563
2564 let exchange = StablecoinDEX::new();
2565 let orderbook = exchange.books[book_key].read()?;
2566
2567 let expected_book_key = compute_book_key(orderbook.base, orderbook.quote);
2568 assert_eq!(book_key, expected_book_key, "Book key should match");
2569
2570 let expected_direction = token_in == orderbook.base;
2571 assert_eq!(
2572 is_base_for_quote, expected_direction,
2573 "Direction should be correct: token_in={}, base={}, is_base_for_quote={}",
2574 token_in, orderbook.base, is_base_for_quote
2575 );
2576
2577 Ok(())
2578 }
2579
2580 #[test]
2581 fn test_find_path_to_root() -> eyre::Result<()> {
2582 let mut storage = HashMapStorageProvider::new(1);
2583 StorageCtx::enter(&mut storage, || {
2584 let mut exchange = StablecoinDEX::new();
2585 exchange.initialize()?;
2586
2587 let admin = Address::random();
2588
2589 let usdc = TIP20Setup::create("USDC", "USDC", admin).apply()?;
2591 let token_a = TIP20Setup::create("TokenA", "TKA", admin)
2592 .quote_token(usdc.address())
2593 .apply()?;
2594
2595 let path = exchange.find_path_to_root(token_a.address())?;
2597
2598 assert_eq!(path.len(), 3);
2600 assert_eq!(path[0], token_a.address());
2601 assert_eq!(path[1], usdc.address());
2602 assert_eq!(path[2], PATH_USD_ADDRESS);
2603
2604 Ok(())
2605 })
2606 }
2607
2608 #[test]
2609 fn test_find_trade_path_same_token_errors() -> eyre::Result<()> {
2610 let mut storage = HashMapStorageProvider::new(1);
2611 StorageCtx::enter(&mut storage, || {
2612 let mut exchange = StablecoinDEX::new();
2613 exchange.initialize()?;
2614
2615 let admin = Address::random();
2616 let user = Address::random();
2617
2618 let min_order_amount = MIN_ORDER_AMOUNT;
2619 let (token, _) = setup_test_tokens(admin, user, exchange.address, min_order_amount)?;
2620
2621 let result = exchange.find_trade_path(token, token);
2623 assert_eq!(
2624 result,
2625 Err(StablecoinDEXError::identical_tokens().into()),
2626 "Should return IdenticalTokens error when token_in == token_out"
2627 );
2628
2629 Ok(())
2630 })
2631 }
2632
2633 #[test]
2634 fn test_find_trade_path_direct_pair() -> eyre::Result<()> {
2635 let mut storage = HashMapStorageProvider::new(1);
2636 StorageCtx::enter(&mut storage, || {
2637 let mut exchange = StablecoinDEX::new();
2638 exchange.initialize()?;
2639
2640 let admin = Address::random();
2641 let user = Address::random();
2642
2643 let min_order_amount = MIN_ORDER_AMOUNT;
2644 let (token, path_usd) =
2646 setup_test_tokens(admin, user, exchange.address, min_order_amount)?;
2647
2648 exchange.create_pair(token).expect("Failed to create pair");
2650
2651 let route = exchange
2653 .find_trade_path(token, path_usd)
2654 .expect("Should find direct pair");
2655
2656 assert_eq!(route.len(), 1, "Should have 1 hop for direct pair");
2658 verify_hop(route[0], token)?;
2659
2660 Ok(())
2661 })
2662 }
2663
2664 #[test]
2665 fn test_find_trade_path_reverse_pair() -> eyre::Result<()> {
2666 let mut storage = HashMapStorageProvider::new(1);
2667 StorageCtx::enter(&mut storage, || {
2668 let mut exchange = StablecoinDEX::new();
2669 exchange.initialize()?;
2670
2671 let admin = Address::random();
2672 let user = Address::random();
2673
2674 let min_order_amount = MIN_ORDER_AMOUNT;
2675 let (token, path_usd) =
2677 setup_test_tokens(admin, user, exchange.address, min_order_amount)?;
2678
2679 exchange.create_pair(token).expect("Failed to create pair");
2681
2682 let route = exchange
2684 .find_trade_path(path_usd, token)
2685 .expect("Should find reverse pair");
2686
2687 assert_eq!(route.len(), 1, "Should have 1 hop for reverse pair");
2689 verify_hop(route[0], path_usd)?;
2690
2691 Ok(())
2692 })
2693 }
2694
2695 #[test]
2696 fn test_find_trade_path_two_hop_siblings() -> eyre::Result<()> {
2697 let mut storage = HashMapStorageProvider::new(1);
2698 StorageCtx::enter(&mut storage, || {
2699 let mut exchange = StablecoinDEX::new();
2700 exchange.initialize()?;
2701
2702 let admin = Address::random();
2703
2704 let usdc = TIP20Setup::create("USDC", "USDC", admin).apply()?;
2708 let eurc = TIP20Setup::create("EURC", "EURC", admin).apply()?;
2709
2710 exchange.create_pair(usdc.address())?;
2712 exchange.create_pair(eurc.address())?;
2713
2714 let route = exchange.find_trade_path(usdc.address(), eurc.address())?;
2716
2717 assert_eq!(route.len(), 2, "Should have 2 hops for sibling tokens");
2719 verify_hop(route[0], usdc.address())?;
2720 verify_hop(route[1], PATH_USD_ADDRESS)?;
2721
2722 Ok(())
2723 })
2724 }
2725
2726 #[test]
2727 fn test_quote_exact_in_multi_hop() -> eyre::Result<()> {
2728 let mut storage = HashMapStorageProvider::new(1);
2729 StorageCtx::enter(&mut storage, || {
2730 let mut exchange = StablecoinDEX::new();
2731 exchange.initialize()?;
2732
2733 let admin = Address::random();
2734 let alice = Address::random();
2735 let min_order_amount = MIN_ORDER_AMOUNT;
2736 let min_order_amount_x10 = U256::from(MIN_ORDER_AMOUNT * 10);
2737
2738 let _path_usd = TIP20Setup::path_usd(admin)
2741 .with_issuer(admin)
2742 .with_mint(alice, min_order_amount_x10)
2743 .with_approval(alice, exchange.address, min_order_amount_x10)
2744 .apply()?;
2745 let usdc = TIP20Setup::create("USDC", "USDC", admin)
2746 .with_issuer(admin)
2747 .with_mint(alice, min_order_amount_x10)
2748 .with_approval(alice, exchange.address, min_order_amount_x10)
2749 .apply()?;
2750 let eurc = TIP20Setup::create("EURC", "EURC", admin)
2751 .with_issuer(admin)
2752 .with_mint(alice, min_order_amount_x10)
2753 .with_approval(alice, exchange.address, min_order_amount_x10)
2754 .apply()?;
2755
2756 exchange.place(alice, usdc.address(), min_order_amount * 5, true, 0)?;
2763
2764 exchange.place(alice, eurc.address(), min_order_amount * 5, false, 0)?;
2766
2767 let amount_in = min_order_amount;
2769 let amount_out =
2770 exchange.quote_swap_exact_amount_in(usdc.address(), eurc.address(), amount_in)?;
2771
2772 assert_eq!(
2774 amount_out, amount_in,
2775 "With 1:1 rates, output should equal input"
2776 );
2777
2778 Ok(())
2779 })
2780 }
2781
2782 #[test]
2783 fn test_quote_exact_out_multi_hop() -> eyre::Result<()> {
2784 let mut storage = HashMapStorageProvider::new(1);
2785 StorageCtx::enter(&mut storage, || {
2786 let mut exchange = StablecoinDEX::new();
2787 exchange.initialize()?;
2788
2789 let admin = Address::random();
2790 let alice = Address::random();
2791 let min_order_amount = MIN_ORDER_AMOUNT;
2792 let min_order_amount_x10 = U256::from(MIN_ORDER_AMOUNT * 10);
2793
2794 let _path_usd = TIP20Setup::path_usd(admin)
2797 .with_issuer(admin)
2798 .with_mint(alice, min_order_amount_x10)
2799 .with_approval(alice, exchange.address, min_order_amount_x10)
2800 .apply()?;
2801 let usdc = TIP20Setup::create("USDC", "USDC", admin)
2802 .with_issuer(admin)
2803 .with_mint(alice, min_order_amount_x10)
2804 .with_approval(alice, exchange.address, min_order_amount_x10)
2805 .apply()?;
2806 let eurc = TIP20Setup::create("EURC", "EURC", admin)
2807 .with_issuer(admin)
2808 .with_mint(alice, min_order_amount_x10)
2809 .with_approval(alice, exchange.address, min_order_amount_x10)
2810 .apply()?;
2811
2812 exchange.place(alice, usdc.address(), min_order_amount * 5, true, 0)?;
2814 exchange.place(alice, eurc.address(), min_order_amount * 5, false, 0)?;
2815
2816 let amount_out = min_order_amount;
2818 let amount_in =
2819 exchange.quote_swap_exact_amount_out(usdc.address(), eurc.address(), amount_out)?;
2820
2821 assert_eq!(
2824 amount_in, amount_out,
2825 "With 1:1 rates and no rounding, input should equal output"
2826 );
2827
2828 Ok(())
2829 })
2830 }
2831
2832 #[test]
2833 fn test_swap_exact_in_multi_hop_transitory_balances() -> eyre::Result<()> {
2834 let mut storage = HashMapStorageProvider::new(1);
2835 StorageCtx::enter(&mut storage, || {
2836 let mut exchange = StablecoinDEX::new();
2837 exchange.initialize()?;
2838
2839 let admin = Address::random();
2840 let alice = Address::random();
2841 let bob = Address::random();
2842
2843 let min_order_amount = MIN_ORDER_AMOUNT;
2844 let min_order_amount_x10 = U256::from(MIN_ORDER_AMOUNT * 10);
2845
2846 let path_usd = TIP20Setup::path_usd(admin)
2848 .with_issuer(admin)
2849 .with_mint(alice, min_order_amount_x10)
2851 .with_approval(alice, exchange.address, min_order_amount_x10)
2852 .apply()?;
2853
2854 let usdc = TIP20Setup::create("USDC", "USDC", admin)
2855 .with_issuer(admin)
2856 .with_mint(alice, min_order_amount_x10)
2858 .with_approval(alice, exchange.address, min_order_amount_x10)
2859 .with_mint(bob, min_order_amount_x10)
2861 .with_approval(bob, exchange.address, min_order_amount_x10)
2862 .apply()?;
2863
2864 let eurc = TIP20Setup::create("EURC", "EURC", admin)
2865 .with_issuer(admin)
2866 .with_mint(alice, min_order_amount_x10)
2868 .with_approval(alice, exchange.address, min_order_amount_x10)
2869 .apply()?;
2870
2871 exchange.place(alice, usdc.address(), min_order_amount * 5, true, 0)?;
2873 exchange.place(alice, eurc.address(), min_order_amount * 5, false, 0)?;
2874
2875 let bob_usdc_before = usdc.balance_of(ITIP20::balanceOfCall { account: bob })?;
2877 let bob_eurc_before = eurc.balance_of(ITIP20::balanceOfCall { account: bob })?;
2878
2879 let amount_in = min_order_amount;
2881 let amount_out = exchange.swap_exact_amount_in(
2882 bob,
2883 usdc.address(),
2884 eurc.address(),
2885 amount_in,
2886 0, )?;
2888
2889 let bob_usdc_after = usdc.balance_of(ITIP20::balanceOfCall { account: bob })?;
2891 let bob_eurc_after = eurc.balance_of(ITIP20::balanceOfCall { account: bob })?;
2892
2893 assert_eq!(
2895 bob_usdc_before - bob_usdc_after,
2896 U256::from(amount_in),
2897 "Bob should have spent exact amount_in USDC"
2898 );
2899 assert_eq!(
2900 bob_eurc_after - bob_eurc_before,
2901 U256::from(amount_out),
2902 "Bob should have received amount_out EURC"
2903 );
2904
2905 let bob_path_usd_wallet =
2907 path_usd.balance_of(ITIP20::balanceOfCall { account: bob })?;
2908 assert_eq!(
2909 bob_path_usd_wallet,
2910 U256::ZERO,
2911 "Bob should have ZERO pathUSD in wallet (transitory)"
2912 );
2913
2914 let bob_path_usd_exchange = exchange.balance_of(bob, path_usd.address())?;
2915 assert_eq!(
2916 bob_path_usd_exchange, 0,
2917 "Bob should have ZERO pathUSD on exchange (transitory)"
2918 );
2919
2920 Ok(())
2921 })
2922 }
2923
2924 #[test]
2925 fn test_swap_exact_out_multi_hop_transitory_balances() -> eyre::Result<()> {
2926 let mut storage = HashMapStorageProvider::new(1);
2927 StorageCtx::enter(&mut storage, || {
2928 let mut exchange = StablecoinDEX::new();
2929 exchange.initialize()?;
2930
2931 let admin = Address::random();
2932 let alice = Address::random();
2933 let bob = Address::random();
2934
2935 let min_order_amount = MIN_ORDER_AMOUNT;
2936 let min_order_amount_x10 = U256::from(MIN_ORDER_AMOUNT * 10);
2937
2938 let path_usd = TIP20Setup::path_usd(admin)
2940 .with_issuer(admin)
2941 .with_mint(alice, min_order_amount_x10)
2943 .with_approval(alice, exchange.address, min_order_amount_x10)
2944 .apply()?;
2945
2946 let usdc = TIP20Setup::create("USDC", "USDC", admin)
2947 .with_issuer(admin)
2948 .with_mint(alice, min_order_amount_x10)
2950 .with_approval(alice, exchange.address, min_order_amount_x10)
2951 .with_mint(bob, min_order_amount_x10)
2953 .with_approval(bob, exchange.address, min_order_amount_x10)
2954 .apply()?;
2955
2956 let eurc = TIP20Setup::create("EURC", "EURC", admin)
2957 .with_issuer(admin)
2958 .with_mint(alice, min_order_amount_x10)
2960 .with_approval(alice, exchange.address, min_order_amount_x10)
2961 .apply()?;
2962
2963 exchange.place(alice, usdc.address(), min_order_amount * 5, true, 0)?;
2965 exchange.place(alice, eurc.address(), min_order_amount * 5, false, 0)?;
2966
2967 let bob_usdc_before = usdc.balance_of(ITIP20::balanceOfCall { account: bob })?;
2969 let bob_eurc_before = eurc.balance_of(ITIP20::balanceOfCall { account: bob })?;
2970
2971 let amount_out = 90u128;
2973 let amount_in = exchange.swap_exact_amount_out(
2974 bob,
2975 usdc.address(),
2976 eurc.address(),
2977 amount_out,
2978 u128::MAX, )?;
2980
2981 let bob_usdc_after = usdc.balance_of(ITIP20::balanceOfCall { account: bob })?;
2983 let bob_eurc_after = eurc.balance_of(ITIP20::balanceOfCall { account: bob })?;
2984
2985 assert_eq!(
2987 bob_usdc_before - bob_usdc_after,
2988 U256::from(amount_in),
2989 "Bob should have spent amount_in USDC"
2990 );
2991 assert_eq!(
2992 bob_eurc_after - bob_eurc_before,
2993 U256::from(amount_out),
2994 "Bob should have received exact amount_out EURC"
2995 );
2996
2997 let bob_path_usd_wallet =
2999 path_usd.balance_of(ITIP20::balanceOfCall { account: bob })?;
3000 assert_eq!(
3001 bob_path_usd_wallet,
3002 U256::ZERO,
3003 "Bob should have ZERO pathUSD in wallet (transitory)"
3004 );
3005
3006 let bob_path_usd_exchange = exchange
3007 .balance_of(bob, path_usd.address())
3008 .expect("Failed to get bob's pathUSD exchange balance");
3009 assert_eq!(
3010 bob_path_usd_exchange, 0,
3011 "Bob should have ZERO pathUSD on exchange (transitory)"
3012 );
3013
3014 Ok(())
3015 })
3016 }
3017
3018 #[test]
3019 fn test_create_pair_invalid_currency() -> eyre::Result<()> {
3020 let mut storage = HashMapStorageProvider::new(1);
3021 StorageCtx::enter(&mut storage, || {
3022 let admin = Address::random();
3023
3024 let token_0 = TIP20Setup::create("EuroToken", "EURO", admin)
3026 .currency("EUR")
3027 .apply()?;
3028
3029 let mut exchange = StablecoinDEX::new();
3030 exchange.initialize()?;
3031
3032 let result = exchange.create_pair(token_0.address());
3034 assert!(matches!(
3035 result,
3036 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
3037 ));
3038
3039 Ok(())
3040 })
3041 }
3042
3043 #[test]
3044 fn test_create_pair_rejects_non_tip20_base() -> eyre::Result<()> {
3045 let mut storage = HashMapStorageProvider::new(1);
3046 StorageCtx::enter(&mut storage, || {
3047 let admin = Address::random();
3048 let _path_usd = TIP20Setup::path_usd(admin).apply()?;
3049
3050 let mut exchange = StablecoinDEX::new();
3051 exchange.initialize()?;
3052
3053 let non_tip20_address = Address::random();
3055 let result = exchange.create_pair(non_tip20_address);
3056 assert!(matches!(
3057 result,
3058 Err(TempoPrecompileError::StablecoinDEX(
3059 StablecoinDEXError::InvalidBaseToken(_)
3060 ))
3061 ));
3062
3063 Ok(())
3064 })
3065 }
3066
3067 #[test]
3068 fn test_max_in_check() -> eyre::Result<()> {
3069 let mut storage = HashMapStorageProvider::new(1);
3070 StorageCtx::enter(&mut storage, || {
3071 let mut exchange = StablecoinDEX::new();
3072 exchange.initialize()?;
3073
3074 let alice = Address::random();
3075 let bob = Address::random();
3076 let admin = Address::random();
3077
3078 let (base_token, quote_token) =
3079 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
3080 exchange.create_pair(base_token)?;
3081
3082 let tick_50 = 50i16;
3083 let tick_100 = 100i16;
3084 let order_amount = MIN_ORDER_AMOUNT;
3085
3086 exchange.place(alice, base_token, order_amount, false, tick_50)?;
3087 exchange.place(alice, base_token, order_amount, false, tick_100)?;
3088
3089 exchange.set_balance(bob, quote_token, 200_000_000u128)?;
3090
3091 let price_50 = orderbook::tick_to_price(tick_50);
3092 let price_100 = orderbook::tick_to_price(tick_100);
3093 let quote_for_first =
3095 (order_amount * price_50 as u128).div_ceil(orderbook::PRICE_SCALE as u128);
3096 let quote_for_partial_second =
3097 (999 * price_100 as u128).div_ceil(orderbook::PRICE_SCALE as u128);
3098 let total_needed = quote_for_first + quote_for_partial_second;
3099
3100 let result = exchange.swap_exact_amount_out(
3101 bob,
3102 quote_token,
3103 base_token,
3104 order_amount + 999,
3105 total_needed,
3106 );
3107 assert!(result.is_ok());
3108
3109 Ok(())
3110 })
3111 }
3112
3113 #[test]
3114 fn test_exact_out_bid_side() -> eyre::Result<()> {
3115 let mut storage = HashMapStorageProvider::new(1);
3116 StorageCtx::enter(&mut storage, || {
3117 let mut exchange = StablecoinDEX::new();
3118 exchange.initialize()?;
3119
3120 let alice = Address::random();
3121 let bob = Address::random();
3122 let admin = Address::random();
3123
3124 let (base_token, quote_token) =
3125 setup_test_tokens(admin, alice, exchange.address, 1_000_000_000u128)?;
3126 exchange.create_pair(base_token)?;
3127
3128 let tick = 1000i16;
3129 let price = tick_to_price(tick);
3130 let order_amount_base = MIN_ORDER_AMOUNT;
3131
3132 exchange.place(alice, base_token, order_amount_base, true, tick)?;
3133
3134 let amount_out_quote = 5_000_000u128;
3135 let base_needed = (amount_out_quote * PRICE_SCALE as u128) / price as u128;
3136 let max_amount_in = base_needed + 10000;
3137
3138 exchange.set_balance(bob, base_token, max_amount_in * 2)?;
3139
3140 let _amount_in = exchange.swap_exact_amount_out(
3141 bob,
3142 base_token,
3143 quote_token,
3144 amount_out_quote,
3145 max_amount_in,
3146 )?;
3147
3148 let bob_quote_balance = TIP20Token::from_address(quote_token)?
3150 .balance_of(ITIP20::balanceOfCall { account: bob })?;
3151 assert_eq!(bob_quote_balance, U256::from(amount_out_quote));
3152
3153 Ok(())
3154 })
3155 }
3156
3157 #[test]
3158 fn test_exact_in_ask_side() -> eyre::Result<()> {
3159 let mut storage = HashMapStorageProvider::new(1);
3160 StorageCtx::enter(&mut storage, || {
3161 let mut exchange = StablecoinDEX::new();
3162 exchange.initialize()?;
3163
3164 let alice = Address::random();
3165 let bob = Address::random();
3166 let admin = Address::random();
3167
3168 let (base_token, quote_token) =
3169 setup_test_tokens(admin, alice, exchange.address, 1_000_000_000u128)?;
3170 exchange.create_pair(base_token)?;
3171
3172 let tick = 1000i16;
3173 let price = tick_to_price(tick);
3174 let order_amount_base = MIN_ORDER_AMOUNT;
3175
3176 exchange.place(alice, base_token, order_amount_base, false, tick)?;
3177
3178 let amount_in_quote = 5_000_000u128;
3179 let min_amount_out = 0;
3180
3181 exchange.set_balance(bob, quote_token, amount_in_quote * 2)?;
3182
3183 let amount_out = exchange.swap_exact_amount_in(
3184 bob,
3185 quote_token,
3186 base_token,
3187 amount_in_quote,
3188 min_amount_out,
3189 )?;
3190
3191 let expected_base = (amount_in_quote * PRICE_SCALE as u128) / price as u128;
3192 assert_eq!(amount_out, expected_base);
3193
3194 Ok(())
3195 })
3196 }
3197
3198 #[test]
3199 fn test_clear_order() -> eyre::Result<()> {
3200 const AMOUNT: u128 = 1_000_000_000;
3201
3202 let mut storage = HashMapStorageProvider::new(1);
3204 StorageCtx::enter(&mut storage, || {
3205 let mut exchange = StablecoinDEX::new();
3206 exchange.initialize()?;
3207
3208 let alice = Address::random();
3209 let bob = Address::random();
3210 let carol = Address::random();
3211 let admin = Address::random();
3212
3213 let (base_token, quote_token) =
3214 setup_test_tokens(admin, alice, exchange.address, AMOUNT)?;
3215 exchange.create_pair(base_token)?;
3216
3217 TIP20Setup::config(base_token)
3219 .with_mint(bob, U256::from(AMOUNT))
3220 .with_approval(bob, exchange.address, U256::from(AMOUNT))
3221 .apply()?;
3222 TIP20Setup::config(quote_token)
3223 .with_mint(carol, U256::from(AMOUNT))
3224 .with_approval(carol, exchange.address, U256::from(AMOUNT))
3225 .apply()?;
3226
3227 let tick = 100i16;
3228
3229 let order1_amount = MIN_ORDER_AMOUNT;
3231 let order2_amount = MIN_ORDER_AMOUNT;
3232
3233 let order1_id = exchange.place(alice, base_token, order1_amount, false, tick)?;
3234 let order2_id = exchange.place(bob, base_token, order2_amount, false, tick)?;
3235
3236 let order1 = exchange.orders[order1_id].read()?;
3238 let order2 = exchange.orders[order2_id].read()?;
3239 assert_eq!(order1.next(), order2_id);
3240 assert_eq!(order2.prev(), order1_id);
3241
3242 let swap_amount = order1_amount;
3244 exchange.swap_exact_amount_out(
3245 carol,
3246 quote_token,
3247 base_token,
3248 swap_amount,
3249 u128::MAX,
3250 )?;
3251
3252 let order2_after = exchange.orders[order2_id].read()?;
3254 assert_eq!(
3255 order2_after.prev(),
3256 0,
3257 "New head order should have prev = 0 after previous head was filled"
3258 );
3259
3260 Ok(())
3261 })
3262 }
3263
3264 #[test]
3265 fn test_best_tick_updates_on_fill() -> eyre::Result<()> {
3266 let mut storage = HashMapStorageProvider::new(1);
3267 StorageCtx::enter(&mut storage, || {
3268 let mut exchange = StablecoinDEX::new();
3269 exchange.initialize()?;
3270
3271 let alice = Address::random();
3272 let bob = Address::random();
3273 let admin = Address::random();
3274 let amount = MIN_ORDER_AMOUNT;
3275
3276 let (bid_tick_1, bid_tick_2) = (100_i16, 90_i16); let (ask_tick_1, ask_tick_2) = (50_i16, 60_i16); let bid_price_1 = orderbook::tick_to_price(bid_tick_1);
3282 let bid_price_2 = orderbook::tick_to_price(bid_tick_2);
3283 let bid_escrow_1 = (amount * bid_price_1 as u128) / orderbook::PRICE_SCALE as u128;
3284 let bid_escrow_2 = (amount * bid_price_2 as u128) / orderbook::PRICE_SCALE as u128;
3285 let total_bid_escrow = bid_escrow_1 + bid_escrow_2;
3286
3287 let (base_token, quote_token) =
3288 setup_test_tokens(admin, alice, exchange.address, total_bid_escrow)?;
3289 exchange.create_pair(base_token)?;
3290 let book_key = compute_book_key(base_token, quote_token);
3291
3292 exchange.place(alice, base_token, amount, true, bid_tick_1)?;
3294 exchange.place(alice, base_token, amount, true, bid_tick_2)?;
3295
3296 TIP20Setup::config(base_token)
3298 .with_mint(alice, U256::from(amount * 2))
3299 .with_approval(alice, exchange.address, U256::from(amount * 2))
3300 .apply()?;
3301 exchange.place(alice, base_token, amount, false, ask_tick_1)?;
3302 exchange.place(alice, base_token, amount, false, ask_tick_2)?;
3303
3304 let orderbook = exchange.books[book_key].read()?;
3306 assert_eq!(orderbook.best_bid_tick, bid_tick_1);
3307 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3308
3309 exchange.set_balance(bob, base_token, amount)?;
3311 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
3312 let orderbook = exchange.books[book_key].read()?;
3314 assert_eq!(orderbook.best_bid_tick, bid_tick_2);
3315 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3316
3317 exchange.set_balance(bob, base_token, amount)?;
3319 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
3320 let orderbook = exchange.books[book_key].read()?;
3322 assert_eq!(orderbook.best_bid_tick, i16::MIN);
3323 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3324
3325 let ask_price_1 = orderbook::tick_to_price(ask_tick_1);
3327 let quote_needed = (amount * ask_price_1 as u128) / orderbook::PRICE_SCALE as u128;
3328 exchange.set_balance(bob, quote_token, quote_needed)?;
3329 exchange.swap_exact_amount_in(bob, quote_token, base_token, quote_needed, 0)?;
3330 let orderbook = exchange.books[book_key].read()?;
3332 assert_eq!(orderbook.best_ask_tick, ask_tick_2);
3333 assert_eq!(orderbook.best_bid_tick, i16::MIN);
3334
3335 Ok(())
3336 })
3337 }
3338
3339 #[test]
3340 fn test_best_tick_updates_on_cancel() -> eyre::Result<()> {
3341 let mut storage = HashMapStorageProvider::new(1);
3342 StorageCtx::enter(&mut storage, || {
3343 let mut exchange = StablecoinDEX::new();
3344 exchange.initialize()?;
3345
3346 let alice = Address::random();
3347 let admin = Address::random();
3348 let amount = MIN_ORDER_AMOUNT;
3349
3350 let (bid_tick_1, bid_tick_2) = (100_i16, 90_i16); let (ask_tick_1, ask_tick_2) = (50_i16, 60_i16); let price_1 = orderbook::tick_to_price(bid_tick_1);
3355 let price_2 = orderbook::tick_to_price(bid_tick_2);
3356 let escrow_1 = (amount * price_1 as u128) / orderbook::PRICE_SCALE as u128;
3357 let escrow_2 = (amount * price_2 as u128) / orderbook::PRICE_SCALE as u128;
3358 let total_escrow = escrow_1 * 2 + escrow_2;
3359
3360 let (base_token, quote_token) =
3361 setup_test_tokens(admin, alice, exchange.address, total_escrow)?;
3362 exchange.create_pair(base_token)?;
3363 let book_key = compute_book_key(base_token, quote_token);
3364
3365 let bid_order_1 = exchange.place(alice, base_token, amount, true, bid_tick_1)?;
3367 let bid_order_2 = exchange.place(alice, base_token, amount, true, bid_tick_1)?;
3368 let bid_order_3 = exchange.place(alice, base_token, amount, true, bid_tick_2)?;
3369
3370 TIP20Setup::config(base_token)
3372 .with_mint(alice, U256::from(amount * 2))
3373 .with_approval(alice, exchange.address, U256::from(amount * 2))
3374 .apply()?;
3375 let ask_order_1 = exchange.place(alice, base_token, amount, false, ask_tick_1)?;
3376 let ask_order_2 = exchange.place(alice, base_token, amount, false, ask_tick_2)?;
3377
3378 let orderbook = exchange.books[book_key].read()?;
3380 assert_eq!(orderbook.best_bid_tick, bid_tick_1);
3381 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3382
3383 exchange.cancel(alice, bid_order_1)?;
3385 let orderbook = exchange.books[book_key].read()?;
3387 assert_eq!(orderbook.best_bid_tick, bid_tick_1);
3388 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3389
3390 exchange.cancel(alice, bid_order_2)?;
3392 let orderbook = exchange.books[book_key].read()?;
3394 assert_eq!(orderbook.best_bid_tick, bid_tick_2);
3395 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3396
3397 exchange.cancel(alice, ask_order_1)?;
3399 let orderbook = exchange.books[book_key].read()?;
3401 assert_eq!(orderbook.best_bid_tick, bid_tick_2);
3402 assert_eq!(orderbook.best_ask_tick, ask_tick_2);
3403
3404 exchange.cancel(alice, bid_order_3)?;
3406 let orderbook = exchange.books[book_key].read()?;
3408 assert_eq!(orderbook.best_bid_tick, i16::MIN);
3409 assert_eq!(orderbook.best_ask_tick, ask_tick_2);
3410
3411 exchange.cancel(alice, ask_order_2)?;
3413 let orderbook = exchange.books[book_key].read()?;
3415 assert_eq!(orderbook.best_bid_tick, i16::MIN);
3416 assert_eq!(orderbook.best_ask_tick, i16::MAX);
3417
3418 Ok(())
3419 })
3420 }
3421
3422 #[test]
3423 fn test_place() -> eyre::Result<()> {
3424 const AMOUNT: u128 = 1_000_000_000;
3425
3426 let mut storage = HashMapStorageProvider::new(1);
3427 StorageCtx::enter(&mut storage, || {
3428 let mut exchange = StablecoinDEX::new();
3429 exchange.initialize()?;
3430
3431 let alice = Address::random();
3432 let admin = Address::random();
3433
3434 let (base_token, _quote_token) =
3435 setup_test_tokens(admin, alice, exchange.address, AMOUNT)?;
3436 exchange.create_pair(base_token)?;
3437
3438 TIP20Setup::config(base_token)
3440 .with_mint(alice, U256::from(AMOUNT))
3441 .with_approval(alice, exchange.address, U256::from(AMOUNT))
3442 .apply()?;
3443
3444 let invalid_tick = 15i16;
3446 let result = exchange.place(alice, base_token, MIN_ORDER_AMOUNT, true, invalid_tick);
3447
3448 let error = result.unwrap_err();
3449 assert!(matches!(
3450 error,
3451 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidTick(_))
3452 ));
3453
3454 let valid_tick = -20i16;
3456 let result = exchange.place(alice, base_token, MIN_ORDER_AMOUNT, true, valid_tick);
3457 assert!(result.is_ok());
3458
3459 Ok(())
3460 })
3461 }
3462
3463 #[test]
3464 fn test_place_flip_checks() -> eyre::Result<()> {
3465 const AMOUNT: u128 = 1_000_000_000;
3466
3467 let mut storage = HashMapStorageProvider::new(1);
3468 StorageCtx::enter(&mut storage, || {
3469 let mut exchange = StablecoinDEX::new();
3470 exchange.initialize()?;
3471
3472 let alice = Address::random();
3473 let admin = Address::random();
3474
3475 let (base_token, _quote_token) =
3476 setup_test_tokens(admin, alice, exchange.address, AMOUNT)?;
3477 exchange.create_pair(base_token)?;
3478
3479 TIP20Setup::config(base_token)
3481 .with_mint(alice, U256::from(AMOUNT))
3482 .with_approval(alice, exchange.address, U256::from(AMOUNT))
3483 .apply()?;
3484
3485 let invalid_tick = 15i16;
3487 let invalid_flip_tick = 25i16;
3488 let result = exchange.place_flip(
3489 alice,
3490 base_token,
3491 MIN_ORDER_AMOUNT,
3492 true,
3493 invalid_tick,
3494 invalid_flip_tick,
3495 false,
3496 );
3497
3498 let error = result.unwrap_err();
3499 assert!(matches!(
3500 error,
3501 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidTick(_))
3502 ));
3503
3504 let valid_tick = 20i16;
3506 let invalid_flip_tick = 25i16;
3507 let result = exchange.place_flip(
3508 alice,
3509 base_token,
3510 MIN_ORDER_AMOUNT,
3511 true,
3512 valid_tick,
3513 invalid_flip_tick,
3514 false,
3515 );
3516
3517 let error = result.unwrap_err();
3518 assert!(matches!(
3519 error,
3520 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidFlipTick(_))
3521 ));
3522
3523 let valid_flip_tick = 30i16;
3524 let result = exchange.place_flip(
3525 alice,
3526 base_token,
3527 MIN_ORDER_AMOUNT,
3528 true,
3529 valid_tick,
3530 valid_flip_tick,
3531 false,
3532 );
3533 assert!(result.is_ok());
3534
3535 Ok(())
3536 })
3537 }
3538
3539 #[test]
3540 fn test_find_trade_path_rejects_non_tip20() -> eyre::Result<()> {
3541 let mut storage = HashMapStorageProvider::new(1);
3542 StorageCtx::enter(&mut storage, || {
3543 let mut exchange = StablecoinDEX::new();
3544 exchange.initialize()?;
3545
3546 let admin = Address::random();
3547 let user = Address::random();
3548
3549 let (_, quote_token) =
3550 setup_test_tokens(admin, user, exchange.address, MIN_ORDER_AMOUNT)?;
3551
3552 let non_tip20_address = Address::random();
3553 let result = exchange.find_trade_path(non_tip20_address, quote_token);
3554 assert!(
3555 matches!(
3556 result,
3557 Err(TempoPrecompileError::StablecoinDEX(
3558 StablecoinDEXError::InvalidToken(_)
3559 ))
3560 ),
3561 "Should return InvalidToken error for non-TIP20 token"
3562 );
3563
3564 Ok(())
3565 })
3566 }
3567
3568 #[test]
3569 fn test_quote_exact_in_handles_both_directions() -> eyre::Result<()> {
3570 let mut storage = HashMapStorageProvider::new(1);
3571 StorageCtx::enter(&mut storage, || {
3572 let mut exchange = StablecoinDEX::new();
3573 exchange.initialize()?;
3574
3575 let alice = Address::random();
3576 let admin = Address::random();
3577 let amount = MIN_ORDER_AMOUNT;
3578 let tick = 100_i16;
3579 let price = orderbook::tick_to_price(tick);
3580
3581 let bid_escrow = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
3583
3584 let (base_token, quote_token) =
3585 setup_test_tokens(admin, alice, exchange.address, bid_escrow)?;
3586
3587 TIP20Setup::config(base_token)
3588 .with_mint(alice, U256::from(amount))
3589 .with_approval(alice, exchange.address, U256::from(amount))
3590 .apply()?;
3591
3592 exchange.create_pair(base_token)?;
3593 let book_key = compute_book_key(base_token, quote_token);
3594
3595 exchange.place(alice, base_token, amount, true, tick)?;
3597
3598 let quoted_out_bid = exchange.quote_exact_in(book_key, amount, true)?;
3600 let expected_quote_out = amount
3601 .checked_mul(price as u128)
3602 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
3603 .expect("calculation");
3604 assert_eq!(
3605 quoted_out_bid, expected_quote_out,
3606 "quote_exact_in with is_bid=true should return quote amount"
3607 );
3608
3609 exchange.place(alice, base_token, amount, false, tick)?;
3611
3612 let quote_in = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
3614 let quoted_out_ask = exchange.quote_exact_in(book_key, quote_in, false)?;
3615 let expected_base_out = quote_in
3616 .checked_mul(orderbook::PRICE_SCALE as u128)
3617 .and_then(|v| v.checked_div(price as u128))
3618 .expect("calculation");
3619 assert_eq!(
3620 quoted_out_ask, expected_base_out,
3621 "quote_exact_in with is_bid=false should return base amount"
3622 );
3623
3624 Ok(())
3625 })
3626 }
3627
3628 #[test]
3629 fn test_place_auto_creates_pair() -> Result<()> {
3630 let mut storage = HashMapStorageProvider::new(1);
3631 StorageCtx::enter(&mut storage, || {
3632 let mut exchange = StablecoinDEX::new();
3633 exchange.initialize()?;
3634 let admin = Address::random();
3635 let user = Address::random();
3636
3637 let (base_token, quote_token) =
3639 setup_test_tokens(admin, user, exchange.address, 100_000_000)?;
3640
3641 let book_key = compute_book_key(base_token, quote_token);
3643 let book_before = exchange.books[book_key].read()?;
3644 assert!(book_before.base.is_zero(),);
3645
3646 let mut base = TIP20Token::from_address(base_token)?;
3648 base.transfer(
3649 user,
3650 ITIP20::transferCall {
3651 to: exchange.address,
3652 amount: U256::from(MIN_ORDER_AMOUNT),
3653 },
3654 )
3655 .expect("Base token transfer failed");
3656
3657 exchange.place(user, base_token, MIN_ORDER_AMOUNT, true, 0)?;
3659
3660 let book_after = exchange.books[book_key].read()?;
3661 assert_eq!(book_after.base, base_token);
3662
3663 let events = exchange.emitted_events();
3665 assert_eq!(events.len(), 2);
3666 assert_eq!(
3667 events[0],
3668 StablecoinDEXEvents::PairCreated(IStablecoinDEX::PairCreated {
3669 key: book_key,
3670 base: base_token,
3671 quote: quote_token,
3672 })
3673 .into_log_data()
3674 );
3675
3676 Ok(())
3677 })
3678 }
3679
3680 #[test]
3681 fn test_decrement_balance_preserves_balance() -> eyre::Result<()> {
3682 let mut storage = HashMapStorageProvider::new(1);
3683 StorageCtx::enter(&mut storage, || {
3684 let mut exchange = StablecoinDEX::new();
3685 exchange.initialize()?;
3686
3687 let admin = Address::random();
3688 let alice = Address::random();
3689
3690 let base = TIP20Setup::create("BASE", "BASE", admin).apply()?;
3691 let base_address = base.address();
3692
3693 exchange.create_pair(base_address)?;
3694
3695 let internal_balance = MIN_ORDER_AMOUNT / 2;
3696 exchange.set_balance(alice, base_address, internal_balance)?;
3697
3698 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
3699
3700 let tick = 0i16;
3701 let result = exchange.place(alice, base_address, MIN_ORDER_AMOUNT * 2, false, tick);
3702
3703 assert!(result.is_err());
3704 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
3705
3706 Ok(())
3707 })
3708 }
3709
3710 #[test]
3711 fn test_place_order_immediately_active() -> eyre::Result<()> {
3712 let mut storage = HashMapStorageProvider::new(1);
3713 StorageCtx::enter(&mut storage, || {
3714 let mut exchange = StablecoinDEX::new();
3715 exchange.initialize()?;
3716
3717 let admin = Address::random();
3718 let alice = Address::random();
3719 let min_order_amount = MIN_ORDER_AMOUNT;
3720 let tick = 100i16;
3721
3722 let price = orderbook::tick_to_price(tick);
3723 let expected_escrow =
3724 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
3725
3726 TIP20Setup::path_usd(admin)
3727 .with_issuer(admin)
3728 .with_mint(alice, U256::from(expected_escrow))
3729 .with_approval(alice, exchange.address, U256::from(expected_escrow))
3730 .apply()?;
3731
3732 let base = TIP20Setup::create("BASE", "BASE", admin).apply()?;
3733 let base_token = base.address();
3734 let quote_token = base.quote_token()?;
3735
3736 exchange.create_pair(base_token)?;
3737
3738 let order_id = exchange.place(alice, base_token, min_order_amount, true, tick)?;
3739
3740 assert_eq!(order_id, 1);
3741
3742 let book_key = compute_book_key(base_token, quote_token);
3743 let book_handler = &exchange.books[book_key];
3744 let level = book_handler.tick_level_handler(tick, true).read()?;
3745 assert_eq!(level.head, order_id, "Order should be head of tick level");
3746 assert_eq!(level.tail, order_id, "Order should be tail of tick level");
3747 assert_eq!(
3748 level.total_liquidity, min_order_amount,
3749 "Tick level should have order's liquidity"
3750 );
3751
3752 let orderbook = book_handler.read()?;
3753 assert_eq!(
3754 orderbook.best_bid_tick, tick,
3755 "Best bid tick should be updated"
3756 );
3757
3758 Ok(())
3759 })
3760 }
3761
3762 #[test]
3763 fn test_place_flip_order_immediately_active() -> eyre::Result<()> {
3764 let mut storage = HashMapStorageProvider::new(1);
3765 StorageCtx::enter(&mut storage, || {
3766 let mut exchange = StablecoinDEX::new();
3767 exchange.initialize()?;
3768
3769 let admin = Address::random();
3770 let alice = Address::random();
3771 let min_order_amount = MIN_ORDER_AMOUNT;
3772 let tick = 100i16;
3773 let flip_tick = 200i16;
3774
3775 let price = orderbook::tick_to_price(tick);
3776 let expected_escrow =
3777 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
3778
3779 TIP20Setup::path_usd(admin)
3780 .with_issuer(admin)
3781 .with_mint(alice, U256::from(expected_escrow))
3782 .with_approval(alice, exchange.address, U256::from(expected_escrow))
3783 .apply()?;
3784
3785 let base = TIP20Setup::create("BASE", "BASE", admin).apply()?;
3786 let base_token = base.address();
3787 let quote_token = base.quote_token()?;
3788
3789 exchange.create_pair(base_token)?;
3790
3791 let order_id = exchange.place_flip(
3792 alice,
3793 base_token,
3794 min_order_amount,
3795 true,
3796 tick,
3797 flip_tick,
3798 false,
3799 )?;
3800
3801 assert_eq!(order_id, 1);
3802
3803 let book_key = compute_book_key(base_token, quote_token);
3804 let book_handler = &exchange.books[book_key];
3805 let level = book_handler.tick_level_handler(tick, true).read()?;
3806 assert_eq!(level.head, order_id, "Order should be head of tick level");
3807 assert_eq!(level.tail, order_id, "Order should be tail of tick level");
3808 assert_eq!(
3809 level.total_liquidity, min_order_amount,
3810 "Tick level should have order's liquidity"
3811 );
3812
3813 let orderbook = book_handler.read()?;
3814 assert_eq!(
3815 orderbook.best_bid_tick, tick,
3816 "Best bid tick should be updated"
3817 );
3818
3819 let stored_order = exchange.orders[order_id].read()?;
3820 assert!(stored_order.is_flip(), "Order should be a flip order");
3821 assert_eq!(
3822 stored_order.flip_tick(),
3823 flip_tick,
3824 "Flip tick should match"
3825 );
3826
3827 Ok(())
3828 })
3829 }
3830
3831 #[test]
3832 fn test_place_post() -> eyre::Result<()> {
3833 let mut storage = HashMapStorageProvider::new(1);
3834 StorageCtx::enter(&mut storage, || {
3835 let mut exchange = StablecoinDEX::new();
3836 exchange.initialize()?;
3837
3838 let admin = Address::random();
3839 let alice = Address::random();
3840 let min_order_amount = MIN_ORDER_AMOUNT;
3841 let tick = 100i16;
3842
3843 let price = orderbook::tick_to_price(tick);
3844 let expected_escrow =
3845 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
3846
3847 TIP20Setup::path_usd(admin)
3848 .with_issuer(admin)
3849 .with_mint(alice, U256::from(expected_escrow))
3850 .with_approval(alice, exchange.address, U256::from(expected_escrow))
3851 .apply()?;
3852
3853 let base = TIP20Setup::create("BASE", "BASE", admin).apply()?;
3854 let base_token = base.address();
3855 let quote_token = base.quote_token()?;
3856
3857 exchange.create_pair(base_token)?;
3858
3859 let order_id = exchange.place(alice, base_token, min_order_amount, true, tick)?;
3860
3861 let stored_order = exchange.orders[order_id].read()?;
3862 assert_eq!(stored_order.maker(), alice);
3863 assert_eq!(stored_order.remaining(), min_order_amount);
3864 assert_eq!(stored_order.tick(), tick);
3865 assert!(stored_order.is_bid());
3866
3867 let book_key = compute_book_key(base_token, quote_token);
3868 let level = exchange.books[book_key]
3869 .tick_level_handler(tick, true)
3870 .read()?;
3871 assert_eq!(level.head, order_id);
3872 assert_eq!(level.tail, order_id);
3873 assert_eq!(level.total_liquidity, min_order_amount);
3874
3875 let book = exchange.books[book_key].read()?;
3876 assert_eq!(book.best_bid_tick, tick);
3877
3878 assert_eq!(exchange.next_order_id()?, 2);
3879
3880 Ok(())
3881 })
3882 }
3883
3884 #[test]
3885 fn test_blacklisted_user_cannot_use_internal_balance() -> eyre::Result<()> {
3886 use crate::tip403_registry::{ITIP403Registry, TIP403Registry};
3887
3888 let mut storage = HashMapStorageProvider::new(1);
3889 StorageCtx::enter(&mut storage, || {
3890 let mut exchange = StablecoinDEX::new();
3891 exchange.initialize()?;
3892
3893 let alice = Address::random();
3894 let admin = Address::random();
3895
3896 let mut registry = TIP403Registry::new();
3898 let policy_id = registry.create_policy(
3899 admin,
3900 ITIP403Registry::createPolicyCall {
3901 admin,
3902 policyType: ITIP403Registry::PolicyType::BLACKLIST,
3903 },
3904 )?;
3905
3906 let mut quote = TIP20Setup::path_usd(admin).with_issuer(admin).apply()?;
3908
3909 quote.change_transfer_policy_id(
3910 admin,
3911 ITIP20::changeTransferPolicyIdCall {
3912 newPolicyId: policy_id,
3913 },
3914 )?;
3915
3916 let mut base = TIP20Setup::create("BASE", "BASE", admin)
3918 .with_issuer(admin)
3919 .apply()?;
3920 let base_address = base.address();
3921
3922 base.change_transfer_policy_id(
3923 admin,
3924 ITIP20::changeTransferPolicyIdCall {
3925 newPolicyId: policy_id,
3926 },
3927 )?;
3928
3929 exchange.create_pair(base_address)?;
3930
3931 let internal_balance = MIN_ORDER_AMOUNT * 2;
3933 exchange.set_balance(alice, base_address, internal_balance)?;
3934 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
3935
3936 registry.modify_policy_blacklist(
3938 admin,
3939 ITIP403Registry::modifyPolicyBlacklistCall {
3940 policyId: policy_id,
3941 account: alice,
3942 restricted: true,
3943 },
3944 )?;
3945 assert!(!registry.is_authorized_as(policy_id, alice, AuthRole::sender())?);
3946
3947 let tick = 0i16;
3949 let result = exchange.place(alice, base_address, MIN_ORDER_AMOUNT, false, tick);
3950
3951 assert!(
3952 result.is_err(),
3953 "Blacklisted user should not be able to place orders using internal balance"
3954 );
3955 let err = result.unwrap_err();
3956 assert!(
3957 matches!(
3958 err,
3959 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
3960 ),
3961 "Expected PolicyForbids error, got: {err:?}"
3962 );
3963 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
3964
3965 Ok(())
3966 })
3967 }
3968
3969 #[test]
3970 fn test_cancel_stale_order() -> eyre::Result<()> {
3971 let mut storage = HashMapStorageProvider::new(1);
3972 StorageCtx::enter(&mut storage, || {
3973 let mut exchange = StablecoinDEX::new();
3974 exchange.initialize()?;
3975
3976 let alice = Address::random();
3977 let admin = Address::random();
3978
3979 let mut registry = TIP403Registry::new();
3980 let policy_id = registry.create_policy(
3981 admin,
3982 ITIP403Registry::createPolicyCall {
3983 admin,
3984 policyType: ITIP403Registry::PolicyType::BLACKLIST,
3985 },
3986 )?;
3987
3988 let mut base = TIP20Setup::create("USDC", "USDC", admin)
3989 .with_issuer(admin)
3990 .with_mint(alice, U256::from(MIN_ORDER_AMOUNT * 2))
3991 .with_approval(alice, exchange.address, U256::from(MIN_ORDER_AMOUNT * 2))
3992 .apply()?;
3993 base.change_transfer_policy_id(
3994 admin,
3995 ITIP20::changeTransferPolicyIdCall {
3996 newPolicyId: policy_id,
3997 },
3998 )?;
3999
4000 exchange.create_pair(base.address())?;
4001 let order_id = exchange.place(alice, base.address(), MIN_ORDER_AMOUNT, false, 0)?;
4002
4003 registry.modify_policy_blacklist(
4004 admin,
4005 ITIP403Registry::modifyPolicyBlacklistCall {
4006 policyId: policy_id,
4007 account: alice,
4008 restricted: true,
4009 },
4010 )?;
4011
4012 exchange.cancel_stale_order(order_id)?;
4013
4014 assert_eq!(
4015 exchange.balance_of(alice, base.address())?,
4016 MIN_ORDER_AMOUNT
4017 );
4018
4019 Ok(())
4020 })
4021 }
4022
4023 #[test]
4024 fn test_cancel_stale_not_stale() -> eyre::Result<()> {
4025 let mut storage = HashMapStorageProvider::new(1);
4026 StorageCtx::enter(&mut storage, || {
4027 let mut exchange = StablecoinDEX::new();
4028 exchange.initialize()?;
4029
4030 let alice = Address::random();
4031 let admin = Address::random();
4032
4033 let mut registry = TIP403Registry::new();
4034 let policy_id = registry.create_policy(
4035 admin,
4036 ITIP403Registry::createPolicyCall {
4037 admin,
4038 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4039 },
4040 )?;
4041
4042 let mut base = TIP20Setup::create("USDC", "USDC", admin)
4043 .with_issuer(admin)
4044 .with_mint(alice, U256::from(MIN_ORDER_AMOUNT * 2))
4045 .with_approval(alice, exchange.address, U256::from(MIN_ORDER_AMOUNT * 2))
4046 .apply()?;
4047 base.change_transfer_policy_id(
4048 admin,
4049 ITIP20::changeTransferPolicyIdCall {
4050 newPolicyId: policy_id,
4051 },
4052 )?;
4053
4054 exchange.create_pair(base.address())?;
4055 let order_id = exchange.place(alice, base.address(), MIN_ORDER_AMOUNT, false, 0)?;
4056
4057 let result = exchange.cancel_stale_order(order_id);
4058 assert!(result.is_err());
4059 assert!(matches!(
4060 result.unwrap_err(),
4061 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::OrderNotStale(_))
4062 ));
4063
4064 Ok(())
4065 })
4066 }
4067
4068 #[test]
4069 fn test_cancel_stale_order_with_invalid_policy_type() -> eyre::Result<()> {
4070 for spec in [TempoHardfork::T0, TempoHardfork::T1C, TempoHardfork::T2] {
4076 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
4077
4078 let alice = Address::random();
4079 let admin = Address::random();
4080
4081 let (order_id, base_token, invalid_policy_id) =
4082 StorageCtx::enter(&mut storage, || {
4083 let mut exchange = StablecoinDEX::new();
4084 exchange.initialize()?;
4085
4086 let mut base = TIP20Setup::create("USDC", "USDC", admin)
4087 .with_issuer(admin)
4088 .with_mint(alice, U256::from(MIN_ORDER_AMOUNT * 2))
4089 .with_approval(alice, exchange.address, U256::from(MIN_ORDER_AMOUNT * 2))
4090 .apply()?;
4091
4092 exchange.create_pair(base.address())?;
4093 let order_id =
4094 exchange.place(alice, base.address(), MIN_ORDER_AMOUNT, false, 0)?;
4095
4096 let mut registry = TIP403Registry::new();
4099 let invalid_policy_id = registry.create_policy(
4100 admin,
4101 ITIP403Registry::createPolicyCall {
4102 admin,
4103 policyType: ITIP403Registry::PolicyType::COMPOUND,
4104 },
4105 )?;
4106 base.change_transfer_policy_id(
4107 admin,
4108 ITIP20::changeTransferPolicyIdCall {
4109 newPolicyId: invalid_policy_id,
4110 },
4111 )?;
4112
4113 Ok::<_, TempoPrecompileError>((order_id, base.address(), invalid_policy_id))
4114 })?;
4115
4116 let mut storage = storage.with_spec(spec);
4118 StorageCtx::enter(&mut storage, || {
4119 let mut exchange = StablecoinDEX::new();
4120
4121 let registry = TIP403Registry::new();
4123 let auth_result =
4124 registry.is_authorized_as(invalid_policy_id, alice, AuthRole::sender());
4125 assert!(
4126 auth_result.is_err(),
4127 "[{spec:?}] is_authorized_as should fail for invalid policy type"
4128 );
4129
4130 exchange.cancel_stale_order(order_id)?;
4132
4133 assert_eq!(
4134 exchange.balance_of(alice, base_token)?,
4135 MIN_ORDER_AMOUNT,
4136 "[{spec:?}] alice should get her funds back"
4137 );
4138
4139 Ok::<_, eyre::Report>(())
4140 })?;
4141 }
4142 Ok(())
4143 }
4144
4145 #[test]
4146 fn test_place_when_base_blacklisted() -> eyre::Result<()> {
4147 let mut storage = HashMapStorageProvider::new(1);
4148 StorageCtx::enter(&mut storage, || {
4149 let mut exchange = StablecoinDEX::new();
4150 exchange.initialize()?;
4151
4152 let alice = Address::random();
4153 let admin = Address::random();
4154
4155 let mut registry = TIP403Registry::new();
4157 let policy_id = registry.create_policy(
4158 admin,
4159 ITIP403Registry::createPolicyCall {
4160 admin,
4161 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4162 },
4163 )?;
4164
4165 let (base_addr, _quote_addr) =
4167 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 4)?;
4168
4169 let mut base = TIP20Token::from_address(base_addr)?;
4171 base.change_transfer_policy_id(
4172 admin,
4173 ITIP20::changeTransferPolicyIdCall {
4174 newPolicyId: policy_id,
4175 },
4176 )?;
4177
4178 registry.modify_policy_blacklist(
4180 admin,
4181 ITIP403Registry::modifyPolicyBlacklistCall {
4182 policyId: policy_id,
4183 account: alice,
4184 restricted: true,
4185 },
4186 )?;
4187
4188 exchange.create_pair(base_addr)?;
4189
4190 let result = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, true, 0);
4192 assert!(result.is_err());
4193 assert!(matches!(
4194 result.unwrap_err(),
4195 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4196 ));
4197
4198 let result =
4200 exchange.place_flip(alice, base_addr, MIN_ORDER_AMOUNT, true, 0, 100, false);
4201 assert!(result.is_err());
4202 assert!(matches!(
4203 result.unwrap_err(),
4204 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4205 ));
4206
4207 Ok(())
4208 })
4209 }
4210
4211 #[test]
4212 fn test_place_when_quote_blacklisted() -> eyre::Result<()> {
4213 let mut storage = HashMapStorageProvider::new(1);
4214 StorageCtx::enter(&mut storage, || {
4215 let mut exchange = StablecoinDEX::new();
4216 exchange.initialize()?;
4217
4218 let alice = Address::random();
4219 let admin = Address::random();
4220
4221 let mut registry = TIP403Registry::new();
4223 let policy_id = registry.create_policy(
4224 admin,
4225 ITIP403Registry::createPolicyCall {
4226 admin,
4227 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4228 },
4229 )?;
4230
4231 let (base_addr, quote_addr) =
4233 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 4)?;
4234
4235 let mut quote = TIP20Token::from_address(quote_addr)?;
4237 quote.change_transfer_policy_id(
4238 admin,
4239 ITIP20::changeTransferPolicyIdCall {
4240 newPolicyId: policy_id,
4241 },
4242 )?;
4243
4244 registry.modify_policy_blacklist(
4246 admin,
4247 ITIP403Registry::modifyPolicyBlacklistCall {
4248 policyId: policy_id,
4249 account: alice,
4250 restricted: true,
4251 },
4252 )?;
4253
4254 exchange.create_pair(base_addr)?;
4255
4256 let result = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, false, 0);
4258 assert!(result.is_err());
4259 assert!(matches!(
4260 result.unwrap_err(),
4261 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4262 ));
4263
4264 let result =
4266 exchange.place_flip(alice, base_addr, MIN_ORDER_AMOUNT, false, 100, 0, false);
4267 assert!(result.is_err());
4268 assert!(matches!(
4269 result.unwrap_err(),
4270 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4271 ));
4272
4273 Ok(())
4274 })
4275 }
4276
4277 #[test]
4278 fn test_compound_policy_non_escrow_token_direction() -> eyre::Result<()> {
4279 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
4280 StorageCtx::enter(&mut storage, || {
4281 let mut exchange = StablecoinDEX::new();
4282 exchange.initialize()?;
4283
4284 let (alice, admin) = (Address::random(), Address::random());
4285 let mut registry = TIP403Registry::new();
4286
4287 let recipient_policy = registry.create_policy(
4290 admin,
4291 ITIP403Registry::createPolicyCall {
4292 admin,
4293 policyType: ITIP403Registry::PolicyType::WHITELIST,
4294 },
4295 )?;
4296 let compound_id = registry.create_compound_policy(
4300 admin,
4301 ITIP403Registry::createCompoundPolicyCall {
4302 senderPolicyId: 1, recipientPolicyId: recipient_policy, mintRecipientPolicyId: 1, },
4306 )?;
4307
4308 let (base_addr, quote_addr) =
4310 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 4)?;
4311
4312 let mut quote = TIP20Token::from_address(quote_addr)?;
4314 quote.change_transfer_policy_id(
4315 admin,
4316 ITIP20::changeTransferPolicyIdCall {
4317 newPolicyId: compound_id,
4318 },
4319 )?;
4320
4321 exchange.create_pair(base_addr)?;
4322
4323 let res_ask = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, false, 0);
4327 let res_flip =
4329 exchange.place_flip(alice, base_addr, MIN_ORDER_AMOUNT, false, 100, 0, false);
4330
4331 for res in [res_ask, res_flip] {
4332 assert!(
4333 matches!(
4334 res.unwrap_err(),
4335 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4336 ),
4337 "Order should fail: alice cannot receive quote token (non-escrow) per compound policy"
4338 );
4339 }
4340 Ok(())
4341 })
4342 }
4343
4344 #[test]
4345 fn test_swap_exact_amount_out_rounding() -> eyre::Result<()> {
4346 let mut storage = HashMapStorageProvider::new(1);
4347 StorageCtx::enter(&mut storage, || {
4348 let mut exchange = StablecoinDEX::new();
4349 exchange.initialize()?;
4350
4351 let alice = Address::random();
4352 let bob = Address::random();
4353 let admin = Address::random();
4354 let tick = 10;
4355
4356 let (base_token, quote_token) =
4357 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
4358 exchange
4359 .create_pair(base_token)
4360 .expect("Could not create pair");
4361
4362 let order_amount = 100000000u128;
4363
4364 let tip20_quote_token = TIP20Token::from_address(quote_token)?;
4365 let alice_initial_balance =
4366 tip20_quote_token.balance_of(ITIP20::balanceOfCall { account: alice })?;
4367
4368 exchange
4369 .place(alice, base_token, order_amount, true, tick)
4370 .expect("Order should succeed");
4371
4372 let alice_balance_after_place =
4373 tip20_quote_token.balance_of(ITIP20::balanceOfCall { account: alice })?;
4374 let escrowed = alice_initial_balance - alice_balance_after_place;
4375 assert_eq!(escrowed, U256::from(100010000u128));
4376
4377 exchange
4378 .set_balance(bob, base_token, 200_000_000u128)
4379 .expect("Could not set balance");
4380
4381 exchange
4382 .swap_exact_amount_out(bob, base_token, quote_token, 100009999, u128::MAX)
4383 .expect("Swap should succeed");
4384
4385 Ok(())
4386 })
4387 }
4388
4389 #[test]
4390 fn test_stablecoin_dex_address_returns_correct_precompile() -> eyre::Result<()> {
4391 let mut storage = HashMapStorageProvider::new(1);
4392 StorageCtx::enter(&mut storage, || {
4393 let exchange = StablecoinDEX::new();
4394 assert_eq!(exchange.address(), STABLECOIN_DEX_ADDRESS);
4395 Ok(())
4396 })
4397 }
4398
4399 #[test]
4400 fn test_stablecoin_dex_initialize_sets_storage_state() -> eyre::Result<()> {
4401 let mut storage = HashMapStorageProvider::new(1);
4402 StorageCtx::enter(&mut storage, || {
4403 let mut exchange = StablecoinDEX::new();
4404
4405 assert!(!exchange.is_initialized()?);
4407
4408 exchange.initialize()?;
4410
4411 assert!(exchange.is_initialized()?);
4413
4414 let exchange2 = StablecoinDEX::new();
4416 assert!(exchange2.is_initialized()?);
4417
4418 Ok(())
4419 })
4420 }
4421
4422 #[test]
4423 fn test_get_order_validates_maker_and_order_id() -> eyre::Result<()> {
4424 let mut storage = HashMapStorageProvider::new(1);
4425 StorageCtx::enter(&mut storage, || {
4426 let mut exchange = StablecoinDEX::new();
4427 exchange.initialize()?;
4428
4429 let admin = Address::random();
4430 let alice = Address::random();
4431 let min_order_amount = MIN_ORDER_AMOUNT;
4432 let tick = 100i16;
4433
4434 let price = orderbook::tick_to_price(tick);
4435 let escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
4436
4437 let (base_token, _quote_token) =
4438 setup_test_tokens(admin, alice, exchange.address, escrow)?;
4439 exchange.create_pair(base_token)?;
4440
4441 let order_id = exchange.place(alice, base_token, min_order_amount, true, tick)?;
4442
4443 let order = exchange.get_order(order_id)?;
4445 assert_eq!(order.maker(), alice);
4446 assert!(!order.maker().is_zero());
4447 assert!(order.order_id() < exchange.next_order_id()?);
4448
4449 let result = exchange.get_order(999);
4451 assert!(result.is_err());
4452 assert_eq!(
4453 result.unwrap_err(),
4454 StablecoinDEXError::order_does_not_exist().into()
4455 );
4456
4457 let next_id = exchange.next_order_id()?;
4459 let result = exchange.get_order(next_id);
4460 assert!(result.is_err());
4461 assert_eq!(
4462 result.unwrap_err(),
4463 StablecoinDEXError::order_does_not_exist().into()
4464 );
4465
4466 Ok(())
4467 })
4468 }
4469
4470 struct FlipOrderTestCtx {
4472 exchange: StablecoinDEX,
4473 alice: Address,
4474 bob: Address,
4475 admin: Address,
4476 base_token: Address,
4477 quote_token: Address,
4478 book_key: B256,
4479 amount: u128,
4480 flip_tick: i16,
4481 }
4482
4483 fn setup_flip_order_test() -> eyre::Result<FlipOrderTestCtx> {
4485 let mut exchange = StablecoinDEX::new();
4486 exchange.initialize()?;
4487
4488 let alice = Address::random();
4489 let bob = Address::random();
4490 let admin = Address::random();
4491 let amount = MIN_ORDER_AMOUNT;
4492 let tick = 100i16;
4493 let flip_tick = 200i16;
4494
4495 let price = orderbook::tick_to_price(tick);
4496 let expected_escrow = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
4497
4498 let (base_token, quote_token) =
4499 setup_test_tokens(admin, alice, exchange.address, expected_escrow * 2)?;
4500 exchange.create_pair(base_token)?;
4501
4502 let book_key = compute_book_key(base_token, quote_token);
4503
4504 exchange.place_flip(alice, base_token, amount, true, tick, flip_tick, false)?;
4506
4507 Ok(FlipOrderTestCtx {
4508 exchange,
4509 alice,
4510 bob,
4511 admin,
4512 base_token,
4513 quote_token,
4514 book_key,
4515 amount,
4516 flip_tick,
4517 })
4518 }
4519
4520 #[test]
4521 fn test_flip_order_fill_ignores_business_logic_error() -> eyre::Result<()> {
4522 for spec in [TempoHardfork::T1, TempoHardfork::T1A, TempoHardfork::T2] {
4524 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
4525 StorageCtx::enter(&mut storage, || {
4526 let FlipOrderTestCtx {
4527 mut exchange,
4528 alice,
4529 bob,
4530 admin,
4531 base_token,
4532 quote_token,
4533 book_key,
4534 amount,
4535 flip_tick,
4536 } = setup_flip_order_test()?;
4537
4538 let mut registry = TIP403Registry::new();
4542 let policy_id = registry.create_policy(
4543 admin,
4544 ITIP403Registry::createPolicyCall {
4545 admin,
4546 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4547 },
4548 )?;
4549
4550 let mut base = TIP20Token::from_address(base_token)?;
4551 base.change_transfer_policy_id(
4552 admin,
4553 ITIP20::changeTransferPolicyIdCall {
4554 newPolicyId: policy_id,
4555 },
4556 )?;
4557
4558 registry.modify_policy_blacklist(
4559 admin,
4560 ITIP403Registry::modifyPolicyBlacklistCall {
4561 policyId: policy_id,
4562 account: alice,
4563 restricted: true,
4564 },
4565 )?;
4566
4567 exchange.set_balance(bob, base_token, amount)?;
4569
4570 let result = exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0);
4572 assert!(
4573 result.is_ok(),
4574 "[{spec:?}] Swap should succeed when flip hits a business logic error"
4575 );
4576
4577 assert_eq!(exchange.balance_of(alice, base_token)?, amount);
4579
4580 let level = exchange.books[book_key]
4582 .tick_level_handler(flip_tick, false)
4583 .read()?;
4584 assert_eq!(
4585 level.total_liquidity, 0,
4586 "[{spec:?}] No flipped order should exist"
4587 );
4588
4589 Ok::<_, eyre::Report>(())
4590 })?;
4591 }
4592 Ok(())
4593 }
4594
4595 #[test]
4596 fn test_flip_order_fill_reverts_on_system_error_post_t1a() -> eyre::Result<()> {
4597 for spec in [TempoHardfork::T1, TempoHardfork::T1A, TempoHardfork::T2] {
4599 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
4600 StorageCtx::enter(&mut storage, || {
4601 let FlipOrderTestCtx {
4602 mut exchange,
4603 alice,
4604 bob,
4605 base_token,
4606 quote_token,
4607 book_key,
4608 amount,
4609 flip_tick,
4610 ..
4611 } = setup_flip_order_test()?;
4612
4613 let alice_quote_before = exchange.balance_of(alice, quote_token)?;
4614
4615 let poisoned_level = TickLevel::with_values(0, 0, u128::MAX);
4617 exchange.books[book_key]
4618 .tick_level_handler_mut(flip_tick, false)
4619 .write(poisoned_level)?;
4620
4621 exchange.set_balance(bob, base_token, amount)?;
4623
4624 let result = exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0);
4625
4626 if spec.is_t1a() {
4627 assert!(
4629 result.is_err(),
4630 "Swap should revert when flip hits a system error"
4631 );
4632 assert!(
4633 result.unwrap_err().is_system_error(),
4634 "Error must be classified as a system error",
4635 );
4636
4637 let alice_quote_after = exchange.balance_of(alice, quote_token)?;
4639 assert_eq!(alice_quote_before, alice_quote_after);
4640 } else {
4641 assert!(
4643 result.is_ok(),
4644 "[{spec:?}] Swap should succeed when system error is pre-T1A"
4645 );
4646 }
4647
4648 Ok::<_, eyre::Report>(())
4649 })?;
4650 }
4651 Ok(())
4652 }
4653
4654 #[test]
4655 fn test_orderbook_invariants_after_all_orders_filled() -> eyre::Result<()> {
4656 let mut storage = HashMapStorageProvider::new(1);
4657 StorageCtx::enter(&mut storage, || {
4658 let mut exchange = StablecoinDEX::new();
4659 exchange.initialize()?;
4660
4661 assert_eq!(exchange.next_order_id()?, 1);
4663
4664 let alice = Address::random();
4665 let bob = Address::random();
4666 let admin = Address::random();
4667 let amount = MIN_ORDER_AMOUNT;
4668 let tick = 100i16;
4669
4670 let price = orderbook::tick_to_price(tick) as u128;
4671 let quote_amount = (amount * price).div_ceil(orderbook::PRICE_SCALE as u128);
4672
4673 let base = TIP20Setup::create("BASE", "BASE", admin)
4674 .with_issuer(admin)
4675 .with_mint(alice, U256::from(amount * 4))
4676 .with_mint(bob, U256::from(amount * 4))
4677 .with_approval(alice, exchange.address, U256::MAX)
4678 .with_approval(bob, exchange.address, U256::MAX)
4679 .apply()?;
4680 let base_token = base.address();
4681 let quote_token = base.quote_token()?;
4682
4683 TIP20Setup::path_usd(admin)
4684 .with_issuer(admin)
4685 .with_mint(alice, U256::from(quote_amount * 4))
4686 .with_mint(bob, U256::from(quote_amount * 4))
4687 .with_approval(alice, exchange.address, U256::MAX)
4688 .with_approval(bob, exchange.address, U256::MAX)
4689 .apply()?;
4690
4691 let book_key = compute_book_key(base_token, quote_token);
4692 exchange.create_pair(base_token)?;
4693
4694 let bid_id = exchange.place(alice, base_token, amount, true, tick)?;
4696 assert_eq!(bid_id, 1);
4697 let ask_id = exchange.place(bob, base_token, amount, false, tick)?;
4698 assert_eq!(ask_id, 2);
4699
4700 let book = exchange.books[book_key].read()?;
4702 assert_eq!(book.best_bid_tick, tick);
4703 assert_eq!(book.best_ask_tick, tick);
4704
4705 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
4707
4708 exchange.swap_exact_amount_in(alice, quote_token, base_token, quote_amount, 0)?;
4710
4711 let book = exchange.books[book_key].read()?;
4713 assert_eq!(
4714 book.best_bid_tick,
4715 i16::MIN,
4716 "best_bid_tick must be sentinel after all bids filled"
4717 );
4718 assert_eq!(
4719 book.best_ask_tick,
4720 i16::MAX,
4721 "best_ask_tick must be sentinel after all asks filled"
4722 );
4723
4724 let bid_level = exchange.books[book_key]
4726 .tick_level_handler(tick, true)
4727 .read()?;
4728 assert_eq!(bid_level.head, 0, "bid level head must be 0 after drain");
4729 assert_eq!(bid_level.tail, 0, "bid level tail must be 0 after drain");
4730 assert_eq!(
4731 bid_level.total_liquidity, 0,
4732 "bid level liquidity must be 0 after drain"
4733 );
4734
4735 let ask_level = exchange.books[book_key]
4736 .tick_level_handler(tick, false)
4737 .read()?;
4738 assert_eq!(ask_level.head, 0, "ask level head must be 0 after drain");
4739 assert_eq!(ask_level.tail, 0, "ask level tail must be 0 after drain");
4740 assert_eq!(
4741 ask_level.total_liquidity, 0,
4742 "ask level liquidity must be 0 after drain"
4743 );
4744
4745 assert_eq!(
4747 exchange.next_order_id()?,
4748 3,
4749 "next_order_id must remain monotonic after drain"
4750 );
4751
4752 let result = exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0);
4755 assert_eq!(
4756 result,
4757 Err(StablecoinDEXError::insufficient_liquidity().into()),
4758 "swap against drained bid side must fail"
4759 );
4760 let result =
4762 exchange.swap_exact_amount_in(alice, quote_token, base_token, quote_amount, 0);
4763 assert_eq!(
4764 result,
4765 Err(StablecoinDEXError::insufficient_liquidity().into()),
4766 "swap against drained ask side must fail"
4767 );
4768
4769 Ok(())
4770 })
4771 }
4772
4773 #[test]
4774 fn test_orderbook_invariants_after_all_orders_cancelled() -> eyre::Result<()> {
4775 let mut storage = HashMapStorageProvider::new(1);
4776 StorageCtx::enter(&mut storage, || {
4777 let mut exchange = StablecoinDEX::new();
4778 exchange.initialize()?;
4779
4780 let alice = Address::random();
4781 let admin = Address::random();
4782 let amount = MIN_ORDER_AMOUNT;
4783 let tick = 100i16;
4784
4785 let price = orderbook::tick_to_price(tick) as u128;
4786 let quote_amount = (amount * price).div_ceil(orderbook::PRICE_SCALE as u128);
4787
4788 let base = TIP20Setup::create("BASE", "BASE", admin)
4789 .with_issuer(admin)
4790 .with_mint(alice, U256::from(amount * 2))
4791 .with_approval(alice, exchange.address, U256::MAX)
4792 .apply()?;
4793 let base_token = base.address();
4794 let quote_token = base.quote_token()?;
4795
4796 TIP20Setup::path_usd(admin)
4797 .with_issuer(admin)
4798 .with_mint(alice, U256::from(quote_amount * 2))
4799 .with_approval(alice, exchange.address, U256::MAX)
4800 .apply()?;
4801
4802 let book_key = compute_book_key(base_token, quote_token);
4803 exchange.create_pair(base_token)?;
4804
4805 let bid_id = exchange.place(alice, base_token, amount, true, tick)?;
4807 let ask_id = exchange.place(alice, base_token, amount, false, tick)?;
4808
4809 exchange.cancel(alice, bid_id)?;
4811 exchange.cancel(alice, ask_id)?;
4812
4813 let book = exchange.books[book_key].read()?;
4815 assert_eq!(
4816 book.best_bid_tick,
4817 i16::MIN,
4818 "best_bid_tick must be sentinel after all bids cancelled"
4819 );
4820 assert_eq!(
4821 book.best_ask_tick,
4822 i16::MAX,
4823 "best_ask_tick must be sentinel after all asks cancelled"
4824 );
4825
4826 let bid_level = exchange.books[book_key]
4828 .tick_level_handler(tick, true)
4829 .read()?;
4830 assert_eq!(bid_level.head, 0, "bid level head must be 0");
4831 assert_eq!(bid_level.tail, 0, "bid level tail must be 0");
4832 assert_eq!(bid_level.total_liquidity, 0, "bid liquidity must be 0");
4833
4834 let ask_level = exchange.books[book_key]
4835 .tick_level_handler(tick, false)
4836 .read()?;
4837 assert_eq!(ask_level.head, 0, "ask level head must be 0");
4838 assert_eq!(ask_level.tail, 0, "ask level tail must be 0");
4839 assert_eq!(ask_level.total_liquidity, 0, "ask liquidity must be 0");
4840
4841 let result = exchange.swap_exact_amount_in(alice, base_token, quote_token, amount, 0);
4843 assert_eq!(
4844 result,
4845 Err(StablecoinDEXError::insufficient_liquidity().into()),
4846 "swap against cancelled book must fail"
4847 );
4848
4849 Ok(())
4850 })
4851 }
4852
4853 #[test]
4854 fn test_sub_balance_errors_on_underflow() -> eyre::Result<()> {
4855 let mut storage = HashMapStorageProvider::new(1);
4856 StorageCtx::enter(&mut storage, || {
4857 let mut exchange = StablecoinDEX::new();
4858 exchange.initialize()?;
4859
4860 let user = Address::random();
4861 let admin = Address::random();
4862
4863 let base = TIP20Setup::create("BASE", "BASE", admin)
4864 .with_issuer(admin)
4865 .apply()?;
4866 let token = base.address();
4867
4868 exchange.set_balance(user, token, 100)?;
4870 assert_eq!(exchange.balance_of(user, token)?, 100);
4871
4872 let result = exchange.sub_balance(user, token, 101);
4874 assert_eq!(
4875 result,
4876 Err(TempoPrecompileError::under_overflow()),
4877 "sub_balance should error on underflow instead of saturating"
4878 );
4879
4880 assert_eq!(exchange.balance_of(user, token)?, 100);
4882
4883 Ok(())
4884 })
4885 }
4886
4887 #[test]
4888 fn test_flip_checkpoint_reverts_partial_state_post_t1c() -> eyre::Result<()> {
4889 for spec in [TempoHardfork::T1A, TempoHardfork::T1C] {
4895 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
4896 StorageCtx::enter(&mut storage, || {
4897 let FlipOrderTestCtx {
4898 mut exchange,
4899 alice,
4900 bob,
4901 base_token,
4902 quote_token,
4903 book_key,
4904 amount,
4905 flip_tick,
4906 ..
4907 } = setup_flip_order_test()?;
4908
4909 let next_id_before = exchange.next_order_id()?;
4910
4911 let poisoned = TickLevel::with_values(0, 0, u128::MAX);
4914 exchange.books[book_key]
4915 .tick_level_handler_mut(flip_tick, false)
4916 .write(poisoned)?;
4917
4918 exchange.set_balance(bob, base_token, amount)?;
4920
4921 let result = exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0);
4922 assert!(result.is_err(), "[{spec:?}] swap should fail");
4923
4924 let alice_base = exchange.balance_of(alice, base_token)?;
4928 let next_id_after = exchange.next_order_id()?;
4929
4930 if spec.is_t1c() {
4931 assert_eq!(alice_base, amount);
4933 assert_eq!(next_id_after, next_id_before);
4934 } else {
4935 assert_eq!(alice_base, 0);
4937 assert_eq!(next_id_after, next_id_before + 1);
4938 }
4939
4940 assert!(
4942 exchange.emitted_events().last().is_some_and(
4943 |e| e.topics()[0] != IStablecoinDEX::OrderPlaced::SIGNATURE_HASH
4944 )
4945 );
4946
4947 Ok::<_, eyre::Report>(())
4948 })?;
4949 }
4950 Ok(())
4951 }
4952}