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, 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;
32use tempo_primitives::TempoAddressExt;
33
34pub const MIN_ORDER_AMOUNT: u128 = 100_000_000;
36
37pub const TICK_SPACING: i16 = 10;
39
40#[contract(addr = STABLECOIN_DEX_ADDRESS)]
48pub struct StablecoinDEX {
49 books: Mapping<B256, Orderbook>,
50 orders: Mapping<u128, Order>,
51 balances: Mapping<Address, Mapping<Address, u128>>,
52 next_order_id: u128,
53 book_keys: Vec<B256>,
54}
55
56impl StablecoinDEX {
57 pub fn address(&self) -> Address {
59 self.address
60 }
61
62 pub fn initialize(&mut self) -> Result<()> {
64 self.__initialize()
66 }
67
68 fn next_order_id(&self) -> Result<u128> {
70 Ok(self.next_order_id.read()?.max(1))
71 }
72
73 fn increment_next_order_id(&mut self) -> Result<()> {
75 let next_order_id = self.next_order_id()?;
76 self.next_order_id.write(next_order_id + 1)
77 }
78
79 pub fn balance_of(&self, user: Address, token: Address) -> Result<u128> {
81 self.balances[user][token].read()
82 }
83
84 pub fn min_price(&self) -> u32 {
86 MIN_PRICE
87 }
88
89 pub fn max_price(&self) -> u32 {
91 MAX_PRICE
92 }
93
94 fn validate_or_create_pair(&mut self, book: &Orderbook, token: Address) -> Result<()> {
96 if !book.is_initialized() {
97 self.create_pair(token)?;
98 }
99 Ok(())
100 }
101
102 pub fn get_order(&self, order_id: u128) -> Result<Order> {
108 let order = self.orders[order_id].read()?;
109
110 if !order.maker().is_zero() && order.order_id() < self.next_order_id()? {
112 Ok(order)
113 } else {
114 Err(StablecoinDEXError::order_does_not_exist().into())
115 }
116 }
117
118 fn set_balance(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
120 self.balances[user][token].write(amount)
121 }
122
123 fn increment_balance(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
125 let current = self.balance_of(user, token)?;
126 self.set_balance(
127 user,
128 token,
129 current
130 .checked_add(amount)
131 .ok_or(TempoPrecompileError::under_overflow())?,
132 )
133 }
134
135 fn sub_balance(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
137 let current = self.balance_of(user, token)?;
138 self.set_balance(
139 user,
140 token,
141 current
142 .checked_sub(amount)
143 .ok_or(TempoPrecompileError::under_overflow())?,
144 )
145 }
146
147 fn emit_order_filled(
149 &mut self,
150 order_id: u128,
151 maker: Address,
152 taker: Address,
153 amount_filled: u128,
154 partial_fill: bool,
155 ) -> Result<()> {
156 self.emit_event(StablecoinDEXEvents::order_filled(
157 order_id,
158 maker,
159 taker,
160 amount_filled,
161 partial_fill,
162 ))?;
163
164 Ok(())
165 }
166
167 fn transfer(&mut self, token: Address, to: Address, amount: u128) -> Result<()> {
169 TIP20Token::from_address(token)?.transfer(
170 self.address,
171 ITIP20::transferCall {
172 to,
173 amount: U256::from(amount),
174 },
175 )?;
176 Ok(())
177 }
178
179 fn transfer_from(&mut self, token: Address, sender: Address, amount: u128) -> Result<()> {
181 if self.storage.spec().is_t5() {
182 TIP20Token::from_address(token)?.system_transfer_from(
183 self.address,
184 sender,
185 U256::from(amount),
186 )?;
187 } else {
188 TIP20Token::from_address(token)?.transfer_from(
189 self.address,
190 ITIP20::transferFromCall {
191 from: sender,
192 to: self.address,
193 amount: U256::from(amount),
194 },
195 )?;
196 }
197 Ok(())
198 }
199
200 fn decrement_balance_or_transfer_from(
207 &mut self,
208 sender: Address,
209 token: Address,
210 amount: u128,
211 check_pause: bool,
212 ) -> Result<()> {
213 let tip20 = TIP20Token::from_address(token)?;
215 tip20.ensure_transfer_authorized(sender, self.address)?;
216
217 let user_balance = self.balance_of(sender, token)?;
218 if user_balance >= amount {
219 if check_pause && self.storage.spec().is_t4() {
222 tip20.check_not_paused()?;
223 }
224 self.sub_balance(sender, token, amount)
225 } else {
226 let remaining = amount
227 .checked_sub(user_balance)
228 .ok_or(TempoPrecompileError::under_overflow())?;
229
230 self.transfer_from(token, sender, remaining)?;
231 self.set_balance(sender, token, 0)
232 }
233 }
234
235 pub fn quote_swap_exact_amount_out(
244 &self,
245 token_in: Address,
246 token_out: Address,
247 amount_out: u128,
248 ) -> Result<u128> {
249 let route = self.find_trade_path(token_in, token_out)?;
251
252 let mut current_amount = amount_out;
254 for (book_key, base_for_quote) in route.iter().rev() {
255 current_amount = self.quote_exact_out(*book_key, current_amount, *base_for_quote)?;
256 }
257
258 Ok(current_amount)
259 }
260
261 pub fn quote_swap_exact_amount_in(
270 &self,
271 token_in: Address,
272 token_out: Address,
273 amount_in: u128,
274 ) -> Result<u128> {
275 let route = self.find_trade_path(token_in, token_out)?;
277
278 let mut current_amount = amount_in;
280 for (book_key, base_for_quote) in route {
281 current_amount = self.quote_exact_in(book_key, current_amount, base_for_quote)?;
282 }
283
284 Ok(current_amount)
285 }
286
287 pub fn swap_exact_amount_in(
297 &mut self,
298 sender: Address,
299 token_in: Address,
300 token_out: Address,
301 amount_in: u128,
302 min_amount_out: u128,
303 ) -> Result<u128> {
304 let route = self.find_trade_path(token_in, token_out)?;
306
307 self.decrement_balance_or_transfer_from(sender, token_in, amount_in, false)?;
310
311 let mut amount = amount_in;
313 for (book_key, base_for_quote) in route {
314 amount = self.fill_orders_exact_in(book_key, base_for_quote, amount, sender)?;
316 }
317
318 if amount < min_amount_out {
320 return Err(StablecoinDEXError::insufficient_output().into());
321 }
322
323 self.transfer(token_out, sender, amount)?;
324
325 Ok(amount)
326 }
327
328 pub fn swap_exact_amount_out(
338 &mut self,
339 sender: Address,
340 token_in: Address,
341 token_out: Address,
342 amount_out: u128,
343 max_amount_in: u128,
344 ) -> Result<u128> {
345 let route = self.find_trade_path(token_in, token_out)?;
347
348 let mut amount = amount_out;
350 for (book_key, base_for_quote) in route.iter().rev() {
351 amount = self.fill_orders_exact_out(*book_key, *base_for_quote, amount, sender)?;
352 }
353
354 if amount > max_amount_in {
355 return Err(StablecoinDEXError::max_input_exceeded().into());
356 }
357
358 self.decrement_balance_or_transfer_from(sender, token_in, amount, false)?;
361
362 self.transfer(token_out, sender, amount_out)?;
364
365 Ok(amount)
366 }
367
368 pub fn get_price_level(&self, base: Address, tick: i16, is_bid: bool) -> Result<TickLevel> {
374 let quote = TIP20Token::from_address(base)?.quote_token()?;
375 let book_key = compute_book_key(base, quote);
376 if is_bid {
377 self.books[book_key].bids[tick].read()
378 } else {
379 self.books[book_key].asks[tick].read()
380 }
381 }
382
383 pub fn books(&self, pair_key: B256) -> Result<Orderbook> {
385 self.books[pair_key].read()
386 }
387
388 pub fn get_book_keys(&self) -> Result<Vec<B256>> {
390 self.book_keys.read()
391 }
392
393 pub fn tick_to_price(&self, tick: i16) -> Result<u32> {
398 if self.storage.spec().is_t2() {
399 orderbook::validate_tick_spacing(tick)?;
400 }
401
402 Ok(orderbook::tick_to_price(tick))
403 }
404
405 pub fn price_to_tick(&self, price: u32) -> Result<i16> {
411 let tick = orderbook::price_to_tick(price)?;
412
413 if self.storage.spec().is_t2() {
414 orderbook::validate_tick_spacing(tick)?;
415 }
416
417 Ok(tick)
418 }
419
420 pub fn create_pair(&mut self, base: Address) -> Result<B256> {
429 if !TIP20Factory::new().is_tip20(base)? {
431 return Err(StablecoinDEXError::invalid_base_token().into());
432 }
433
434 let quote = TIP20Token::from_address(base)?.quote_token()?;
435 validate_usd_currency(base)?;
436 validate_usd_currency(quote)?;
437
438 let book_key = compute_book_key(base, quote);
439
440 if self.books[book_key].read()?.is_initialized() {
441 return Err(StablecoinDEXError::pair_already_exists().into());
442 }
443
444 let book = Orderbook::new(base, quote);
445 self.books[book_key].write(book)?;
446 self.book_keys.push(book_key)?;
447
448 self.emit_event(StablecoinDEXEvents::pair_created(book_key, base, quote))?;
450
451 Ok(book_key)
452 }
453
454 pub fn place(
469 &mut self,
470 sender: Address,
471 token: Address,
472 amount: u128,
473 is_bid: bool,
474 tick: i16,
475 ) -> Result<u128> {
476 let quote_token = TIP20Token::from_address(token)?.quote_token()?;
477
478 let book_key = compute_book_key(token, quote_token);
480
481 let book = self.books[book_key].read()?;
482 self.validate_or_create_pair(&book, token)?;
483
484 if !(MIN_TICK..=MAX_TICK).contains(&tick) {
486 return Err(StablecoinDEXError::tick_out_of_bounds(tick).into());
487 }
488
489 if tick % TICK_SPACING != 0 {
491 return Err(StablecoinDEXError::invalid_tick().into());
492 }
493
494 if amount < MIN_ORDER_AMOUNT {
496 return Err(StablecoinDEXError::below_minimum_order_size(amount).into());
497 }
498
499 let (escrow_token, escrow_amount, non_escrow_token) = if is_bid {
501 let quote_amount = base_to_quote(amount, tick, RoundingDirection::Up)
503 .ok_or(StablecoinDEXError::insufficient_balance())?;
504 (quote_token, quote_amount, token)
505 } else {
506 (token, amount, quote_token)
508 };
509
510 let non_escrow_tip20 = TIP20Token::from_address(non_escrow_token)?;
513 non_escrow_tip20.ensure_transfer_authorized(self.address, sender)?;
514
515 if self.storage.spec().is_t4() {
519 non_escrow_tip20.check_not_paused()?;
520 }
521
522 self.decrement_balance_or_transfer_from(sender, escrow_token, escrow_amount, true)?;
524
525 let order_id = self.next_order_id()?;
527 self.increment_next_order_id()?;
528 let order = if is_bid {
529 Order::new_bid(order_id, sender, book_key, amount, tick)
530 } else {
531 Order::new_ask(order_id, sender, book_key, amount, tick)
532 };
533 self.commit_order_to_book(order)?;
534
535 self.emit_event(StablecoinDEXEvents::order_placed(
537 order_id, sender, token, amount, is_bid, tick, false, 0,
538 ))?;
539
540 Ok(order_id)
541 }
542
543 fn commit_order_to_book(&mut self, mut order: Order) -> Result<()> {
545 let orderbook = self.books[order.book_key()].read()?;
546 let mut level = self.books[order.book_key()]
547 .tick_level_handler(order.tick(), order.is_bid())
548 .read()?;
549
550 let prev_tail = level.tail;
551 if prev_tail == 0 {
552 level.head = order.order_id();
553 level.tail = order.order_id();
554
555 self.books[order.book_key()].set_tick_bit(order.tick(), order.is_bid())?;
556
557 if order.is_bid() {
558 if order.tick() > orderbook.best_bid_tick {
559 self.books[order.book_key()]
560 .best_bid_tick
561 .write(order.tick())?;
562 }
563 } else if order.tick() < orderbook.best_ask_tick {
564 self.books[order.book_key()]
565 .best_ask_tick
566 .write(order.tick())?;
567 }
568 } else {
569 let mut prev_order = self.orders[prev_tail].read()?;
571 prev_order.next = order.order_id();
572 self.orders[prev_tail].write(prev_order)?;
573
574 order.prev = prev_tail;
576 level.tail = order.order_id();
577 }
578
579 let new_liquidity = level
580 .total_liquidity
581 .checked_add(order.remaining())
582 .ok_or(TempoPrecompileError::under_overflow())?;
583 level.total_liquidity = new_liquidity;
584
585 self.books[order.book_key()]
586 .tick_level_handler_mut(order.tick(), order.is_bid())
587 .write(level)?;
588
589 self.orders[order.order_id()].write(order)
590 }
591
592 #[allow(clippy::too_many_arguments)]
607 pub fn place_flip(
608 &mut self,
609 sender: Address,
610 token: Address,
611 amount: u128,
612 is_bid: bool,
613 tick: i16,
614 flip_tick: i16,
615 internal_balance_only: bool,
616 ) -> Result<u128> {
617 let quote_token = TIP20Token::from_address(token)?.quote_token()?;
618
619 let book_key = compute_book_key(token, quote_token);
621
622 let batch = self.storage.checkpoint();
625
626 let book = self.books[book_key].read()?;
628 self.validate_or_create_pair(&book, token)?;
629
630 if !(MIN_TICK..=MAX_TICK).contains(&tick) {
632 return Err(StablecoinDEXError::tick_out_of_bounds(tick).into());
633 }
634
635 if tick % TICK_SPACING != 0 {
637 return Err(StablecoinDEXError::invalid_tick().into());
638 }
639
640 if !(MIN_TICK..=MAX_TICK).contains(&flip_tick) {
641 return Err(StablecoinDEXError::tick_out_of_bounds(flip_tick).into());
642 }
643
644 if flip_tick % TICK_SPACING != 0 {
646 return Err(StablecoinDEXError::invalid_flip_tick().into());
647 }
648
649 if (flip_tick == tick && !self.storage.spec().is_t5())
655 || (is_bid && flip_tick < tick)
656 || (!is_bid && flip_tick > tick)
657 {
658 return Err(StablecoinDEXError::invalid_flip_tick().into());
659 }
660
661 if amount < MIN_ORDER_AMOUNT {
663 return Err(StablecoinDEXError::below_minimum_order_size(amount).into());
664 }
665
666 let (escrow_token, escrow_amount, non_escrow_token) = if is_bid {
668 let quote_amount = base_to_quote(amount, tick, RoundingDirection::Up)
670 .ok_or(StablecoinDEXError::insufficient_balance())?;
671 (quote_token, quote_amount, token)
672 } else {
673 (token, amount, quote_token)
675 };
676
677 let non_escrow_tip20 = TIP20Token::from_address(non_escrow_token)?;
680 non_escrow_tip20.ensure_transfer_authorized(self.address, sender)?;
681
682 if self.storage.spec().is_t4() {
686 non_escrow_tip20.check_not_paused()?;
687 }
688
689 if internal_balance_only {
692 let tip20 = TIP20Token::from_address(escrow_token)?;
693 tip20.ensure_transfer_authorized(sender, self.address)?;
694 if self.storage.spec().is_t4() {
697 tip20.check_not_paused()?;
698 }
699 let user_balance = self.balance_of(sender, escrow_token)?;
700 if user_balance < escrow_amount {
701 return Err(StablecoinDEXError::insufficient_balance().into());
702 }
703 self.sub_balance(sender, escrow_token, escrow_amount)?;
704 } else {
705 self.decrement_balance_or_transfer_from(sender, escrow_token, escrow_amount, true)?;
706 }
707
708 let order_id = self.next_order_id()?;
710 let order = Order::new_flip(
711 order_id,
712 sender,
713 book_key,
714 amount,
715 tick,
716 is_bid,
717 flip_tick,
718 self.storage.spec(),
719 )
720 .map_err(|_| StablecoinDEXError::invalid_flip_tick())?;
721
722 if self.storage.spec().is_t1c() {
724 self.next_order_id.write(order_id + 1)?;
726 } else {
727 self.increment_next_order_id()?;
728 }
729 self.commit_order_to_book(order)?;
730
731 self.emit_event(StablecoinDEXEvents::order_placed(
733 order_id, sender, token, amount, is_bid, tick, true, flip_tick,
734 ))?;
735
736 batch.commit();
738
739 Ok(order_id)
740 }
741
742 fn flip_in_place(
743 &mut self,
744 order: &Order,
745 base_token: Address,
746 quote_token: Address,
747 ) -> Result<()> {
748 let batch = self.storage.checkpoint();
751
752 let flipped = order.create_flipped_order(order.order_id);
754
755 let (escrow_token, escrow_amount, non_escrow_token) = if flipped.is_bid {
757 let quote_amount = base_to_quote(flipped.amount, flipped.tick, RoundingDirection::Up)
759 .ok_or(StablecoinDEXError::insufficient_balance())?;
760 (quote_token, quote_amount, base_token)
761 } else {
762 (base_token, flipped.amount, quote_token)
764 };
765
766 let user_balance = self.balance_of(flipped.maker, escrow_token)?;
767 if user_balance < escrow_amount {
768 return Err(StablecoinDEXError::insufficient_balance().into());
769 }
770
771 let escrow_tip20 = TIP20Token::from_address(escrow_token)?;
774 escrow_tip20.check_not_paused()?;
775 escrow_tip20.ensure_transfer_authorized(flipped.maker, self.address)?;
776
777 let non_escrow_tip20 = TIP20Token::from_address(non_escrow_token)?;
780 non_escrow_tip20.check_not_paused()?;
781 non_escrow_tip20.ensure_transfer_authorized(self.address, flipped.maker)?;
782
783 self.sub_balance(flipped.maker, escrow_token, escrow_amount)?;
784
785 self.commit_order_to_book(flipped)?;
786
787 self.emit_event(StablecoinDEXEvents::OrderFlipped(
789 IStablecoinDEX::OrderFlipped {
790 orderId: flipped.order_id,
791 maker: flipped.maker,
792 token: base_token,
793 amount: flipped.amount,
794 isBid: flipped.is_bid,
795 tick: flipped.tick,
796 flipTick: flipped.flip_tick,
797 },
798 ))?;
799
800 batch.commit();
802
803 Ok(())
804 }
805
806 fn partial_fill_order(
808 &mut self,
809 order: &mut Order,
810 level: &mut TickLevel,
811 fill_amount: u128,
812 taker: Address,
813 ) -> Result<u128> {
814 let orderbook = self.books[order.book_key()].read()?;
815
816 let new_remaining = order.remaining() - fill_amount;
818 self.orders[order.order_id()]
819 .remaining
820 .write(new_remaining)?;
821
822 let quote_amount = base_to_quote(
824 fill_amount,
825 order.tick(),
826 if order.is_bid() {
827 RoundingDirection::Down } else {
829 RoundingDirection::Up },
831 )
832 .ok_or(TempoPrecompileError::under_overflow())?;
833
834 if order.is_bid() {
835 self.increment_balance(order.maker(), orderbook.base, fill_amount)?;
837 } else {
838 self.increment_balance(order.maker(), orderbook.quote, quote_amount)?;
840 }
841
842 let amount_out = if order.is_bid() {
844 quote_amount
845 } else {
846 fill_amount
847 };
848
849 let new_liquidity = level
851 .total_liquidity
852 .checked_sub(fill_amount)
853 .ok_or(TempoPrecompileError::under_overflow())?;
854 level.total_liquidity = new_liquidity;
855
856 self.books[order.book_key()]
857 .tick_level_handler_mut(order.tick(), order.is_bid())
858 .write(*level)?;
859
860 self.emit_order_filled(order.order_id(), order.maker(), taker, fill_amount, true)?;
862
863 Ok(amount_out)
864 }
865
866 fn fill_order(
872 &mut self,
873 book_key: B256,
874 order: &mut Order,
875 mut level: TickLevel,
876 taker: Address,
877 ) -> Result<(u128, Option<(TickLevel, Order)>)> {
878 debug_assert_eq!(order.book_key(), book_key);
879
880 let orderbook = self.books[book_key].read()?;
881 let fill_amount = order.remaining();
882
883 let amount_out = if order.is_bid() {
885 self.increment_balance(order.maker(), orderbook.base, fill_amount)?;
887 base_to_quote(fill_amount, order.tick(), RoundingDirection::Down)
889 .ok_or(TempoPrecompileError::under_overflow())?
890 } else {
891 let quote_amount = base_to_quote(fill_amount, order.tick(), RoundingDirection::Up)
893 .ok_or(TempoPrecompileError::under_overflow())?;
894
895 self.increment_balance(order.maker(), orderbook.quote, quote_amount)?;
896
897 fill_amount
899 };
900
901 self.emit_order_filled(order.order_id(), order.maker(), taker, fill_amount, false)?;
903
904 if order.is_flip() {
905 let res = if self.storage.spec().is_t5() {
910 self.flip_in_place(order, orderbook.base, orderbook.quote)
912 } else {
913 self.place_flip(
914 order.maker(),
915 orderbook.base,
916 order.amount(),
917 !order.is_bid(),
918 order.flip_tick(),
919 order.tick(),
920 true,
921 )
922 .map(|_| ())
923 };
924
925 if let Err(err) = &res {
928 if err.is_system_error() && self.storage.spec().is_t1a() {
929 return Err(res.unwrap_err());
930 }
931
932 if self.storage.spec().is_t5() {
933 self.emit_event(StablecoinDEXEvents::flip_failed(
934 order.order_id(),
935 order.maker(),
936 err.selector(),
937 ))?;
938 }
939 }
940
941 let keep_record = self.storage.spec().is_t5() && res.is_ok();
946 if !keep_record {
947 self.orders[order.order_id()].delete()?;
948 }
949 } else {
950 self.orders[order.order_id()].delete()?;
952 }
953
954 let next_tick_info = if order.next() == 0 {
956 self.books[book_key]
957 .tick_level_handler_mut(order.tick(), order.is_bid())
958 .delete()?;
959 self.books[book_key].delete_tick_bit(order.tick(), order.is_bid())?;
960
961 let (tick, has_liquidity) =
962 self.books[book_key].next_initialized_tick(order.tick(), order.is_bid())?;
963
964 if order.is_bid() {
966 let new_best = if has_liquidity { tick } else { i16::MIN };
967 self.books[book_key].best_bid_tick.write(new_best)?;
968 } else {
969 let new_best = if has_liquidity { tick } else { i16::MAX };
970 self.books[book_key].best_ask_tick.write(new_best)?;
971 }
972
973 if !has_liquidity {
974 None
976 } else {
977 let new_level = self.books[book_key]
978 .tick_level_handler(tick, order.is_bid())
979 .read()?;
980 let new_order = self.orders[new_level.head].read()?;
981
982 Some((new_level, new_order))
983 }
984 } else {
985 level.head = order.next();
987 self.orders[order.next()].prev.delete()?;
988
989 let new_liquidity = level
990 .total_liquidity
991 .checked_sub(fill_amount)
992 .ok_or(TempoPrecompileError::under_overflow())?;
993 level.total_liquidity = new_liquidity;
994
995 self.books[book_key]
996 .tick_level_handler_mut(order.tick(), order.is_bid())
997 .write(level)?;
998
999 let new_order = self.orders[order.next()].read()?;
1000 Some((level, new_order))
1001 };
1002
1003 Ok((amount_out, next_tick_info))
1004 }
1005
1006 fn fill_orders_exact_out(
1008 &mut self,
1009 book_key: B256,
1010 bid: bool,
1011 mut amount_out: u128,
1012 taker: Address,
1013 ) -> Result<u128> {
1014 let mut level = self.get_best_price_level(book_key, bid)?;
1015 let mut order = self.orders[level.head].read()?;
1016
1017 let mut total_amount_in: u128 = 0;
1018
1019 while amount_out > 0 {
1020 let tick = order.tick();
1021
1022 let (fill_amount, amount_in) = if bid {
1023 let base_needed = quote_to_base(amount_out, tick, RoundingDirection::Up)
1026 .ok_or(TempoPrecompileError::under_overflow())?;
1027 let fill_amount = base_needed.min(order.remaining());
1028 (fill_amount, fill_amount)
1029 } else {
1030 let fill_amount = amount_out.min(order.remaining());
1033 let amount_in = base_to_quote(fill_amount, tick, RoundingDirection::Up)
1034 .ok_or(TempoPrecompileError::under_overflow())?;
1035 (fill_amount, amount_in)
1036 };
1037
1038 if fill_amount < order.remaining() {
1039 self.partial_fill_order(&mut order, &mut level, fill_amount, taker)?;
1040 total_amount_in = total_amount_in
1041 .checked_add(amount_in)
1042 .ok_or(TempoPrecompileError::under_overflow())?;
1043 break;
1044 } else {
1045 let (amount_out_received, next_order_info) =
1046 self.fill_order(book_key, &mut order, level, taker)?;
1047 total_amount_in = total_amount_in
1048 .checked_add(amount_in)
1049 .ok_or(TempoPrecompileError::under_overflow())?;
1050
1051 if bid {
1053 let base_needed = quote_to_base(amount_out, tick, RoundingDirection::Up)
1055 .ok_or(TempoPrecompileError::under_overflow())?;
1056 if base_needed > order.remaining() {
1057 amount_out = amount_out
1058 .checked_sub(amount_out_received)
1059 .ok_or(TempoPrecompileError::under_overflow())?;
1060 } else {
1061 amount_out = 0;
1062 }
1063 } else if amount_out > order.remaining() {
1064 amount_out = amount_out
1065 .checked_sub(amount_out_received)
1066 .ok_or(TempoPrecompileError::under_overflow())?;
1067 } else {
1068 amount_out = 0;
1069 }
1070
1071 if let Some((new_level, new_order)) = next_order_info {
1072 level = new_level;
1073 order = new_order;
1074 } else {
1075 if amount_out > 0 {
1076 return Err(StablecoinDEXError::insufficient_liquidity().into());
1077 }
1078 break;
1079 }
1080 }
1081 }
1082
1083 Ok(total_amount_in)
1084 }
1085
1086 fn fill_orders_exact_in(
1088 &mut self,
1089 book_key: B256,
1090 bid: bool,
1091 mut amount_in: u128,
1092 taker: Address,
1093 ) -> Result<u128> {
1094 let mut level = self.get_best_price_level(book_key, bid)?;
1095 let mut order = self.orders[level.head].read()?;
1096
1097 let mut total_amount_out: u128 = 0;
1098
1099 while amount_in > 0 {
1100 let tick = order.tick();
1101
1102 let fill_amount = if bid {
1103 amount_in.min(order.remaining())
1105 } else {
1106 let base_out = quote_to_base(amount_in, tick, RoundingDirection::Down)
1109 .ok_or(TempoPrecompileError::under_overflow())?;
1110 base_out.min(order.remaining())
1111 };
1112
1113 if fill_amount < order.remaining() {
1114 let amount_out =
1115 self.partial_fill_order(&mut order, &mut level, fill_amount, taker)?;
1116 total_amount_out = total_amount_out
1117 .checked_add(amount_out)
1118 .ok_or(TempoPrecompileError::under_overflow())?;
1119 break;
1120 } else {
1121 let (amount_out, next_order_info) =
1122 self.fill_order(book_key, &mut order, level, taker)?;
1123 total_amount_out = total_amount_out
1124 .checked_add(amount_out)
1125 .ok_or(TempoPrecompileError::under_overflow())?;
1126
1127 if bid {
1129 if amount_in > order.remaining() {
1130 amount_in = amount_in
1131 .checked_sub(order.remaining())
1132 .ok_or(TempoPrecompileError::under_overflow())?;
1133 } else {
1134 amount_in = 0;
1135 }
1136 } else {
1137 let base_out = quote_to_base(amount_in, tick, RoundingDirection::Down)
1139 .ok_or(TempoPrecompileError::under_overflow())?;
1140 if base_out > order.remaining() {
1141 let quote_needed =
1143 base_to_quote(order.remaining(), tick, RoundingDirection::Up)
1144 .ok_or(TempoPrecompileError::under_overflow())?;
1145 amount_in = amount_in
1146 .checked_sub(quote_needed)
1147 .ok_or(TempoPrecompileError::under_overflow())?;
1148 } else {
1149 amount_in = 0;
1150 }
1151 }
1152
1153 if let Some((new_level, new_order)) = next_order_info {
1154 level = new_level;
1155 order = new_order;
1156 } else {
1157 if amount_in > 0 {
1158 return Err(StablecoinDEXError::insufficient_liquidity().into());
1159 }
1160 break;
1161 }
1162 }
1163 }
1164
1165 Ok(total_amount_out)
1166 }
1167
1168 fn get_best_price_level(&mut self, book_key: B256, is_bid: bool) -> Result<TickLevel> {
1170 let orderbook = self.books[book_key].read()?;
1171
1172 let current_tick = if is_bid {
1173 if orderbook.best_bid_tick == i16::MIN {
1174 return Err(StablecoinDEXError::insufficient_liquidity().into());
1175 }
1176 orderbook.best_bid_tick
1177 } else {
1178 if orderbook.best_ask_tick == i16::MAX {
1179 return Err(StablecoinDEXError::insufficient_liquidity().into());
1180 }
1181 orderbook.best_ask_tick
1182 };
1183
1184 self.books[book_key]
1185 .tick_level_handler(current_tick, is_bid)
1186 .read()
1187 }
1188
1189 pub fn cancel(&mut self, sender: Address, order_id: u128) -> Result<()> {
1196 let order = self.orders[order_id].read()?;
1197
1198 if order.maker().is_zero() {
1199 return Err(StablecoinDEXError::order_does_not_exist().into());
1200 }
1201
1202 if order.maker() != sender {
1203 return Err(StablecoinDEXError::unauthorized().into());
1204 }
1205
1206 if order.remaining() == 0 {
1207 return Err(StablecoinDEXError::order_does_not_exist().into());
1208 }
1209
1210 self.cancel_active_order(order)
1211 }
1212
1213 fn cancel_active_order(&mut self, order: Order) -> Result<()> {
1215 let mut level = self.books[order.book_key()]
1216 .tick_level_handler(order.tick(), order.is_bid())
1217 .read()?;
1218
1219 if order.prev() != 0 {
1221 self.orders[order.prev()].next.write(order.next())?;
1222 } else {
1223 level.head = order.next();
1224 }
1225
1226 if order.next() != 0 {
1227 self.orders[order.next()].prev.write(order.prev())?;
1228 } else {
1229 level.tail = order.prev();
1230 }
1231
1232 let new_liquidity = level
1234 .total_liquidity
1235 .checked_sub(order.remaining())
1236 .ok_or(TempoPrecompileError::under_overflow())?;
1237 level.total_liquidity = new_liquidity;
1238
1239 if level.head == 0 {
1241 self.books[order.book_key()].delete_tick_bit(order.tick(), order.is_bid())?;
1242
1243 let orderbook = self.books[order.book_key()].read()?;
1245 let best_tick = if order.is_bid() {
1246 orderbook.best_bid_tick
1247 } else {
1248 orderbook.best_ask_tick
1249 };
1250
1251 if best_tick == order.tick() {
1252 let (next_tick, has_liquidity) = self.books[order.book_key()]
1253 .next_initialized_tick(order.tick(), order.is_bid())?;
1254
1255 if order.is_bid() {
1256 let new_best = if has_liquidity { next_tick } else { i16::MIN };
1257 self.books[order.book_key()].best_bid_tick.write(new_best)?;
1258 } else {
1259 let new_best = if has_liquidity { next_tick } else { i16::MAX };
1260 self.books[order.book_key()].best_ask_tick.write(new_best)?;
1261 }
1262 }
1263 }
1264
1265 self.books[order.book_key()]
1266 .tick_level_handler_mut(order.tick(), order.is_bid())
1267 .write(level)?;
1268
1269 let orderbook = self.books[order.book_key()].read()?;
1271 if order.is_bid() {
1272 let quote_amount =
1275 base_to_quote(order.remaining(), order.tick(), RoundingDirection::Up)
1276 .ok_or(TempoPrecompileError::under_overflow())?;
1277
1278 self.increment_balance(order.maker(), orderbook.quote, quote_amount)?;
1279 } else {
1280 self.increment_balance(order.maker(), orderbook.base, order.remaining())?;
1282 }
1283
1284 self.orders[order.order_id()].delete()?;
1286
1287 self.emit_event(StablecoinDEXEvents::order_cancelled(order.order_id()))
1289 }
1290
1291 pub fn cancel_stale_order(&mut self, order_id: u128) -> Result<()> {
1303 let order = self.orders[order_id].read()?;
1304
1305 if order.maker().is_zero() {
1306 return Err(StablecoinDEXError::order_does_not_exist().into());
1307 }
1308
1309 if self.is_maker_authorized(&order)? {
1310 Err(StablecoinDEXError::order_not_stale().into())
1311 } else {
1312 self.cancel_active_order(order)
1313 }
1314 }
1315
1316 fn is_maker_authorized(&self, order: &Order) -> Result<bool> {
1321 let book = self.books[order.book_key()].read()?;
1322
1323 let (token_in, token_out) = if order.is_bid() {
1324 (book.quote, book.base)
1325 } else {
1326 (book.base, book.quote)
1327 };
1328
1329 if !is_authorized_for_token(token_in, order.maker(), AuthRole::sender())? {
1330 return Ok(false);
1331 }
1332
1333 if self.storage.spec().is_t4() {
1334 is_authorized_for_token(token_out, order.maker(), AuthRole::recipient())
1335 } else {
1336 Ok(true)
1337 }
1338 }
1339
1340 pub fn withdraw(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
1346 let current_balance = self.balance_of(user, token)?;
1347 if current_balance < amount {
1348 return Err(StablecoinDEXError::insufficient_balance().into());
1349 }
1350 self.sub_balance(user, token, amount)?;
1351 self.transfer(token, user, amount)?;
1352
1353 Ok(())
1354 }
1355
1356 fn quote_exact_out(&self, book_key: B256, amount_out: u128, is_bid: bool) -> Result<u128> {
1358 let mut remaining_out = amount_out;
1359 let mut amount_in = 0u128;
1360 let orderbook = self.books[book_key].read()?;
1361
1362 let mut current_tick = if is_bid {
1363 orderbook.best_bid_tick
1364 } else {
1365 orderbook.best_ask_tick
1366 };
1367 if current_tick == i16::MIN || current_tick == i16::MAX {
1369 return Err(StablecoinDEXError::insufficient_liquidity().into());
1370 }
1371
1372 while remaining_out > 0 {
1373 let level = self.books[book_key]
1374 .tick_level_handler(current_tick, is_bid)
1375 .read()?;
1376
1377 if level.total_liquidity == 0 {
1379 let (next_tick, initialized) =
1380 self.books[book_key].next_initialized_tick(current_tick, is_bid)?;
1381
1382 if !initialized {
1383 return Err(StablecoinDEXError::insufficient_liquidity().into());
1384 }
1385 current_tick = next_tick;
1386 continue;
1387 }
1388
1389 let (fill_amount, amount_in_tick) = if is_bid {
1390 let base_needed = quote_to_base(remaining_out, current_tick, RoundingDirection::Up)
1396 .ok_or(TempoPrecompileError::under_overflow())?;
1397 let fill_amount = if base_needed > level.total_liquidity {
1398 level.total_liquidity
1399 } else {
1400 base_needed
1401 };
1402 (fill_amount, fill_amount)
1403 } else {
1404 let fill_amount = if remaining_out > level.total_liquidity {
1407 level.total_liquidity
1408 } else {
1409 remaining_out
1410 };
1411 let quote_needed = base_to_quote(fill_amount, current_tick, RoundingDirection::Up)
1412 .ok_or(TempoPrecompileError::under_overflow())?;
1413 (fill_amount, quote_needed)
1414 };
1415
1416 let amount_out_tick = if is_bid {
1417 base_to_quote(fill_amount, current_tick, RoundingDirection::Down)
1421 .ok_or(TempoPrecompileError::under_overflow())?
1422 .min(remaining_out)
1423 } else {
1424 fill_amount
1425 };
1426
1427 remaining_out = remaining_out.saturating_sub(amount_out_tick);
1428 amount_in = amount_in
1429 .checked_add(amount_in_tick)
1430 .ok_or(TempoPrecompileError::under_overflow())?;
1431
1432 if fill_amount == level.total_liquidity {
1434 let (next_tick, initialized) =
1435 self.books[book_key].next_initialized_tick(current_tick, is_bid)?;
1436
1437 if !initialized && remaining_out > 0 {
1438 return Err(StablecoinDEXError::insufficient_liquidity().into());
1439 }
1440 current_tick = next_tick;
1441 } else {
1442 break;
1443 }
1444 }
1445
1446 Ok(amount_in)
1447 }
1448
1449 fn find_trade_path(&self, token_in: Address, token_out: Address) -> Result<Vec<(B256, bool)>> {
1453 if token_in == token_out {
1455 return Err(StablecoinDEXError::identical_tokens().into());
1456 }
1457
1458 if !token_in.is_tip20() || !token_out.is_tip20() {
1460 return Err(StablecoinDEXError::invalid_token().into());
1461 }
1462
1463 let in_quote = TIP20Token::from_address(token_in)?.quote_token()?;
1465 let out_quote = TIP20Token::from_address(token_out)?.quote_token()?;
1466
1467 if in_quote == token_out || out_quote == token_in {
1468 return self.validate_and_build_route(&[token_in, token_out]);
1469 }
1470
1471 let path_in = self.find_path_to_root(token_in)?;
1473 let path_out = self.find_path_to_root(token_out)?;
1474
1475 let path_out_set: std::collections::HashSet<Address> = path_out.iter().copied().collect();
1478 let mut lca = None;
1479 for token_a in &path_in {
1480 if path_out_set.contains(token_a) {
1481 lca = Some(*token_a);
1482 break;
1483 }
1484 }
1485
1486 let lca = lca.ok_or_else(StablecoinDEXError::pair_does_not_exist)?;
1487
1488 let mut trade_path = Vec::new();
1490
1491 for token in &path_in {
1493 trade_path.push(*token);
1494 if *token == lca {
1495 break;
1496 }
1497 }
1498
1499 let lca_to_out: Vec<Address> = path_out
1501 .iter()
1502 .take_while(|&&t| t != lca)
1503 .copied()
1504 .collect();
1505
1506 trade_path.extend(lca_to_out.iter().rev());
1508
1509 self.validate_and_build_route(&trade_path)
1510 }
1511
1512 fn validate_and_build_route(&self, path: &[Address]) -> Result<Vec<(B256, bool)>> {
1523 let mut route = Vec::new();
1524
1525 for i in 0..path.len() - 1 {
1526 let token_in = path[i];
1527 let token_out = path[i + 1];
1528
1529 let (base, quote) = {
1530 let token_in_tip20 = TIP20Token::from_address(token_in)?;
1531
1532 if self.storage.spec().is_t3() {
1535 token_in_tip20.check_not_paused()?;
1536 }
1537
1538 if token_in_tip20.quote_token()? == token_out {
1539 (token_in, token_out)
1540 } else {
1541 let token_out_tip20 = TIP20Token::from_address(token_out)?;
1542 if token_out_tip20.quote_token()? == token_in {
1543 (token_out, token_in)
1544 } else {
1545 return Err(StablecoinDEXError::pair_does_not_exist().into());
1546 }
1547 }
1548 };
1549
1550 let book_key = compute_book_key(base, quote);
1551 let orderbook = self.books[book_key].read()?;
1552
1553 if orderbook.base.is_zero() {
1554 return Err(StablecoinDEXError::pair_does_not_exist().into());
1555 }
1556
1557 let is_base_for_quote = token_in == base;
1558 route.push((book_key, is_base_for_quote));
1559 }
1560
1561 Ok(route)
1562 }
1563
1564 fn find_path_to_root(&self, mut token: Address) -> Result<Vec<Address>> {
1567 let mut path = vec![token];
1568
1569 while token != PATH_USD_ADDRESS {
1570 token = TIP20Token::from_address(token)?.quote_token()?;
1571 path.push(token);
1572 }
1573
1574 Ok(path)
1575 }
1576
1577 fn quote_exact_in(&self, book_key: B256, amount_in: u128, is_bid: bool) -> Result<u128> {
1579 let mut remaining_in = amount_in;
1580 let mut amount_out = 0u128;
1581 let orderbook = self.books[book_key].read()?;
1582
1583 let mut current_tick = if is_bid {
1584 orderbook.best_bid_tick
1585 } else {
1586 orderbook.best_ask_tick
1587 };
1588
1589 if current_tick == i16::MIN || current_tick == i16::MAX {
1591 return Err(StablecoinDEXError::insufficient_liquidity().into());
1592 }
1593
1594 while remaining_in > 0 {
1595 let level = self.books[book_key]
1596 .tick_level_handler(current_tick, is_bid)
1597 .read()?;
1598
1599 if level.total_liquidity == 0 {
1601 let (next_tick, initialized) =
1602 self.books[book_key].next_initialized_tick(current_tick, is_bid)?;
1603
1604 if !initialized {
1605 return Err(StablecoinDEXError::insufficient_liquidity().into());
1606 }
1607 current_tick = next_tick;
1608 continue;
1609 }
1610
1611 let (fill_amount, amount_out_tick, amount_consumed) = if is_bid {
1613 let fill = remaining_in.min(level.total_liquidity);
1615 let quote_out = base_to_quote(fill, current_tick, RoundingDirection::Down)
1617 .ok_or(TempoPrecompileError::under_overflow())?;
1618 (fill, quote_out, fill)
1619 } else {
1620 let base_to_get =
1623 quote_to_base(remaining_in, current_tick, RoundingDirection::Down)
1624 .ok_or(TempoPrecompileError::under_overflow())?;
1625 let fill = base_to_get.min(level.total_liquidity);
1626 let quote_consumed = base_to_quote(fill, current_tick, RoundingDirection::Up)
1627 .ok_or(TempoPrecompileError::under_overflow())?;
1628 (fill, fill, quote_consumed)
1629 };
1630
1631 remaining_in = remaining_in
1632 .checked_sub(amount_consumed)
1633 .ok_or(TempoPrecompileError::under_overflow())?;
1634 amount_out = amount_out
1635 .checked_add(amount_out_tick)
1636 .ok_or(TempoPrecompileError::under_overflow())?;
1637
1638 if fill_amount == level.total_liquidity {
1640 let (next_tick, initialized) =
1641 self.books[book_key].next_initialized_tick(current_tick, is_bid)?;
1642
1643 if !initialized && remaining_in > 0 {
1644 return Err(StablecoinDEXError::insufficient_liquidity().into());
1645 }
1646 current_tick = next_tick;
1647 } else {
1648 break;
1649 }
1650 }
1651
1652 Ok(amount_out)
1653 }
1654}
1655
1656fn is_authorized_for_token(token: Address, address: Address, role: AuthRole) -> Result<bool> {
1659 let policy_id = TIP20Token::from_address(token)?.transfer_policy_id()?;
1660 let registry = TIP403Registry::new();
1661 match registry.is_authorized_as(policy_id, address, role) {
1662 Ok(authorized) => Ok(authorized),
1663 Err(e) if is_policy_lookup_error(&e) => Ok(false),
1664 Err(e) => Err(e),
1665 }
1666}
1667
1668#[cfg(test)]
1669mod tests {
1670 use alloy::{
1671 primitives::{FixedBytes, IntoLogData},
1672 sol_types::{SolEvent, SolInterface},
1673 };
1674 use tempo_chainspec::hardfork::TempoHardfork;
1675 use tempo_contracts::precompiles::TIP20Error;
1676
1677 use crate::{
1678 error::TempoPrecompileError,
1679 storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
1680 test_util::TIP20Setup,
1681 tip20::PAUSE_ROLE,
1682 tip403_registry::{ITIP403Registry, TIP403Registry},
1683 };
1684
1685 use super::*;
1686 use crate::STABLECOIN_DEX_ADDRESS;
1687
1688 fn setup_test_tokens(
1689 admin: Address,
1690 user: Address,
1691 exchange_address: Address,
1692 amount: u128,
1693 ) -> Result<(Address, Address)> {
1694 let quote = TIP20Setup::path_usd(admin)
1696 .with_issuer(admin)
1697 .with_mint(user, U256::from(amount))
1698 .with_approval(user, exchange_address, U256::from(amount))
1699 .apply()?;
1700
1701 let base = TIP20Setup::create("BASE", "BASE", admin)
1703 .with_issuer(admin)
1704 .with_mint(user, U256::from(amount))
1705 .with_approval(user, exchange_address, U256::from(amount))
1706 .apply()?;
1707
1708 Ok((base.address(), quote.address()))
1709 }
1710
1711 #[test]
1712 fn test_tick_to_price() {
1713 let test_ticks = [-2000i16, -1000, -100, -1, 0, 1, 100, 1000, 2000];
1714 for tick in test_ticks {
1715 let price = orderbook::tick_to_price(tick);
1716 let expected_price = (orderbook::PRICE_SCALE as i32 + i32::from(tick)) as u32;
1717 assert_eq!(price, expected_price);
1718 }
1719 }
1720
1721 #[test]
1722 fn test_price_to_tick() -> eyre::Result<()> {
1723 let mut storage = HashMapStorageProvider::new(1);
1724 StorageCtx::enter(&mut storage, || {
1725 let exchange = StablecoinDEX::new();
1726
1727 assert_eq!(exchange.price_to_tick(orderbook::PRICE_SCALE)?, 0);
1729 assert_eq!(exchange.price_to_tick(orderbook::MIN_PRICE)?, MIN_TICK);
1730 assert_eq!(exchange.price_to_tick(orderbook::MAX_PRICE)?, MAX_TICK);
1731
1732 let result = exchange.price_to_tick(orderbook::MIN_PRICE - 1);
1734 assert!(result.is_err());
1735 assert!(matches!(
1736 result.unwrap_err(),
1737 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::TickOutOfBounds(_))
1738 ));
1739
1740 let result = exchange.price_to_tick(orderbook::MAX_PRICE + 1);
1741 assert!(result.is_err());
1742 assert!(matches!(
1743 result.unwrap_err(),
1744 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::TickOutOfBounds(_))
1745 ));
1746
1747 Ok(())
1748 })
1749 }
1750
1751 #[test]
1752 fn test_calculate_quote_amount_rounding() -> eyre::Result<()> {
1753 let amount = 100u128;
1758 let tick = 1i16;
1759 let result_floor = base_to_quote(amount, tick, RoundingDirection::Down).unwrap();
1760 assert_eq!(
1761 result_floor, 100,
1762 "Expected 100 (rounded down from 100.001)"
1763 );
1764
1765 let result_ceil = base_to_quote(amount, tick, RoundingDirection::Up).unwrap();
1767 assert_eq!(result_ceil, 101, "Expected 101 (rounded up from 100.001)");
1768
1769 let amount2 = 999u128;
1771 let tick2 = 5i16; let result2_floor = base_to_quote(amount2, tick2, RoundingDirection::Down).unwrap();
1773 assert_eq!(
1775 result2_floor, 999,
1776 "Expected 999 (rounded down from 999.04995)"
1777 );
1778
1779 let result2_ceil = base_to_quote(amount2, tick2, RoundingDirection::Up).unwrap();
1781 assert_eq!(
1782 result2_ceil, 1000,
1783 "Expected 1000 (rounded up from 999.04995)"
1784 );
1785
1786 let amount3 = 100000u128;
1788 let tick3 = 0i16; let result3_floor = base_to_quote(amount3, tick3, RoundingDirection::Down).unwrap();
1790 let result3_ceil = base_to_quote(amount3, tick3, RoundingDirection::Up).unwrap();
1791 assert_eq!(result3_floor, 100000, "Exact division should remain exact");
1793 assert_eq!(result3_ceil, 100000, "Exact division should remain exact");
1794
1795 Ok(())
1796 }
1797
1798 #[test]
1799 fn test_settlement_rounding_favors_protocol() -> eyre::Result<()> {
1800 let mut storage = HashMapStorageProvider::new(1);
1801 StorageCtx::enter(&mut storage, || {
1802 let mut exchange = StablecoinDEX::new();
1803 exchange.initialize()?;
1804
1805 let alice = Address::random();
1806 let bob = Address::random();
1807 let admin = Address::random();
1808
1809 let base_amount = 100_000_003u128;
1811 let tick = 100i16;
1812
1813 let price = u128::from(orderbook::tick_to_price(tick));
1814 let expected_quote_floor = (base_amount * price) / u128::from(orderbook::PRICE_SCALE);
1815 let expected_quote_ceil =
1816 (base_amount * price).div_ceil(u128::from(orderbook::PRICE_SCALE));
1817
1818 let max_escrow = expected_quote_ceil * 2;
1819
1820 let base = TIP20Setup::create("BASE", "BASE", admin)
1821 .with_issuer(admin)
1822 .with_mint(alice, U256::from(base_amount * 2))
1823 .with_mint(bob, U256::from(base_amount * 2))
1824 .with_approval(alice, exchange.address, U256::MAX)
1825 .with_approval(bob, exchange.address, U256::MAX)
1826 .apply()?;
1827 let base_token = base.address();
1828 let quote_token = base.quote_token()?;
1829
1830 TIP20Setup::path_usd(admin)
1831 .with_issuer(admin)
1832 .with_mint(alice, U256::from(max_escrow))
1833 .with_mint(bob, U256::from(max_escrow))
1834 .with_approval(alice, exchange.address, U256::MAX)
1835 .with_approval(bob, exchange.address, U256::MAX)
1836 .apply()?;
1837
1838 exchange.create_pair(base_token)?;
1839
1840 exchange.place(alice, base_token, base_amount, false, tick)?;
1841
1842 let alice_quote_before = exchange.balance_of(alice, quote_token)?;
1843 assert_eq!(alice_quote_before, 0);
1844
1845 exchange.swap_exact_amount_in(bob, quote_token, base_token, expected_quote_ceil, 0)?;
1846
1847 let alice_quote_after = exchange.balance_of(alice, quote_token)?;
1848
1849 assert_eq!(
1851 alice_quote_after, expected_quote_ceil,
1852 "Ask order maker should receive quote rounded UP. Got {alice_quote_after}, expected ceil {expected_quote_ceil}"
1853 );
1854
1855 assert!(
1856 expected_quote_ceil > expected_quote_floor,
1857 "Test setup error: should have a non-zero remainder"
1858 );
1859
1860 Ok(())
1861 })
1862 }
1863
1864 #[test]
1865 fn test_cancellation_refund_equals_escrow_for_bid_orders() -> eyre::Result<()> {
1866 let mut storage = HashMapStorageProvider::new(1);
1867 StorageCtx::enter(&mut storage, || {
1868 let mut exchange = StablecoinDEX::new();
1869 exchange.initialize()?;
1870
1871 let alice = Address::random();
1872 let admin = Address::random();
1873
1874 let base_amount = 100_000_003u128;
1876 let tick = 100i16;
1877
1878 let price = u128::from(orderbook::tick_to_price(tick));
1879 let escrow_ceil = (base_amount * price).div_ceil(u128::from(orderbook::PRICE_SCALE));
1880
1881 let base = TIP20Setup::create("BASE", "BASE", admin)
1882 .with_issuer(admin)
1883 .apply()?;
1884 let base_token = base.address();
1885 let quote_token = base.quote_token()?;
1886
1887 TIP20Setup::path_usd(admin)
1888 .with_issuer(admin)
1889 .with_mint(alice, U256::from(escrow_ceil))
1890 .with_approval(alice, exchange.address, U256::MAX)
1891 .apply()?;
1892
1893 exchange.create_pair(base_token)?;
1894
1895 let order_id = exchange.place(alice, base_token, base_amount, true, tick)?;
1896
1897 let alice_balance_after_place = exchange.balance_of(alice, quote_token)?;
1899 assert_eq!(
1900 alice_balance_after_place, 0,
1901 "All quote tokens should be escrowed"
1902 );
1903
1904 exchange.cancel(alice, order_id)?;
1905
1906 let alice_refund = exchange.balance_of(alice, quote_token)?;
1907
1908 assert_eq!(
1911 alice_refund, escrow_ceil,
1912 "Cancellation refund must equal escrow amount. User escrowed {escrow_ceil} but got back {alice_refund}"
1913 );
1914
1915 Ok(())
1916 })
1917 }
1918
1919 #[test]
1920 fn test_place_order_pair_auto_created() -> eyre::Result<()> {
1921 let mut storage = HashMapStorageProvider::new(1);
1922 StorageCtx::enter(&mut storage, || {
1923 let mut exchange = StablecoinDEX::new();
1924 exchange.initialize()?;
1925
1926 let alice = Address::random();
1927 let admin = Address::random();
1928 let min_order_amount = MIN_ORDER_AMOUNT;
1929 let tick = 100i16;
1930
1931 let price = orderbook::tick_to_price(tick);
1932 let expected_escrow =
1933 (min_order_amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
1934
1935 let (base_token, _quote_token) =
1936 setup_test_tokens(admin, alice, exchange.address, expected_escrow)?;
1937
1938 let result = exchange.place(alice, base_token, min_order_amount, true, tick);
1940 assert!(result.is_ok());
1941
1942 Ok(())
1943 })
1944 }
1945
1946 #[test]
1947 fn test_place_order_below_minimum_amount() -> eyre::Result<()> {
1948 let mut storage = HashMapStorageProvider::new(1);
1949 StorageCtx::enter(&mut storage, || {
1950 let mut exchange = StablecoinDEX::new();
1951 exchange.initialize()?;
1952
1953 let alice = Address::random();
1954 let admin = Address::random();
1955 let min_order_amount = MIN_ORDER_AMOUNT;
1956 let below_minimum = min_order_amount - 1;
1957 let tick = 100i16;
1958
1959 let price = orderbook::tick_to_price(tick);
1960 let escrow_amount =
1961 (below_minimum * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
1962
1963 let (base_token, _quote_token) =
1964 setup_test_tokens(admin, alice, exchange.address, escrow_amount)?;
1965
1966 exchange
1968 .create_pair(base_token)
1969 .expect("Could not create pair");
1970
1971 let result = exchange.place(alice, base_token, below_minimum, true, tick);
1973 assert_eq!(
1974 result,
1975 Err(StablecoinDEXError::below_minimum_order_size(below_minimum).into())
1976 );
1977
1978 Ok(())
1979 })
1980 }
1981
1982 #[test]
1983 fn test_place_bid_order() -> eyre::Result<()> {
1984 let mut storage = HashMapStorageProvider::new(1);
1985 StorageCtx::enter(&mut storage, || {
1986 let mut exchange = StablecoinDEX::new();
1987 exchange.initialize()?;
1988
1989 let alice = Address::random();
1990 let admin = Address::random();
1991 let min_order_amount = MIN_ORDER_AMOUNT;
1992 let tick = 100i16;
1993
1994 let price = orderbook::tick_to_price(tick);
1995 let expected_escrow =
1996 (min_order_amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
1997
1998 let (base_token, quote_token) =
2000 setup_test_tokens(admin, alice, exchange.address, expected_escrow)?;
2001
2002 exchange
2004 .create_pair(base_token)
2005 .expect("Could not create pair");
2006
2007 let order_id = exchange
2009 .place(alice, base_token, min_order_amount, true, tick)
2010 .expect("Place bid order should succeed");
2011
2012 assert_eq!(order_id, 1);
2013 assert_eq!(exchange.next_order_id()?, 2);
2014
2015 let stored_order = exchange.orders[order_id].read()?;
2017 assert_eq!(stored_order.maker(), alice);
2018 assert_eq!(stored_order.amount(), min_order_amount);
2019 assert_eq!(stored_order.remaining(), min_order_amount);
2020 assert_eq!(stored_order.tick(), tick);
2021 assert!(stored_order.is_bid());
2022 assert!(!stored_order.is_flip());
2023
2024 let book_key = compute_book_key(base_token, quote_token);
2026 let book_handler = &exchange.books[book_key];
2027 let level = book_handler.tick_level_handler(tick, true).read()?;
2028 assert_eq!(level.head, order_id);
2029 assert_eq!(level.tail, order_id);
2030 assert_eq!(level.total_liquidity, min_order_amount);
2031
2032 let quote_tip20 = TIP20Token::from_address(quote_token)?;
2034 let remaining_balance =
2035 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?;
2036 assert_eq!(remaining_balance, U256::ZERO);
2037
2038 let exchange_balance = quote_tip20.balance_of(ITIP20::balanceOfCall {
2040 account: exchange.address,
2041 })?;
2042 assert_eq!(exchange_balance, U256::from(expected_escrow));
2043
2044 Ok(())
2045 })
2046 }
2047
2048 #[test]
2049 fn test_place_ask_order() -> eyre::Result<()> {
2050 let mut storage = HashMapStorageProvider::new(1);
2051 StorageCtx::enter(&mut storage, || {
2052 let mut exchange = StablecoinDEX::new();
2053 exchange.initialize()?;
2054
2055 let alice = Address::random();
2056 let admin = Address::random();
2057 let min_order_amount = MIN_ORDER_AMOUNT;
2058 let tick = 50i16; let (base_token, quote_token) =
2062 setup_test_tokens(admin, alice, exchange.address, min_order_amount)?;
2063 exchange
2065 .create_pair(base_token)
2066 .expect("Could not create pair");
2067
2068 let order_id = exchange
2069 .place(alice, base_token, min_order_amount, false, tick) .expect("Place ask order should succeed");
2071
2072 assert_eq!(order_id, 1);
2073 assert_eq!(exchange.next_order_id()?, 2);
2074
2075 let stored_order = exchange.orders[order_id].read()?;
2077 assert_eq!(stored_order.maker(), alice);
2078 assert_eq!(stored_order.amount(), min_order_amount);
2079 assert_eq!(stored_order.remaining(), min_order_amount);
2080 assert_eq!(stored_order.tick(), tick);
2081 assert!(!stored_order.is_bid());
2082 assert!(!stored_order.is_flip());
2083
2084 let book_key = compute_book_key(base_token, quote_token);
2086 let book_handler = &exchange.books[book_key];
2087 let level = book_handler.tick_level_handler(tick, false).read()?;
2088 assert_eq!(level.head, order_id);
2089 assert_eq!(level.tail, order_id);
2090 assert_eq!(level.total_liquidity, min_order_amount);
2091
2092 let base_tip20 = TIP20Token::from_address(base_token)?;
2094 let remaining_balance =
2095 base_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?;
2096 assert_eq!(remaining_balance, U256::ZERO); let exchange_balance = base_tip20.balance_of(ITIP20::balanceOfCall {
2100 account: exchange.address,
2101 })?;
2102 assert_eq!(exchange_balance, U256::from(min_order_amount));
2103
2104 Ok(())
2105 })
2106 }
2107
2108 #[test]
2109 fn test_place_flip_order_below_minimum_amount() -> eyre::Result<()> {
2110 let mut storage = HashMapStorageProvider::new(1);
2111 StorageCtx::enter(&mut storage, || {
2112 let mut exchange = StablecoinDEX::new();
2113 exchange.initialize()?;
2114
2115 let alice = Address::random();
2116 let admin = Address::random();
2117 let min_order_amount = MIN_ORDER_AMOUNT;
2118 let below_minimum = min_order_amount - 1;
2119 let tick = 100i16;
2120 let flip_tick = 200i16;
2121
2122 let price = orderbook::tick_to_price(tick);
2123 let escrow_amount =
2124 (below_minimum * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
2125
2126 let (base_token, _quote_token) =
2127 setup_test_tokens(admin, alice, exchange.address, escrow_amount)?;
2128
2129 exchange
2131 .create_pair(base_token)
2132 .expect("Could not create pair");
2133
2134 let result = exchange.place_flip(
2136 alice,
2137 base_token,
2138 below_minimum,
2139 true,
2140 tick,
2141 flip_tick,
2142 false,
2143 );
2144 assert_eq!(
2145 result,
2146 Err(StablecoinDEXError::below_minimum_order_size(below_minimum).into())
2147 );
2148
2149 Ok(())
2150 })
2151 }
2152
2153 #[test]
2154 fn test_place_flip_auto_creates_pair() -> Result<()> {
2155 let mut storage = HashMapStorageProvider::new(1);
2156 StorageCtx::enter(&mut storage, || {
2157 let mut exchange = StablecoinDEX::new();
2158 exchange.initialize()?;
2159
2160 let admin = Address::random();
2161 let user = Address::random();
2162
2163 let (base_token, quote_token) =
2165 setup_test_tokens(admin, user, exchange.address, 100_000_000)?;
2166
2167 let book_key = compute_book_key(base_token, quote_token);
2169 let book_before = exchange.books[book_key].read()?;
2170 assert!(book_before.base.is_zero(),);
2171
2172 let mut base = TIP20Token::from_address(base_token)?;
2174 base.transfer(
2175 user,
2176 ITIP20::transferCall {
2177 to: exchange.address,
2178 amount: U256::from(MIN_ORDER_AMOUNT),
2179 },
2180 )
2181 .expect("Base token transfer failed");
2182
2183 exchange.place_flip(user, base_token, MIN_ORDER_AMOUNT, true, 0, 10, false)?;
2185
2186 let book_after = exchange.books[book_key].read()?;
2187 assert_eq!(book_after.base, base_token);
2188
2189 let events = exchange.emitted_events();
2191 assert_eq!(events.len(), 2);
2192 assert_eq!(
2193 events[0],
2194 StablecoinDEXEvents::pair_created(book_key, base_token, quote_token)
2195 .into_log_data()
2196 );
2197
2198 Ok(())
2199 })
2200 }
2201
2202 #[test]
2203 fn test_place_flip_order() -> eyre::Result<()> {
2204 let mut storage = HashMapStorageProvider::new(1);
2205 StorageCtx::enter(&mut storage, || {
2206 let mut exchange = StablecoinDEX::new();
2207 exchange.initialize()?;
2208
2209 let alice = Address::random();
2210 let admin = Address::random();
2211 let min_order_amount = MIN_ORDER_AMOUNT;
2212 let tick = 100i16;
2213 let flip_tick = 200i16; let price = orderbook::tick_to_price(tick);
2217 let expected_escrow =
2218 (min_order_amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
2219
2220 let (base_token, quote_token) =
2222 setup_test_tokens(admin, alice, exchange.address, expected_escrow)?;
2223 exchange
2224 .create_pair(base_token)
2225 .expect("Could not create pair");
2226
2227 let order_id = exchange
2228 .place_flip(
2229 alice,
2230 base_token,
2231 min_order_amount,
2232 true,
2233 tick,
2234 flip_tick,
2235 false,
2236 )
2237 .expect("Place flip bid order should succeed");
2238
2239 assert_eq!(order_id, 1);
2240 assert_eq!(exchange.next_order_id()?, 2);
2241
2242 let stored_order = exchange.orders[order_id].read()?;
2244 assert_eq!(stored_order.maker(), alice);
2245 assert_eq!(stored_order.amount(), min_order_amount);
2246 assert_eq!(stored_order.remaining(), min_order_amount);
2247 assert_eq!(stored_order.tick(), tick);
2248 assert!(stored_order.is_bid());
2249 assert!(stored_order.is_flip());
2250 assert_eq!(stored_order.flip_tick(), flip_tick);
2251
2252 let book_key = compute_book_key(base_token, quote_token);
2254 let book_handler = &exchange.books[book_key];
2255 let level = book_handler.tick_level_handler(tick, true).read()?;
2256 assert_eq!(level.head, order_id);
2257 assert_eq!(level.tail, order_id);
2258 assert_eq!(level.total_liquidity, min_order_amount);
2259
2260 let quote_tip20 = TIP20Token::from_address(quote_token)?;
2262 let remaining_balance =
2263 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?;
2264 assert_eq!(remaining_balance, U256::ZERO);
2265
2266 let exchange_balance = quote_tip20.balance_of(ITIP20::balanceOfCall {
2268 account: exchange.address,
2269 })?;
2270 assert_eq!(exchange_balance, U256::from(expected_escrow));
2271
2272 Ok(())
2273 })
2274 }
2275
2276 #[test]
2279 fn test_place_flip_same_tick_per_hardfork() -> eyre::Result<()> {
2280 for spec in [TempoHardfork::T4, TempoHardfork::T5] {
2281 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
2282 StorageCtx::enter(&mut storage, || {
2283 let mut exchange = StablecoinDEX::new();
2284 exchange.initialize()?;
2285
2286 let alice = Address::random();
2287 let admin = Address::random();
2288 let tick = 100i16;
2289
2290 let price = orderbook::tick_to_price(tick);
2291 let escrow =
2292 (MIN_ORDER_AMOUNT * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
2293
2294 let (base_token, _) = setup_test_tokens(admin, alice, exchange.address, escrow)?;
2295 exchange.create_pair(base_token)?;
2296
2297 let result = exchange.place_flip(
2298 alice,
2299 base_token,
2300 MIN_ORDER_AMOUNT,
2301 true,
2302 tick,
2303 tick,
2304 false,
2305 );
2306
2307 if spec.is_t5() {
2308 let order_id = result.expect("same-tick flip should succeed on T5+");
2309 let stored = exchange.orders[order_id].read()?;
2310 assert_eq!(stored.tick(), tick);
2311 assert_eq!(stored.flip_tick(), tick);
2312 assert!(stored.is_bid());
2313 assert!(stored.is_flip());
2314 } else {
2315 assert_eq!(result, Err(StablecoinDEXError::invalid_flip_tick().into()));
2316 }
2317
2318 Ok::<_, eyre::Report>(())
2319 })?;
2320 }
2321 Ok(())
2322 }
2323
2324 #[test]
2329 fn test_place_flip_wrong_side_still_rejected_t5() -> eyre::Result<()> {
2330 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
2331 StorageCtx::enter(&mut storage, || {
2332 let mut exchange = StablecoinDEX::new();
2333 exchange.initialize()?;
2334
2335 let alice = Address::random();
2336 let admin = Address::random();
2337 let tick = 100i16;
2338
2339 let price = orderbook::tick_to_price(tick);
2340 let escrow =
2341 (MIN_ORDER_AMOUNT * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
2342
2343 let (base_token, _) = setup_test_tokens(admin, alice, exchange.address, escrow)?;
2344 exchange.create_pair(base_token)?;
2345
2346 let bid_result = exchange.place_flip(
2348 alice,
2349 base_token,
2350 MIN_ORDER_AMOUNT,
2351 true,
2352 tick,
2353 tick - TICK_SPACING,
2354 false,
2355 );
2356 assert_eq!(
2357 bid_result,
2358 Err(StablecoinDEXError::invalid_flip_tick().into())
2359 );
2360
2361 let ask_result = exchange.place_flip(
2363 alice,
2364 base_token,
2365 MIN_ORDER_AMOUNT,
2366 false,
2367 tick,
2368 tick + TICK_SPACING,
2369 false,
2370 );
2371 assert_eq!(
2372 ask_result,
2373 Err(StablecoinDEXError::invalid_flip_tick().into())
2374 );
2375
2376 Ok(())
2377 })
2378 }
2379
2380 #[test]
2392 fn test_flip_same_tick_locked_book_t5() -> eyre::Result<()> {
2393 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
2394 StorageCtx::enter(&mut storage, || {
2395 let mut exchange = StablecoinDEX::new();
2396 exchange.initialize()?;
2397
2398 let alice = Address::random();
2399 let bob = Address::random();
2400 let admin = Address::random();
2401 let amount = MIN_ORDER_AMOUNT;
2402 let tick = 100i16;
2403
2404 let price = orderbook::tick_to_price(tick);
2405 let expected_escrow = (amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
2406
2407 let base = TIP20Setup::create("BASE", "BASE", admin)
2413 .with_issuer(admin)
2414 .with_mint(bob, U256::from(amount * 4))
2415 .with_approval(bob, exchange.address, U256::MAX)
2416 .apply()?;
2417 let base_token = base.address();
2418 let quote_token = base.quote_token()?;
2419
2420 TIP20Setup::path_usd(admin)
2421 .with_issuer(admin)
2422 .with_mint(alice, U256::from(expected_escrow * 4))
2423 .with_mint(bob, U256::from(expected_escrow * 4))
2424 .with_approval(alice, exchange.address, U256::MAX)
2425 .with_approval(bob, exchange.address, U256::MAX)
2426 .apply()?;
2427
2428 exchange.create_pair(base_token)?;
2429
2430 let flip_id = exchange
2433 .place_flip(alice, base_token, amount, true, tick, tick, false)
2434 .expect("same-tick flip should succeed on T5");
2435
2436 let resting_bid_id = exchange
2439 .place(alice, base_token, amount, true, tick)
2440 .expect("regular bid should succeed");
2441
2442 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
2446
2447 let resting = exchange.orders[resting_bid_id].read()?;
2450 assert_eq!(resting.maker(), alice);
2451 assert_eq!(resting.remaining(), amount);
2452 assert!(resting.is_bid());
2453
2454 let new_ask_id = flip_id;
2458 assert_eq!(exchange.next_order_id()?, resting_bid_id + 1);
2459 let new_ask = exchange.orders[new_ask_id].read()?;
2460 assert_eq!(new_ask.maker(), alice);
2461 assert!(new_ask.is_ask());
2462 assert!(new_ask.is_flip());
2463 assert_eq!(new_ask.tick(), tick);
2464 assert_eq!(new_ask.flip_tick(), tick);
2465 assert_eq!(new_ask.remaining(), amount);
2466
2467 let book_key = compute_book_key(base_token, quote_token);
2470 let bid_level = exchange.books[book_key]
2471 .tick_level_handler(tick, true)
2472 .read()?;
2473 assert_eq!(bid_level.head, resting_bid_id);
2474 assert_eq!(bid_level.tail, resting_bid_id);
2475 assert_eq!(bid_level.total_liquidity, amount);
2476
2477 let ask_level = exchange.books[book_key]
2478 .tick_level_handler(tick, false)
2479 .read()?;
2480 assert_eq!(ask_level.head, new_ask_id);
2481 assert_eq!(ask_level.tail, new_ask_id);
2482 assert_eq!(ask_level.total_liquidity, amount);
2483
2484 let book = exchange.books[book_key].read()?;
2485 assert_eq!(book.best_bid_tick, tick, "best bid should remain at tick");
2487 assert_eq!(
2488 book.best_ask_tick, tick,
2489 "best ask should now equal best bid (locked)"
2490 );
2491
2492 let quote_in =
2497 base_to_quote(amount, tick, RoundingDirection::Up).expect("quote_in should fit");
2498 exchange.swap_exact_amount_in(bob, quote_token, base_token, quote_in, 0)?;
2499
2500 let resting_after = exchange.orders[resting_bid_id].read()?;
2502 assert_eq!(resting_after.maker(), alice);
2503 assert_eq!(resting_after.remaining(), amount);
2504
2505 let flipped_back_id = new_ask_id;
2509 assert_eq!(exchange.next_order_id()?, resting_bid_id + 1);
2510 let flipped_back = exchange.orders[flipped_back_id].read()?;
2511 assert_eq!(flipped_back.maker(), alice);
2512 assert!(flipped_back.is_bid());
2513 assert!(flipped_back.is_flip());
2514 assert_eq!(flipped_back.tick(), tick);
2515 assert_eq!(flipped_back.flip_tick(), tick);
2516
2517 let book_after = exchange.books[book_key].read()?;
2520 assert_eq!(book_after.best_bid_tick, tick);
2521 assert_eq!(book_after.best_ask_tick, i16::MAX);
2522
2523 let bid_level_after = exchange.books[book_key]
2524 .tick_level_handler(tick, true)
2525 .read()?;
2526 assert_eq!(bid_level_after.head, resting_bid_id);
2527 assert_eq!(bid_level_after.tail, flipped_back_id);
2528 assert_eq!(bid_level_after.total_liquidity, amount * 2);
2529
2530 Ok(())
2531 })
2532 }
2533
2534 #[test]
2535 fn test_withdraw() -> eyre::Result<()> {
2536 let mut storage = HashMapStorageProvider::new(1);
2537 StorageCtx::enter(&mut storage, || {
2538 let mut exchange = StablecoinDEX::new();
2539 exchange.initialize()?;
2540
2541 let alice = Address::random();
2542 let admin = Address::random();
2543 let min_order_amount = MIN_ORDER_AMOUNT;
2544 let tick = 100i16;
2545 let price = orderbook::tick_to_price(tick);
2546 let expected_escrow =
2547 (min_order_amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
2548
2549 let (base_token, quote_token) =
2551 setup_test_tokens(admin, alice, exchange.address, expected_escrow)?;
2552 exchange
2553 .create_pair(base_token)
2554 .expect("Could not create pair");
2555
2556 let order_id = exchange
2558 .place(alice, base_token, min_order_amount, true, tick)
2559 .expect("Place bid order should succeed");
2560
2561 exchange
2562 .cancel(alice, order_id)
2563 .expect("Cancel pending order should succeed");
2564
2565 assert_eq!(exchange.balance_of(alice, quote_token)?, expected_escrow);
2566
2567 exchange
2569 .withdraw(alice, quote_token, expected_escrow)
2570 .expect("Withdraw should succeed");
2571 assert_eq!(exchange.balance_of(alice, quote_token)?, 0);
2572
2573 let quote_tip20 = TIP20Token::from_address(quote_token)?;
2575 assert_eq!(
2576 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?,
2577 expected_escrow
2578 );
2579 assert_eq!(
2580 quote_tip20.balance_of(ITIP20::balanceOfCall {
2581 account: exchange.address
2582 })?,
2583 0
2584 );
2585
2586 Ok(())
2587 })
2588 }
2589
2590 #[test]
2591 fn test_withdraw_insufficient_balance() -> eyre::Result<()> {
2592 let mut storage = HashMapStorageProvider::new(1);
2593 StorageCtx::enter(&mut storage, || {
2594 let mut exchange = StablecoinDEX::new();
2595 exchange.initialize()?;
2596
2597 let alice = Address::random();
2598 let admin = Address::random();
2599
2600 let min_order_amount = MIN_ORDER_AMOUNT;
2601 let (_base_token, quote_token) =
2602 setup_test_tokens(admin, alice, exchange.address, min_order_amount)?;
2603
2604 assert_eq!(exchange.balance_of(alice, quote_token)?, 0);
2606
2607 let result = exchange.withdraw(alice, quote_token, 100u128);
2609
2610 assert_eq!(
2611 result,
2612 Err(StablecoinDEXError::insufficient_balance().into())
2613 );
2614
2615 Ok(())
2616 })
2617 }
2618
2619 #[test]
2620 fn test_quote_swap_exact_amount_out() -> eyre::Result<()> {
2621 let mut storage = HashMapStorageProvider::new(1);
2622 StorageCtx::enter(&mut storage, || {
2623 let mut exchange = StablecoinDEX::new();
2624 exchange.initialize()?;
2625
2626 let alice = Address::random();
2627 let admin = Address::random();
2628 let min_order_amount = MIN_ORDER_AMOUNT;
2629 let amount_out = 500_000u128;
2630 let tick = 10;
2631
2632 let (base_token, quote_token) =
2633 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2634 exchange
2635 .create_pair(base_token)
2636 .expect("Could not create pair");
2637
2638 let order_amount = min_order_amount;
2639 exchange
2640 .place(alice, base_token, order_amount, false, tick)
2641 .expect("Order should succeed");
2642
2643 let amount_in = exchange
2644 .quote_swap_exact_amount_out(quote_token, base_token, amount_out)
2645 .expect("Swap should succeed");
2646
2647 let price = orderbook::tick_to_price(tick);
2648 let expected_amount_in =
2649 (amount_out * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
2650 assert_eq!(amount_in, expected_amount_in);
2651
2652 Ok(())
2653 })
2654 }
2655
2656 #[test]
2657 fn test_quote_swap_exact_amount_in() -> eyre::Result<()> {
2658 let mut storage = HashMapStorageProvider::new(1);
2659 StorageCtx::enter(&mut storage, || {
2660 let mut exchange = StablecoinDEX::new();
2661 exchange.initialize()?;
2662
2663 let alice = Address::random();
2664 let admin = Address::random();
2665 let min_order_amount = MIN_ORDER_AMOUNT;
2666 let amount_in = 500_000u128;
2667 let tick = 10;
2668
2669 let (base_token, quote_token) =
2670 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2671 exchange
2672 .create_pair(base_token)
2673 .expect("Could not create pair");
2674
2675 let order_amount = min_order_amount;
2676 exchange
2677 .place(alice, base_token, order_amount, true, tick)
2678 .expect("Place bid order should succeed");
2679
2680 let amount_out = exchange
2681 .quote_swap_exact_amount_in(base_token, quote_token, amount_in)
2682 .expect("Swap should succeed");
2683
2684 let price = orderbook::tick_to_price(tick);
2686 let expected_amount_out =
2687 (amount_in * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
2688 assert_eq!(amount_out, expected_amount_out);
2689
2690 Ok(())
2691 })
2692 }
2693
2694 #[test]
2695 fn test_quote_swap_exact_amount_out_base_for_quote() -> eyre::Result<()> {
2696 let mut storage = HashMapStorageProvider::new(1);
2697 StorageCtx::enter(&mut storage, || {
2698 let mut exchange = StablecoinDEX::new();
2699 exchange.initialize()?;
2700
2701 let alice = Address::random();
2702 let admin = Address::random();
2703 let min_order_amount = MIN_ORDER_AMOUNT;
2704 let amount_out = 500_000u128;
2705 let tick = 0;
2706
2707 let (base_token, quote_token) =
2708 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2709 exchange
2710 .create_pair(base_token)
2711 .expect("Could not create pair");
2712
2713 let order_amount = min_order_amount;
2715 exchange
2716 .place(alice, base_token, order_amount, true, tick)
2717 .expect("Place bid order should succeed");
2718
2719 let amount_in = exchange
2722 .quote_swap_exact_amount_out(base_token, quote_token, amount_out)
2723 .expect("Quote should succeed");
2724
2725 let price = orderbook::tick_to_price(tick);
2726 let expected_amount_in =
2728 (amount_out * u128::from(orderbook::PRICE_SCALE)).div_ceil(u128::from(price));
2729 assert_eq!(amount_in, expected_amount_in);
2730
2731 Ok(())
2732 })
2733 }
2734
2735 #[test]
2736 fn test_quote_exact_out_bid_positive_tick_no_underflow() -> eyre::Result<()> {
2737 let mut storage = HashMapStorageProvider::new(1);
2738 StorageCtx::enter(&mut storage, || {
2739 let mut exchange = StablecoinDEX::new();
2740 exchange.initialize()?;
2741
2742 let alice = Address::random();
2743 let admin = Address::random();
2744
2745 let (base_token, quote_token) =
2746 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2747 exchange.create_pair(base_token)?;
2748
2749 let tick = 10;
2750 let order_amount = MIN_ORDER_AMOUNT;
2751 exchange.place(alice, base_token, order_amount, true, tick)?;
2752
2753 for amount_out in [100_001u128, 100_003, 100_007, 100_009, 100_011] {
2754 let amount_in = exchange
2755 .quote_swap_exact_amount_out(base_token, quote_token, amount_out)
2756 .unwrap_or_else(|_| {
2757 panic!("quote_exact_out should not underflow for amount_out={amount_out}")
2758 });
2759
2760 let expected =
2761 orderbook::quote_to_base(amount_out, tick, RoundingDirection::Up).unwrap();
2762 assert_eq!(
2763 amount_in, expected,
2764 "amount_in should equal quote_to_base(amount_out, tick, Up) for amount_out={amount_out}"
2765 );
2766 }
2767
2768 Ok(())
2769 })
2770 }
2771
2772 #[test]
2773 fn test_swap_exact_amount_out() -> eyre::Result<()> {
2774 let mut storage = HashMapStorageProvider::new(1);
2775 StorageCtx::enter(&mut storage, || {
2776 let mut exchange = StablecoinDEX::new();
2777 exchange.initialize()?;
2778
2779 let alice = Address::random();
2780 let bob = Address::random();
2781 let admin = Address::random();
2782 let min_order_amount = MIN_ORDER_AMOUNT;
2783 let amount_out = 500_000u128;
2784 let tick = 10;
2785
2786 let (base_token, quote_token) =
2787 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2788 exchange
2789 .create_pair(base_token)
2790 .expect("Could not create pair");
2791
2792 let order_amount = min_order_amount;
2793 exchange
2794 .place(alice, base_token, order_amount, false, tick)
2795 .expect("Order should succeed");
2796
2797 exchange
2798 .set_balance(bob, quote_token, 200_000_000u128)
2799 .expect("Could not set balance");
2800
2801 let price = orderbook::tick_to_price(tick);
2802 let max_amount_in =
2803 (amount_out * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
2804
2805 let amount_in = exchange
2806 .swap_exact_amount_out(bob, quote_token, base_token, amount_out, max_amount_in)
2807 .expect("Swap should succeed");
2808
2809 let base_tip20 = TIP20Token::from_address(base_token)?;
2810 let bob_base_balance = base_tip20.balance_of(ITIP20::balanceOfCall { account: bob })?;
2811 assert_eq!(bob_base_balance, U256::from(amount_out));
2812
2813 let alice_quote_exchange_balance = exchange.balance_of(alice, quote_token)?;
2814 assert_eq!(alice_quote_exchange_balance, amount_in);
2815
2816 Ok(())
2817 })
2818 }
2819
2820 #[test]
2821 fn test_swap_exact_amount_in() -> eyre::Result<()> {
2822 let mut storage = HashMapStorageProvider::new(1);
2823 StorageCtx::enter(&mut storage, || {
2824 let mut exchange = StablecoinDEX::new();
2825 exchange.initialize()?;
2826
2827 let alice = Address::random();
2828 let bob = Address::random();
2829 let admin = Address::random();
2830 let min_order_amount = MIN_ORDER_AMOUNT;
2831 let amount_in = 500_000u128;
2832 let tick = 10;
2833
2834 let (base_token, quote_token) =
2835 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2836 exchange
2837 .create_pair(base_token)
2838 .expect("Could not create pair");
2839
2840 let order_amount = min_order_amount;
2841 exchange
2842 .place(alice, base_token, order_amount, true, tick)
2843 .expect("Order should succeed");
2844
2845 exchange
2846 .set_balance(bob, base_token, 200_000_000u128)
2847 .expect("Could not set balance");
2848
2849 let price = orderbook::tick_to_price(tick);
2850 let min_amount_out =
2851 (amount_in * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
2852
2853 let amount_out = exchange
2854 .swap_exact_amount_in(bob, base_token, quote_token, amount_in, min_amount_out)
2855 .expect("Swap should succeed");
2856
2857 let quote_tip20 = TIP20Token::from_address(quote_token)?;
2858 let bob_quote_balance =
2859 quote_tip20.balance_of(ITIP20::balanceOfCall { account: bob })?;
2860 assert_eq!(bob_quote_balance, U256::from(amount_out));
2861
2862 let alice_base_exchange_balance = exchange.balance_of(alice, base_token)?;
2863 assert_eq!(alice_base_exchange_balance, amount_in);
2864
2865 Ok(())
2866 })
2867 }
2868
2869 #[test]
2870 fn test_flip_order_execution() -> eyre::Result<()> {
2871 let mut storage = HashMapStorageProvider::new(1);
2872 StorageCtx::enter(&mut storage, || {
2873 let mut exchange = StablecoinDEX::new();
2874 exchange.initialize()?;
2875
2876 let alice = Address::random();
2877 let bob = Address::random();
2878 let admin = Address::random();
2879 let min_order_amount = MIN_ORDER_AMOUNT;
2880 let amount = min_order_amount;
2881 let tick = 100i16;
2882 let flip_tick = 200i16;
2883
2884 let price = orderbook::tick_to_price(tick);
2885 let expected_escrow = (amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
2886
2887 let (base_token, quote_token) =
2888 setup_test_tokens(admin, alice, exchange.address, expected_escrow * 2)?;
2889 exchange
2890 .create_pair(base_token)
2891 .expect("Could not create pair");
2892
2893 let flip_order_id = exchange
2895 .place_flip(alice, base_token, amount, true, tick, flip_tick, false)
2896 .expect("Place flip order should succeed");
2897
2898 exchange
2899 .set_balance(bob, base_token, amount)
2900 .expect("Could not set balance");
2901
2902 exchange
2903 .swap_exact_amount_in(bob, base_token, quote_token, amount, 0)
2904 .expect("Swap should succeed");
2905
2906 let filled_order = exchange.orders[flip_order_id].read()?;
2908 assert_eq!(filled_order.remaining(), 0);
2909
2910 let new_order_id = exchange.next_order_id()? - 1;
2912 assert_eq!(new_order_id, flip_order_id + 1);
2913
2914 let new_order = exchange.orders[new_order_id].read()?;
2915 assert_eq!(new_order.maker(), alice);
2916 assert_eq!(new_order.tick(), flip_tick);
2917 assert_eq!(new_order.flip_tick(), tick);
2918 assert!(new_order.is_ask());
2919 assert_eq!(new_order.amount(), amount);
2920 assert_eq!(new_order.remaining(), amount);
2921
2922 Ok(())
2923 })
2924 }
2925
2926 #[test]
2932 fn test_flip_same_tick_execution_t5() -> eyre::Result<()> {
2933 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
2934 StorageCtx::enter(&mut storage, || {
2935 let mut exchange = StablecoinDEX::new();
2936 exchange.initialize()?;
2937
2938 let alice = Address::random();
2939 let bob = Address::random();
2940 let admin = Address::random();
2941 let amount = MIN_ORDER_AMOUNT;
2942 let tick = 100i16;
2943 let flip_tick = tick;
2945
2946 let price = orderbook::tick_to_price(tick);
2947 let expected_escrow = (amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
2948
2949 let (base_token, quote_token) =
2950 setup_test_tokens(admin, alice, exchange.address, expected_escrow * 2)?;
2951 exchange.create_pair(base_token)?;
2952
2953 let flip_order_id =
2954 exchange.place_flip(alice, base_token, amount, true, tick, flip_tick, false)?;
2955
2956 exchange.set_balance(bob, base_token, amount)?;
2957 let next_order_id_before = exchange.next_order_id()?;
2958 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
2959
2960 assert_eq!(exchange.next_order_id()?, next_order_id_before);
2963
2964 let new_order = exchange.orders[flip_order_id].read()?;
2965 assert_eq!(new_order.order_id(), flip_order_id);
2966 assert_eq!(new_order.maker(), alice);
2967 assert!(new_order.is_ask());
2968 assert!(new_order.is_flip());
2969 assert_eq!(new_order.tick(), tick);
2970 assert_eq!(new_order.flip_tick(), tick);
2971 assert_eq!(new_order.amount(), amount);
2972 assert_eq!(new_order.remaining(), amount);
2973
2974 assert_eq!(exchange.balance_of(alice, base_token)?, 0);
2978 assert_eq!(exchange.balance_of(alice, quote_token)?, 0);
2979
2980 let book_key = compute_book_key(base_token, quote_token);
2982 let book = exchange.books[book_key].read()?;
2983 assert_eq!(book.best_ask_tick, tick);
2984 assert_eq!(book.best_bid_tick, i16::MIN);
2985
2986 Ok(())
2987 })
2988 }
2989
2990 #[test]
2996 fn test_flip_in_place_keeps_order_id_t5() -> eyre::Result<()> {
2997 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
2998 StorageCtx::enter(&mut storage, || {
2999 let FlipOrderTestCtx {
3000 mut exchange,
3001 alice,
3002 bob,
3003 base_token,
3004 quote_token,
3005 book_key,
3006 amount,
3007 flip_tick,
3008 ..
3009 } = setup_flip_order_test()?;
3010
3011 let flip_order_id = 1u128;
3013 let next_order_id_before = exchange.next_order_id()?;
3014
3015 exchange.set_balance(bob, base_token, amount)?;
3017 let events_before = exchange.emitted_events().len();
3018 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
3019
3020 assert_eq!(exchange.next_order_id()?, next_order_id_before);
3022
3023 let flipped = exchange.get_order(flip_order_id)?;
3025 assert_eq!(flipped.order_id(), flip_order_id);
3026 assert_eq!(flipped.maker(), alice);
3027 assert!(flipped.is_ask());
3028 assert!(flipped.is_flip());
3029 assert_eq!(flipped.tick(), flip_tick);
3030 assert_eq!(flipped.flip_tick(), 100i16); assert_eq!(flipped.amount(), amount);
3032 assert_eq!(flipped.remaining(), amount);
3033
3034 let new_events = &exchange.emitted_events()[events_before..];
3037 let saw_flipped = new_events
3038 .iter()
3039 .any(|e| e.topics()[0] == IStablecoinDEX::OrderFlipped::SIGNATURE_HASH);
3040 let saw_placed = new_events
3041 .iter()
3042 .any(|e| e.topics()[0] == IStablecoinDEX::OrderPlaced::SIGNATURE_HASH);
3043 assert!(saw_flipped, "expected OrderFlipped to be emitted");
3044 assert!(
3045 !saw_placed,
3046 "OrderPlaced must NOT be emitted for an automatic flip"
3047 );
3048
3049 let alice_base_before = exchange.balance_of(alice, base_token)?;
3052 exchange.cancel(alice, flip_order_id)?;
3053 let alice_base_after = exchange.balance_of(alice, base_token)?;
3054 assert_eq!(alice_base_after, alice_base_before + amount);
3055
3056 assert!(exchange.get_order(flip_order_id).is_err());
3058
3059 let ask_level = exchange.books[book_key]
3061 .tick_level_handler(flip_tick, false)
3062 .read()?;
3063 assert_eq!(ask_level.head, 0);
3064 assert_eq!(ask_level.tail, 0);
3065 assert_eq!(ask_level.total_liquidity, 0);
3066
3067 Ok(())
3068 })
3069 }
3070
3071 #[test]
3076 fn test_flip_in_place_failure_no_orphan_t5() -> eyre::Result<()> {
3077 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
3078 StorageCtx::enter(&mut storage, || {
3079 let FlipOrderTestCtx {
3080 mut exchange,
3081 alice,
3082 bob,
3083 admin,
3084 base_token,
3085 quote_token,
3086 book_key,
3087 amount,
3088 flip_tick,
3089 } = setup_flip_order_test()?;
3090
3091 let flip_order_id = 1u128;
3092
3093 let mut registry = TIP403Registry::new();
3097 let policy_id = registry.create_policy(
3098 admin,
3099 ITIP403Registry::createPolicyCall {
3100 admin,
3101 policyType: ITIP403Registry::PolicyType::BLACKLIST,
3102 },
3103 )?;
3104 let mut base = TIP20Token::from_address(base_token)?;
3105 base.change_transfer_policy_id(
3106 admin,
3107 ITIP20::changeTransferPolicyIdCall {
3108 newPolicyId: policy_id,
3109 },
3110 )?;
3111 registry.modify_policy_blacklist(
3112 admin,
3113 ITIP403Registry::modifyPolicyBlacklistCall {
3114 policyId: policy_id,
3115 account: alice,
3116 restricted: true,
3117 },
3118 )?;
3119
3120 exchange.set_balance(bob, base_token, amount)?;
3121 let events_before = exchange.emitted_events().len();
3122 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
3124
3125 let new_events = &exchange.emitted_events()[events_before..];
3126 let flip_failed = new_events
3127 .iter()
3128 .find(|event| event.topics()[0] == IStablecoinDEX::FlipFailed::SIGNATURE_HASH)
3129 .expect("expected FlipFailed event");
3130 let decoded = IStablecoinDEX::FlipFailed::decode_log_data(flip_failed)?;
3131 assert_eq!(decoded.orderId, flip_order_id);
3132 assert_eq!(decoded.maker, alice);
3133 assert_eq!(
3134 decoded.reason,
3135 FixedBytes::from(TIP20Error::policy_forbids().selector())
3136 );
3137
3138 assert!(
3141 exchange.get_order(flip_order_id).is_err(),
3142 "filled flip order must not remain in storage after a failed flip"
3143 );
3144 assert!(exchange.cancel(alice, flip_order_id).is_err());
3145
3146 let bid_level = exchange.books[book_key]
3150 .tick_level_handler(100i16, true)
3151 .read()?;
3152 assert_eq!(bid_level.total_liquidity, 0);
3153 let ask_level = exchange.books[book_key]
3154 .tick_level_handler(flip_tick, false)
3155 .read()?;
3156 assert_eq!(ask_level.total_liquidity, 0);
3157
3158 Ok(())
3159 })
3160 }
3161
3162 #[test]
3163 fn test_pair_created() -> eyre::Result<()> {
3164 let mut storage = HashMapStorageProvider::new(1);
3165 StorageCtx::enter(&mut storage, || {
3166 let mut exchange = StablecoinDEX::new();
3167 exchange.initialize()?;
3168
3169 let admin = Address::random();
3170 let alice = Address::random();
3171
3172 let min_order_amount = MIN_ORDER_AMOUNT;
3173 let (base_token, quote_token) =
3175 setup_test_tokens(admin, alice, exchange.address, min_order_amount)?;
3176
3177 let key = exchange
3179 .create_pair(base_token)
3180 .expect("Could not create pair");
3181
3182 exchange.assert_emitted_events(vec![StablecoinDEXEvents::pair_created(
3184 key,
3185 base_token,
3186 quote_token,
3187 )]);
3188
3189 Ok(())
3190 })
3191 }
3192
3193 #[test]
3194 fn test_pair_already_created() -> eyre::Result<()> {
3195 let mut storage = HashMapStorageProvider::new(1);
3196 StorageCtx::enter(&mut storage, || {
3197 let mut exchange = StablecoinDEX::new();
3198 exchange.initialize()?;
3199
3200 let admin = Address::random();
3201 let alice = Address::random();
3202
3203 let min_order_amount = MIN_ORDER_AMOUNT;
3204 let (base_token, _) =
3206 setup_test_tokens(admin, alice, exchange.address, min_order_amount)?;
3207
3208 exchange
3209 .create_pair(base_token)
3210 .expect("Could not create pair");
3211
3212 let result = exchange.create_pair(base_token);
3213 assert_eq!(
3214 result,
3215 Err(StablecoinDEXError::pair_already_exists().into())
3216 );
3217
3218 Ok(())
3219 })
3220 }
3221
3222 fn verify_hop(hop: (B256, bool), token_in: Address) -> eyre::Result<()> {
3224 let (book_key, is_base_for_quote) = hop;
3225
3226 let exchange = StablecoinDEX::new();
3227 let orderbook = exchange.books[book_key].read()?;
3228
3229 let expected_book_key = compute_book_key(orderbook.base, orderbook.quote);
3230 assert_eq!(book_key, expected_book_key, "Book key should match");
3231
3232 let expected_direction = token_in == orderbook.base;
3233 assert_eq!(
3234 is_base_for_quote, expected_direction,
3235 "Direction should be correct: token_in={}, base={}, is_base_for_quote={}",
3236 token_in, orderbook.base, is_base_for_quote
3237 );
3238
3239 Ok(())
3240 }
3241
3242 #[test]
3243 fn test_find_path_to_root() -> eyre::Result<()> {
3244 let mut storage = HashMapStorageProvider::new(1);
3245 StorageCtx::enter(&mut storage, || {
3246 let mut exchange = StablecoinDEX::new();
3247 exchange.initialize()?;
3248
3249 let admin = Address::random();
3250
3251 let usdc = TIP20Setup::create("USDC", "USDC", admin).apply()?;
3253 let token_a = TIP20Setup::create("TokenA", "TKA", admin)
3254 .quote_token(usdc.address())
3255 .apply()?;
3256
3257 let path = exchange.find_path_to_root(token_a.address())?;
3259
3260 assert_eq!(path.len(), 3);
3262 assert_eq!(path[0], token_a.address());
3263 assert_eq!(path[1], usdc.address());
3264 assert_eq!(path[2], PATH_USD_ADDRESS);
3265
3266 Ok(())
3267 })
3268 }
3269
3270 #[test]
3271 fn test_find_trade_path_same_token_errors() -> eyre::Result<()> {
3272 let mut storage = HashMapStorageProvider::new(1);
3273 StorageCtx::enter(&mut storage, || {
3274 let mut exchange = StablecoinDEX::new();
3275 exchange.initialize()?;
3276
3277 let admin = Address::random();
3278 let user = Address::random();
3279
3280 let min_order_amount = MIN_ORDER_AMOUNT;
3281 let (token, _) = setup_test_tokens(admin, user, exchange.address, min_order_amount)?;
3282
3283 let result = exchange.find_trade_path(token, token);
3285 assert_eq!(
3286 result,
3287 Err(StablecoinDEXError::identical_tokens().into()),
3288 "Should return IdenticalTokens error when token_in == token_out"
3289 );
3290
3291 Ok(())
3292 })
3293 }
3294
3295 #[test]
3296 fn test_find_trade_path_direct_pair() -> eyre::Result<()> {
3297 let mut storage = HashMapStorageProvider::new(1);
3298 StorageCtx::enter(&mut storage, || {
3299 let mut exchange = StablecoinDEX::new();
3300 exchange.initialize()?;
3301
3302 let admin = Address::random();
3303 let user = Address::random();
3304
3305 let min_order_amount = MIN_ORDER_AMOUNT;
3306 let (token, path_usd) =
3308 setup_test_tokens(admin, user, exchange.address, min_order_amount)?;
3309
3310 exchange.create_pair(token).expect("Failed to create pair");
3312
3313 let route = exchange
3315 .find_trade_path(token, path_usd)
3316 .expect("Should find direct pair");
3317
3318 assert_eq!(route.len(), 1, "Should have 1 hop for direct pair");
3320 verify_hop(route[0], token)?;
3321
3322 Ok(())
3323 })
3324 }
3325
3326 #[test]
3327 fn test_find_trade_path_reverse_pair() -> eyre::Result<()> {
3328 let mut storage = HashMapStorageProvider::new(1);
3329 StorageCtx::enter(&mut storage, || {
3330 let mut exchange = StablecoinDEX::new();
3331 exchange.initialize()?;
3332
3333 let admin = Address::random();
3334 let user = Address::random();
3335
3336 let min_order_amount = MIN_ORDER_AMOUNT;
3337 let (token, path_usd) =
3339 setup_test_tokens(admin, user, exchange.address, min_order_amount)?;
3340
3341 exchange.create_pair(token).expect("Failed to create pair");
3343
3344 let route = exchange
3346 .find_trade_path(path_usd, token)
3347 .expect("Should find reverse pair");
3348
3349 assert_eq!(route.len(), 1, "Should have 1 hop for reverse pair");
3351 verify_hop(route[0], path_usd)?;
3352
3353 Ok(())
3354 })
3355 }
3356
3357 #[test]
3358 fn test_find_trade_path_two_hop_siblings() -> eyre::Result<()> {
3359 let mut storage = HashMapStorageProvider::new(1);
3360 StorageCtx::enter(&mut storage, || {
3361 let mut exchange = StablecoinDEX::new();
3362 exchange.initialize()?;
3363
3364 let admin = Address::random();
3365
3366 let usdc = TIP20Setup::create("USDC", "USDC", admin).apply()?;
3370 let eurc = TIP20Setup::create("EURC", "EURC", admin).apply()?;
3371
3372 exchange.create_pair(usdc.address())?;
3374 exchange.create_pair(eurc.address())?;
3375
3376 let route = exchange.find_trade_path(usdc.address(), eurc.address())?;
3378
3379 assert_eq!(route.len(), 2, "Should have 2 hops for sibling tokens");
3381 verify_hop(route[0], usdc.address())?;
3382 verify_hop(route[1], PATH_USD_ADDRESS)?;
3383
3384 Ok(())
3385 })
3386 }
3387
3388 #[test]
3389 fn test_quote_exact_in_multi_hop() -> eyre::Result<()> {
3390 let mut storage = HashMapStorageProvider::new(1);
3391 StorageCtx::enter(&mut storage, || {
3392 let mut exchange = StablecoinDEX::new();
3393 exchange.initialize()?;
3394
3395 let admin = Address::random();
3396 let alice = Address::random();
3397 let min_order_amount = MIN_ORDER_AMOUNT;
3398 let min_order_amount_x10 = U256::from(MIN_ORDER_AMOUNT * 10);
3399
3400 let _path_usd = TIP20Setup::path_usd(admin)
3403 .with_issuer(admin)
3404 .with_mint(alice, min_order_amount_x10)
3405 .with_approval(alice, exchange.address, min_order_amount_x10)
3406 .apply()?;
3407 let usdc = TIP20Setup::create("USDC", "USDC", admin)
3408 .with_issuer(admin)
3409 .with_mint(alice, min_order_amount_x10)
3410 .with_approval(alice, exchange.address, min_order_amount_x10)
3411 .apply()?;
3412 let eurc = TIP20Setup::create("EURC", "EURC", admin)
3413 .with_issuer(admin)
3414 .with_mint(alice, min_order_amount_x10)
3415 .with_approval(alice, exchange.address, min_order_amount_x10)
3416 .apply()?;
3417
3418 exchange.place(alice, usdc.address(), min_order_amount * 5, true, 0)?;
3425
3426 exchange.place(alice, eurc.address(), min_order_amount * 5, false, 0)?;
3428
3429 let amount_in = min_order_amount;
3431 let amount_out =
3432 exchange.quote_swap_exact_amount_in(usdc.address(), eurc.address(), amount_in)?;
3433
3434 assert_eq!(
3436 amount_out, amount_in,
3437 "With 1:1 rates, output should equal input"
3438 );
3439
3440 Ok(())
3441 })
3442 }
3443
3444 #[test]
3445 fn test_quote_exact_out_multi_hop() -> eyre::Result<()> {
3446 let mut storage = HashMapStorageProvider::new(1);
3447 StorageCtx::enter(&mut storage, || {
3448 let mut exchange = StablecoinDEX::new();
3449 exchange.initialize()?;
3450
3451 let admin = Address::random();
3452 let alice = Address::random();
3453 let min_order_amount = MIN_ORDER_AMOUNT;
3454 let min_order_amount_x10 = U256::from(MIN_ORDER_AMOUNT * 10);
3455
3456 let _path_usd = TIP20Setup::path_usd(admin)
3459 .with_issuer(admin)
3460 .with_mint(alice, min_order_amount_x10)
3461 .with_approval(alice, exchange.address, min_order_amount_x10)
3462 .apply()?;
3463 let usdc = TIP20Setup::create("USDC", "USDC", admin)
3464 .with_issuer(admin)
3465 .with_mint(alice, min_order_amount_x10)
3466 .with_approval(alice, exchange.address, min_order_amount_x10)
3467 .apply()?;
3468 let eurc = TIP20Setup::create("EURC", "EURC", admin)
3469 .with_issuer(admin)
3470 .with_mint(alice, min_order_amount_x10)
3471 .with_approval(alice, exchange.address, min_order_amount_x10)
3472 .apply()?;
3473
3474 exchange.place(alice, usdc.address(), min_order_amount * 5, true, 0)?;
3476 exchange.place(alice, eurc.address(), min_order_amount * 5, false, 0)?;
3477
3478 let amount_out = min_order_amount;
3480 let amount_in =
3481 exchange.quote_swap_exact_amount_out(usdc.address(), eurc.address(), amount_out)?;
3482
3483 assert_eq!(
3486 amount_in, amount_out,
3487 "With 1:1 rates and no rounding, input should equal output"
3488 );
3489
3490 Ok(())
3491 })
3492 }
3493
3494 #[test]
3495 fn test_swap_exact_in_multi_hop_transitory_balances() -> eyre::Result<()> {
3496 let mut storage = HashMapStorageProvider::new(1);
3497 StorageCtx::enter(&mut storage, || {
3498 let mut exchange = StablecoinDEX::new();
3499 exchange.initialize()?;
3500
3501 let admin = Address::random();
3502 let alice = Address::random();
3503 let bob = Address::random();
3504
3505 let min_order_amount = MIN_ORDER_AMOUNT;
3506 let min_order_amount_x10 = U256::from(MIN_ORDER_AMOUNT * 10);
3507
3508 let path_usd = TIP20Setup::path_usd(admin)
3510 .with_issuer(admin)
3511 .with_mint(alice, min_order_amount_x10)
3513 .with_approval(alice, exchange.address, min_order_amount_x10)
3514 .apply()?;
3515
3516 let usdc = TIP20Setup::create("USDC", "USDC", admin)
3517 .with_issuer(admin)
3518 .with_mint(alice, min_order_amount_x10)
3520 .with_approval(alice, exchange.address, min_order_amount_x10)
3521 .with_mint(bob, min_order_amount_x10)
3523 .with_approval(bob, exchange.address, min_order_amount_x10)
3524 .apply()?;
3525
3526 let eurc = TIP20Setup::create("EURC", "EURC", admin)
3527 .with_issuer(admin)
3528 .with_mint(alice, min_order_amount_x10)
3530 .with_approval(alice, exchange.address, min_order_amount_x10)
3531 .apply()?;
3532
3533 exchange.place(alice, usdc.address(), min_order_amount * 5, true, 0)?;
3535 exchange.place(alice, eurc.address(), min_order_amount * 5, false, 0)?;
3536
3537 let bob_usdc_before = usdc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3539 let bob_eurc_before = eurc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3540
3541 let amount_in = min_order_amount;
3543 let amount_out = exchange.swap_exact_amount_in(
3544 bob,
3545 usdc.address(),
3546 eurc.address(),
3547 amount_in,
3548 0, )?;
3550
3551 let bob_usdc_after = usdc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3553 let bob_eurc_after = eurc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3554
3555 assert_eq!(
3557 bob_usdc_before - bob_usdc_after,
3558 U256::from(amount_in),
3559 "Bob should have spent exact amount_in USDC"
3560 );
3561 assert_eq!(
3562 bob_eurc_after - bob_eurc_before,
3563 U256::from(amount_out),
3564 "Bob should have received amount_out EURC"
3565 );
3566
3567 let bob_path_usd_wallet =
3569 path_usd.balance_of(ITIP20::balanceOfCall { account: bob })?;
3570 assert_eq!(
3571 bob_path_usd_wallet,
3572 U256::ZERO,
3573 "Bob should have ZERO pathUSD in wallet (transitory)"
3574 );
3575
3576 let bob_path_usd_exchange = exchange.balance_of(bob, path_usd.address())?;
3577 assert_eq!(
3578 bob_path_usd_exchange, 0,
3579 "Bob should have ZERO pathUSD on exchange (transitory)"
3580 );
3581
3582 Ok(())
3583 })
3584 }
3585
3586 #[test]
3587 fn test_swap_exact_out_multi_hop_transitory_balances() -> eyre::Result<()> {
3588 let mut storage = HashMapStorageProvider::new(1);
3589 StorageCtx::enter(&mut storage, || {
3590 let mut exchange = StablecoinDEX::new();
3591 exchange.initialize()?;
3592
3593 let admin = Address::random();
3594 let alice = Address::random();
3595 let bob = Address::random();
3596
3597 let min_order_amount = MIN_ORDER_AMOUNT;
3598 let min_order_amount_x10 = U256::from(MIN_ORDER_AMOUNT * 10);
3599
3600 let path_usd = TIP20Setup::path_usd(admin)
3602 .with_issuer(admin)
3603 .with_mint(alice, min_order_amount_x10)
3605 .with_approval(alice, exchange.address, min_order_amount_x10)
3606 .apply()?;
3607
3608 let usdc = TIP20Setup::create("USDC", "USDC", admin)
3609 .with_issuer(admin)
3610 .with_mint(alice, min_order_amount_x10)
3612 .with_approval(alice, exchange.address, min_order_amount_x10)
3613 .with_mint(bob, min_order_amount_x10)
3615 .with_approval(bob, exchange.address, min_order_amount_x10)
3616 .apply()?;
3617
3618 let eurc = TIP20Setup::create("EURC", "EURC", admin)
3619 .with_issuer(admin)
3620 .with_mint(alice, min_order_amount_x10)
3622 .with_approval(alice, exchange.address, min_order_amount_x10)
3623 .apply()?;
3624
3625 exchange.place(alice, usdc.address(), min_order_amount * 5, true, 0)?;
3627 exchange.place(alice, eurc.address(), min_order_amount * 5, false, 0)?;
3628
3629 let bob_usdc_before = usdc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3631 let bob_eurc_before = eurc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3632
3633 let amount_out = 90u128;
3635 let amount_in = exchange.swap_exact_amount_out(
3636 bob,
3637 usdc.address(),
3638 eurc.address(),
3639 amount_out,
3640 u128::MAX, )?;
3642
3643 let bob_usdc_after = usdc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3645 let bob_eurc_after = eurc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3646
3647 assert_eq!(
3649 bob_usdc_before - bob_usdc_after,
3650 U256::from(amount_in),
3651 "Bob should have spent amount_in USDC"
3652 );
3653 assert_eq!(
3654 bob_eurc_after - bob_eurc_before,
3655 U256::from(amount_out),
3656 "Bob should have received exact amount_out EURC"
3657 );
3658
3659 let bob_path_usd_wallet =
3661 path_usd.balance_of(ITIP20::balanceOfCall { account: bob })?;
3662 assert_eq!(
3663 bob_path_usd_wallet,
3664 U256::ZERO,
3665 "Bob should have ZERO pathUSD in wallet (transitory)"
3666 );
3667
3668 let bob_path_usd_exchange = exchange
3669 .balance_of(bob, path_usd.address())
3670 .expect("Failed to get bob's pathUSD exchange balance");
3671 assert_eq!(
3672 bob_path_usd_exchange, 0,
3673 "Bob should have ZERO pathUSD on exchange (transitory)"
3674 );
3675
3676 Ok(())
3677 })
3678 }
3679
3680 #[test]
3681 fn test_create_pair_invalid_currency() -> eyre::Result<()> {
3682 let mut storage = HashMapStorageProvider::new(1);
3683 StorageCtx::enter(&mut storage, || {
3684 let admin = Address::random();
3685
3686 let token_0 = TIP20Setup::create("EuroToken", "EURO", admin)
3688 .currency("EUR")
3689 .apply()?;
3690
3691 let mut exchange = StablecoinDEX::new();
3692 exchange.initialize()?;
3693
3694 let result = exchange.create_pair(token_0.address());
3696 assert!(matches!(
3697 result,
3698 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
3699 ));
3700
3701 Ok(())
3702 })
3703 }
3704
3705 #[test]
3706 fn test_create_pair_rejects_non_tip20_base() -> eyre::Result<()> {
3707 let mut storage = HashMapStorageProvider::new(1);
3708 StorageCtx::enter(&mut storage, || {
3709 let admin = Address::random();
3710 let _path_usd = TIP20Setup::path_usd(admin).apply()?;
3711
3712 let mut exchange = StablecoinDEX::new();
3713 exchange.initialize()?;
3714
3715 let non_tip20_address = Address::random();
3717 let result = exchange.create_pair(non_tip20_address);
3718 assert!(matches!(
3719 result,
3720 Err(TempoPrecompileError::StablecoinDEX(
3721 StablecoinDEXError::InvalidBaseToken(_)
3722 ))
3723 ));
3724
3725 Ok(())
3726 })
3727 }
3728
3729 #[test]
3730 fn test_max_in_check() -> eyre::Result<()> {
3731 let mut storage = HashMapStorageProvider::new(1);
3732 StorageCtx::enter(&mut storage, || {
3733 let mut exchange = StablecoinDEX::new();
3734 exchange.initialize()?;
3735
3736 let alice = Address::random();
3737 let bob = Address::random();
3738 let admin = Address::random();
3739
3740 let (base_token, quote_token) =
3741 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
3742 exchange.create_pair(base_token)?;
3743
3744 let tick_50 = 50i16;
3745 let tick_100 = 100i16;
3746 let order_amount = MIN_ORDER_AMOUNT;
3747
3748 exchange.place(alice, base_token, order_amount, false, tick_50)?;
3749 exchange.place(alice, base_token, order_amount, false, tick_100)?;
3750
3751 exchange.set_balance(bob, quote_token, 200_000_000u128)?;
3752
3753 let price_50 = orderbook::tick_to_price(tick_50);
3754 let price_100 = orderbook::tick_to_price(tick_100);
3755 let quote_for_first =
3757 (order_amount * u128::from(price_50)).div_ceil(u128::from(orderbook::PRICE_SCALE));
3758 let quote_for_partial_second =
3759 (999 * u128::from(price_100)).div_ceil(u128::from(orderbook::PRICE_SCALE));
3760 let total_needed = quote_for_first + quote_for_partial_second;
3761
3762 let result = exchange.swap_exact_amount_out(
3763 bob,
3764 quote_token,
3765 base_token,
3766 order_amount + 999,
3767 total_needed,
3768 );
3769 assert!(result.is_ok());
3770
3771 Ok(())
3772 })
3773 }
3774
3775 #[test]
3776 fn test_exact_out_bid_side() -> eyre::Result<()> {
3777 let mut storage = HashMapStorageProvider::new(1);
3778 StorageCtx::enter(&mut storage, || {
3779 let mut exchange = StablecoinDEX::new();
3780 exchange.initialize()?;
3781
3782 let alice = Address::random();
3783 let bob = Address::random();
3784 let admin = Address::random();
3785
3786 let (base_token, quote_token) =
3787 setup_test_tokens(admin, alice, exchange.address, 1_000_000_000u128)?;
3788 exchange.create_pair(base_token)?;
3789
3790 let tick = 1000i16;
3791 let price = tick_to_price(tick);
3792 let order_amount_base = MIN_ORDER_AMOUNT;
3793
3794 exchange.place(alice, base_token, order_amount_base, true, tick)?;
3795
3796 let amount_out_quote = 5_000_000u128;
3797 let base_needed = (amount_out_quote * u128::from(PRICE_SCALE)) / u128::from(price);
3798 let max_amount_in = base_needed + 10000;
3799
3800 exchange.set_balance(bob, base_token, max_amount_in * 2)?;
3801
3802 let _amount_in = exchange.swap_exact_amount_out(
3803 bob,
3804 base_token,
3805 quote_token,
3806 amount_out_quote,
3807 max_amount_in,
3808 )?;
3809
3810 let bob_quote_balance = TIP20Token::from_address(quote_token)?
3812 .balance_of(ITIP20::balanceOfCall { account: bob })?;
3813 assert_eq!(bob_quote_balance, U256::from(amount_out_quote));
3814
3815 Ok(())
3816 })
3817 }
3818
3819 #[test]
3820 fn test_exact_in_ask_side() -> eyre::Result<()> {
3821 let mut storage = HashMapStorageProvider::new(1);
3822 StorageCtx::enter(&mut storage, || {
3823 let mut exchange = StablecoinDEX::new();
3824 exchange.initialize()?;
3825
3826 let alice = Address::random();
3827 let bob = Address::random();
3828 let admin = Address::random();
3829
3830 let (base_token, quote_token) =
3831 setup_test_tokens(admin, alice, exchange.address, 1_000_000_000u128)?;
3832 exchange.create_pair(base_token)?;
3833
3834 let tick = 1000i16;
3835 let price = tick_to_price(tick);
3836 let order_amount_base = MIN_ORDER_AMOUNT;
3837
3838 exchange.place(alice, base_token, order_amount_base, false, tick)?;
3839
3840 let amount_in_quote = 5_000_000u128;
3841 let min_amount_out = 0;
3842
3843 exchange.set_balance(bob, quote_token, amount_in_quote * 2)?;
3844
3845 let amount_out = exchange.swap_exact_amount_in(
3846 bob,
3847 quote_token,
3848 base_token,
3849 amount_in_quote,
3850 min_amount_out,
3851 )?;
3852
3853 let expected_base = (amount_in_quote * u128::from(PRICE_SCALE)) / u128::from(price);
3854 assert_eq!(amount_out, expected_base);
3855
3856 Ok(())
3857 })
3858 }
3859
3860 #[test]
3861 fn test_clear_order() -> eyre::Result<()> {
3862 const AMOUNT: u128 = 1_000_000_000;
3863
3864 let mut storage = HashMapStorageProvider::new(1);
3866 StorageCtx::enter(&mut storage, || {
3867 let mut exchange = StablecoinDEX::new();
3868 exchange.initialize()?;
3869
3870 let alice = Address::random();
3871 let bob = Address::random();
3872 let carol = Address::random();
3873 let admin = Address::random();
3874
3875 let (base_token, quote_token) =
3876 setup_test_tokens(admin, alice, exchange.address, AMOUNT)?;
3877 exchange.create_pair(base_token)?;
3878
3879 TIP20Setup::config(base_token)
3881 .with_mint(bob, U256::from(AMOUNT))
3882 .with_approval(bob, exchange.address, U256::from(AMOUNT))
3883 .apply()?;
3884 TIP20Setup::config(quote_token)
3885 .with_mint(carol, U256::from(AMOUNT))
3886 .with_approval(carol, exchange.address, U256::from(AMOUNT))
3887 .apply()?;
3888
3889 let tick = 100i16;
3890
3891 let order1_amount = MIN_ORDER_AMOUNT;
3893 let order2_amount = MIN_ORDER_AMOUNT;
3894
3895 let order1_id = exchange.place(alice, base_token, order1_amount, false, tick)?;
3896 let order2_id = exchange.place(bob, base_token, order2_amount, false, tick)?;
3897
3898 let order1 = exchange.orders[order1_id].read()?;
3900 let order2 = exchange.orders[order2_id].read()?;
3901 assert_eq!(order1.next(), order2_id);
3902 assert_eq!(order2.prev(), order1_id);
3903
3904 let swap_amount = order1_amount;
3906 exchange.swap_exact_amount_out(
3907 carol,
3908 quote_token,
3909 base_token,
3910 swap_amount,
3911 u128::MAX,
3912 )?;
3913
3914 let order2_after = exchange.orders[order2_id].read()?;
3916 assert_eq!(
3917 order2_after.prev(),
3918 0,
3919 "New head order should have prev = 0 after previous head was filled"
3920 );
3921
3922 Ok(())
3923 })
3924 }
3925
3926 #[test]
3927 fn test_best_tick_updates_on_fill() -> eyre::Result<()> {
3928 let mut storage = HashMapStorageProvider::new(1);
3929 StorageCtx::enter(&mut storage, || {
3930 let mut exchange = StablecoinDEX::new();
3931 exchange.initialize()?;
3932
3933 let alice = Address::random();
3934 let bob = Address::random();
3935 let admin = Address::random();
3936 let amount = MIN_ORDER_AMOUNT;
3937
3938 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);
3944 let bid_price_2 = orderbook::tick_to_price(bid_tick_2);
3945 let bid_escrow_1 =
3946 (amount * u128::from(bid_price_1)) / u128::from(orderbook::PRICE_SCALE);
3947 let bid_escrow_2 =
3948 (amount * u128::from(bid_price_2)) / u128::from(orderbook::PRICE_SCALE);
3949 let total_bid_escrow = bid_escrow_1 + bid_escrow_2;
3950
3951 let (base_token, quote_token) =
3952 setup_test_tokens(admin, alice, exchange.address, total_bid_escrow)?;
3953 exchange.create_pair(base_token)?;
3954 let book_key = compute_book_key(base_token, quote_token);
3955
3956 exchange.place(alice, base_token, amount, true, bid_tick_1)?;
3958 exchange.place(alice, base_token, amount, true, bid_tick_2)?;
3959
3960 TIP20Setup::config(base_token)
3962 .with_mint(alice, U256::from(amount * 2))
3963 .with_approval(alice, exchange.address, U256::from(amount * 2))
3964 .apply()?;
3965 exchange.place(alice, base_token, amount, false, ask_tick_1)?;
3966 exchange.place(alice, base_token, amount, false, ask_tick_2)?;
3967
3968 let orderbook = exchange.books[book_key].read()?;
3970 assert_eq!(orderbook.best_bid_tick, bid_tick_1);
3971 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3972
3973 exchange.set_balance(bob, base_token, amount)?;
3975 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
3976 let orderbook = exchange.books[book_key].read()?;
3978 assert_eq!(orderbook.best_bid_tick, bid_tick_2);
3979 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3980
3981 exchange.set_balance(bob, base_token, amount)?;
3983 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
3984 let orderbook = exchange.books[book_key].read()?;
3986 assert_eq!(orderbook.best_bid_tick, i16::MIN);
3987 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3988
3989 let ask_price_1 = orderbook::tick_to_price(ask_tick_1);
3991 let quote_needed =
3992 (amount * u128::from(ask_price_1)) / u128::from(orderbook::PRICE_SCALE);
3993 exchange.set_balance(bob, quote_token, quote_needed)?;
3994 exchange.swap_exact_amount_in(bob, quote_token, base_token, quote_needed, 0)?;
3995 let orderbook = exchange.books[book_key].read()?;
3997 assert_eq!(orderbook.best_ask_tick, ask_tick_2);
3998 assert_eq!(orderbook.best_bid_tick, i16::MIN);
3999
4000 Ok(())
4001 })
4002 }
4003
4004 #[test]
4005 fn test_best_tick_updates_on_cancel() -> eyre::Result<()> {
4006 let mut storage = HashMapStorageProvider::new(1);
4007 StorageCtx::enter(&mut storage, || {
4008 let mut exchange = StablecoinDEX::new();
4009 exchange.initialize()?;
4010
4011 let alice = Address::random();
4012 let admin = Address::random();
4013 let amount = MIN_ORDER_AMOUNT;
4014
4015 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);
4020 let price_2 = orderbook::tick_to_price(bid_tick_2);
4021 let escrow_1 = (amount * u128::from(price_1)) / u128::from(orderbook::PRICE_SCALE);
4022 let escrow_2 = (amount * u128::from(price_2)) / u128::from(orderbook::PRICE_SCALE);
4023 let total_escrow = escrow_1 * 2 + escrow_2;
4024
4025 let (base_token, quote_token) =
4026 setup_test_tokens(admin, alice, exchange.address, total_escrow)?;
4027 exchange.create_pair(base_token)?;
4028 let book_key = compute_book_key(base_token, quote_token);
4029
4030 let bid_order_1 = exchange.place(alice, base_token, amount, true, bid_tick_1)?;
4032 let bid_order_2 = exchange.place(alice, base_token, amount, true, bid_tick_1)?;
4033 let bid_order_3 = exchange.place(alice, base_token, amount, true, bid_tick_2)?;
4034
4035 TIP20Setup::config(base_token)
4037 .with_mint(alice, U256::from(amount * 2))
4038 .with_approval(alice, exchange.address, U256::from(amount * 2))
4039 .apply()?;
4040 let ask_order_1 = exchange.place(alice, base_token, amount, false, ask_tick_1)?;
4041 let ask_order_2 = exchange.place(alice, base_token, amount, false, ask_tick_2)?;
4042
4043 let orderbook = exchange.books[book_key].read()?;
4045 assert_eq!(orderbook.best_bid_tick, bid_tick_1);
4046 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
4047
4048 exchange.cancel(alice, bid_order_1)?;
4050 let orderbook = exchange.books[book_key].read()?;
4052 assert_eq!(orderbook.best_bid_tick, bid_tick_1);
4053 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
4054
4055 exchange.cancel(alice, bid_order_2)?;
4057 let orderbook = exchange.books[book_key].read()?;
4059 assert_eq!(orderbook.best_bid_tick, bid_tick_2);
4060 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
4061
4062 exchange.cancel(alice, ask_order_1)?;
4064 let orderbook = exchange.books[book_key].read()?;
4066 assert_eq!(orderbook.best_bid_tick, bid_tick_2);
4067 assert_eq!(orderbook.best_ask_tick, ask_tick_2);
4068
4069 exchange.cancel(alice, bid_order_3)?;
4071 let orderbook = exchange.books[book_key].read()?;
4073 assert_eq!(orderbook.best_bid_tick, i16::MIN);
4074 assert_eq!(orderbook.best_ask_tick, ask_tick_2);
4075
4076 exchange.cancel(alice, ask_order_2)?;
4078 let orderbook = exchange.books[book_key].read()?;
4080 assert_eq!(orderbook.best_bid_tick, i16::MIN);
4081 assert_eq!(orderbook.best_ask_tick, i16::MAX);
4082
4083 Ok(())
4084 })
4085 }
4086
4087 #[test]
4088 fn test_place() -> eyre::Result<()> {
4089 const AMOUNT: u128 = 1_000_000_000;
4090
4091 let mut storage = HashMapStorageProvider::new(1);
4092 StorageCtx::enter(&mut storage, || {
4093 let mut exchange = StablecoinDEX::new();
4094 exchange.initialize()?;
4095
4096 let alice = Address::random();
4097 let admin = Address::random();
4098
4099 let (base_token, _quote_token) =
4100 setup_test_tokens(admin, alice, exchange.address, AMOUNT)?;
4101 exchange.create_pair(base_token)?;
4102
4103 TIP20Setup::config(base_token)
4105 .with_mint(alice, U256::from(AMOUNT))
4106 .with_approval(alice, exchange.address, U256::from(AMOUNT))
4107 .apply()?;
4108
4109 let invalid_tick = 15i16;
4111 let result = exchange.place(alice, base_token, MIN_ORDER_AMOUNT, true, invalid_tick);
4112
4113 let error = result.unwrap_err();
4114 assert!(matches!(
4115 error,
4116 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidTick(_))
4117 ));
4118
4119 let valid_tick = -20i16;
4121 let result = exchange.place(alice, base_token, MIN_ORDER_AMOUNT, true, valid_tick);
4122 assert!(result.is_ok());
4123
4124 Ok(())
4125 })
4126 }
4127
4128 #[test]
4129 fn test_place_flip_checks() -> eyre::Result<()> {
4130 const AMOUNT: u128 = 1_000_000_000;
4131
4132 let mut storage = HashMapStorageProvider::new(1);
4133 StorageCtx::enter(&mut storage, || {
4134 let mut exchange = StablecoinDEX::new();
4135 exchange.initialize()?;
4136
4137 let alice = Address::random();
4138 let admin = Address::random();
4139
4140 let (base_token, _quote_token) =
4141 setup_test_tokens(admin, alice, exchange.address, AMOUNT)?;
4142 exchange.create_pair(base_token)?;
4143
4144 TIP20Setup::config(base_token)
4146 .with_mint(alice, U256::from(AMOUNT))
4147 .with_approval(alice, exchange.address, U256::from(AMOUNT))
4148 .apply()?;
4149
4150 let invalid_tick = 15i16;
4152 let invalid_flip_tick = 25i16;
4153 let result = exchange.place_flip(
4154 alice,
4155 base_token,
4156 MIN_ORDER_AMOUNT,
4157 true,
4158 invalid_tick,
4159 invalid_flip_tick,
4160 false,
4161 );
4162
4163 let error = result.unwrap_err();
4164 assert!(matches!(
4165 error,
4166 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidTick(_))
4167 ));
4168
4169 let valid_tick = 20i16;
4171 let invalid_flip_tick = 25i16;
4172 let result = exchange.place_flip(
4173 alice,
4174 base_token,
4175 MIN_ORDER_AMOUNT,
4176 true,
4177 valid_tick,
4178 invalid_flip_tick,
4179 false,
4180 );
4181
4182 let error = result.unwrap_err();
4183 assert!(matches!(
4184 error,
4185 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidFlipTick(_))
4186 ));
4187
4188 let valid_flip_tick = 30i16;
4189 let result = exchange.place_flip(
4190 alice,
4191 base_token,
4192 MIN_ORDER_AMOUNT,
4193 true,
4194 valid_tick,
4195 valid_flip_tick,
4196 false,
4197 );
4198 assert!(result.is_ok());
4199
4200 Ok(())
4201 })
4202 }
4203
4204 #[test]
4205 fn test_find_trade_path_rejects_non_tip20() -> eyre::Result<()> {
4206 let mut storage = HashMapStorageProvider::new(1);
4207 StorageCtx::enter(&mut storage, || {
4208 let mut exchange = StablecoinDEX::new();
4209 exchange.initialize()?;
4210
4211 let admin = Address::random();
4212 let user = Address::random();
4213
4214 let (_, quote_token) =
4215 setup_test_tokens(admin, user, exchange.address, MIN_ORDER_AMOUNT)?;
4216
4217 let non_tip20_address = Address::random();
4218 let result = exchange.find_trade_path(non_tip20_address, quote_token);
4219 assert!(
4220 matches!(
4221 result,
4222 Err(TempoPrecompileError::StablecoinDEX(
4223 StablecoinDEXError::InvalidToken(_)
4224 ))
4225 ),
4226 "Should return InvalidToken error for non-TIP20 token"
4227 );
4228
4229 Ok(())
4230 })
4231 }
4232
4233 #[test]
4234 fn test_quote_exact_in_handles_both_directions() -> eyre::Result<()> {
4235 let mut storage = HashMapStorageProvider::new(1);
4236 StorageCtx::enter(&mut storage, || {
4237 let mut exchange = StablecoinDEX::new();
4238 exchange.initialize()?;
4239
4240 let alice = Address::random();
4241 let admin = Address::random();
4242 let amount = MIN_ORDER_AMOUNT;
4243 let tick = 100_i16;
4244 let price = orderbook::tick_to_price(tick);
4245
4246 let bid_escrow = (amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
4248
4249 let (base_token, quote_token) =
4250 setup_test_tokens(admin, alice, exchange.address, bid_escrow)?;
4251
4252 TIP20Setup::config(base_token)
4253 .with_mint(alice, U256::from(amount))
4254 .with_approval(alice, exchange.address, U256::from(amount))
4255 .apply()?;
4256
4257 exchange.create_pair(base_token)?;
4258 let book_key = compute_book_key(base_token, quote_token);
4259
4260 exchange.place(alice, base_token, amount, true, tick)?;
4262
4263 let quoted_out_bid = exchange.quote_exact_in(book_key, amount, true)?;
4265 let expected_quote_out = amount
4266 .checked_mul(u128::from(price))
4267 .and_then(|v| v.checked_div(u128::from(orderbook::PRICE_SCALE)))
4268 .expect("calculation");
4269 assert_eq!(
4270 quoted_out_bid, expected_quote_out,
4271 "quote_exact_in with is_bid=true should return quote amount"
4272 );
4273
4274 exchange.place(alice, base_token, amount, false, tick)?;
4276
4277 let quote_in = (amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
4279 let quoted_out_ask = exchange.quote_exact_in(book_key, quote_in, false)?;
4280 let expected_base_out = quote_in
4281 .checked_mul(u128::from(orderbook::PRICE_SCALE))
4282 .and_then(|v| v.checked_div(u128::from(price)))
4283 .expect("calculation");
4284 assert_eq!(
4285 quoted_out_ask, expected_base_out,
4286 "quote_exact_in with is_bid=false should return base amount"
4287 );
4288
4289 Ok(())
4290 })
4291 }
4292
4293 #[test]
4294 fn test_place_auto_creates_pair() -> Result<()> {
4295 let mut storage = HashMapStorageProvider::new(1);
4296 StorageCtx::enter(&mut storage, || {
4297 let mut exchange = StablecoinDEX::new();
4298 exchange.initialize()?;
4299 let admin = Address::random();
4300 let user = Address::random();
4301
4302 let (base_token, quote_token) =
4304 setup_test_tokens(admin, user, exchange.address, 100_000_000)?;
4305
4306 let book_key = compute_book_key(base_token, quote_token);
4308 let book_before = exchange.books[book_key].read()?;
4309 assert!(book_before.base.is_zero(),);
4310
4311 let mut base = TIP20Token::from_address(base_token)?;
4313 base.transfer(
4314 user,
4315 ITIP20::transferCall {
4316 to: exchange.address,
4317 amount: U256::from(MIN_ORDER_AMOUNT),
4318 },
4319 )
4320 .expect("Base token transfer failed");
4321
4322 exchange.place(user, base_token, MIN_ORDER_AMOUNT, true, 0)?;
4324
4325 let book_after = exchange.books[book_key].read()?;
4326 assert_eq!(book_after.base, base_token);
4327
4328 let events = exchange.emitted_events();
4330 assert_eq!(events.len(), 2);
4331 assert_eq!(
4332 events[0],
4333 StablecoinDEXEvents::pair_created(book_key, base_token, quote_token)
4334 .into_log_data()
4335 );
4336
4337 Ok(())
4338 })
4339 }
4340
4341 #[test]
4342 fn test_decrement_balance_preserves_balance() -> eyre::Result<()> {
4343 let mut storage = HashMapStorageProvider::new(1);
4344 StorageCtx::enter(&mut storage, || {
4345 let mut exchange = StablecoinDEX::new();
4346 exchange.initialize()?;
4347
4348 let admin = Address::random();
4349 let alice = Address::random();
4350
4351 let base = TIP20Setup::create("BASE", "BASE", admin).apply()?;
4352 let base_address = base.address();
4353
4354 exchange.create_pair(base_address)?;
4355
4356 let internal_balance = MIN_ORDER_AMOUNT / 2;
4357 exchange.set_balance(alice, base_address, internal_balance)?;
4358
4359 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
4360
4361 let tick = 0i16;
4362 let result = exchange.place(alice, base_address, MIN_ORDER_AMOUNT * 2, false, tick);
4363
4364 assert!(result.is_err());
4365 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
4366
4367 Ok(())
4368 })
4369 }
4370
4371 #[test]
4372 fn test_place_order_immediately_active() -> eyre::Result<()> {
4373 let mut storage = HashMapStorageProvider::new(1);
4374 StorageCtx::enter(&mut storage, || {
4375 let mut exchange = StablecoinDEX::new();
4376 exchange.initialize()?;
4377
4378 let admin = Address::random();
4379 let alice = Address::random();
4380 let min_order_amount = MIN_ORDER_AMOUNT;
4381 let tick = 100i16;
4382
4383 let price = orderbook::tick_to_price(tick);
4384 let expected_escrow =
4385 (min_order_amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
4386
4387 TIP20Setup::path_usd(admin)
4388 .with_issuer(admin)
4389 .with_mint(alice, U256::from(expected_escrow))
4390 .with_approval(alice, exchange.address, U256::from(expected_escrow))
4391 .apply()?;
4392
4393 let base = TIP20Setup::create("BASE", "BASE", admin).apply()?;
4394 let base_token = base.address();
4395 let quote_token = base.quote_token()?;
4396
4397 exchange.create_pair(base_token)?;
4398
4399 let order_id = exchange.place(alice, base_token, min_order_amount, true, tick)?;
4400
4401 assert_eq!(order_id, 1);
4402
4403 let book_key = compute_book_key(base_token, quote_token);
4404 let book_handler = &exchange.books[book_key];
4405 let level = book_handler.tick_level_handler(tick, true).read()?;
4406 assert_eq!(level.head, order_id, "Order should be head of tick level");
4407 assert_eq!(level.tail, order_id, "Order should be tail of tick level");
4408 assert_eq!(
4409 level.total_liquidity, min_order_amount,
4410 "Tick level should have order's liquidity"
4411 );
4412
4413 let orderbook = book_handler.read()?;
4414 assert_eq!(
4415 orderbook.best_bid_tick, tick,
4416 "Best bid tick should be updated"
4417 );
4418
4419 Ok(())
4420 })
4421 }
4422
4423 #[test]
4424 fn test_place_flip_order_immediately_active() -> eyre::Result<()> {
4425 let mut storage = HashMapStorageProvider::new(1);
4426 StorageCtx::enter(&mut storage, || {
4427 let mut exchange = StablecoinDEX::new();
4428 exchange.initialize()?;
4429
4430 let admin = Address::random();
4431 let alice = Address::random();
4432 let min_order_amount = MIN_ORDER_AMOUNT;
4433 let tick = 100i16;
4434 let flip_tick = 200i16;
4435
4436 let price = orderbook::tick_to_price(tick);
4437 let expected_escrow =
4438 (min_order_amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
4439
4440 TIP20Setup::path_usd(admin)
4441 .with_issuer(admin)
4442 .with_mint(alice, U256::from(expected_escrow))
4443 .with_approval(alice, exchange.address, U256::from(expected_escrow))
4444 .apply()?;
4445
4446 let base = TIP20Setup::create("BASE", "BASE", admin).apply()?;
4447 let base_token = base.address();
4448 let quote_token = base.quote_token()?;
4449
4450 exchange.create_pair(base_token)?;
4451
4452 let order_id = exchange.place_flip(
4453 alice,
4454 base_token,
4455 min_order_amount,
4456 true,
4457 tick,
4458 flip_tick,
4459 false,
4460 )?;
4461
4462 assert_eq!(order_id, 1);
4463
4464 let book_key = compute_book_key(base_token, quote_token);
4465 let book_handler = &exchange.books[book_key];
4466 let level = book_handler.tick_level_handler(tick, true).read()?;
4467 assert_eq!(level.head, order_id, "Order should be head of tick level");
4468 assert_eq!(level.tail, order_id, "Order should be tail of tick level");
4469 assert_eq!(
4470 level.total_liquidity, min_order_amount,
4471 "Tick level should have order's liquidity"
4472 );
4473
4474 let orderbook = book_handler.read()?;
4475 assert_eq!(
4476 orderbook.best_bid_tick, tick,
4477 "Best bid tick should be updated"
4478 );
4479
4480 let stored_order = exchange.orders[order_id].read()?;
4481 assert!(stored_order.is_flip(), "Order should be a flip order");
4482 assert_eq!(
4483 stored_order.flip_tick(),
4484 flip_tick,
4485 "Flip tick should match"
4486 );
4487
4488 Ok(())
4489 })
4490 }
4491
4492 #[test]
4493 fn test_place_post() -> eyre::Result<()> {
4494 let mut storage = HashMapStorageProvider::new(1);
4495 StorageCtx::enter(&mut storage, || {
4496 let mut exchange = StablecoinDEX::new();
4497 exchange.initialize()?;
4498
4499 let admin = Address::random();
4500 let alice = Address::random();
4501 let min_order_amount = MIN_ORDER_AMOUNT;
4502 let tick = 100i16;
4503
4504 let price = orderbook::tick_to_price(tick);
4505 let expected_escrow =
4506 (min_order_amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
4507
4508 TIP20Setup::path_usd(admin)
4509 .with_issuer(admin)
4510 .with_mint(alice, U256::from(expected_escrow))
4511 .with_approval(alice, exchange.address, U256::from(expected_escrow))
4512 .apply()?;
4513
4514 let base = TIP20Setup::create("BASE", "BASE", admin).apply()?;
4515 let base_token = base.address();
4516 let quote_token = base.quote_token()?;
4517
4518 exchange.create_pair(base_token)?;
4519
4520 let order_id = exchange.place(alice, base_token, min_order_amount, true, tick)?;
4521
4522 let stored_order = exchange.orders[order_id].read()?;
4523 assert_eq!(stored_order.maker(), alice);
4524 assert_eq!(stored_order.remaining(), min_order_amount);
4525 assert_eq!(stored_order.tick(), tick);
4526 assert!(stored_order.is_bid());
4527
4528 let book_key = compute_book_key(base_token, quote_token);
4529 let level = exchange.books[book_key]
4530 .tick_level_handler(tick, true)
4531 .read()?;
4532 assert_eq!(level.head, order_id);
4533 assert_eq!(level.tail, order_id);
4534 assert_eq!(level.total_liquidity, min_order_amount);
4535
4536 let book = exchange.books[book_key].read()?;
4537 assert_eq!(book.best_bid_tick, tick);
4538
4539 assert_eq!(exchange.next_order_id()?, 2);
4540
4541 Ok(())
4542 })
4543 }
4544
4545 #[test]
4546 fn test_blacklisted_user_cannot_use_internal_balance() -> eyre::Result<()> {
4547 use crate::tip403_registry::{ITIP403Registry, TIP403Registry};
4548
4549 let mut storage = HashMapStorageProvider::new(1);
4550 StorageCtx::enter(&mut storage, || {
4551 let mut exchange = StablecoinDEX::new();
4552 exchange.initialize()?;
4553
4554 let alice = Address::random();
4555 let admin = Address::random();
4556
4557 let mut registry = TIP403Registry::new();
4559 let policy_id = registry.create_policy(
4560 admin,
4561 ITIP403Registry::createPolicyCall {
4562 admin,
4563 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4564 },
4565 )?;
4566
4567 let mut quote = TIP20Setup::path_usd(admin).with_issuer(admin).apply()?;
4569
4570 quote.change_transfer_policy_id(
4571 admin,
4572 ITIP20::changeTransferPolicyIdCall {
4573 newPolicyId: policy_id,
4574 },
4575 )?;
4576
4577 let mut base = TIP20Setup::create("BASE", "BASE", admin)
4579 .with_issuer(admin)
4580 .apply()?;
4581 let base_address = base.address();
4582
4583 base.change_transfer_policy_id(
4584 admin,
4585 ITIP20::changeTransferPolicyIdCall {
4586 newPolicyId: policy_id,
4587 },
4588 )?;
4589
4590 exchange.create_pair(base_address)?;
4591
4592 let internal_balance = MIN_ORDER_AMOUNT * 2;
4594 exchange.set_balance(alice, base_address, internal_balance)?;
4595 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
4596
4597 registry.modify_policy_blacklist(
4599 admin,
4600 ITIP403Registry::modifyPolicyBlacklistCall {
4601 policyId: policy_id,
4602 account: alice,
4603 restricted: true,
4604 },
4605 )?;
4606 assert!(!registry.is_authorized_as(policy_id, alice, AuthRole::sender())?);
4607
4608 let tick = 0i16;
4610 let result = exchange.place(alice, base_address, MIN_ORDER_AMOUNT, false, tick);
4611
4612 assert!(
4613 result.is_err(),
4614 "Blacklisted user should not be able to place orders using internal balance"
4615 );
4616 let err = result.unwrap_err();
4617 assert!(
4618 matches!(
4619 err,
4620 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4621 ),
4622 "Expected PolicyForbids error, got: {err:?}"
4623 );
4624 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
4625
4626 Ok(())
4627 })
4628 }
4629
4630 #[test]
4631 fn test_cancel_stale_order() -> eyre::Result<()> {
4632 let mut storage = HashMapStorageProvider::new(1);
4633 StorageCtx::enter(&mut storage, || {
4634 let mut exchange = StablecoinDEX::new();
4635 exchange.initialize()?;
4636
4637 let alice = Address::random();
4638 let admin = Address::random();
4639
4640 let mut registry = TIP403Registry::new();
4641 let policy_id = registry.create_policy(
4642 admin,
4643 ITIP403Registry::createPolicyCall {
4644 admin,
4645 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4646 },
4647 )?;
4648
4649 let mut base = TIP20Setup::create("USDC", "USDC", admin)
4650 .with_issuer(admin)
4651 .with_mint(alice, U256::from(MIN_ORDER_AMOUNT * 2))
4652 .with_approval(alice, exchange.address, U256::from(MIN_ORDER_AMOUNT * 2))
4653 .apply()?;
4654 base.change_transfer_policy_id(
4655 admin,
4656 ITIP20::changeTransferPolicyIdCall {
4657 newPolicyId: policy_id,
4658 },
4659 )?;
4660
4661 exchange.create_pair(base.address())?;
4662 let order_id = exchange.place(alice, base.address(), MIN_ORDER_AMOUNT, false, 0)?;
4663
4664 registry.modify_policy_blacklist(
4665 admin,
4666 ITIP403Registry::modifyPolicyBlacklistCall {
4667 policyId: policy_id,
4668 account: alice,
4669 restricted: true,
4670 },
4671 )?;
4672
4673 exchange.cancel_stale_order(order_id)?;
4674
4675 assert_eq!(
4676 exchange.balance_of(alice, base.address())?,
4677 MIN_ORDER_AMOUNT
4678 );
4679
4680 Ok(())
4681 })
4682 }
4683
4684 #[test]
4685 fn test_cancel_stale_not_stale() -> eyre::Result<()> {
4686 let mut storage = HashMapStorageProvider::new(1);
4687 StorageCtx::enter(&mut storage, || {
4688 let mut exchange = StablecoinDEX::new();
4689 exchange.initialize()?;
4690
4691 let alice = Address::random();
4692 let admin = Address::random();
4693
4694 let mut registry = TIP403Registry::new();
4695 let policy_id = registry.create_policy(
4696 admin,
4697 ITIP403Registry::createPolicyCall {
4698 admin,
4699 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4700 },
4701 )?;
4702
4703 let mut base = TIP20Setup::create("USDC", "USDC", admin)
4704 .with_issuer(admin)
4705 .with_mint(alice, U256::from(MIN_ORDER_AMOUNT * 2))
4706 .with_approval(alice, exchange.address, U256::from(MIN_ORDER_AMOUNT * 2))
4707 .apply()?;
4708 base.change_transfer_policy_id(
4709 admin,
4710 ITIP20::changeTransferPolicyIdCall {
4711 newPolicyId: policy_id,
4712 },
4713 )?;
4714
4715 exchange.create_pair(base.address())?;
4716 let order_id = exchange.place(alice, base.address(), MIN_ORDER_AMOUNT, false, 0)?;
4717
4718 let result = exchange.cancel_stale_order(order_id);
4719 assert!(result.is_err());
4720 assert!(matches!(
4721 result.unwrap_err(),
4722 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::OrderNotStale(_))
4723 ));
4724
4725 Ok(())
4726 })
4727 }
4728
4729 #[test]
4730 fn test_cancel_stale_order_with_invalid_policy_type() -> eyre::Result<()> {
4731 for spec in [TempoHardfork::T0, TempoHardfork::T1C, TempoHardfork::T2] {
4737 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
4738
4739 let alice = Address::random();
4740 let admin = Address::random();
4741
4742 let (order_id, base_token, invalid_policy_id) =
4743 StorageCtx::enter(&mut storage, || {
4744 let mut exchange = StablecoinDEX::new();
4745 exchange.initialize()?;
4746
4747 let mut base = TIP20Setup::create("USDC", "USDC", admin)
4748 .with_issuer(admin)
4749 .with_mint(alice, U256::from(MIN_ORDER_AMOUNT * 2))
4750 .with_approval(alice, exchange.address, U256::from(MIN_ORDER_AMOUNT * 2))
4751 .apply()?;
4752
4753 exchange.create_pair(base.address())?;
4754 let order_id =
4755 exchange.place(alice, base.address(), MIN_ORDER_AMOUNT, false, 0)?;
4756
4757 let mut registry = TIP403Registry::new();
4760 let invalid_policy_id = registry.create_policy(
4761 admin,
4762 ITIP403Registry::createPolicyCall {
4763 admin,
4764 policyType: ITIP403Registry::PolicyType::COMPOUND,
4765 },
4766 )?;
4767 base.change_transfer_policy_id(
4768 admin,
4769 ITIP20::changeTransferPolicyIdCall {
4770 newPolicyId: invalid_policy_id,
4771 },
4772 )?;
4773
4774 Ok::<_, TempoPrecompileError>((order_id, base.address(), invalid_policy_id))
4775 })?;
4776
4777 let mut storage = storage.with_spec(spec);
4779 StorageCtx::enter(&mut storage, || {
4780 let mut exchange = StablecoinDEX::new();
4781
4782 let registry = TIP403Registry::new();
4784 let auth_result =
4785 registry.is_authorized_as(invalid_policy_id, alice, AuthRole::sender());
4786 assert!(
4787 auth_result.is_err(),
4788 "[{spec:?}] is_authorized_as should fail for invalid policy type"
4789 );
4790
4791 exchange.cancel_stale_order(order_id)?;
4793
4794 assert_eq!(
4795 exchange.balance_of(alice, base_token)?,
4796 MIN_ORDER_AMOUNT,
4797 "[{spec:?}] alice should get her funds back"
4798 );
4799
4800 Ok::<_, eyre::Report>(())
4801 })?;
4802 }
4803 Ok(())
4804 }
4805
4806 #[test]
4807 fn test_cancel_stale_order_recipient_blacklisted_on_payout_token_pre_t4() -> eyre::Result<()> {
4808 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4809 StorageCtx::enter(&mut storage, || {
4810 let mut exchange = StablecoinDEX::new();
4811 exchange.initialize()?;
4812
4813 let alice = Address::random();
4814 let admin = Address::random();
4815
4816 let mut registry = TIP403Registry::new();
4817 let policy_id = registry.create_policy(
4818 admin,
4819 ITIP403Registry::createPolicyCall {
4820 admin,
4821 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4822 },
4823 )?;
4824
4825 let (base_addr, quote_addr) =
4826 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 2)?;
4827
4828 exchange.create_pair(base_addr)?;
4829 let order_id = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, false, 0)?;
4830
4831 let mut quote = TIP20Token::from_address(quote_addr)?;
4832 quote.change_transfer_policy_id(
4833 admin,
4834 ITIP20::changeTransferPolicyIdCall {
4835 newPolicyId: policy_id,
4836 },
4837 )?;
4838
4839 registry.modify_policy_blacklist(
4840 admin,
4841 ITIP403Registry::modifyPolicyBlacklistCall {
4842 policyId: policy_id,
4843 account: alice,
4844 restricted: true,
4845 },
4846 )?;
4847
4848 let result = exchange.cancel_stale_order(order_id);
4850 assert!(result.is_err());
4851 assert!(matches!(
4852 result.unwrap_err(),
4853 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::OrderNotStale(_))
4854 ));
4855
4856 Ok(())
4857 })
4858 }
4859
4860 #[test]
4861 fn test_cancel_stale_order_recipient_blacklisted_on_payout_token_t4() -> eyre::Result<()> {
4862 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T4);
4863 StorageCtx::enter(&mut storage, || {
4864 let mut exchange = StablecoinDEX::new();
4865 exchange.initialize()?;
4866
4867 let alice = Address::random();
4868 let admin = Address::random();
4869
4870 let mut registry = TIP403Registry::new();
4871 let policy_id = registry.create_policy(
4872 admin,
4873 ITIP403Registry::createPolicyCall {
4874 admin,
4875 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4876 },
4877 )?;
4878
4879 let (base_addr, quote_addr) =
4880 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 2)?;
4881
4882 exchange.create_pair(base_addr)?;
4883 let order_id = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, false, 0)?;
4884
4885 let mut quote = TIP20Token::from_address(quote_addr)?;
4886 quote.change_transfer_policy_id(
4887 admin,
4888 ITIP20::changeTransferPolicyIdCall {
4889 newPolicyId: policy_id,
4890 },
4891 )?;
4892
4893 registry.modify_policy_blacklist(
4894 admin,
4895 ITIP403Registry::modifyPolicyBlacklistCall {
4896 policyId: policy_id,
4897 account: alice,
4898 restricted: true,
4899 },
4900 )?;
4901
4902 exchange.cancel_stale_order(order_id)?;
4904
4905 assert_eq!(exchange.balance_of(alice, base_addr)?, MIN_ORDER_AMOUNT);
4906
4907 Ok(())
4908 })
4909 }
4910
4911 #[test]
4912 fn test_place_when_base_blacklisted() -> eyre::Result<()> {
4913 let mut storage = HashMapStorageProvider::new(1);
4914 StorageCtx::enter(&mut storage, || {
4915 let mut exchange = StablecoinDEX::new();
4916 exchange.initialize()?;
4917
4918 let alice = Address::random();
4919 let admin = Address::random();
4920
4921 let mut registry = TIP403Registry::new();
4923 let policy_id = registry.create_policy(
4924 admin,
4925 ITIP403Registry::createPolicyCall {
4926 admin,
4927 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4928 },
4929 )?;
4930
4931 let (base_addr, _quote_addr) =
4933 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 4)?;
4934
4935 let mut base = TIP20Token::from_address(base_addr)?;
4937 base.change_transfer_policy_id(
4938 admin,
4939 ITIP20::changeTransferPolicyIdCall {
4940 newPolicyId: policy_id,
4941 },
4942 )?;
4943
4944 registry.modify_policy_blacklist(
4946 admin,
4947 ITIP403Registry::modifyPolicyBlacklistCall {
4948 policyId: policy_id,
4949 account: alice,
4950 restricted: true,
4951 },
4952 )?;
4953
4954 exchange.create_pair(base_addr)?;
4955
4956 let result = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, true, 0);
4958 assert!(result.is_err());
4959 assert!(matches!(
4960 result.unwrap_err(),
4961 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4962 ));
4963
4964 let result =
4966 exchange.place_flip(alice, base_addr, MIN_ORDER_AMOUNT, true, 0, 100, false);
4967 assert!(result.is_err());
4968 assert!(matches!(
4969 result.unwrap_err(),
4970 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4971 ));
4972
4973 Ok(())
4974 })
4975 }
4976
4977 #[test]
4978 fn test_place_when_quote_blacklisted() -> eyre::Result<()> {
4979 let mut storage = HashMapStorageProvider::new(1);
4980 StorageCtx::enter(&mut storage, || {
4981 let mut exchange = StablecoinDEX::new();
4982 exchange.initialize()?;
4983
4984 let alice = Address::random();
4985 let admin = Address::random();
4986
4987 let mut registry = TIP403Registry::new();
4989 let policy_id = registry.create_policy(
4990 admin,
4991 ITIP403Registry::createPolicyCall {
4992 admin,
4993 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4994 },
4995 )?;
4996
4997 let (base_addr, quote_addr) =
4999 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 4)?;
5000
5001 let mut quote = TIP20Token::from_address(quote_addr)?;
5003 quote.change_transfer_policy_id(
5004 admin,
5005 ITIP20::changeTransferPolicyIdCall {
5006 newPolicyId: policy_id,
5007 },
5008 )?;
5009
5010 registry.modify_policy_blacklist(
5012 admin,
5013 ITIP403Registry::modifyPolicyBlacklistCall {
5014 policyId: policy_id,
5015 account: alice,
5016 restricted: true,
5017 },
5018 )?;
5019
5020 exchange.create_pair(base_addr)?;
5021
5022 let result = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, false, 0);
5024 assert!(result.is_err());
5025 assert!(matches!(
5026 result.unwrap_err(),
5027 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
5028 ));
5029
5030 let result =
5032 exchange.place_flip(alice, base_addr, MIN_ORDER_AMOUNT, false, 100, 0, false);
5033 assert!(result.is_err());
5034 assert!(matches!(
5035 result.unwrap_err(),
5036 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
5037 ));
5038
5039 Ok(())
5040 })
5041 }
5042
5043 #[test]
5044 fn test_compound_policy_non_escrow_token_direction() -> eyre::Result<()> {
5045 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
5046 StorageCtx::enter(&mut storage, || {
5047 let mut exchange = StablecoinDEX::new();
5048 exchange.initialize()?;
5049
5050 let (alice, admin) = (Address::random(), Address::random());
5051 let mut registry = TIP403Registry::new();
5052
5053 let recipient_policy = registry.create_policy(
5056 admin,
5057 ITIP403Registry::createPolicyCall {
5058 admin,
5059 policyType: ITIP403Registry::PolicyType::WHITELIST,
5060 },
5061 )?;
5062 let compound_id = registry.create_compound_policy(
5066 admin,
5067 ITIP403Registry::createCompoundPolicyCall {
5068 senderPolicyId: 1, recipientPolicyId: recipient_policy, mintRecipientPolicyId: 1, },
5072 )?;
5073
5074 let (base_addr, quote_addr) =
5076 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 4)?;
5077
5078 let mut quote = TIP20Token::from_address(quote_addr)?;
5080 quote.change_transfer_policy_id(
5081 admin,
5082 ITIP20::changeTransferPolicyIdCall {
5083 newPolicyId: compound_id,
5084 },
5085 )?;
5086
5087 exchange.create_pair(base_addr)?;
5088
5089 let res_ask = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, false, 0);
5093 let res_flip =
5095 exchange.place_flip(alice, base_addr, MIN_ORDER_AMOUNT, false, 100, 0, false);
5096
5097 for res in [res_ask, res_flip] {
5098 assert!(
5099 matches!(
5100 res.unwrap_err(),
5101 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
5102 ),
5103 "Order should fail: alice cannot receive quote token (non-escrow) per compound policy"
5104 );
5105 }
5106 Ok(())
5107 })
5108 }
5109
5110 #[test]
5111 fn test_swap_exact_amount_out_rounding() -> eyre::Result<()> {
5112 let mut storage = HashMapStorageProvider::new(1);
5113 StorageCtx::enter(&mut storage, || {
5114 let mut exchange = StablecoinDEX::new();
5115 exchange.initialize()?;
5116
5117 let alice = Address::random();
5118 let bob = Address::random();
5119 let admin = Address::random();
5120 let tick = 10;
5121
5122 let (base_token, quote_token) =
5123 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
5124 exchange
5125 .create_pair(base_token)
5126 .expect("Could not create pair");
5127
5128 let order_amount = 100000000u128;
5129
5130 let tip20_quote_token = TIP20Token::from_address(quote_token)?;
5131 let alice_initial_balance =
5132 tip20_quote_token.balance_of(ITIP20::balanceOfCall { account: alice })?;
5133
5134 exchange
5135 .place(alice, base_token, order_amount, true, tick)
5136 .expect("Order should succeed");
5137
5138 let alice_balance_after_place =
5139 tip20_quote_token.balance_of(ITIP20::balanceOfCall { account: alice })?;
5140 let escrowed = alice_initial_balance - alice_balance_after_place;
5141 assert_eq!(escrowed, U256::from(100010000u128));
5142
5143 exchange
5144 .set_balance(bob, base_token, 200_000_000u128)
5145 .expect("Could not set balance");
5146
5147 exchange
5148 .swap_exact_amount_out(bob, base_token, quote_token, 100009999, u128::MAX)
5149 .expect("Swap should succeed");
5150
5151 Ok(())
5152 })
5153 }
5154
5155 #[test]
5156 fn test_stablecoin_dex_address_returns_correct_precompile() -> eyre::Result<()> {
5157 let mut storage = HashMapStorageProvider::new(1);
5158 StorageCtx::enter(&mut storage, || {
5159 let exchange = StablecoinDEX::new();
5160 assert_eq!(exchange.address(), STABLECOIN_DEX_ADDRESS);
5161 Ok(())
5162 })
5163 }
5164
5165 #[test]
5166 fn test_stablecoin_dex_initialize_sets_storage_state() -> eyre::Result<()> {
5167 let mut storage = HashMapStorageProvider::new(1);
5168 StorageCtx::enter(&mut storage, || {
5169 let mut exchange = StablecoinDEX::new();
5170
5171 assert!(!exchange.is_initialized()?);
5173
5174 exchange.initialize()?;
5176
5177 assert!(exchange.is_initialized()?);
5179
5180 let exchange2 = StablecoinDEX::new();
5182 assert!(exchange2.is_initialized()?);
5183
5184 Ok(())
5185 })
5186 }
5187
5188 #[test]
5189 fn test_get_order_validates_maker_and_order_id() -> eyre::Result<()> {
5190 let mut storage = HashMapStorageProvider::new(1);
5191 StorageCtx::enter(&mut storage, || {
5192 let mut exchange = StablecoinDEX::new();
5193 exchange.initialize()?;
5194
5195 let admin = Address::random();
5196 let alice = Address::random();
5197 let min_order_amount = MIN_ORDER_AMOUNT;
5198 let tick = 100i16;
5199
5200 let price = orderbook::tick_to_price(tick);
5201 let escrow =
5202 (min_order_amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
5203
5204 let (base_token, _quote_token) =
5205 setup_test_tokens(admin, alice, exchange.address, escrow)?;
5206 exchange.create_pair(base_token)?;
5207
5208 let order_id = exchange.place(alice, base_token, min_order_amount, true, tick)?;
5209
5210 let order = exchange.get_order(order_id)?;
5212 assert_eq!(order.maker(), alice);
5213 assert!(!order.maker().is_zero());
5214 assert!(order.order_id() < exchange.next_order_id()?);
5215
5216 let result = exchange.get_order(999);
5218 assert!(result.is_err());
5219 assert_eq!(
5220 result.unwrap_err(),
5221 StablecoinDEXError::order_does_not_exist().into()
5222 );
5223
5224 let next_id = exchange.next_order_id()?;
5226 let result = exchange.get_order(next_id);
5227 assert!(result.is_err());
5228 assert_eq!(
5229 result.unwrap_err(),
5230 StablecoinDEXError::order_does_not_exist().into()
5231 );
5232
5233 Ok(())
5234 })
5235 }
5236
5237 struct FlipOrderTestCtx {
5239 exchange: StablecoinDEX,
5240 alice: Address,
5241 bob: Address,
5242 admin: Address,
5243 base_token: Address,
5244 quote_token: Address,
5245 book_key: B256,
5246 amount: u128,
5247 flip_tick: i16,
5248 }
5249
5250 fn setup_flip_order_test() -> eyre::Result<FlipOrderTestCtx> {
5252 let mut exchange = StablecoinDEX::new();
5253 exchange.initialize()?;
5254
5255 let alice = Address::random();
5256 let bob = Address::random();
5257 let admin = Address::random();
5258 let amount = MIN_ORDER_AMOUNT;
5259 let tick = 100i16;
5260 let flip_tick = 200i16;
5261
5262 let price = orderbook::tick_to_price(tick);
5263 let expected_escrow = (amount * u128::from(price)) / u128::from(orderbook::PRICE_SCALE);
5264
5265 let (base_token, quote_token) =
5266 setup_test_tokens(admin, alice, exchange.address, expected_escrow * 2)?;
5267 exchange.create_pair(base_token)?;
5268
5269 let book_key = compute_book_key(base_token, quote_token);
5270
5271 exchange.place_flip(alice, base_token, amount, true, tick, flip_tick, false)?;
5273
5274 Ok(FlipOrderTestCtx {
5275 exchange,
5276 alice,
5277 bob,
5278 admin,
5279 base_token,
5280 quote_token,
5281 book_key,
5282 amount,
5283 flip_tick,
5284 })
5285 }
5286
5287 #[test]
5288 fn test_flip_order_fill_ignores_business_logic_error() -> eyre::Result<()> {
5289 for spec in [TempoHardfork::T1, TempoHardfork::T1A, TempoHardfork::T2] {
5291 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
5292 StorageCtx::enter(&mut storage, || {
5293 let FlipOrderTestCtx {
5294 mut exchange,
5295 alice,
5296 bob,
5297 admin,
5298 base_token,
5299 quote_token,
5300 book_key,
5301 amount,
5302 flip_tick,
5303 } = setup_flip_order_test()?;
5304
5305 let mut registry = TIP403Registry::new();
5309 let policy_id = registry.create_policy(
5310 admin,
5311 ITIP403Registry::createPolicyCall {
5312 admin,
5313 policyType: ITIP403Registry::PolicyType::BLACKLIST,
5314 },
5315 )?;
5316
5317 let mut base = TIP20Token::from_address(base_token)?;
5318 base.change_transfer_policy_id(
5319 admin,
5320 ITIP20::changeTransferPolicyIdCall {
5321 newPolicyId: policy_id,
5322 },
5323 )?;
5324
5325 registry.modify_policy_blacklist(
5326 admin,
5327 ITIP403Registry::modifyPolicyBlacklistCall {
5328 policyId: policy_id,
5329 account: alice,
5330 restricted: true,
5331 },
5332 )?;
5333
5334 exchange.set_balance(bob, base_token, amount)?;
5336
5337 let result = exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0);
5339 assert!(
5340 result.is_ok(),
5341 "[{spec:?}] Swap should succeed when flip hits a business logic error"
5342 );
5343
5344 assert_eq!(exchange.balance_of(alice, base_token)?, amount);
5346
5347 let level = exchange.books[book_key]
5349 .tick_level_handler(flip_tick, false)
5350 .read()?;
5351 assert_eq!(
5352 level.total_liquidity, 0,
5353 "[{spec:?}] No flipped order should exist"
5354 );
5355
5356 Ok::<_, eyre::Report>(())
5357 })?;
5358 }
5359 Ok(())
5360 }
5361
5362 #[test]
5363 fn test_flip_order_fill_reverts_on_system_error_post_t1a() -> eyre::Result<()> {
5364 for spec in [TempoHardfork::T1, TempoHardfork::T1A, TempoHardfork::T2] {
5366 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
5367 StorageCtx::enter(&mut storage, || {
5368 let FlipOrderTestCtx {
5369 mut exchange,
5370 alice,
5371 bob,
5372 base_token,
5373 quote_token,
5374 book_key,
5375 amount,
5376 flip_tick,
5377 ..
5378 } = setup_flip_order_test()?;
5379
5380 let alice_quote_before = exchange.balance_of(alice, quote_token)?;
5381
5382 let poisoned_level = TickLevel::with_values(0, 0, u128::MAX);
5384 exchange.books[book_key]
5385 .tick_level_handler_mut(flip_tick, false)
5386 .write(poisoned_level)?;
5387
5388 exchange.set_balance(bob, base_token, amount)?;
5390
5391 let result = exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0);
5392
5393 if spec.is_t1a() {
5394 assert!(
5396 result.is_err(),
5397 "Swap should revert when flip hits a system error"
5398 );
5399 assert!(
5400 result.unwrap_err().is_system_error(),
5401 "Error must be classified as a system error",
5402 );
5403
5404 let alice_quote_after = exchange.balance_of(alice, quote_token)?;
5406 assert_eq!(alice_quote_before, alice_quote_after);
5407 } else {
5408 assert!(
5410 result.is_ok(),
5411 "[{spec:?}] Swap should succeed when system error is pre-T1A"
5412 );
5413 }
5414
5415 Ok::<_, eyre::Report>(())
5416 })?;
5417 }
5418 Ok(())
5419 }
5420
5421 #[test]
5422 fn test_orderbook_invariants_after_all_orders_filled() -> eyre::Result<()> {
5423 let mut storage = HashMapStorageProvider::new(1);
5424 StorageCtx::enter(&mut storage, || {
5425 let mut exchange = StablecoinDEX::new();
5426 exchange.initialize()?;
5427
5428 assert_eq!(exchange.next_order_id()?, 1);
5430
5431 let alice = Address::random();
5432 let bob = Address::random();
5433 let admin = Address::random();
5434 let amount = MIN_ORDER_AMOUNT;
5435 let tick = 100i16;
5436
5437 let price = u128::from(orderbook::tick_to_price(tick));
5438 let quote_amount = (amount * price).div_ceil(u128::from(orderbook::PRICE_SCALE));
5439
5440 let base = TIP20Setup::create("BASE", "BASE", admin)
5441 .with_issuer(admin)
5442 .with_mint(alice, U256::from(amount * 4))
5443 .with_mint(bob, U256::from(amount * 4))
5444 .with_approval(alice, exchange.address, U256::MAX)
5445 .with_approval(bob, exchange.address, U256::MAX)
5446 .apply()?;
5447 let base_token = base.address();
5448 let quote_token = base.quote_token()?;
5449
5450 TIP20Setup::path_usd(admin)
5451 .with_issuer(admin)
5452 .with_mint(alice, U256::from(quote_amount * 4))
5453 .with_mint(bob, U256::from(quote_amount * 4))
5454 .with_approval(alice, exchange.address, U256::MAX)
5455 .with_approval(bob, exchange.address, U256::MAX)
5456 .apply()?;
5457
5458 let book_key = compute_book_key(base_token, quote_token);
5459 exchange.create_pair(base_token)?;
5460
5461 let bid_id = exchange.place(alice, base_token, amount, true, tick)?;
5463 assert_eq!(bid_id, 1);
5464 let ask_id = exchange.place(bob, base_token, amount, false, tick)?;
5465 assert_eq!(ask_id, 2);
5466
5467 let book = exchange.books[book_key].read()?;
5469 assert_eq!(book.best_bid_tick, tick);
5470 assert_eq!(book.best_ask_tick, tick);
5471
5472 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
5474
5475 exchange.swap_exact_amount_in(alice, quote_token, base_token, quote_amount, 0)?;
5477
5478 let book = exchange.books[book_key].read()?;
5480 assert_eq!(
5481 book.best_bid_tick,
5482 i16::MIN,
5483 "best_bid_tick must be sentinel after all bids filled"
5484 );
5485 assert_eq!(
5486 book.best_ask_tick,
5487 i16::MAX,
5488 "best_ask_tick must be sentinel after all asks filled"
5489 );
5490
5491 let bid_level = exchange.books[book_key]
5493 .tick_level_handler(tick, true)
5494 .read()?;
5495 assert_eq!(bid_level.head, 0, "bid level head must be 0 after drain");
5496 assert_eq!(bid_level.tail, 0, "bid level tail must be 0 after drain");
5497 assert_eq!(
5498 bid_level.total_liquidity, 0,
5499 "bid level liquidity must be 0 after drain"
5500 );
5501
5502 let ask_level = exchange.books[book_key]
5503 .tick_level_handler(tick, false)
5504 .read()?;
5505 assert_eq!(ask_level.head, 0, "ask level head must be 0 after drain");
5506 assert_eq!(ask_level.tail, 0, "ask level tail must be 0 after drain");
5507 assert_eq!(
5508 ask_level.total_liquidity, 0,
5509 "ask level liquidity must be 0 after drain"
5510 );
5511
5512 assert_eq!(
5514 exchange.next_order_id()?,
5515 3,
5516 "next_order_id must remain monotonic after drain"
5517 );
5518
5519 let result = exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0);
5522 assert_eq!(
5523 result,
5524 Err(StablecoinDEXError::insufficient_liquidity().into()),
5525 "swap against drained bid side must fail"
5526 );
5527 let result =
5529 exchange.swap_exact_amount_in(alice, quote_token, base_token, quote_amount, 0);
5530 assert_eq!(
5531 result,
5532 Err(StablecoinDEXError::insufficient_liquidity().into()),
5533 "swap against drained ask side must fail"
5534 );
5535
5536 Ok(())
5537 })
5538 }
5539
5540 #[test]
5541 fn test_orderbook_invariants_after_all_orders_cancelled() -> eyre::Result<()> {
5542 let mut storage = HashMapStorageProvider::new(1);
5543 StorageCtx::enter(&mut storage, || {
5544 let mut exchange = StablecoinDEX::new();
5545 exchange.initialize()?;
5546
5547 let alice = Address::random();
5548 let admin = Address::random();
5549 let amount = MIN_ORDER_AMOUNT;
5550 let tick = 100i16;
5551
5552 let price = u128::from(orderbook::tick_to_price(tick));
5553 let quote_amount = (amount * price).div_ceil(u128::from(orderbook::PRICE_SCALE));
5554
5555 let base = TIP20Setup::create("BASE", "BASE", admin)
5556 .with_issuer(admin)
5557 .with_mint(alice, U256::from(amount * 2))
5558 .with_approval(alice, exchange.address, U256::MAX)
5559 .apply()?;
5560 let base_token = base.address();
5561 let quote_token = base.quote_token()?;
5562
5563 TIP20Setup::path_usd(admin)
5564 .with_issuer(admin)
5565 .with_mint(alice, U256::from(quote_amount * 2))
5566 .with_approval(alice, exchange.address, U256::MAX)
5567 .apply()?;
5568
5569 let book_key = compute_book_key(base_token, quote_token);
5570 exchange.create_pair(base_token)?;
5571
5572 let bid_id = exchange.place(alice, base_token, amount, true, tick)?;
5574 let ask_id = exchange.place(alice, base_token, amount, false, tick)?;
5575
5576 exchange.cancel(alice, bid_id)?;
5578 exchange.cancel(alice, ask_id)?;
5579
5580 let book = exchange.books[book_key].read()?;
5582 assert_eq!(
5583 book.best_bid_tick,
5584 i16::MIN,
5585 "best_bid_tick must be sentinel after all bids cancelled"
5586 );
5587 assert_eq!(
5588 book.best_ask_tick,
5589 i16::MAX,
5590 "best_ask_tick must be sentinel after all asks cancelled"
5591 );
5592
5593 let bid_level = exchange.books[book_key]
5595 .tick_level_handler(tick, true)
5596 .read()?;
5597 assert_eq!(bid_level.head, 0, "bid level head must be 0");
5598 assert_eq!(bid_level.tail, 0, "bid level tail must be 0");
5599 assert_eq!(bid_level.total_liquidity, 0, "bid liquidity must be 0");
5600
5601 let ask_level = exchange.books[book_key]
5602 .tick_level_handler(tick, false)
5603 .read()?;
5604 assert_eq!(ask_level.head, 0, "ask level head must be 0");
5605 assert_eq!(ask_level.tail, 0, "ask level tail must be 0");
5606 assert_eq!(ask_level.total_liquidity, 0, "ask liquidity must be 0");
5607
5608 let result = exchange.swap_exact_amount_in(alice, base_token, quote_token, amount, 0);
5610 assert_eq!(
5611 result,
5612 Err(StablecoinDEXError::insufficient_liquidity().into()),
5613 "swap against cancelled book must fail"
5614 );
5615
5616 Ok(())
5617 })
5618 }
5619
5620 #[test]
5621 fn test_sub_balance_errors_on_underflow() -> eyre::Result<()> {
5622 let mut storage = HashMapStorageProvider::new(1);
5623 StorageCtx::enter(&mut storage, || {
5624 let mut exchange = StablecoinDEX::new();
5625 exchange.initialize()?;
5626
5627 let user = Address::random();
5628 let admin = Address::random();
5629
5630 let base = TIP20Setup::create("BASE", "BASE", admin)
5631 .with_issuer(admin)
5632 .apply()?;
5633 let token = base.address();
5634
5635 exchange.set_balance(user, token, 100)?;
5637 assert_eq!(exchange.balance_of(user, token)?, 100);
5638
5639 let result = exchange.sub_balance(user, token, 101);
5641 assert_eq!(
5642 result,
5643 Err(TempoPrecompileError::under_overflow()),
5644 "sub_balance should error on underflow instead of saturating"
5645 );
5646
5647 assert_eq!(exchange.balance_of(user, token)?, 100);
5649
5650 Ok(())
5651 })
5652 }
5653
5654 #[test]
5655 fn test_flip_checkpoint_reverts_partial_state_post_t1c() -> eyre::Result<()> {
5656 for spec in [TempoHardfork::T1A, TempoHardfork::T1C] {
5662 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
5663 StorageCtx::enter(&mut storage, || {
5664 let FlipOrderTestCtx {
5665 mut exchange,
5666 alice,
5667 bob,
5668 base_token,
5669 quote_token,
5670 book_key,
5671 amount,
5672 flip_tick,
5673 ..
5674 } = setup_flip_order_test()?;
5675
5676 let next_id_before = exchange.next_order_id()?;
5677
5678 let poisoned = TickLevel::with_values(0, 0, u128::MAX);
5681 exchange.books[book_key]
5682 .tick_level_handler_mut(flip_tick, false)
5683 .write(poisoned)?;
5684
5685 exchange.set_balance(bob, base_token, amount)?;
5687
5688 let result = exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0);
5689 assert!(result.is_err(), "[{spec:?}] swap should fail");
5690
5691 let alice_base = exchange.balance_of(alice, base_token)?;
5695 let next_id_after = exchange.next_order_id()?;
5696
5697 if spec.is_t1c() {
5698 assert_eq!(alice_base, amount);
5700 assert_eq!(next_id_after, next_id_before);
5701 } else {
5702 assert_eq!(alice_base, 0);
5704 assert_eq!(next_id_after, next_id_before + 1);
5705 }
5706
5707 assert!(
5709 exchange.emitted_events().last().is_some_and(
5710 |e| e.topics()[0] != IStablecoinDEX::OrderPlaced::SIGNATURE_HASH
5711 )
5712 );
5713
5714 Ok::<_, eyre::Report>(())
5715 })?;
5716 }
5717 Ok(())
5718 }
5719
5720 #[test]
5721 fn test_swap_paused_token_allowed_pre_t3_blocked_on_t3() -> eyre::Result<()> {
5722 for spec in [TempoHardfork::T2, TempoHardfork::T3] {
5723 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
5724 StorageCtx::enter(&mut storage, || {
5725 let mut exchange = StablecoinDEX::new();
5726 exchange.initialize()?;
5727
5728 let (alice, bob, admin) = (Address::random(), Address::random(), Address::random());
5729 let amount_in = 500_000u128;
5730 let tick = 10;
5731
5732 let (base_token, quote_token) =
5733 setup_test_tokens(admin, alice, exchange.address, 500_000_000u128)?;
5734 exchange.create_pair(base_token)?;
5735
5736 exchange.place(alice, base_token, MIN_ORDER_AMOUNT * 2, true, tick)?;
5738
5739 exchange.set_balance(bob, base_token, amount_in * 2)?;
5741
5742 let mut base_tip20 = TIP20Token::from_address(base_token)?;
5744 base_tip20.grant_role_internal(admin, *PAUSE_ROLE)?;
5745 base_tip20.pause(admin, ITIP20::pauseCall {})?;
5746
5747 let res_in =
5748 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount_in, 0);
5749 let res_out = exchange.swap_exact_amount_out(
5750 bob,
5751 base_token,
5752 quote_token,
5753 amount_in,
5754 u128::MAX,
5755 );
5756
5757 if spec.is_t3() {
5758 assert_eq!(res_in, res_out);
5759 assert_eq!(res_in.unwrap_err(), TIP20Error::contract_paused().into());
5760 } else {
5761 assert!(res_in.is_ok());
5762 assert!(res_out.is_ok());
5763 }
5764
5765 Ok::<_, eyre::Report>(())
5766 })?;
5767 }
5768 Ok(())
5769 }
5770
5771 fn assert_paused_token_order<F>(
5775 pause_escrow_side: bool,
5776 internal_balance_amount: u128,
5777 is_bid: bool,
5778 mut place_order: F,
5779 ) -> eyre::Result<()>
5780 where
5781 F: FnMut(&mut StablecoinDEX, Address, Address, u128) -> Result<u128>,
5782 {
5783 for spec in [TempoHardfork::T3, TempoHardfork::T4] {
5784 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
5785 StorageCtx::enter(&mut storage, || {
5786 let mut exchange = StablecoinDEX::new();
5787 exchange.initialize()?;
5788
5789 let (alice, admin) = (Address::random(), Address::random());
5790 let amount = MIN_ORDER_AMOUNT;
5791
5792 let (base_token, quote_token) =
5793 setup_test_tokens(admin, alice, exchange.address, 500_000_000u128)?;
5794 exchange.create_pair(base_token)?;
5795
5796 let escrow_token = if is_bid { quote_token } else { base_token };
5797 let non_escrow_token = if is_bid { base_token } else { quote_token };
5798 exchange.set_balance(alice, escrow_token, internal_balance_amount)?;
5799
5800 let token_to_pause = if pause_escrow_side {
5801 escrow_token
5802 } else {
5803 non_escrow_token
5804 };
5805 let mut tip20 = TIP20Token::from_address(token_to_pause)?;
5806 tip20.grant_role_internal(admin, *PAUSE_ROLE)?;
5807 tip20.pause(admin, ITIP20::pauseCall {})?;
5808
5809 let next_order_id_before = exchange.next_order_id()?;
5810 let escrow_balance_before = exchange.balance_of(alice, escrow_token)?;
5811 let res = place_order(&mut exchange, alice, base_token, amount);
5812
5813 let should_succeed =
5819 !spec.is_t4() && (!pause_escrow_side || internal_balance_amount >= amount);
5820
5821 if should_succeed {
5822 let order_id = res?;
5823 assert_eq!(order_id, next_order_id_before);
5824 assert_eq!(exchange.next_order_id()?, next_order_id_before + 1);
5825 assert_eq!(
5826 exchange.balance_of(alice, escrow_token)?,
5827 escrow_balance_before.saturating_sub(amount)
5828 );
5829 } else {
5830 assert_eq!(res.unwrap_err(), TIP20Error::contract_paused().into());
5831 assert_eq!(exchange.next_order_id()?, next_order_id_before);
5832 assert_eq!(
5833 exchange.balance_of(alice, escrow_token)?,
5834 escrow_balance_before
5835 );
5836 }
5837
5838 Ok::<_, eyre::Report>(())
5839 })?;
5840 }
5841 Ok(())
5842 }
5843
5844 #[test]
5845 fn test_place_orders_on_paused_token_respects_internal_balance_path() -> eyre::Result<()> {
5846 let partial_internal_balance = MIN_ORDER_AMOUNT - 1;
5847
5848 assert_paused_token_order(
5851 true,
5852 MIN_ORDER_AMOUNT,
5853 false,
5854 |exchange, alice, base, amount| exchange.place(alice, base, amount, false, 0),
5855 )?;
5856 assert_paused_token_order(
5857 true,
5858 MIN_ORDER_AMOUNT,
5859 true,
5860 |exchange, alice, base, amount| exchange.place(alice, base, amount, true, 0),
5861 )?;
5862 assert_paused_token_order(
5863 true,
5864 MIN_ORDER_AMOUNT,
5865 false,
5866 |exchange, alice, base, amount| {
5867 exchange.place_flip(alice, base, amount, false, 100, 0, true)
5868 },
5869 )?;
5870 assert_paused_token_order(
5871 true,
5872 MIN_ORDER_AMOUNT,
5873 true,
5874 |exchange, alice, base, amount| {
5875 exchange.place_flip(alice, base, amount, true, 0, 100, true)
5876 },
5877 )?;
5878
5879 assert_paused_token_order(
5882 true,
5883 partial_internal_balance,
5884 false,
5885 |exchange, alice, base, amount| exchange.place(alice, base, amount, false, 0),
5886 )?;
5887 assert_paused_token_order(
5888 true,
5889 partial_internal_balance,
5890 true,
5891 |exchange, alice, base, amount| exchange.place(alice, base, amount, true, 0),
5892 )?;
5893 assert_paused_token_order(
5894 true,
5895 partial_internal_balance,
5896 false,
5897 |exchange, alice, base, amount| {
5898 exchange.place_flip(alice, base, amount, false, 100, 0, false)
5899 },
5900 )?;
5901 assert_paused_token_order(
5902 true,
5903 partial_internal_balance,
5904 true,
5905 |exchange, alice, base, amount| {
5906 exchange.place_flip(alice, base, amount, true, 0, 100, false)
5907 },
5908 )
5909 }
5910
5911 #[test]
5912 fn test_place_orders_on_paused_non_escrow_token_blocked_on_t4() -> eyre::Result<()> {
5913 assert_paused_token_order(false, 0, false, |exchange, alice, base, amount| {
5915 exchange.place(alice, base, amount, false, 0)
5916 })?;
5917 assert_paused_token_order(false, 0, true, |exchange, alice, base, amount| {
5918 exchange.place(alice, base, amount, true, 0)
5919 })?;
5920
5921 assert_paused_token_order(false, 0, false, |exchange, alice, base, amount| {
5923 exchange.place_flip(alice, base, amount, false, 100, 0, false)
5924 })?;
5925 assert_paused_token_order(false, 0, true, |exchange, alice, base, amount| {
5926 exchange.place_flip(alice, base, amount, true, 0, 100, false)
5927 })?;
5928
5929 assert_paused_token_order(
5931 false,
5932 MIN_ORDER_AMOUNT,
5933 false,
5934 |exchange, alice, base, amount| {
5935 exchange.place_flip(alice, base, amount, false, 100, 0, true)
5936 },
5937 )?;
5938 assert_paused_token_order(
5939 false,
5940 MIN_ORDER_AMOUNT,
5941 true,
5942 |exchange, alice, base, amount| {
5943 exchange.place_flip(alice, base, amount, true, 0, 100, true)
5944 },
5945 )
5946 }
5947
5948 #[test]
5949 fn test_swap_paused_intermediate_token_allowed_pre_t3_blocked_on_t3() -> eyre::Result<()> {
5950 for spec in [TempoHardfork::T2, TempoHardfork::T3] {
5951 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
5952 StorageCtx::enter(&mut storage, || {
5953 let mut exchange = StablecoinDEX::new();
5954 exchange.initialize()?;
5955
5956 let admin = Address::random();
5957 let alice = Address::random();
5958 let bob = Address::random();
5959
5960 let amount = MIN_ORDER_AMOUNT * 10;
5961 let amount_u256 = U256::from(amount);
5962
5963 let path_usd = TIP20Setup::path_usd(admin)
5965 .with_issuer(admin)
5966 .with_mint(alice, amount_u256)
5967 .with_approval(alice, exchange.address, amount_u256)
5968 .apply()?;
5969
5970 let usdc = TIP20Setup::create("USDC", "USDC", admin)
5971 .with_issuer(admin)
5972 .with_mint(alice, amount_u256)
5973 .with_approval(alice, exchange.address, amount_u256)
5974 .with_mint(bob, amount_u256)
5975 .with_approval(bob, exchange.address, amount_u256)
5976 .apply()?;
5977
5978 let eurc = TIP20Setup::create("EURC", "EURC", admin)
5979 .with_issuer(admin)
5980 .with_mint(alice, amount_u256)
5981 .with_approval(alice, exchange.address, amount_u256)
5982 .apply()?;
5983
5984 exchange.place(alice, usdc.address(), MIN_ORDER_AMOUNT * 5, true, 0)?;
5986 exchange.place(alice, eurc.address(), MIN_ORDER_AMOUNT * 5, false, 0)?;
5987
5988 let mut path_usd_tip20 = TIP20Token::from_address(path_usd.address())?;
5990 path_usd_tip20.grant_role_internal(admin, *PAUSE_ROLE)?;
5991 path_usd_tip20.pause(admin, ITIP20::pauseCall {})?;
5992
5993 let res_in = exchange.swap_exact_amount_in(
5995 bob,
5996 usdc.address(),
5997 eurc.address(),
5998 MIN_ORDER_AMOUNT,
5999 0,
6000 );
6001 let res_out = exchange.swap_exact_amount_out(
6002 bob,
6003 usdc.address(),
6004 eurc.address(),
6005 MIN_ORDER_AMOUNT,
6006 u128::MAX,
6007 );
6008
6009 if spec.is_t3() {
6010 assert_eq!(res_in, res_out);
6011 assert_eq!(res_in.unwrap_err(), TIP20Error::contract_paused().into());
6012 } else {
6013 assert!(res_in.is_ok());
6014 assert!(res_out.is_ok());
6015 }
6016
6017 Ok::<_, eyre::Report>(())
6018 })?;
6019 }
6020 Ok(())
6021 }
6022}