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::OrderFilled(
157 IStablecoinDEX::OrderFilled {
158 orderId: order_id,
159 maker,
160 taker,
161 amountFilled: amount_filled,
162 partialFill: partial_fill,
163 },
164 ))?;
165
166 Ok(())
167 }
168
169 fn transfer(&mut self, token: Address, to: Address, amount: u128) -> Result<()> {
171 TIP20Token::from_address(token)?.transfer(
172 self.address,
173 ITIP20::transferCall {
174 to,
175 amount: U256::from(amount),
176 },
177 )?;
178 Ok(())
179 }
180
181 fn transfer_from(&mut self, token: Address, from: Address, amount: u128) -> Result<()> {
183 TIP20Token::from_address(token)?.transfer_from(
184 self.address,
185 ITIP20::transferFromCall {
186 from,
187 to: self.address,
188 amount: U256::from(amount),
189 },
190 )?;
191 Ok(())
192 }
193
194 fn decrement_balance_or_transfer_from(
201 &mut self,
202 user: Address,
203 token: Address,
204 amount: u128,
205 check_pause: bool,
206 ) -> Result<()> {
207 let tip20 = TIP20Token::from_address(token)?;
209 tip20.ensure_transfer_authorized(user, self.address)?;
210
211 let user_balance = self.balance_of(user, token)?;
212 if user_balance >= amount {
213 if check_pause && self.storage.spec().is_t4() {
216 tip20.check_not_paused()?;
217 }
218 self.sub_balance(user, token, amount)
219 } else {
220 let remaining = amount
221 .checked_sub(user_balance)
222 .ok_or(TempoPrecompileError::under_overflow())?;
223
224 self.transfer_from(token, user, remaining)?;
225 self.set_balance(user, token, 0)
226 }
227 }
228
229 pub fn quote_swap_exact_amount_out(
238 &self,
239 token_in: Address,
240 token_out: Address,
241 amount_out: u128,
242 ) -> Result<u128> {
243 let route = self.find_trade_path(token_in, token_out)?;
245
246 let mut current_amount = amount_out;
248 for (book_key, base_for_quote) in route.iter().rev() {
249 current_amount = self.quote_exact_out(*book_key, current_amount, *base_for_quote)?;
250 }
251
252 Ok(current_amount)
253 }
254
255 pub fn quote_swap_exact_amount_in(
264 &self,
265 token_in: Address,
266 token_out: Address,
267 amount_in: u128,
268 ) -> Result<u128> {
269 let route = self.find_trade_path(token_in, token_out)?;
271
272 let mut current_amount = amount_in;
274 for (book_key, base_for_quote) in route {
275 current_amount = self.quote_exact_in(book_key, current_amount, base_for_quote)?;
276 }
277
278 Ok(current_amount)
279 }
280
281 pub fn swap_exact_amount_in(
291 &mut self,
292 sender: Address,
293 token_in: Address,
294 token_out: Address,
295 amount_in: u128,
296 min_amount_out: u128,
297 ) -> Result<u128> {
298 let route = self.find_trade_path(token_in, token_out)?;
300
301 self.decrement_balance_or_transfer_from(sender, token_in, amount_in, false)?;
304
305 let mut amount = amount_in;
307 for (book_key, base_for_quote) in route {
308 amount = self.fill_orders_exact_in(book_key, base_for_quote, amount, sender)?;
310 }
311
312 if amount < min_amount_out {
314 return Err(StablecoinDEXError::insufficient_output().into());
315 }
316
317 self.transfer(token_out, sender, amount)?;
318
319 Ok(amount)
320 }
321
322 pub fn swap_exact_amount_out(
332 &mut self,
333 sender: Address,
334 token_in: Address,
335 token_out: Address,
336 amount_out: u128,
337 max_amount_in: u128,
338 ) -> Result<u128> {
339 let route = self.find_trade_path(token_in, token_out)?;
341
342 let mut amount = amount_out;
344 for (book_key, base_for_quote) in route.iter().rev() {
345 amount = self.fill_orders_exact_out(*book_key, *base_for_quote, amount, sender)?;
346 }
347
348 if amount > max_amount_in {
349 return Err(StablecoinDEXError::max_input_exceeded().into());
350 }
351
352 self.decrement_balance_or_transfer_from(sender, token_in, amount, false)?;
355
356 self.transfer(token_out, sender, amount_out)?;
358
359 Ok(amount)
360 }
361
362 pub fn get_price_level(&self, base: Address, tick: i16, is_bid: bool) -> Result<TickLevel> {
368 let quote = TIP20Token::from_address(base)?.quote_token()?;
369 let book_key = compute_book_key(base, quote);
370 if is_bid {
371 self.books[book_key].bids[tick].read()
372 } else {
373 self.books[book_key].asks[tick].read()
374 }
375 }
376
377 pub fn books(&self, pair_key: B256) -> Result<Orderbook> {
379 self.books[pair_key].read()
380 }
381
382 pub fn get_book_keys(&self) -> Result<Vec<B256>> {
384 self.book_keys.read()
385 }
386
387 pub fn tick_to_price(&self, tick: i16) -> Result<u32> {
392 if self.storage.spec().is_t2() {
393 orderbook::validate_tick_spacing(tick)?;
394 }
395
396 Ok(orderbook::tick_to_price(tick))
397 }
398
399 pub fn price_to_tick(&self, price: u32) -> Result<i16> {
405 let tick = orderbook::price_to_tick(price)?;
406
407 if self.storage.spec().is_t2() {
408 orderbook::validate_tick_spacing(tick)?;
409 }
410
411 Ok(tick)
412 }
413
414 pub fn create_pair(&mut self, base: Address) -> Result<B256> {
423 if !TIP20Factory::new().is_tip20(base)? {
425 return Err(StablecoinDEXError::invalid_base_token().into());
426 }
427
428 let quote = TIP20Token::from_address(base)?.quote_token()?;
429 validate_usd_currency(base)?;
430 validate_usd_currency(quote)?;
431
432 let book_key = compute_book_key(base, quote);
433
434 if self.books[book_key].read()?.is_initialized() {
435 return Err(StablecoinDEXError::pair_already_exists().into());
436 }
437
438 let book = Orderbook::new(base, quote);
439 self.books[book_key].write(book)?;
440 self.book_keys.push(book_key)?;
441
442 self.emit_event(StablecoinDEXEvents::PairCreated(
444 IStablecoinDEX::PairCreated {
445 key: book_key,
446 base,
447 quote,
448 },
449 ))?;
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::OrderPlaced(
537 IStablecoinDEX::OrderPlaced {
538 orderId: order_id,
539 maker: sender,
540 token,
541 amount,
542 isBid: is_bid,
543 tick,
544 isFlipOrder: false,
545 flipTick: 0,
546 },
547 ))?;
548
549 Ok(order_id)
550 }
551
552 fn commit_order_to_book(&mut self, mut order: Order) -> Result<()> {
554 let orderbook = self.books[order.book_key()].read()?;
555 let mut level = self.books[order.book_key()]
556 .tick_level_handler(order.tick(), order.is_bid())
557 .read()?;
558
559 let prev_tail = level.tail;
560 if prev_tail == 0 {
561 level.head = order.order_id();
562 level.tail = order.order_id();
563
564 self.books[order.book_key()].set_tick_bit(order.tick(), order.is_bid())?;
565
566 if order.is_bid() {
567 if order.tick() > orderbook.best_bid_tick {
568 self.books[order.book_key()]
569 .best_bid_tick
570 .write(order.tick())?;
571 }
572 } else if order.tick() < orderbook.best_ask_tick {
573 self.books[order.book_key()]
574 .best_ask_tick
575 .write(order.tick())?;
576 }
577 } else {
578 let mut prev_order = self.orders[prev_tail].read()?;
580 prev_order.next = order.order_id();
581 self.orders[prev_tail].write(prev_order)?;
582
583 order.prev = prev_tail;
585 level.tail = order.order_id();
586 }
587
588 let new_liquidity = level
589 .total_liquidity
590 .checked_add(order.remaining())
591 .ok_or(TempoPrecompileError::under_overflow())?;
592 level.total_liquidity = new_liquidity;
593
594 self.books[order.book_key()]
595 .tick_level_handler_mut(order.tick(), order.is_bid())
596 .write(level)?;
597
598 self.orders[order.order_id()].write(order)
599 }
600
601 #[allow(clippy::too_many_arguments)]
616 pub fn place_flip(
617 &mut self,
618 sender: Address,
619 token: Address,
620 amount: u128,
621 is_bid: bool,
622 tick: i16,
623 flip_tick: i16,
624 internal_balance_only: bool,
625 ) -> Result<u128> {
626 let quote_token = TIP20Token::from_address(token)?.quote_token()?;
627
628 let book_key = compute_book_key(token, quote_token);
630
631 let batch = self.storage.checkpoint();
634
635 let book = self.books[book_key].read()?;
637 self.validate_or_create_pair(&book, token)?;
638
639 if !(MIN_TICK..=MAX_TICK).contains(&tick) {
641 return Err(StablecoinDEXError::tick_out_of_bounds(tick).into());
642 }
643
644 if tick % TICK_SPACING != 0 {
646 return Err(StablecoinDEXError::invalid_tick().into());
647 }
648
649 if !(MIN_TICK..=MAX_TICK).contains(&flip_tick) {
650 return Err(StablecoinDEXError::tick_out_of_bounds(flip_tick).into());
651 }
652
653 if flip_tick % TICK_SPACING != 0 {
655 return Err(StablecoinDEXError::invalid_flip_tick().into());
656 }
657
658 if (flip_tick == tick && !self.storage.spec().is_t5())
664 || (is_bid && flip_tick < tick)
665 || (!is_bid && flip_tick > tick)
666 {
667 return Err(StablecoinDEXError::invalid_flip_tick().into());
668 }
669
670 if amount < MIN_ORDER_AMOUNT {
672 return Err(StablecoinDEXError::below_minimum_order_size(amount).into());
673 }
674
675 let (escrow_token, escrow_amount, non_escrow_token) = if is_bid {
677 let quote_amount = base_to_quote(amount, tick, RoundingDirection::Up)
679 .ok_or(StablecoinDEXError::insufficient_balance())?;
680 (quote_token, quote_amount, token)
681 } else {
682 (token, amount, quote_token)
684 };
685
686 let non_escrow_tip20 = TIP20Token::from_address(non_escrow_token)?;
689 non_escrow_tip20.ensure_transfer_authorized(self.address, sender)?;
690
691 if self.storage.spec().is_t4() {
695 non_escrow_tip20.check_not_paused()?;
696 }
697
698 if internal_balance_only {
701 let tip20 = TIP20Token::from_address(escrow_token)?;
702 tip20.ensure_transfer_authorized(sender, self.address)?;
703 if self.storage.spec().is_t4() {
706 tip20.check_not_paused()?;
707 }
708 let user_balance = self.balance_of(sender, escrow_token)?;
709 if user_balance < escrow_amount {
710 return Err(StablecoinDEXError::insufficient_balance().into());
711 }
712 self.sub_balance(sender, escrow_token, escrow_amount)?;
713 } else {
714 self.decrement_balance_or_transfer_from(sender, escrow_token, escrow_amount, true)?;
715 }
716
717 let order_id = self.next_order_id()?;
719 let order = Order::new_flip(
720 order_id,
721 sender,
722 book_key,
723 amount,
724 tick,
725 is_bid,
726 flip_tick,
727 self.storage.spec(),
728 )
729 .map_err(|_| StablecoinDEXError::invalid_flip_tick())?;
730
731 if self.storage.spec().is_t1c() {
733 self.next_order_id.write(order_id + 1)?;
735 } else {
736 self.increment_next_order_id()?;
737 }
738 self.commit_order_to_book(order)?;
739
740 self.emit_event(StablecoinDEXEvents::OrderPlaced(
742 IStablecoinDEX::OrderPlaced {
743 orderId: order_id,
744 maker: sender,
745 token,
746 amount,
747 isBid: is_bid,
748 tick,
749 isFlipOrder: true,
750 flipTick: flip_tick,
751 },
752 ))?;
753
754 batch.commit();
756
757 Ok(order_id)
758 }
759
760 fn partial_fill_order(
762 &mut self,
763 order: &mut Order,
764 level: &mut TickLevel,
765 fill_amount: u128,
766 taker: Address,
767 ) -> Result<u128> {
768 let orderbook = self.books[order.book_key()].read()?;
769
770 let new_remaining = order.remaining() - fill_amount;
772 self.orders[order.order_id()]
773 .remaining
774 .write(new_remaining)?;
775
776 let quote_amount = base_to_quote(
778 fill_amount,
779 order.tick(),
780 if order.is_bid() {
781 RoundingDirection::Down } else {
783 RoundingDirection::Up },
785 )
786 .ok_or(TempoPrecompileError::under_overflow())?;
787
788 if order.is_bid() {
789 self.increment_balance(order.maker(), orderbook.base, fill_amount)?;
791 } else {
792 self.increment_balance(order.maker(), orderbook.quote, quote_amount)?;
794 }
795
796 let amount_out = if order.is_bid() {
798 quote_amount
799 } else {
800 fill_amount
801 };
802
803 let new_liquidity = level
805 .total_liquidity
806 .checked_sub(fill_amount)
807 .ok_or(TempoPrecompileError::under_overflow())?;
808 level.total_liquidity = new_liquidity;
809
810 self.books[order.book_key()]
811 .tick_level_handler_mut(order.tick(), order.is_bid())
812 .write(*level)?;
813
814 self.emit_order_filled(order.order_id(), order.maker(), taker, fill_amount, true)?;
816
817 Ok(amount_out)
818 }
819
820 fn fill_order(
826 &mut self,
827 book_key: B256,
828 order: &mut Order,
829 mut level: TickLevel,
830 taker: Address,
831 ) -> Result<(u128, Option<(TickLevel, Order)>)> {
832 debug_assert_eq!(order.book_key(), book_key);
833
834 let orderbook = self.books[book_key].read()?;
835 let fill_amount = order.remaining();
836
837 let amount_out = if order.is_bid() {
839 self.increment_balance(order.maker(), orderbook.base, fill_amount)?;
841 base_to_quote(fill_amount, order.tick(), RoundingDirection::Down)
843 .ok_or(TempoPrecompileError::under_overflow())?
844 } else {
845 let quote_amount = base_to_quote(fill_amount, order.tick(), RoundingDirection::Up)
847 .ok_or(TempoPrecompileError::under_overflow())?;
848
849 self.increment_balance(order.maker(), orderbook.quote, quote_amount)?;
850
851 fill_amount
853 };
854
855 self.emit_order_filled(order.order_id(), order.maker(), taker, fill_amount, false)?;
857
858 if order.is_flip() {
859 if let Err(e) = self.place_flip(
867 order.maker(),
868 orderbook.base,
869 order.amount(),
870 !order.is_bid(),
871 order.flip_tick(),
872 order.tick(),
873 true,
874 ) && e.is_system_error()
875 && self.storage.spec().is_t1a()
876 {
877 return Err(e);
878 }
879 }
880
881 self.orders[order.order_id()].delete()?;
883
884 let next_tick_info = if order.next() == 0 {
886 self.books[book_key]
887 .tick_level_handler_mut(order.tick(), order.is_bid())
888 .delete()?;
889 self.books[book_key].delete_tick_bit(order.tick(), order.is_bid())?;
890
891 let (tick, has_liquidity) =
892 self.books[book_key].next_initialized_tick(order.tick(), order.is_bid())?;
893
894 if order.is_bid() {
896 let new_best = if has_liquidity { tick } else { i16::MIN };
897 self.books[book_key].best_bid_tick.write(new_best)?;
898 } else {
899 let new_best = if has_liquidity { tick } else { i16::MAX };
900 self.books[book_key].best_ask_tick.write(new_best)?;
901 }
902
903 if !has_liquidity {
904 None
906 } else {
907 let new_level = self.books[book_key]
908 .tick_level_handler(tick, order.is_bid())
909 .read()?;
910 let new_order = self.orders[new_level.head].read()?;
911
912 Some((new_level, new_order))
913 }
914 } else {
915 level.head = order.next();
917 self.orders[order.next()].prev.delete()?;
918
919 let new_liquidity = level
920 .total_liquidity
921 .checked_sub(fill_amount)
922 .ok_or(TempoPrecompileError::under_overflow())?;
923 level.total_liquidity = new_liquidity;
924
925 self.books[book_key]
926 .tick_level_handler_mut(order.tick(), order.is_bid())
927 .write(level)?;
928
929 let new_order = self.orders[order.next()].read()?;
930 Some((level, new_order))
931 };
932
933 Ok((amount_out, next_tick_info))
934 }
935
936 fn fill_orders_exact_out(
938 &mut self,
939 book_key: B256,
940 bid: bool,
941 mut amount_out: u128,
942 taker: Address,
943 ) -> Result<u128> {
944 let mut level = self.get_best_price_level(book_key, bid)?;
945 let mut order = self.orders[level.head].read()?;
946
947 let mut total_amount_in: u128 = 0;
948
949 while amount_out > 0 {
950 let tick = order.tick();
951
952 let (fill_amount, amount_in) = if bid {
953 let base_needed = quote_to_base(amount_out, tick, RoundingDirection::Up)
956 .ok_or(TempoPrecompileError::under_overflow())?;
957 let fill_amount = base_needed.min(order.remaining());
958 (fill_amount, fill_amount)
959 } else {
960 let fill_amount = amount_out.min(order.remaining());
963 let amount_in = base_to_quote(fill_amount, tick, RoundingDirection::Up)
964 .ok_or(TempoPrecompileError::under_overflow())?;
965 (fill_amount, amount_in)
966 };
967
968 if fill_amount < order.remaining() {
969 self.partial_fill_order(&mut order, &mut level, fill_amount, taker)?;
970 total_amount_in = total_amount_in
971 .checked_add(amount_in)
972 .ok_or(TempoPrecompileError::under_overflow())?;
973 break;
974 } else {
975 let (amount_out_received, next_order_info) =
976 self.fill_order(book_key, &mut order, level, taker)?;
977 total_amount_in = total_amount_in
978 .checked_add(amount_in)
979 .ok_or(TempoPrecompileError::under_overflow())?;
980
981 if bid {
983 let base_needed = quote_to_base(amount_out, tick, RoundingDirection::Up)
985 .ok_or(TempoPrecompileError::under_overflow())?;
986 if base_needed > order.remaining() {
987 amount_out = amount_out
988 .checked_sub(amount_out_received)
989 .ok_or(TempoPrecompileError::under_overflow())?;
990 } else {
991 amount_out = 0;
992 }
993 } else if amount_out > order.remaining() {
994 amount_out = amount_out
995 .checked_sub(amount_out_received)
996 .ok_or(TempoPrecompileError::under_overflow())?;
997 } else {
998 amount_out = 0;
999 }
1000
1001 if let Some((new_level, new_order)) = next_order_info {
1002 level = new_level;
1003 order = new_order;
1004 } else {
1005 if amount_out > 0 {
1006 return Err(StablecoinDEXError::insufficient_liquidity().into());
1007 }
1008 break;
1009 }
1010 }
1011 }
1012
1013 Ok(total_amount_in)
1014 }
1015
1016 fn fill_orders_exact_in(
1018 &mut self,
1019 book_key: B256,
1020 bid: bool,
1021 mut amount_in: u128,
1022 taker: Address,
1023 ) -> Result<u128> {
1024 let mut level = self.get_best_price_level(book_key, bid)?;
1025 let mut order = self.orders[level.head].read()?;
1026
1027 let mut total_amount_out: u128 = 0;
1028
1029 while amount_in > 0 {
1030 let tick = order.tick();
1031
1032 let fill_amount = if bid {
1033 amount_in.min(order.remaining())
1035 } else {
1036 let base_out = quote_to_base(amount_in, tick, RoundingDirection::Down)
1039 .ok_or(TempoPrecompileError::under_overflow())?;
1040 base_out.min(order.remaining())
1041 };
1042
1043 if fill_amount < order.remaining() {
1044 let amount_out =
1045 self.partial_fill_order(&mut order, &mut level, fill_amount, taker)?;
1046 total_amount_out = total_amount_out
1047 .checked_add(amount_out)
1048 .ok_or(TempoPrecompileError::under_overflow())?;
1049 break;
1050 } else {
1051 let (amount_out, next_order_info) =
1052 self.fill_order(book_key, &mut order, level, taker)?;
1053 total_amount_out = total_amount_out
1054 .checked_add(amount_out)
1055 .ok_or(TempoPrecompileError::under_overflow())?;
1056
1057 if bid {
1059 if amount_in > order.remaining() {
1060 amount_in = amount_in
1061 .checked_sub(order.remaining())
1062 .ok_or(TempoPrecompileError::under_overflow())?;
1063 } else {
1064 amount_in = 0;
1065 }
1066 } else {
1067 let base_out = quote_to_base(amount_in, tick, RoundingDirection::Down)
1069 .ok_or(TempoPrecompileError::under_overflow())?;
1070 if base_out > order.remaining() {
1071 let quote_needed =
1073 base_to_quote(order.remaining(), tick, RoundingDirection::Up)
1074 .ok_or(TempoPrecompileError::under_overflow())?;
1075 amount_in = amount_in
1076 .checked_sub(quote_needed)
1077 .ok_or(TempoPrecompileError::under_overflow())?;
1078 } else {
1079 amount_in = 0;
1080 }
1081 }
1082
1083 if let Some((new_level, new_order)) = next_order_info {
1084 level = new_level;
1085 order = new_order;
1086 } else {
1087 if amount_in > 0 {
1088 return Err(StablecoinDEXError::insufficient_liquidity().into());
1089 }
1090 break;
1091 }
1092 }
1093 }
1094
1095 Ok(total_amount_out)
1096 }
1097
1098 fn get_best_price_level(&mut self, book_key: B256, is_bid: bool) -> Result<TickLevel> {
1100 let orderbook = self.books[book_key].read()?;
1101
1102 let current_tick = if is_bid {
1103 if orderbook.best_bid_tick == i16::MIN {
1104 return Err(StablecoinDEXError::insufficient_liquidity().into());
1105 }
1106 orderbook.best_bid_tick
1107 } else {
1108 if orderbook.best_ask_tick == i16::MAX {
1109 return Err(StablecoinDEXError::insufficient_liquidity().into());
1110 }
1111 orderbook.best_ask_tick
1112 };
1113
1114 self.books[book_key]
1115 .tick_level_handler(current_tick, is_bid)
1116 .read()
1117 }
1118
1119 pub fn cancel(&mut self, sender: Address, order_id: u128) -> Result<()> {
1126 let order = self.orders[order_id].read()?;
1127
1128 if order.maker().is_zero() {
1129 return Err(StablecoinDEXError::order_does_not_exist().into());
1130 }
1131
1132 if order.maker() != sender {
1133 return Err(StablecoinDEXError::unauthorized().into());
1134 }
1135
1136 if order.remaining() == 0 {
1137 return Err(StablecoinDEXError::order_does_not_exist().into());
1138 }
1139
1140 self.cancel_active_order(order)
1141 }
1142
1143 fn cancel_active_order(&mut self, order: Order) -> Result<()> {
1145 let mut level = self.books[order.book_key()]
1146 .tick_level_handler(order.tick(), order.is_bid())
1147 .read()?;
1148
1149 if order.prev() != 0 {
1151 self.orders[order.prev()].next.write(order.next())?;
1152 } else {
1153 level.head = order.next();
1154 }
1155
1156 if order.next() != 0 {
1157 self.orders[order.next()].prev.write(order.prev())?;
1158 } else {
1159 level.tail = order.prev();
1160 }
1161
1162 let new_liquidity = level
1164 .total_liquidity
1165 .checked_sub(order.remaining())
1166 .ok_or(TempoPrecompileError::under_overflow())?;
1167 level.total_liquidity = new_liquidity;
1168
1169 if level.head == 0 {
1171 self.books[order.book_key()].delete_tick_bit(order.tick(), order.is_bid())?;
1172
1173 let orderbook = self.books[order.book_key()].read()?;
1175 let best_tick = if order.is_bid() {
1176 orderbook.best_bid_tick
1177 } else {
1178 orderbook.best_ask_tick
1179 };
1180
1181 if best_tick == order.tick() {
1182 let (next_tick, has_liquidity) = self.books[order.book_key()]
1183 .next_initialized_tick(order.tick(), order.is_bid())?;
1184
1185 if order.is_bid() {
1186 let new_best = if has_liquidity { next_tick } else { i16::MIN };
1187 self.books[order.book_key()].best_bid_tick.write(new_best)?;
1188 } else {
1189 let new_best = if has_liquidity { next_tick } else { i16::MAX };
1190 self.books[order.book_key()].best_ask_tick.write(new_best)?;
1191 }
1192 }
1193 }
1194
1195 self.books[order.book_key()]
1196 .tick_level_handler_mut(order.tick(), order.is_bid())
1197 .write(level)?;
1198
1199 let orderbook = self.books[order.book_key()].read()?;
1201 if order.is_bid() {
1202 let quote_amount =
1205 base_to_quote(order.remaining(), order.tick(), RoundingDirection::Up)
1206 .ok_or(TempoPrecompileError::under_overflow())?;
1207
1208 self.increment_balance(order.maker(), orderbook.quote, quote_amount)?;
1209 } else {
1210 self.increment_balance(order.maker(), orderbook.base, order.remaining())?;
1212 }
1213
1214 self.orders[order.order_id()].delete()?;
1216
1217 self.emit_event(StablecoinDEXEvents::OrderCancelled(
1219 IStablecoinDEX::OrderCancelled {
1220 orderId: order.order_id(),
1221 },
1222 ))
1223 }
1224
1225 pub fn cancel_stale_order(&mut self, order_id: u128) -> Result<()> {
1237 let order = self.orders[order_id].read()?;
1238
1239 if order.maker().is_zero() {
1240 return Err(StablecoinDEXError::order_does_not_exist().into());
1241 }
1242
1243 if self.is_maker_authorized(&order)? {
1244 Err(StablecoinDEXError::order_not_stale().into())
1245 } else {
1246 self.cancel_active_order(order)
1247 }
1248 }
1249
1250 fn is_maker_authorized(&self, order: &Order) -> Result<bool> {
1255 let book = self.books[order.book_key()].read()?;
1256
1257 let (token_in, token_out) = if order.is_bid() {
1258 (book.quote, book.base)
1259 } else {
1260 (book.base, book.quote)
1261 };
1262
1263 if !is_authorized_for_token(token_in, order.maker(), AuthRole::sender())? {
1264 return Ok(false);
1265 }
1266
1267 if self.storage.spec().is_t4() {
1268 is_authorized_for_token(token_out, order.maker(), AuthRole::recipient())
1269 } else {
1270 Ok(true)
1271 }
1272 }
1273
1274 pub fn withdraw(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
1280 let current_balance = self.balance_of(user, token)?;
1281 if current_balance < amount {
1282 return Err(StablecoinDEXError::insufficient_balance().into());
1283 }
1284 self.sub_balance(user, token, amount)?;
1285 self.transfer(token, user, amount)?;
1286
1287 Ok(())
1288 }
1289
1290 fn quote_exact_out(&self, book_key: B256, amount_out: u128, is_bid: bool) -> Result<u128> {
1292 let mut remaining_out = amount_out;
1293 let mut amount_in = 0u128;
1294 let orderbook = self.books[book_key].read()?;
1295
1296 let mut current_tick = if is_bid {
1297 orderbook.best_bid_tick
1298 } else {
1299 orderbook.best_ask_tick
1300 };
1301 if current_tick == i16::MIN || current_tick == i16::MAX {
1303 return Err(StablecoinDEXError::insufficient_liquidity().into());
1304 }
1305
1306 while remaining_out > 0 {
1307 let level = self.books[book_key]
1308 .tick_level_handler(current_tick, is_bid)
1309 .read()?;
1310
1311 if level.total_liquidity == 0 {
1313 let (next_tick, initialized) =
1314 self.books[book_key].next_initialized_tick(current_tick, is_bid)?;
1315
1316 if !initialized {
1317 return Err(StablecoinDEXError::insufficient_liquidity().into());
1318 }
1319 current_tick = next_tick;
1320 continue;
1321 }
1322
1323 let (fill_amount, amount_in_tick) = if is_bid {
1324 let base_needed = quote_to_base(remaining_out, current_tick, RoundingDirection::Up)
1330 .ok_or(TempoPrecompileError::under_overflow())?;
1331 let fill_amount = if base_needed > level.total_liquidity {
1332 level.total_liquidity
1333 } else {
1334 base_needed
1335 };
1336 (fill_amount, fill_amount)
1337 } else {
1338 let fill_amount = if remaining_out > level.total_liquidity {
1341 level.total_liquidity
1342 } else {
1343 remaining_out
1344 };
1345 let quote_needed = base_to_quote(fill_amount, current_tick, RoundingDirection::Up)
1346 .ok_or(TempoPrecompileError::under_overflow())?;
1347 (fill_amount, quote_needed)
1348 };
1349
1350 let amount_out_tick = if is_bid {
1351 base_to_quote(fill_amount, current_tick, RoundingDirection::Down)
1355 .ok_or(TempoPrecompileError::under_overflow())?
1356 .min(remaining_out)
1357 } else {
1358 fill_amount
1359 };
1360
1361 remaining_out = remaining_out.saturating_sub(amount_out_tick);
1362 amount_in = amount_in
1363 .checked_add(amount_in_tick)
1364 .ok_or(TempoPrecompileError::under_overflow())?;
1365
1366 if fill_amount == level.total_liquidity {
1368 let (next_tick, initialized) =
1369 self.books[book_key].next_initialized_tick(current_tick, is_bid)?;
1370
1371 if !initialized && remaining_out > 0 {
1372 return Err(StablecoinDEXError::insufficient_liquidity().into());
1373 }
1374 current_tick = next_tick;
1375 } else {
1376 break;
1377 }
1378 }
1379
1380 Ok(amount_in)
1381 }
1382
1383 fn find_trade_path(&self, token_in: Address, token_out: Address) -> Result<Vec<(B256, bool)>> {
1387 if token_in == token_out {
1389 return Err(StablecoinDEXError::identical_tokens().into());
1390 }
1391
1392 if !token_in.is_tip20() || !token_out.is_tip20() {
1394 return Err(StablecoinDEXError::invalid_token().into());
1395 }
1396
1397 let in_quote = TIP20Token::from_address(token_in)?.quote_token()?;
1399 let out_quote = TIP20Token::from_address(token_out)?.quote_token()?;
1400
1401 if in_quote == token_out || out_quote == token_in {
1402 return self.validate_and_build_route(&[token_in, token_out]);
1403 }
1404
1405 let path_in = self.find_path_to_root(token_in)?;
1407 let path_out = self.find_path_to_root(token_out)?;
1408
1409 let path_out_set: std::collections::HashSet<Address> = path_out.iter().copied().collect();
1412 let mut lca = None;
1413 for token_a in &path_in {
1414 if path_out_set.contains(token_a) {
1415 lca = Some(*token_a);
1416 break;
1417 }
1418 }
1419
1420 let lca = lca.ok_or_else(StablecoinDEXError::pair_does_not_exist)?;
1421
1422 let mut trade_path = Vec::new();
1424
1425 for token in &path_in {
1427 trade_path.push(*token);
1428 if *token == lca {
1429 break;
1430 }
1431 }
1432
1433 let lca_to_out: Vec<Address> = path_out
1435 .iter()
1436 .take_while(|&&t| t != lca)
1437 .copied()
1438 .collect();
1439
1440 trade_path.extend(lca_to_out.iter().rev());
1442
1443 self.validate_and_build_route(&trade_path)
1444 }
1445
1446 fn validate_and_build_route(&self, path: &[Address]) -> Result<Vec<(B256, bool)>> {
1453 let mut route = Vec::new();
1454
1455 for i in 0..path.len() - 1 {
1456 let token_in = path[i];
1457 let token_out = path[i + 1];
1458
1459 let (base, quote) = {
1460 let token_in_tip20 = TIP20Token::from_address(token_in)?;
1461
1462 if self.storage.spec().is_t3() {
1465 token_in_tip20.check_not_paused()?;
1466 }
1467
1468 if token_in_tip20.quote_token()? == token_out {
1469 (token_in, token_out)
1470 } else {
1471 let token_out_tip20 = TIP20Token::from_address(token_out)?;
1472 if token_out_tip20.quote_token()? == token_in {
1473 (token_out, token_in)
1474 } else {
1475 return Err(StablecoinDEXError::pair_does_not_exist().into());
1476 }
1477 }
1478 };
1479
1480 let book_key = compute_book_key(base, quote);
1481 let orderbook = self.books[book_key].read()?;
1482
1483 if orderbook.base.is_zero() {
1484 return Err(StablecoinDEXError::pair_does_not_exist().into());
1485 }
1486
1487 let is_base_for_quote = token_in == base;
1488 route.push((book_key, is_base_for_quote));
1489 }
1490
1491 Ok(route)
1492 }
1493
1494 fn find_path_to_root(&self, mut token: Address) -> Result<Vec<Address>> {
1497 let mut path = vec![token];
1498
1499 while token != PATH_USD_ADDRESS {
1500 token = TIP20Token::from_address(token)?.quote_token()?;
1501 path.push(token);
1502 }
1503
1504 Ok(path)
1505 }
1506
1507 fn quote_exact_in(&self, book_key: B256, amount_in: u128, is_bid: bool) -> Result<u128> {
1509 let mut remaining_in = amount_in;
1510 let mut amount_out = 0u128;
1511 let orderbook = self.books[book_key].read()?;
1512
1513 let mut current_tick = if is_bid {
1514 orderbook.best_bid_tick
1515 } else {
1516 orderbook.best_ask_tick
1517 };
1518
1519 if current_tick == i16::MIN || current_tick == i16::MAX {
1521 return Err(StablecoinDEXError::insufficient_liquidity().into());
1522 }
1523
1524 while remaining_in > 0 {
1525 let level = self.books[book_key]
1526 .tick_level_handler(current_tick, is_bid)
1527 .read()?;
1528
1529 if level.total_liquidity == 0 {
1531 let (next_tick, initialized) =
1532 self.books[book_key].next_initialized_tick(current_tick, is_bid)?;
1533
1534 if !initialized {
1535 return Err(StablecoinDEXError::insufficient_liquidity().into());
1536 }
1537 current_tick = next_tick;
1538 continue;
1539 }
1540
1541 let (fill_amount, amount_out_tick, amount_consumed) = if is_bid {
1543 let fill = remaining_in.min(level.total_liquidity);
1545 let quote_out = base_to_quote(fill, current_tick, RoundingDirection::Down)
1547 .ok_or(TempoPrecompileError::under_overflow())?;
1548 (fill, quote_out, fill)
1549 } else {
1550 let base_to_get =
1553 quote_to_base(remaining_in, current_tick, RoundingDirection::Down)
1554 .ok_or(TempoPrecompileError::under_overflow())?;
1555 let fill = base_to_get.min(level.total_liquidity);
1556 let quote_consumed = base_to_quote(fill, current_tick, RoundingDirection::Up)
1557 .ok_or(TempoPrecompileError::under_overflow())?;
1558 (fill, fill, quote_consumed)
1559 };
1560
1561 remaining_in = remaining_in
1562 .checked_sub(amount_consumed)
1563 .ok_or(TempoPrecompileError::under_overflow())?;
1564 amount_out = amount_out
1565 .checked_add(amount_out_tick)
1566 .ok_or(TempoPrecompileError::under_overflow())?;
1567
1568 if fill_amount == level.total_liquidity {
1570 let (next_tick, initialized) =
1571 self.books[book_key].next_initialized_tick(current_tick, is_bid)?;
1572
1573 if !initialized && remaining_in > 0 {
1574 return Err(StablecoinDEXError::insufficient_liquidity().into());
1575 }
1576 current_tick = next_tick;
1577 } else {
1578 break;
1579 }
1580 }
1581
1582 Ok(amount_out)
1583 }
1584}
1585
1586fn is_authorized_for_token(token: Address, address: Address, role: AuthRole) -> Result<bool> {
1589 let policy_id = TIP20Token::from_address(token)?.transfer_policy_id()?;
1590 let registry = TIP403Registry::new();
1591 match registry.is_authorized_as(policy_id, address, role) {
1592 Ok(authorized) => Ok(authorized),
1593 Err(e) if is_policy_lookup_error(&e) => Ok(false),
1594 Err(e) => Err(e),
1595 }
1596}
1597
1598#[cfg(test)]
1599mod tests {
1600 use alloy::{primitives::IntoLogData, sol_types::SolEvent};
1601 use tempo_chainspec::hardfork::TempoHardfork;
1602 use tempo_contracts::precompiles::TIP20Error;
1603
1604 use crate::{
1605 error::TempoPrecompileError,
1606 storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
1607 test_util::TIP20Setup,
1608 tip20::PAUSE_ROLE,
1609 tip403_registry::{ITIP403Registry, TIP403Registry},
1610 };
1611
1612 use super::*;
1613 use crate::STABLECOIN_DEX_ADDRESS;
1614
1615 fn setup_test_tokens(
1616 admin: Address,
1617 user: Address,
1618 exchange_address: Address,
1619 amount: u128,
1620 ) -> Result<(Address, Address)> {
1621 let quote = TIP20Setup::path_usd(admin)
1623 .with_issuer(admin)
1624 .with_mint(user, U256::from(amount))
1625 .with_approval(user, exchange_address, U256::from(amount))
1626 .apply()?;
1627
1628 let base = TIP20Setup::create("BASE", "BASE", admin)
1630 .with_issuer(admin)
1631 .with_mint(user, U256::from(amount))
1632 .with_approval(user, exchange_address, U256::from(amount))
1633 .apply()?;
1634
1635 Ok((base.address(), quote.address()))
1636 }
1637
1638 #[test]
1639 fn test_tick_to_price() {
1640 let test_ticks = [-2000i16, -1000, -100, -1, 0, 1, 100, 1000, 2000];
1641 for tick in test_ticks {
1642 let price = orderbook::tick_to_price(tick);
1643 let expected_price = (orderbook::PRICE_SCALE as i32 + tick as i32) as u32;
1644 assert_eq!(price, expected_price);
1645 }
1646 }
1647
1648 #[test]
1649 fn test_price_to_tick() -> eyre::Result<()> {
1650 let mut storage = HashMapStorageProvider::new(1);
1651 StorageCtx::enter(&mut storage, || {
1652 let exchange = StablecoinDEX::new();
1653
1654 assert_eq!(exchange.price_to_tick(orderbook::PRICE_SCALE)?, 0);
1656 assert_eq!(exchange.price_to_tick(orderbook::MIN_PRICE)?, MIN_TICK);
1657 assert_eq!(exchange.price_to_tick(orderbook::MAX_PRICE)?, MAX_TICK);
1658
1659 let result = exchange.price_to_tick(orderbook::MIN_PRICE - 1);
1661 assert!(result.is_err());
1662 assert!(matches!(
1663 result.unwrap_err(),
1664 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::TickOutOfBounds(_))
1665 ));
1666
1667 let result = exchange.price_to_tick(orderbook::MAX_PRICE + 1);
1668 assert!(result.is_err());
1669 assert!(matches!(
1670 result.unwrap_err(),
1671 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::TickOutOfBounds(_))
1672 ));
1673
1674 Ok(())
1675 })
1676 }
1677
1678 #[test]
1679 fn test_calculate_quote_amount_rounding() -> eyre::Result<()> {
1680 let amount = 100u128;
1685 let tick = 1i16;
1686 let result_floor = base_to_quote(amount, tick, RoundingDirection::Down).unwrap();
1687 assert_eq!(
1688 result_floor, 100,
1689 "Expected 100 (rounded down from 100.001)"
1690 );
1691
1692 let result_ceil = base_to_quote(amount, tick, RoundingDirection::Up).unwrap();
1694 assert_eq!(result_ceil, 101, "Expected 101 (rounded up from 100.001)");
1695
1696 let amount2 = 999u128;
1698 let tick2 = 5i16; let result2_floor = base_to_quote(amount2, tick2, RoundingDirection::Down).unwrap();
1700 assert_eq!(
1702 result2_floor, 999,
1703 "Expected 999 (rounded down from 999.04995)"
1704 );
1705
1706 let result2_ceil = base_to_quote(amount2, tick2, RoundingDirection::Up).unwrap();
1708 assert_eq!(
1709 result2_ceil, 1000,
1710 "Expected 1000 (rounded up from 999.04995)"
1711 );
1712
1713 let amount3 = 100000u128;
1715 let tick3 = 0i16; let result3_floor = base_to_quote(amount3, tick3, RoundingDirection::Down).unwrap();
1717 let result3_ceil = base_to_quote(amount3, tick3, RoundingDirection::Up).unwrap();
1718 assert_eq!(result3_floor, 100000, "Exact division should remain exact");
1720 assert_eq!(result3_ceil, 100000, "Exact division should remain exact");
1721
1722 Ok(())
1723 }
1724
1725 #[test]
1726 fn test_settlement_rounding_favors_protocol() -> eyre::Result<()> {
1727 let mut storage = HashMapStorageProvider::new(1);
1728 StorageCtx::enter(&mut storage, || {
1729 let mut exchange = StablecoinDEX::new();
1730 exchange.initialize()?;
1731
1732 let alice = Address::random();
1733 let bob = Address::random();
1734 let admin = Address::random();
1735
1736 let base_amount = 100_000_003u128;
1738 let tick = 100i16;
1739
1740 let price = orderbook::tick_to_price(tick) as u128;
1741 let expected_quote_floor = (base_amount * price) / orderbook::PRICE_SCALE as u128;
1742 let expected_quote_ceil =
1743 (base_amount * price).div_ceil(orderbook::PRICE_SCALE as u128);
1744
1745 let max_escrow = expected_quote_ceil * 2;
1746
1747 let base = TIP20Setup::create("BASE", "BASE", admin)
1748 .with_issuer(admin)
1749 .with_mint(alice, U256::from(base_amount * 2))
1750 .with_mint(bob, U256::from(base_amount * 2))
1751 .with_approval(alice, exchange.address, U256::MAX)
1752 .with_approval(bob, exchange.address, U256::MAX)
1753 .apply()?;
1754 let base_token = base.address();
1755 let quote_token = base.quote_token()?;
1756
1757 TIP20Setup::path_usd(admin)
1758 .with_issuer(admin)
1759 .with_mint(alice, U256::from(max_escrow))
1760 .with_mint(bob, U256::from(max_escrow))
1761 .with_approval(alice, exchange.address, U256::MAX)
1762 .with_approval(bob, exchange.address, U256::MAX)
1763 .apply()?;
1764
1765 exchange.create_pair(base_token)?;
1766
1767 exchange.place(alice, base_token, base_amount, false, tick)?;
1768
1769 let alice_quote_before = exchange.balance_of(alice, quote_token)?;
1770 assert_eq!(alice_quote_before, 0);
1771
1772 exchange.swap_exact_amount_in(bob, quote_token, base_token, expected_quote_ceil, 0)?;
1773
1774 let alice_quote_after = exchange.balance_of(alice, quote_token)?;
1775
1776 assert_eq!(
1778 alice_quote_after, expected_quote_ceil,
1779 "Ask order maker should receive quote rounded UP. Got {alice_quote_after}, expected ceil {expected_quote_ceil}"
1780 );
1781
1782 assert!(
1783 expected_quote_ceil > expected_quote_floor,
1784 "Test setup error: should have a non-zero remainder"
1785 );
1786
1787 Ok(())
1788 })
1789 }
1790
1791 #[test]
1792 fn test_cancellation_refund_equals_escrow_for_bid_orders() -> eyre::Result<()> {
1793 let mut storage = HashMapStorageProvider::new(1);
1794 StorageCtx::enter(&mut storage, || {
1795 let mut exchange = StablecoinDEX::new();
1796 exchange.initialize()?;
1797
1798 let alice = Address::random();
1799 let admin = Address::random();
1800
1801 let base_amount = 100_000_003u128;
1803 let tick = 100i16;
1804
1805 let price = orderbook::tick_to_price(tick) as u128;
1806 let escrow_ceil = (base_amount * price).div_ceil(orderbook::PRICE_SCALE as u128);
1807
1808 let base = TIP20Setup::create("BASE", "BASE", admin)
1809 .with_issuer(admin)
1810 .apply()?;
1811 let base_token = base.address();
1812 let quote_token = base.quote_token()?;
1813
1814 TIP20Setup::path_usd(admin)
1815 .with_issuer(admin)
1816 .with_mint(alice, U256::from(escrow_ceil))
1817 .with_approval(alice, exchange.address, U256::MAX)
1818 .apply()?;
1819
1820 exchange.create_pair(base_token)?;
1821
1822 let order_id = exchange.place(alice, base_token, base_amount, true, tick)?;
1823
1824 let alice_balance_after_place = exchange.balance_of(alice, quote_token)?;
1826 assert_eq!(
1827 alice_balance_after_place, 0,
1828 "All quote tokens should be escrowed"
1829 );
1830
1831 exchange.cancel(alice, order_id)?;
1832
1833 let alice_refund = exchange.balance_of(alice, quote_token)?;
1834
1835 assert_eq!(
1838 alice_refund, escrow_ceil,
1839 "Cancellation refund must equal escrow amount. User escrowed {escrow_ceil} but got back {alice_refund}"
1840 );
1841
1842 Ok(())
1843 })
1844 }
1845
1846 #[test]
1847 fn test_place_order_pair_auto_created() -> eyre::Result<()> {
1848 let mut storage = HashMapStorageProvider::new(1);
1849 StorageCtx::enter(&mut storage, || {
1850 let mut exchange = StablecoinDEX::new();
1851 exchange.initialize()?;
1852
1853 let alice = Address::random();
1854 let admin = Address::random();
1855 let min_order_amount = MIN_ORDER_AMOUNT;
1856 let tick = 100i16;
1857
1858 let price = orderbook::tick_to_price(tick);
1859 let expected_escrow =
1860 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
1861
1862 let (base_token, _quote_token) =
1863 setup_test_tokens(admin, alice, exchange.address, expected_escrow)?;
1864
1865 let result = exchange.place(alice, base_token, min_order_amount, true, tick);
1867 assert!(result.is_ok());
1868
1869 Ok(())
1870 })
1871 }
1872
1873 #[test]
1874 fn test_place_order_below_minimum_amount() -> eyre::Result<()> {
1875 let mut storage = HashMapStorageProvider::new(1);
1876 StorageCtx::enter(&mut storage, || {
1877 let mut exchange = StablecoinDEX::new();
1878 exchange.initialize()?;
1879
1880 let alice = Address::random();
1881 let admin = Address::random();
1882 let min_order_amount = MIN_ORDER_AMOUNT;
1883 let below_minimum = min_order_amount - 1;
1884 let tick = 100i16;
1885
1886 let price = orderbook::tick_to_price(tick);
1887 let escrow_amount = (below_minimum * price as u128) / orderbook::PRICE_SCALE as u128;
1888
1889 let (base_token, _quote_token) =
1890 setup_test_tokens(admin, alice, exchange.address, escrow_amount)?;
1891
1892 exchange
1894 .create_pair(base_token)
1895 .expect("Could not create pair");
1896
1897 let result = exchange.place(alice, base_token, below_minimum, true, tick);
1899 assert_eq!(
1900 result,
1901 Err(StablecoinDEXError::below_minimum_order_size(below_minimum).into())
1902 );
1903
1904 Ok(())
1905 })
1906 }
1907
1908 #[test]
1909 fn test_place_bid_order() -> eyre::Result<()> {
1910 let mut storage = HashMapStorageProvider::new(1);
1911 StorageCtx::enter(&mut storage, || {
1912 let mut exchange = StablecoinDEX::new();
1913 exchange.initialize()?;
1914
1915 let alice = Address::random();
1916 let admin = Address::random();
1917 let min_order_amount = MIN_ORDER_AMOUNT;
1918 let tick = 100i16;
1919
1920 let price = orderbook::tick_to_price(tick);
1921 let expected_escrow =
1922 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
1923
1924 let (base_token, quote_token) =
1926 setup_test_tokens(admin, alice, exchange.address, expected_escrow)?;
1927
1928 exchange
1930 .create_pair(base_token)
1931 .expect("Could not create pair");
1932
1933 let order_id = exchange
1935 .place(alice, base_token, min_order_amount, true, tick)
1936 .expect("Place bid order should succeed");
1937
1938 assert_eq!(order_id, 1);
1939 assert_eq!(exchange.next_order_id()?, 2);
1940
1941 let stored_order = exchange.orders[order_id].read()?;
1943 assert_eq!(stored_order.maker(), alice);
1944 assert_eq!(stored_order.amount(), min_order_amount);
1945 assert_eq!(stored_order.remaining(), min_order_amount);
1946 assert_eq!(stored_order.tick(), tick);
1947 assert!(stored_order.is_bid());
1948 assert!(!stored_order.is_flip());
1949
1950 let book_key = compute_book_key(base_token, quote_token);
1952 let book_handler = &exchange.books[book_key];
1953 let level = book_handler.tick_level_handler(tick, true).read()?;
1954 assert_eq!(level.head, order_id);
1955 assert_eq!(level.tail, order_id);
1956 assert_eq!(level.total_liquidity, min_order_amount);
1957
1958 let quote_tip20 = TIP20Token::from_address(quote_token)?;
1960 let remaining_balance =
1961 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?;
1962 assert_eq!(remaining_balance, U256::ZERO);
1963
1964 let exchange_balance = quote_tip20.balance_of(ITIP20::balanceOfCall {
1966 account: exchange.address,
1967 })?;
1968 assert_eq!(exchange_balance, U256::from(expected_escrow));
1969
1970 Ok(())
1971 })
1972 }
1973
1974 #[test]
1975 fn test_place_ask_order() -> eyre::Result<()> {
1976 let mut storage = HashMapStorageProvider::new(1);
1977 StorageCtx::enter(&mut storage, || {
1978 let mut exchange = StablecoinDEX::new();
1979 exchange.initialize()?;
1980
1981 let alice = Address::random();
1982 let admin = Address::random();
1983 let min_order_amount = MIN_ORDER_AMOUNT;
1984 let tick = 50i16; let (base_token, quote_token) =
1988 setup_test_tokens(admin, alice, exchange.address, min_order_amount)?;
1989 exchange
1991 .create_pair(base_token)
1992 .expect("Could not create pair");
1993
1994 let order_id = exchange
1995 .place(alice, base_token, min_order_amount, false, tick) .expect("Place ask order should succeed");
1997
1998 assert_eq!(order_id, 1);
1999 assert_eq!(exchange.next_order_id()?, 2);
2000
2001 let stored_order = exchange.orders[order_id].read()?;
2003 assert_eq!(stored_order.maker(), alice);
2004 assert_eq!(stored_order.amount(), min_order_amount);
2005 assert_eq!(stored_order.remaining(), min_order_amount);
2006 assert_eq!(stored_order.tick(), tick);
2007 assert!(!stored_order.is_bid());
2008 assert!(!stored_order.is_flip());
2009
2010 let book_key = compute_book_key(base_token, quote_token);
2012 let book_handler = &exchange.books[book_key];
2013 let level = book_handler.tick_level_handler(tick, false).read()?;
2014 assert_eq!(level.head, order_id);
2015 assert_eq!(level.tail, order_id);
2016 assert_eq!(level.total_liquidity, min_order_amount);
2017
2018 let base_tip20 = TIP20Token::from_address(base_token)?;
2020 let remaining_balance =
2021 base_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?;
2022 assert_eq!(remaining_balance, U256::ZERO); let exchange_balance = base_tip20.balance_of(ITIP20::balanceOfCall {
2026 account: exchange.address,
2027 })?;
2028 assert_eq!(exchange_balance, U256::from(min_order_amount));
2029
2030 Ok(())
2031 })
2032 }
2033
2034 #[test]
2035 fn test_place_flip_order_below_minimum_amount() -> eyre::Result<()> {
2036 let mut storage = HashMapStorageProvider::new(1);
2037 StorageCtx::enter(&mut storage, || {
2038 let mut exchange = StablecoinDEX::new();
2039 exchange.initialize()?;
2040
2041 let alice = Address::random();
2042 let admin = Address::random();
2043 let min_order_amount = MIN_ORDER_AMOUNT;
2044 let below_minimum = min_order_amount - 1;
2045 let tick = 100i16;
2046 let flip_tick = 200i16;
2047
2048 let price = orderbook::tick_to_price(tick);
2049 let escrow_amount = (below_minimum * price as u128) / orderbook::PRICE_SCALE as u128;
2050
2051 let (base_token, _quote_token) =
2052 setup_test_tokens(admin, alice, exchange.address, escrow_amount)?;
2053
2054 exchange
2056 .create_pair(base_token)
2057 .expect("Could not create pair");
2058
2059 let result = exchange.place_flip(
2061 alice,
2062 base_token,
2063 below_minimum,
2064 true,
2065 tick,
2066 flip_tick,
2067 false,
2068 );
2069 assert_eq!(
2070 result,
2071 Err(StablecoinDEXError::below_minimum_order_size(below_minimum).into())
2072 );
2073
2074 Ok(())
2075 })
2076 }
2077
2078 #[test]
2079 fn test_place_flip_auto_creates_pair() -> Result<()> {
2080 let mut storage = HashMapStorageProvider::new(1);
2081 StorageCtx::enter(&mut storage, || {
2082 let mut exchange = StablecoinDEX::new();
2083 exchange.initialize()?;
2084
2085 let admin = Address::random();
2086 let user = Address::random();
2087
2088 let (base_token, quote_token) =
2090 setup_test_tokens(admin, user, exchange.address, 100_000_000)?;
2091
2092 let book_key = compute_book_key(base_token, quote_token);
2094 let book_before = exchange.books[book_key].read()?;
2095 assert!(book_before.base.is_zero(),);
2096
2097 let mut base = TIP20Token::from_address(base_token)?;
2099 base.transfer(
2100 user,
2101 ITIP20::transferCall {
2102 to: exchange.address,
2103 amount: U256::from(MIN_ORDER_AMOUNT),
2104 },
2105 )
2106 .expect("Base token transfer failed");
2107
2108 exchange.place_flip(user, base_token, MIN_ORDER_AMOUNT, true, 0, 10, false)?;
2110
2111 let book_after = exchange.books[book_key].read()?;
2112 assert_eq!(book_after.base, base_token);
2113
2114 let events = exchange.emitted_events();
2116 assert_eq!(events.len(), 2);
2117 assert_eq!(
2118 events[0],
2119 StablecoinDEXEvents::PairCreated(IStablecoinDEX::PairCreated {
2120 key: book_key,
2121 base: base_token,
2122 quote: quote_token,
2123 })
2124 .into_log_data()
2125 );
2126
2127 Ok(())
2128 })
2129 }
2130
2131 #[test]
2132 fn test_place_flip_order() -> eyre::Result<()> {
2133 let mut storage = HashMapStorageProvider::new(1);
2134 StorageCtx::enter(&mut storage, || {
2135 let mut exchange = StablecoinDEX::new();
2136 exchange.initialize()?;
2137
2138 let alice = Address::random();
2139 let admin = Address::random();
2140 let min_order_amount = MIN_ORDER_AMOUNT;
2141 let tick = 100i16;
2142 let flip_tick = 200i16; let price = orderbook::tick_to_price(tick);
2146 let expected_escrow =
2147 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2148
2149 let (base_token, quote_token) =
2151 setup_test_tokens(admin, alice, exchange.address, expected_escrow)?;
2152 exchange
2153 .create_pair(base_token)
2154 .expect("Could not create pair");
2155
2156 let order_id = exchange
2157 .place_flip(
2158 alice,
2159 base_token,
2160 min_order_amount,
2161 true,
2162 tick,
2163 flip_tick,
2164 false,
2165 )
2166 .expect("Place flip bid order should succeed");
2167
2168 assert_eq!(order_id, 1);
2169 assert_eq!(exchange.next_order_id()?, 2);
2170
2171 let stored_order = exchange.orders[order_id].read()?;
2173 assert_eq!(stored_order.maker(), alice);
2174 assert_eq!(stored_order.amount(), min_order_amount);
2175 assert_eq!(stored_order.remaining(), min_order_amount);
2176 assert_eq!(stored_order.tick(), tick);
2177 assert!(stored_order.is_bid());
2178 assert!(stored_order.is_flip());
2179 assert_eq!(stored_order.flip_tick(), flip_tick);
2180
2181 let book_key = compute_book_key(base_token, quote_token);
2183 let book_handler = &exchange.books[book_key];
2184 let level = book_handler.tick_level_handler(tick, true).read()?;
2185 assert_eq!(level.head, order_id);
2186 assert_eq!(level.tail, order_id);
2187 assert_eq!(level.total_liquidity, min_order_amount);
2188
2189 let quote_tip20 = TIP20Token::from_address(quote_token)?;
2191 let remaining_balance =
2192 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?;
2193 assert_eq!(remaining_balance, U256::ZERO);
2194
2195 let exchange_balance = quote_tip20.balance_of(ITIP20::balanceOfCall {
2197 account: exchange.address,
2198 })?;
2199 assert_eq!(exchange_balance, U256::from(expected_escrow));
2200
2201 Ok(())
2202 })
2203 }
2204
2205 #[test]
2208 fn test_place_flip_same_tick_per_hardfork() -> eyre::Result<()> {
2209 for spec in [TempoHardfork::T4, TempoHardfork::T5] {
2210 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
2211 StorageCtx::enter(&mut storage, || {
2212 let mut exchange = StablecoinDEX::new();
2213 exchange.initialize()?;
2214
2215 let alice = Address::random();
2216 let admin = Address::random();
2217 let tick = 100i16;
2218
2219 let price = orderbook::tick_to_price(tick);
2220 let escrow = (MIN_ORDER_AMOUNT * price as u128) / orderbook::PRICE_SCALE as u128;
2221
2222 let (base_token, _) = setup_test_tokens(admin, alice, exchange.address, escrow)?;
2223 exchange.create_pair(base_token)?;
2224
2225 let result = exchange.place_flip(
2226 alice,
2227 base_token,
2228 MIN_ORDER_AMOUNT,
2229 true,
2230 tick,
2231 tick,
2232 false,
2233 );
2234
2235 if spec.is_t5() {
2236 let order_id = result.expect("same-tick flip should succeed on T5+");
2237 let stored = exchange.orders[order_id].read()?;
2238 assert_eq!(stored.tick(), tick);
2239 assert_eq!(stored.flip_tick(), tick);
2240 assert!(stored.is_bid());
2241 assert!(stored.is_flip());
2242 } else {
2243 assert_eq!(result, Err(StablecoinDEXError::invalid_flip_tick().into()));
2244 }
2245
2246 Ok::<_, eyre::Report>(())
2247 })?;
2248 }
2249 Ok(())
2250 }
2251
2252 #[test]
2257 fn test_place_flip_wrong_side_still_rejected_t5() -> eyre::Result<()> {
2258 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
2259 StorageCtx::enter(&mut storage, || {
2260 let mut exchange = StablecoinDEX::new();
2261 exchange.initialize()?;
2262
2263 let alice = Address::random();
2264 let admin = Address::random();
2265 let tick = 100i16;
2266
2267 let price = orderbook::tick_to_price(tick);
2268 let escrow = (MIN_ORDER_AMOUNT * price as u128) / orderbook::PRICE_SCALE as u128;
2269
2270 let (base_token, _) = setup_test_tokens(admin, alice, exchange.address, escrow)?;
2271 exchange.create_pair(base_token)?;
2272
2273 let bid_result = exchange.place_flip(
2275 alice,
2276 base_token,
2277 MIN_ORDER_AMOUNT,
2278 true,
2279 tick,
2280 tick - TICK_SPACING,
2281 false,
2282 );
2283 assert_eq!(
2284 bid_result,
2285 Err(StablecoinDEXError::invalid_flip_tick().into())
2286 );
2287
2288 let ask_result = exchange.place_flip(
2290 alice,
2291 base_token,
2292 MIN_ORDER_AMOUNT,
2293 false,
2294 tick,
2295 tick + TICK_SPACING,
2296 false,
2297 );
2298 assert_eq!(
2299 ask_result,
2300 Err(StablecoinDEXError::invalid_flip_tick().into())
2301 );
2302
2303 Ok(())
2304 })
2305 }
2306
2307 #[test]
2319 fn test_flip_same_tick_locked_book_t5() -> eyre::Result<()> {
2320 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
2321 StorageCtx::enter(&mut storage, || {
2322 let mut exchange = StablecoinDEX::new();
2323 exchange.initialize()?;
2324
2325 let alice = Address::random();
2326 let bob = Address::random();
2327 let admin = Address::random();
2328 let amount = MIN_ORDER_AMOUNT;
2329 let tick = 100i16;
2330
2331 let price = orderbook::tick_to_price(tick);
2332 let expected_escrow = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
2333
2334 let base = TIP20Setup::create("BASE", "BASE", admin)
2340 .with_issuer(admin)
2341 .with_mint(bob, U256::from(amount * 4))
2342 .with_approval(bob, exchange.address, U256::MAX)
2343 .apply()?;
2344 let base_token = base.address();
2345 let quote_token = base.quote_token()?;
2346
2347 TIP20Setup::path_usd(admin)
2348 .with_issuer(admin)
2349 .with_mint(alice, U256::from(expected_escrow * 4))
2350 .with_mint(bob, U256::from(expected_escrow * 4))
2351 .with_approval(alice, exchange.address, U256::MAX)
2352 .with_approval(bob, exchange.address, U256::MAX)
2353 .apply()?;
2354
2355 exchange.create_pair(base_token)?;
2356
2357 let flip_id = exchange
2360 .place_flip(alice, base_token, amount, true, tick, tick, false)
2361 .expect("same-tick flip should succeed on T5");
2362
2363 let resting_bid_id = exchange
2366 .place(alice, base_token, amount, true, tick)
2367 .expect("regular bid should succeed");
2368
2369 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
2373
2374 assert_eq!(exchange.orders[flip_id].read()?.maker(), Address::ZERO);
2376 let resting = exchange.orders[resting_bid_id].read()?;
2377 assert_eq!(resting.maker(), alice);
2378 assert_eq!(resting.remaining(), amount);
2379 assert!(resting.is_bid());
2380
2381 let new_ask_id = exchange.next_order_id()? - 1;
2384 assert_eq!(new_ask_id, resting_bid_id + 1);
2385 let new_ask = exchange.orders[new_ask_id].read()?;
2386 assert_eq!(new_ask.maker(), alice);
2387 assert!(new_ask.is_ask());
2388 assert!(new_ask.is_flip());
2389 assert_eq!(new_ask.tick(), tick);
2390 assert_eq!(new_ask.flip_tick(), tick);
2391 assert_eq!(new_ask.remaining(), amount);
2392
2393 let book_key = compute_book_key(base_token, quote_token);
2396 let bid_level = exchange.books[book_key]
2397 .tick_level_handler(tick, true)
2398 .read()?;
2399 assert_eq!(bid_level.head, resting_bid_id);
2400 assert_eq!(bid_level.tail, resting_bid_id);
2401 assert_eq!(bid_level.total_liquidity, amount);
2402
2403 let ask_level = exchange.books[book_key]
2404 .tick_level_handler(tick, false)
2405 .read()?;
2406 assert_eq!(ask_level.head, new_ask_id);
2407 assert_eq!(ask_level.tail, new_ask_id);
2408 assert_eq!(ask_level.total_liquidity, amount);
2409
2410 let book = exchange.books[book_key].read()?;
2411 assert_eq!(book.best_bid_tick, tick, "best bid should remain at tick");
2413 assert_eq!(
2414 book.best_ask_tick, tick,
2415 "best ask should now equal best bid (locked)"
2416 );
2417
2418 let quote_in =
2423 base_to_quote(amount, tick, RoundingDirection::Up).expect("quote_in should fit");
2424 exchange.swap_exact_amount_in(bob, quote_token, base_token, quote_in, 0)?;
2425
2426 assert_eq!(exchange.orders[new_ask_id].read()?.maker(), Address::ZERO);
2428
2429 let resting_after = exchange.orders[resting_bid_id].read()?;
2431 assert_eq!(resting_after.maker(), alice);
2432 assert_eq!(resting_after.remaining(), amount);
2433
2434 let flipped_back_id = exchange.next_order_id()? - 1;
2436 assert_eq!(flipped_back_id, new_ask_id + 1);
2437 let flipped_back = exchange.orders[flipped_back_id].read()?;
2438 assert_eq!(flipped_back.maker(), alice);
2439 assert!(flipped_back.is_bid());
2440 assert!(flipped_back.is_flip());
2441 assert_eq!(flipped_back.tick(), tick);
2442 assert_eq!(flipped_back.flip_tick(), tick);
2443
2444 let book_after = exchange.books[book_key].read()?;
2447 assert_eq!(book_after.best_bid_tick, tick);
2448 assert_eq!(book_after.best_ask_tick, i16::MAX);
2449
2450 let bid_level_after = exchange.books[book_key]
2451 .tick_level_handler(tick, true)
2452 .read()?;
2453 assert_eq!(bid_level_after.head, resting_bid_id);
2454 assert_eq!(bid_level_after.tail, flipped_back_id);
2455 assert_eq!(bid_level_after.total_liquidity, amount * 2);
2456
2457 Ok(())
2458 })
2459 }
2460
2461 #[test]
2462 fn test_withdraw() -> eyre::Result<()> {
2463 let mut storage = HashMapStorageProvider::new(1);
2464 StorageCtx::enter(&mut storage, || {
2465 let mut exchange = StablecoinDEX::new();
2466 exchange.initialize()?;
2467
2468 let alice = Address::random();
2469 let admin = Address::random();
2470 let min_order_amount = MIN_ORDER_AMOUNT;
2471 let tick = 100i16;
2472 let price = orderbook::tick_to_price(tick);
2473 let expected_escrow =
2474 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2475
2476 let (base_token, quote_token) =
2478 setup_test_tokens(admin, alice, exchange.address, expected_escrow)?;
2479 exchange
2480 .create_pair(base_token)
2481 .expect("Could not create pair");
2482
2483 let order_id = exchange
2485 .place(alice, base_token, min_order_amount, true, tick)
2486 .expect("Place bid order should succeed");
2487
2488 exchange
2489 .cancel(alice, order_id)
2490 .expect("Cancel pending order should succeed");
2491
2492 assert_eq!(exchange.balance_of(alice, quote_token)?, expected_escrow);
2493
2494 exchange
2496 .withdraw(alice, quote_token, expected_escrow)
2497 .expect("Withdraw should succeed");
2498 assert_eq!(exchange.balance_of(alice, quote_token)?, 0);
2499
2500 let quote_tip20 = TIP20Token::from_address(quote_token)?;
2502 assert_eq!(
2503 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?,
2504 expected_escrow
2505 );
2506 assert_eq!(
2507 quote_tip20.balance_of(ITIP20::balanceOfCall {
2508 account: exchange.address
2509 })?,
2510 0
2511 );
2512
2513 Ok(())
2514 })
2515 }
2516
2517 #[test]
2518 fn test_withdraw_insufficient_balance() -> eyre::Result<()> {
2519 let mut storage = HashMapStorageProvider::new(1);
2520 StorageCtx::enter(&mut storage, || {
2521 let mut exchange = StablecoinDEX::new();
2522 exchange.initialize()?;
2523
2524 let alice = Address::random();
2525 let admin = Address::random();
2526
2527 let min_order_amount = MIN_ORDER_AMOUNT;
2528 let (_base_token, quote_token) =
2529 setup_test_tokens(admin, alice, exchange.address, min_order_amount)?;
2530
2531 assert_eq!(exchange.balance_of(alice, quote_token)?, 0);
2533
2534 let result = exchange.withdraw(alice, quote_token, 100u128);
2536
2537 assert_eq!(
2538 result,
2539 Err(StablecoinDEXError::insufficient_balance().into())
2540 );
2541
2542 Ok(())
2543 })
2544 }
2545
2546 #[test]
2547 fn test_quote_swap_exact_amount_out() -> eyre::Result<()> {
2548 let mut storage = HashMapStorageProvider::new(1);
2549 StorageCtx::enter(&mut storage, || {
2550 let mut exchange = StablecoinDEX::new();
2551 exchange.initialize()?;
2552
2553 let alice = Address::random();
2554 let admin = Address::random();
2555 let min_order_amount = MIN_ORDER_AMOUNT;
2556 let amount_out = 500_000u128;
2557 let tick = 10;
2558
2559 let (base_token, quote_token) =
2560 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2561 exchange
2562 .create_pair(base_token)
2563 .expect("Could not create pair");
2564
2565 let order_amount = min_order_amount;
2566 exchange
2567 .place(alice, base_token, order_amount, false, tick)
2568 .expect("Order should succeed");
2569
2570 let amount_in = exchange
2571 .quote_swap_exact_amount_out(quote_token, base_token, amount_out)
2572 .expect("Swap should succeed");
2573
2574 let price = orderbook::tick_to_price(tick);
2575 let expected_amount_in = (amount_out * price as u128) / orderbook::PRICE_SCALE as u128;
2576 assert_eq!(amount_in, expected_amount_in);
2577
2578 Ok(())
2579 })
2580 }
2581
2582 #[test]
2583 fn test_quote_swap_exact_amount_in() -> eyre::Result<()> {
2584 let mut storage = HashMapStorageProvider::new(1);
2585 StorageCtx::enter(&mut storage, || {
2586 let mut exchange = StablecoinDEX::new();
2587 exchange.initialize()?;
2588
2589 let alice = Address::random();
2590 let admin = Address::random();
2591 let min_order_amount = MIN_ORDER_AMOUNT;
2592 let amount_in = 500_000u128;
2593 let tick = 10;
2594
2595 let (base_token, quote_token) =
2596 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2597 exchange
2598 .create_pair(base_token)
2599 .expect("Could not create pair");
2600
2601 let order_amount = min_order_amount;
2602 exchange
2603 .place(alice, base_token, order_amount, true, tick)
2604 .expect("Place bid order should succeed");
2605
2606 let amount_out = exchange
2607 .quote_swap_exact_amount_in(base_token, quote_token, amount_in)
2608 .expect("Swap should succeed");
2609
2610 let price = orderbook::tick_to_price(tick);
2612 let expected_amount_out = (amount_in * price as u128) / orderbook::PRICE_SCALE as u128;
2613 assert_eq!(amount_out, expected_amount_out);
2614
2615 Ok(())
2616 })
2617 }
2618
2619 #[test]
2620 fn test_quote_swap_exact_amount_out_base_for_quote() -> 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 = 0;
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;
2640 exchange
2641 .place(alice, base_token, order_amount, true, tick)
2642 .expect("Place bid order should succeed");
2643
2644 let amount_in = exchange
2647 .quote_swap_exact_amount_out(base_token, quote_token, amount_out)
2648 .expect("Quote should succeed");
2649
2650 let price = orderbook::tick_to_price(tick);
2651 let expected_amount_in =
2653 (amount_out * orderbook::PRICE_SCALE as u128).div_ceil(price as u128);
2654 assert_eq!(amount_in, expected_amount_in);
2655
2656 Ok(())
2657 })
2658 }
2659
2660 #[test]
2661 fn test_quote_exact_out_bid_positive_tick_no_underflow() -> eyre::Result<()> {
2662 let mut storage = HashMapStorageProvider::new(1);
2663 StorageCtx::enter(&mut storage, || {
2664 let mut exchange = StablecoinDEX::new();
2665 exchange.initialize()?;
2666
2667 let alice = Address::random();
2668 let admin = Address::random();
2669
2670 let (base_token, quote_token) =
2671 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2672 exchange.create_pair(base_token)?;
2673
2674 let tick = 10;
2675 let order_amount = MIN_ORDER_AMOUNT;
2676 exchange.place(alice, base_token, order_amount, true, tick)?;
2677
2678 for amount_out in [100_001u128, 100_003, 100_007, 100_009, 100_011] {
2679 let amount_in = exchange
2680 .quote_swap_exact_amount_out(base_token, quote_token, amount_out)
2681 .unwrap_or_else(|_| {
2682 panic!("quote_exact_out should not underflow for amount_out={amount_out}")
2683 });
2684
2685 let expected =
2686 orderbook::quote_to_base(amount_out, tick, RoundingDirection::Up).unwrap();
2687 assert_eq!(
2688 amount_in, expected,
2689 "amount_in should equal quote_to_base(amount_out, tick, Up) for amount_out={amount_out}"
2690 );
2691 }
2692
2693 Ok(())
2694 })
2695 }
2696
2697 #[test]
2698 fn test_swap_exact_amount_out() -> eyre::Result<()> {
2699 let mut storage = HashMapStorageProvider::new(1);
2700 StorageCtx::enter(&mut storage, || {
2701 let mut exchange = StablecoinDEX::new();
2702 exchange.initialize()?;
2703
2704 let alice = Address::random();
2705 let bob = Address::random();
2706 let admin = Address::random();
2707 let min_order_amount = MIN_ORDER_AMOUNT;
2708 let amount_out = 500_000u128;
2709 let tick = 10;
2710
2711 let (base_token, quote_token) =
2712 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2713 exchange
2714 .create_pair(base_token)
2715 .expect("Could not create pair");
2716
2717 let order_amount = min_order_amount;
2718 exchange
2719 .place(alice, base_token, order_amount, false, tick)
2720 .expect("Order should succeed");
2721
2722 exchange
2723 .set_balance(bob, quote_token, 200_000_000u128)
2724 .expect("Could not set balance");
2725
2726 let price = orderbook::tick_to_price(tick);
2727 let max_amount_in = (amount_out * price as u128) / orderbook::PRICE_SCALE as u128;
2728
2729 let amount_in = exchange
2730 .swap_exact_amount_out(bob, quote_token, base_token, amount_out, max_amount_in)
2731 .expect("Swap should succeed");
2732
2733 let base_tip20 = TIP20Token::from_address(base_token)?;
2734 let bob_base_balance = base_tip20.balance_of(ITIP20::balanceOfCall { account: bob })?;
2735 assert_eq!(bob_base_balance, U256::from(amount_out));
2736
2737 let alice_quote_exchange_balance = exchange.balance_of(alice, quote_token)?;
2738 assert_eq!(alice_quote_exchange_balance, amount_in);
2739
2740 Ok(())
2741 })
2742 }
2743
2744 #[test]
2745 fn test_swap_exact_amount_in() -> eyre::Result<()> {
2746 let mut storage = HashMapStorageProvider::new(1);
2747 StorageCtx::enter(&mut storage, || {
2748 let mut exchange = StablecoinDEX::new();
2749 exchange.initialize()?;
2750
2751 let alice = Address::random();
2752 let bob = Address::random();
2753 let admin = Address::random();
2754 let min_order_amount = MIN_ORDER_AMOUNT;
2755 let amount_in = 500_000u128;
2756 let tick = 10;
2757
2758 let (base_token, quote_token) =
2759 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
2760 exchange
2761 .create_pair(base_token)
2762 .expect("Could not create pair");
2763
2764 let order_amount = min_order_amount;
2765 exchange
2766 .place(alice, base_token, order_amount, true, tick)
2767 .expect("Order should succeed");
2768
2769 exchange
2770 .set_balance(bob, base_token, 200_000_000u128)
2771 .expect("Could not set balance");
2772
2773 let price = orderbook::tick_to_price(tick);
2774 let min_amount_out = (amount_in * price as u128) / orderbook::PRICE_SCALE as u128;
2775
2776 let amount_out = exchange
2777 .swap_exact_amount_in(bob, base_token, quote_token, amount_in, min_amount_out)
2778 .expect("Swap should succeed");
2779
2780 let quote_tip20 = TIP20Token::from_address(quote_token)?;
2781 let bob_quote_balance =
2782 quote_tip20.balance_of(ITIP20::balanceOfCall { account: bob })?;
2783 assert_eq!(bob_quote_balance, U256::from(amount_out));
2784
2785 let alice_base_exchange_balance = exchange.balance_of(alice, base_token)?;
2786 assert_eq!(alice_base_exchange_balance, amount_in);
2787
2788 Ok(())
2789 })
2790 }
2791
2792 #[test]
2793 fn test_flip_order_execution() -> eyre::Result<()> {
2794 let mut storage = HashMapStorageProvider::new(1);
2795 StorageCtx::enter(&mut storage, || {
2796 let mut exchange = StablecoinDEX::new();
2797 exchange.initialize()?;
2798
2799 let alice = Address::random();
2800 let bob = Address::random();
2801 let admin = Address::random();
2802 let min_order_amount = MIN_ORDER_AMOUNT;
2803 let amount = min_order_amount;
2804 let tick = 100i16;
2805 let flip_tick = 200i16;
2806
2807 let price = orderbook::tick_to_price(tick);
2808 let expected_escrow = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
2809
2810 let (base_token, quote_token) =
2811 setup_test_tokens(admin, alice, exchange.address, expected_escrow * 2)?;
2812 exchange
2813 .create_pair(base_token)
2814 .expect("Could not create pair");
2815
2816 let flip_order_id = exchange
2818 .place_flip(alice, base_token, amount, true, tick, flip_tick, false)
2819 .expect("Place flip order should succeed");
2820
2821 exchange
2822 .set_balance(bob, base_token, amount)
2823 .expect("Could not set balance");
2824
2825 exchange
2826 .swap_exact_amount_in(bob, base_token, quote_token, amount, 0)
2827 .expect("Swap should succeed");
2828
2829 let filled_order = exchange.orders[flip_order_id].read()?;
2831 assert_eq!(filled_order.remaining(), 0);
2832
2833 let new_order_id = exchange.next_order_id()? - 1;
2835 assert_eq!(new_order_id, flip_order_id + 1);
2836
2837 let new_order = exchange.orders[new_order_id].read()?;
2838 assert_eq!(new_order.maker(), alice);
2839 assert_eq!(new_order.tick(), flip_tick);
2840 assert_eq!(new_order.flip_tick(), tick);
2841 assert!(new_order.is_ask());
2842 assert_eq!(new_order.amount(), amount);
2843 assert_eq!(new_order.remaining(), amount);
2844
2845 Ok(())
2846 })
2847 }
2848
2849 #[test]
2855 fn test_flip_same_tick_execution_t5() -> eyre::Result<()> {
2856 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
2857 StorageCtx::enter(&mut storage, || {
2858 let mut exchange = StablecoinDEX::new();
2859 exchange.initialize()?;
2860
2861 let alice = Address::random();
2862 let bob = Address::random();
2863 let admin = Address::random();
2864 let amount = MIN_ORDER_AMOUNT;
2865 let tick = 100i16;
2866 let flip_tick = tick;
2868
2869 let price = orderbook::tick_to_price(tick);
2870 let expected_escrow = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
2871
2872 let (base_token, quote_token) =
2873 setup_test_tokens(admin, alice, exchange.address, expected_escrow * 2)?;
2874 exchange.create_pair(base_token)?;
2875
2876 let flip_order_id =
2877 exchange.place_flip(alice, base_token, amount, true, tick, flip_tick, false)?;
2878
2879 exchange.set_balance(bob, base_token, amount)?;
2880 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
2881
2882 let filled = exchange.orders[flip_order_id].read()?;
2884 assert_eq!(filled.maker(), Address::ZERO);
2885
2886 let new_order_id = exchange.next_order_id()? - 1;
2889 assert_eq!(new_order_id, flip_order_id + 1);
2890
2891 let new_order = exchange.orders[new_order_id].read()?;
2892 assert_eq!(new_order.maker(), alice);
2893 assert!(new_order.is_ask());
2894 assert!(new_order.is_flip());
2895 assert_eq!(new_order.tick(), tick);
2896 assert_eq!(new_order.flip_tick(), tick);
2897 assert_eq!(new_order.amount(), amount);
2898 assert_eq!(new_order.remaining(), amount);
2899
2900 assert_eq!(exchange.balance_of(alice, base_token)?, 0);
2904 assert_eq!(exchange.balance_of(alice, quote_token)?, 0);
2905
2906 let book_key = compute_book_key(base_token, quote_token);
2908 let book = exchange.books[book_key].read()?;
2909 assert_eq!(book.best_ask_tick, tick);
2910 assert_eq!(book.best_bid_tick, i16::MIN);
2911
2912 Ok(())
2913 })
2914 }
2915
2916 #[test]
2917 fn test_pair_created() -> eyre::Result<()> {
2918 let mut storage = HashMapStorageProvider::new(1);
2919 StorageCtx::enter(&mut storage, || {
2920 let mut exchange = StablecoinDEX::new();
2921 exchange.initialize()?;
2922
2923 let admin = Address::random();
2924 let alice = Address::random();
2925
2926 let min_order_amount = MIN_ORDER_AMOUNT;
2927 let (base_token, quote_token) =
2929 setup_test_tokens(admin, alice, exchange.address, min_order_amount)?;
2930
2931 let key = exchange
2933 .create_pair(base_token)
2934 .expect("Could not create pair");
2935
2936 exchange.assert_emitted_events(vec![StablecoinDEXEvents::PairCreated(
2938 IStablecoinDEX::PairCreated {
2939 key,
2940 base: base_token,
2941 quote: quote_token,
2942 },
2943 )]);
2944
2945 Ok(())
2946 })
2947 }
2948
2949 #[test]
2950 fn test_pair_already_created() -> eyre::Result<()> {
2951 let mut storage = HashMapStorageProvider::new(1);
2952 StorageCtx::enter(&mut storage, || {
2953 let mut exchange = StablecoinDEX::new();
2954 exchange.initialize()?;
2955
2956 let admin = Address::random();
2957 let alice = Address::random();
2958
2959 let min_order_amount = MIN_ORDER_AMOUNT;
2960 let (base_token, _) =
2962 setup_test_tokens(admin, alice, exchange.address, min_order_amount)?;
2963
2964 exchange
2965 .create_pair(base_token)
2966 .expect("Could not create pair");
2967
2968 let result = exchange.create_pair(base_token);
2969 assert_eq!(
2970 result,
2971 Err(StablecoinDEXError::pair_already_exists().into())
2972 );
2973
2974 Ok(())
2975 })
2976 }
2977
2978 fn verify_hop(hop: (B256, bool), token_in: Address) -> eyre::Result<()> {
2980 let (book_key, is_base_for_quote) = hop;
2981
2982 let exchange = StablecoinDEX::new();
2983 let orderbook = exchange.books[book_key].read()?;
2984
2985 let expected_book_key = compute_book_key(orderbook.base, orderbook.quote);
2986 assert_eq!(book_key, expected_book_key, "Book key should match");
2987
2988 let expected_direction = token_in == orderbook.base;
2989 assert_eq!(
2990 is_base_for_quote, expected_direction,
2991 "Direction should be correct: token_in={}, base={}, is_base_for_quote={}",
2992 token_in, orderbook.base, is_base_for_quote
2993 );
2994
2995 Ok(())
2996 }
2997
2998 #[test]
2999 fn test_find_path_to_root() -> eyre::Result<()> {
3000 let mut storage = HashMapStorageProvider::new(1);
3001 StorageCtx::enter(&mut storage, || {
3002 let mut exchange = StablecoinDEX::new();
3003 exchange.initialize()?;
3004
3005 let admin = Address::random();
3006
3007 let usdc = TIP20Setup::create("USDC", "USDC", admin).apply()?;
3009 let token_a = TIP20Setup::create("TokenA", "TKA", admin)
3010 .quote_token(usdc.address())
3011 .apply()?;
3012
3013 let path = exchange.find_path_to_root(token_a.address())?;
3015
3016 assert_eq!(path.len(), 3);
3018 assert_eq!(path[0], token_a.address());
3019 assert_eq!(path[1], usdc.address());
3020 assert_eq!(path[2], PATH_USD_ADDRESS);
3021
3022 Ok(())
3023 })
3024 }
3025
3026 #[test]
3027 fn test_find_trade_path_same_token_errors() -> eyre::Result<()> {
3028 let mut storage = HashMapStorageProvider::new(1);
3029 StorageCtx::enter(&mut storage, || {
3030 let mut exchange = StablecoinDEX::new();
3031 exchange.initialize()?;
3032
3033 let admin = Address::random();
3034 let user = Address::random();
3035
3036 let min_order_amount = MIN_ORDER_AMOUNT;
3037 let (token, _) = setup_test_tokens(admin, user, exchange.address, min_order_amount)?;
3038
3039 let result = exchange.find_trade_path(token, token);
3041 assert_eq!(
3042 result,
3043 Err(StablecoinDEXError::identical_tokens().into()),
3044 "Should return IdenticalTokens error when token_in == token_out"
3045 );
3046
3047 Ok(())
3048 })
3049 }
3050
3051 #[test]
3052 fn test_find_trade_path_direct_pair() -> eyre::Result<()> {
3053 let mut storage = HashMapStorageProvider::new(1);
3054 StorageCtx::enter(&mut storage, || {
3055 let mut exchange = StablecoinDEX::new();
3056 exchange.initialize()?;
3057
3058 let admin = Address::random();
3059 let user = Address::random();
3060
3061 let min_order_amount = MIN_ORDER_AMOUNT;
3062 let (token, path_usd) =
3064 setup_test_tokens(admin, user, exchange.address, min_order_amount)?;
3065
3066 exchange.create_pair(token).expect("Failed to create pair");
3068
3069 let route = exchange
3071 .find_trade_path(token, path_usd)
3072 .expect("Should find direct pair");
3073
3074 assert_eq!(route.len(), 1, "Should have 1 hop for direct pair");
3076 verify_hop(route[0], token)?;
3077
3078 Ok(())
3079 })
3080 }
3081
3082 #[test]
3083 fn test_find_trade_path_reverse_pair() -> eyre::Result<()> {
3084 let mut storage = HashMapStorageProvider::new(1);
3085 StorageCtx::enter(&mut storage, || {
3086 let mut exchange = StablecoinDEX::new();
3087 exchange.initialize()?;
3088
3089 let admin = Address::random();
3090 let user = Address::random();
3091
3092 let min_order_amount = MIN_ORDER_AMOUNT;
3093 let (token, path_usd) =
3095 setup_test_tokens(admin, user, exchange.address, min_order_amount)?;
3096
3097 exchange.create_pair(token).expect("Failed to create pair");
3099
3100 let route = exchange
3102 .find_trade_path(path_usd, token)
3103 .expect("Should find reverse pair");
3104
3105 assert_eq!(route.len(), 1, "Should have 1 hop for reverse pair");
3107 verify_hop(route[0], path_usd)?;
3108
3109 Ok(())
3110 })
3111 }
3112
3113 #[test]
3114 fn test_find_trade_path_two_hop_siblings() -> eyre::Result<()> {
3115 let mut storage = HashMapStorageProvider::new(1);
3116 StorageCtx::enter(&mut storage, || {
3117 let mut exchange = StablecoinDEX::new();
3118 exchange.initialize()?;
3119
3120 let admin = Address::random();
3121
3122 let usdc = TIP20Setup::create("USDC", "USDC", admin).apply()?;
3126 let eurc = TIP20Setup::create("EURC", "EURC", admin).apply()?;
3127
3128 exchange.create_pair(usdc.address())?;
3130 exchange.create_pair(eurc.address())?;
3131
3132 let route = exchange.find_trade_path(usdc.address(), eurc.address())?;
3134
3135 assert_eq!(route.len(), 2, "Should have 2 hops for sibling tokens");
3137 verify_hop(route[0], usdc.address())?;
3138 verify_hop(route[1], PATH_USD_ADDRESS)?;
3139
3140 Ok(())
3141 })
3142 }
3143
3144 #[test]
3145 fn test_quote_exact_in_multi_hop() -> eyre::Result<()> {
3146 let mut storage = HashMapStorageProvider::new(1);
3147 StorageCtx::enter(&mut storage, || {
3148 let mut exchange = StablecoinDEX::new();
3149 exchange.initialize()?;
3150
3151 let admin = Address::random();
3152 let alice = Address::random();
3153 let min_order_amount = MIN_ORDER_AMOUNT;
3154 let min_order_amount_x10 = U256::from(MIN_ORDER_AMOUNT * 10);
3155
3156 let _path_usd = TIP20Setup::path_usd(admin)
3159 .with_issuer(admin)
3160 .with_mint(alice, min_order_amount_x10)
3161 .with_approval(alice, exchange.address, min_order_amount_x10)
3162 .apply()?;
3163 let usdc = TIP20Setup::create("USDC", "USDC", admin)
3164 .with_issuer(admin)
3165 .with_mint(alice, min_order_amount_x10)
3166 .with_approval(alice, exchange.address, min_order_amount_x10)
3167 .apply()?;
3168 let eurc = TIP20Setup::create("EURC", "EURC", admin)
3169 .with_issuer(admin)
3170 .with_mint(alice, min_order_amount_x10)
3171 .with_approval(alice, exchange.address, min_order_amount_x10)
3172 .apply()?;
3173
3174 exchange.place(alice, usdc.address(), min_order_amount * 5, true, 0)?;
3181
3182 exchange.place(alice, eurc.address(), min_order_amount * 5, false, 0)?;
3184
3185 let amount_in = min_order_amount;
3187 let amount_out =
3188 exchange.quote_swap_exact_amount_in(usdc.address(), eurc.address(), amount_in)?;
3189
3190 assert_eq!(
3192 amount_out, amount_in,
3193 "With 1:1 rates, output should equal input"
3194 );
3195
3196 Ok(())
3197 })
3198 }
3199
3200 #[test]
3201 fn test_quote_exact_out_multi_hop() -> eyre::Result<()> {
3202 let mut storage = HashMapStorageProvider::new(1);
3203 StorageCtx::enter(&mut storage, || {
3204 let mut exchange = StablecoinDEX::new();
3205 exchange.initialize()?;
3206
3207 let admin = Address::random();
3208 let alice = Address::random();
3209 let min_order_amount = MIN_ORDER_AMOUNT;
3210 let min_order_amount_x10 = U256::from(MIN_ORDER_AMOUNT * 10);
3211
3212 let _path_usd = TIP20Setup::path_usd(admin)
3215 .with_issuer(admin)
3216 .with_mint(alice, min_order_amount_x10)
3217 .with_approval(alice, exchange.address, min_order_amount_x10)
3218 .apply()?;
3219 let usdc = TIP20Setup::create("USDC", "USDC", admin)
3220 .with_issuer(admin)
3221 .with_mint(alice, min_order_amount_x10)
3222 .with_approval(alice, exchange.address, min_order_amount_x10)
3223 .apply()?;
3224 let eurc = TIP20Setup::create("EURC", "EURC", admin)
3225 .with_issuer(admin)
3226 .with_mint(alice, min_order_amount_x10)
3227 .with_approval(alice, exchange.address, min_order_amount_x10)
3228 .apply()?;
3229
3230 exchange.place(alice, usdc.address(), min_order_amount * 5, true, 0)?;
3232 exchange.place(alice, eurc.address(), min_order_amount * 5, false, 0)?;
3233
3234 let amount_out = min_order_amount;
3236 let amount_in =
3237 exchange.quote_swap_exact_amount_out(usdc.address(), eurc.address(), amount_out)?;
3238
3239 assert_eq!(
3242 amount_in, amount_out,
3243 "With 1:1 rates and no rounding, input should equal output"
3244 );
3245
3246 Ok(())
3247 })
3248 }
3249
3250 #[test]
3251 fn test_swap_exact_in_multi_hop_transitory_balances() -> eyre::Result<()> {
3252 let mut storage = HashMapStorageProvider::new(1);
3253 StorageCtx::enter(&mut storage, || {
3254 let mut exchange = StablecoinDEX::new();
3255 exchange.initialize()?;
3256
3257 let admin = Address::random();
3258 let alice = Address::random();
3259 let bob = Address::random();
3260
3261 let min_order_amount = MIN_ORDER_AMOUNT;
3262 let min_order_amount_x10 = U256::from(MIN_ORDER_AMOUNT * 10);
3263
3264 let path_usd = TIP20Setup::path_usd(admin)
3266 .with_issuer(admin)
3267 .with_mint(alice, min_order_amount_x10)
3269 .with_approval(alice, exchange.address, min_order_amount_x10)
3270 .apply()?;
3271
3272 let usdc = TIP20Setup::create("USDC", "USDC", admin)
3273 .with_issuer(admin)
3274 .with_mint(alice, min_order_amount_x10)
3276 .with_approval(alice, exchange.address, min_order_amount_x10)
3277 .with_mint(bob, min_order_amount_x10)
3279 .with_approval(bob, exchange.address, min_order_amount_x10)
3280 .apply()?;
3281
3282 let eurc = TIP20Setup::create("EURC", "EURC", admin)
3283 .with_issuer(admin)
3284 .with_mint(alice, min_order_amount_x10)
3286 .with_approval(alice, exchange.address, min_order_amount_x10)
3287 .apply()?;
3288
3289 exchange.place(alice, usdc.address(), min_order_amount * 5, true, 0)?;
3291 exchange.place(alice, eurc.address(), min_order_amount * 5, false, 0)?;
3292
3293 let bob_usdc_before = usdc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3295 let bob_eurc_before = eurc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3296
3297 let amount_in = min_order_amount;
3299 let amount_out = exchange.swap_exact_amount_in(
3300 bob,
3301 usdc.address(),
3302 eurc.address(),
3303 amount_in,
3304 0, )?;
3306
3307 let bob_usdc_after = usdc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3309 let bob_eurc_after = eurc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3310
3311 assert_eq!(
3313 bob_usdc_before - bob_usdc_after,
3314 U256::from(amount_in),
3315 "Bob should have spent exact amount_in USDC"
3316 );
3317 assert_eq!(
3318 bob_eurc_after - bob_eurc_before,
3319 U256::from(amount_out),
3320 "Bob should have received amount_out EURC"
3321 );
3322
3323 let bob_path_usd_wallet =
3325 path_usd.balance_of(ITIP20::balanceOfCall { account: bob })?;
3326 assert_eq!(
3327 bob_path_usd_wallet,
3328 U256::ZERO,
3329 "Bob should have ZERO pathUSD in wallet (transitory)"
3330 );
3331
3332 let bob_path_usd_exchange = exchange.balance_of(bob, path_usd.address())?;
3333 assert_eq!(
3334 bob_path_usd_exchange, 0,
3335 "Bob should have ZERO pathUSD on exchange (transitory)"
3336 );
3337
3338 Ok(())
3339 })
3340 }
3341
3342 #[test]
3343 fn test_swap_exact_out_multi_hop_transitory_balances() -> eyre::Result<()> {
3344 let mut storage = HashMapStorageProvider::new(1);
3345 StorageCtx::enter(&mut storage, || {
3346 let mut exchange = StablecoinDEX::new();
3347 exchange.initialize()?;
3348
3349 let admin = Address::random();
3350 let alice = Address::random();
3351 let bob = Address::random();
3352
3353 let min_order_amount = MIN_ORDER_AMOUNT;
3354 let min_order_amount_x10 = U256::from(MIN_ORDER_AMOUNT * 10);
3355
3356 let path_usd = TIP20Setup::path_usd(admin)
3358 .with_issuer(admin)
3359 .with_mint(alice, min_order_amount_x10)
3361 .with_approval(alice, exchange.address, min_order_amount_x10)
3362 .apply()?;
3363
3364 let usdc = TIP20Setup::create("USDC", "USDC", admin)
3365 .with_issuer(admin)
3366 .with_mint(alice, min_order_amount_x10)
3368 .with_approval(alice, exchange.address, min_order_amount_x10)
3369 .with_mint(bob, min_order_amount_x10)
3371 .with_approval(bob, exchange.address, min_order_amount_x10)
3372 .apply()?;
3373
3374 let eurc = TIP20Setup::create("EURC", "EURC", admin)
3375 .with_issuer(admin)
3376 .with_mint(alice, min_order_amount_x10)
3378 .with_approval(alice, exchange.address, min_order_amount_x10)
3379 .apply()?;
3380
3381 exchange.place(alice, usdc.address(), min_order_amount * 5, true, 0)?;
3383 exchange.place(alice, eurc.address(), min_order_amount * 5, false, 0)?;
3384
3385 let bob_usdc_before = usdc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3387 let bob_eurc_before = eurc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3388
3389 let amount_out = 90u128;
3391 let amount_in = exchange.swap_exact_amount_out(
3392 bob,
3393 usdc.address(),
3394 eurc.address(),
3395 amount_out,
3396 u128::MAX, )?;
3398
3399 let bob_usdc_after = usdc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3401 let bob_eurc_after = eurc.balance_of(ITIP20::balanceOfCall { account: bob })?;
3402
3403 assert_eq!(
3405 bob_usdc_before - bob_usdc_after,
3406 U256::from(amount_in),
3407 "Bob should have spent amount_in USDC"
3408 );
3409 assert_eq!(
3410 bob_eurc_after - bob_eurc_before,
3411 U256::from(amount_out),
3412 "Bob should have received exact amount_out EURC"
3413 );
3414
3415 let bob_path_usd_wallet =
3417 path_usd.balance_of(ITIP20::balanceOfCall { account: bob })?;
3418 assert_eq!(
3419 bob_path_usd_wallet,
3420 U256::ZERO,
3421 "Bob should have ZERO pathUSD in wallet (transitory)"
3422 );
3423
3424 let bob_path_usd_exchange = exchange
3425 .balance_of(bob, path_usd.address())
3426 .expect("Failed to get bob's pathUSD exchange balance");
3427 assert_eq!(
3428 bob_path_usd_exchange, 0,
3429 "Bob should have ZERO pathUSD on exchange (transitory)"
3430 );
3431
3432 Ok(())
3433 })
3434 }
3435
3436 #[test]
3437 fn test_create_pair_invalid_currency() -> eyre::Result<()> {
3438 let mut storage = HashMapStorageProvider::new(1);
3439 StorageCtx::enter(&mut storage, || {
3440 let admin = Address::random();
3441
3442 let token_0 = TIP20Setup::create("EuroToken", "EURO", admin)
3444 .currency("EUR")
3445 .apply()?;
3446
3447 let mut exchange = StablecoinDEX::new();
3448 exchange.initialize()?;
3449
3450 let result = exchange.create_pair(token_0.address());
3452 assert!(matches!(
3453 result,
3454 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
3455 ));
3456
3457 Ok(())
3458 })
3459 }
3460
3461 #[test]
3462 fn test_create_pair_rejects_non_tip20_base() -> eyre::Result<()> {
3463 let mut storage = HashMapStorageProvider::new(1);
3464 StorageCtx::enter(&mut storage, || {
3465 let admin = Address::random();
3466 let _path_usd = TIP20Setup::path_usd(admin).apply()?;
3467
3468 let mut exchange = StablecoinDEX::new();
3469 exchange.initialize()?;
3470
3471 let non_tip20_address = Address::random();
3473 let result = exchange.create_pair(non_tip20_address);
3474 assert!(matches!(
3475 result,
3476 Err(TempoPrecompileError::StablecoinDEX(
3477 StablecoinDEXError::InvalidBaseToken(_)
3478 ))
3479 ));
3480
3481 Ok(())
3482 })
3483 }
3484
3485 #[test]
3486 fn test_max_in_check() -> eyre::Result<()> {
3487 let mut storage = HashMapStorageProvider::new(1);
3488 StorageCtx::enter(&mut storage, || {
3489 let mut exchange = StablecoinDEX::new();
3490 exchange.initialize()?;
3491
3492 let alice = Address::random();
3493 let bob = Address::random();
3494 let admin = Address::random();
3495
3496 let (base_token, quote_token) =
3497 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
3498 exchange.create_pair(base_token)?;
3499
3500 let tick_50 = 50i16;
3501 let tick_100 = 100i16;
3502 let order_amount = MIN_ORDER_AMOUNT;
3503
3504 exchange.place(alice, base_token, order_amount, false, tick_50)?;
3505 exchange.place(alice, base_token, order_amount, false, tick_100)?;
3506
3507 exchange.set_balance(bob, quote_token, 200_000_000u128)?;
3508
3509 let price_50 = orderbook::tick_to_price(tick_50);
3510 let price_100 = orderbook::tick_to_price(tick_100);
3511 let quote_for_first =
3513 (order_amount * price_50 as u128).div_ceil(orderbook::PRICE_SCALE as u128);
3514 let quote_for_partial_second =
3515 (999 * price_100 as u128).div_ceil(orderbook::PRICE_SCALE as u128);
3516 let total_needed = quote_for_first + quote_for_partial_second;
3517
3518 let result = exchange.swap_exact_amount_out(
3519 bob,
3520 quote_token,
3521 base_token,
3522 order_amount + 999,
3523 total_needed,
3524 );
3525 assert!(result.is_ok());
3526
3527 Ok(())
3528 })
3529 }
3530
3531 #[test]
3532 fn test_exact_out_bid_side() -> eyre::Result<()> {
3533 let mut storage = HashMapStorageProvider::new(1);
3534 StorageCtx::enter(&mut storage, || {
3535 let mut exchange = StablecoinDEX::new();
3536 exchange.initialize()?;
3537
3538 let alice = Address::random();
3539 let bob = Address::random();
3540 let admin = Address::random();
3541
3542 let (base_token, quote_token) =
3543 setup_test_tokens(admin, alice, exchange.address, 1_000_000_000u128)?;
3544 exchange.create_pair(base_token)?;
3545
3546 let tick = 1000i16;
3547 let price = tick_to_price(tick);
3548 let order_amount_base = MIN_ORDER_AMOUNT;
3549
3550 exchange.place(alice, base_token, order_amount_base, true, tick)?;
3551
3552 let amount_out_quote = 5_000_000u128;
3553 let base_needed = (amount_out_quote * PRICE_SCALE as u128) / price as u128;
3554 let max_amount_in = base_needed + 10000;
3555
3556 exchange.set_balance(bob, base_token, max_amount_in * 2)?;
3557
3558 let _amount_in = exchange.swap_exact_amount_out(
3559 bob,
3560 base_token,
3561 quote_token,
3562 amount_out_quote,
3563 max_amount_in,
3564 )?;
3565
3566 let bob_quote_balance = TIP20Token::from_address(quote_token)?
3568 .balance_of(ITIP20::balanceOfCall { account: bob })?;
3569 assert_eq!(bob_quote_balance, U256::from(amount_out_quote));
3570
3571 Ok(())
3572 })
3573 }
3574
3575 #[test]
3576 fn test_exact_in_ask_side() -> eyre::Result<()> {
3577 let mut storage = HashMapStorageProvider::new(1);
3578 StorageCtx::enter(&mut storage, || {
3579 let mut exchange = StablecoinDEX::new();
3580 exchange.initialize()?;
3581
3582 let alice = Address::random();
3583 let bob = Address::random();
3584 let admin = Address::random();
3585
3586 let (base_token, quote_token) =
3587 setup_test_tokens(admin, alice, exchange.address, 1_000_000_000u128)?;
3588 exchange.create_pair(base_token)?;
3589
3590 let tick = 1000i16;
3591 let price = tick_to_price(tick);
3592 let order_amount_base = MIN_ORDER_AMOUNT;
3593
3594 exchange.place(alice, base_token, order_amount_base, false, tick)?;
3595
3596 let amount_in_quote = 5_000_000u128;
3597 let min_amount_out = 0;
3598
3599 exchange.set_balance(bob, quote_token, amount_in_quote * 2)?;
3600
3601 let amount_out = exchange.swap_exact_amount_in(
3602 bob,
3603 quote_token,
3604 base_token,
3605 amount_in_quote,
3606 min_amount_out,
3607 )?;
3608
3609 let expected_base = (amount_in_quote * PRICE_SCALE as u128) / price as u128;
3610 assert_eq!(amount_out, expected_base);
3611
3612 Ok(())
3613 })
3614 }
3615
3616 #[test]
3617 fn test_clear_order() -> eyre::Result<()> {
3618 const AMOUNT: u128 = 1_000_000_000;
3619
3620 let mut storage = HashMapStorageProvider::new(1);
3622 StorageCtx::enter(&mut storage, || {
3623 let mut exchange = StablecoinDEX::new();
3624 exchange.initialize()?;
3625
3626 let alice = Address::random();
3627 let bob = Address::random();
3628 let carol = Address::random();
3629 let admin = Address::random();
3630
3631 let (base_token, quote_token) =
3632 setup_test_tokens(admin, alice, exchange.address, AMOUNT)?;
3633 exchange.create_pair(base_token)?;
3634
3635 TIP20Setup::config(base_token)
3637 .with_mint(bob, U256::from(AMOUNT))
3638 .with_approval(bob, exchange.address, U256::from(AMOUNT))
3639 .apply()?;
3640 TIP20Setup::config(quote_token)
3641 .with_mint(carol, U256::from(AMOUNT))
3642 .with_approval(carol, exchange.address, U256::from(AMOUNT))
3643 .apply()?;
3644
3645 let tick = 100i16;
3646
3647 let order1_amount = MIN_ORDER_AMOUNT;
3649 let order2_amount = MIN_ORDER_AMOUNT;
3650
3651 let order1_id = exchange.place(alice, base_token, order1_amount, false, tick)?;
3652 let order2_id = exchange.place(bob, base_token, order2_amount, false, tick)?;
3653
3654 let order1 = exchange.orders[order1_id].read()?;
3656 let order2 = exchange.orders[order2_id].read()?;
3657 assert_eq!(order1.next(), order2_id);
3658 assert_eq!(order2.prev(), order1_id);
3659
3660 let swap_amount = order1_amount;
3662 exchange.swap_exact_amount_out(
3663 carol,
3664 quote_token,
3665 base_token,
3666 swap_amount,
3667 u128::MAX,
3668 )?;
3669
3670 let order2_after = exchange.orders[order2_id].read()?;
3672 assert_eq!(
3673 order2_after.prev(),
3674 0,
3675 "New head order should have prev = 0 after previous head was filled"
3676 );
3677
3678 Ok(())
3679 })
3680 }
3681
3682 #[test]
3683 fn test_best_tick_updates_on_fill() -> eyre::Result<()> {
3684 let mut storage = HashMapStorageProvider::new(1);
3685 StorageCtx::enter(&mut storage, || {
3686 let mut exchange = StablecoinDEX::new();
3687 exchange.initialize()?;
3688
3689 let alice = Address::random();
3690 let bob = Address::random();
3691 let admin = Address::random();
3692 let amount = MIN_ORDER_AMOUNT;
3693
3694 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);
3700 let bid_price_2 = orderbook::tick_to_price(bid_tick_2);
3701 let bid_escrow_1 = (amount * bid_price_1 as u128) / orderbook::PRICE_SCALE as u128;
3702 let bid_escrow_2 = (amount * bid_price_2 as u128) / orderbook::PRICE_SCALE as u128;
3703 let total_bid_escrow = bid_escrow_1 + bid_escrow_2;
3704
3705 let (base_token, quote_token) =
3706 setup_test_tokens(admin, alice, exchange.address, total_bid_escrow)?;
3707 exchange.create_pair(base_token)?;
3708 let book_key = compute_book_key(base_token, quote_token);
3709
3710 exchange.place(alice, base_token, amount, true, bid_tick_1)?;
3712 exchange.place(alice, base_token, amount, true, bid_tick_2)?;
3713
3714 TIP20Setup::config(base_token)
3716 .with_mint(alice, U256::from(amount * 2))
3717 .with_approval(alice, exchange.address, U256::from(amount * 2))
3718 .apply()?;
3719 exchange.place(alice, base_token, amount, false, ask_tick_1)?;
3720 exchange.place(alice, base_token, amount, false, ask_tick_2)?;
3721
3722 let orderbook = exchange.books[book_key].read()?;
3724 assert_eq!(orderbook.best_bid_tick, bid_tick_1);
3725 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3726
3727 exchange.set_balance(bob, base_token, amount)?;
3729 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
3730 let orderbook = exchange.books[book_key].read()?;
3732 assert_eq!(orderbook.best_bid_tick, bid_tick_2);
3733 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3734
3735 exchange.set_balance(bob, base_token, amount)?;
3737 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
3738 let orderbook = exchange.books[book_key].read()?;
3740 assert_eq!(orderbook.best_bid_tick, i16::MIN);
3741 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3742
3743 let ask_price_1 = orderbook::tick_to_price(ask_tick_1);
3745 let quote_needed = (amount * ask_price_1 as u128) / orderbook::PRICE_SCALE as u128;
3746 exchange.set_balance(bob, quote_token, quote_needed)?;
3747 exchange.swap_exact_amount_in(bob, quote_token, base_token, quote_needed, 0)?;
3748 let orderbook = exchange.books[book_key].read()?;
3750 assert_eq!(orderbook.best_ask_tick, ask_tick_2);
3751 assert_eq!(orderbook.best_bid_tick, i16::MIN);
3752
3753 Ok(())
3754 })
3755 }
3756
3757 #[test]
3758 fn test_best_tick_updates_on_cancel() -> eyre::Result<()> {
3759 let mut storage = HashMapStorageProvider::new(1);
3760 StorageCtx::enter(&mut storage, || {
3761 let mut exchange = StablecoinDEX::new();
3762 exchange.initialize()?;
3763
3764 let alice = Address::random();
3765 let admin = Address::random();
3766 let amount = MIN_ORDER_AMOUNT;
3767
3768 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);
3773 let price_2 = orderbook::tick_to_price(bid_tick_2);
3774 let escrow_1 = (amount * price_1 as u128) / orderbook::PRICE_SCALE as u128;
3775 let escrow_2 = (amount * price_2 as u128) / orderbook::PRICE_SCALE as u128;
3776 let total_escrow = escrow_1 * 2 + escrow_2;
3777
3778 let (base_token, quote_token) =
3779 setup_test_tokens(admin, alice, exchange.address, total_escrow)?;
3780 exchange.create_pair(base_token)?;
3781 let book_key = compute_book_key(base_token, quote_token);
3782
3783 let bid_order_1 = exchange.place(alice, base_token, amount, true, bid_tick_1)?;
3785 let bid_order_2 = exchange.place(alice, base_token, amount, true, bid_tick_1)?;
3786 let bid_order_3 = exchange.place(alice, base_token, amount, true, bid_tick_2)?;
3787
3788 TIP20Setup::config(base_token)
3790 .with_mint(alice, U256::from(amount * 2))
3791 .with_approval(alice, exchange.address, U256::from(amount * 2))
3792 .apply()?;
3793 let ask_order_1 = exchange.place(alice, base_token, amount, false, ask_tick_1)?;
3794 let ask_order_2 = exchange.place(alice, base_token, amount, false, ask_tick_2)?;
3795
3796 let orderbook = exchange.books[book_key].read()?;
3798 assert_eq!(orderbook.best_bid_tick, bid_tick_1);
3799 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3800
3801 exchange.cancel(alice, bid_order_1)?;
3803 let orderbook = exchange.books[book_key].read()?;
3805 assert_eq!(orderbook.best_bid_tick, bid_tick_1);
3806 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3807
3808 exchange.cancel(alice, bid_order_2)?;
3810 let orderbook = exchange.books[book_key].read()?;
3812 assert_eq!(orderbook.best_bid_tick, bid_tick_2);
3813 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
3814
3815 exchange.cancel(alice, ask_order_1)?;
3817 let orderbook = exchange.books[book_key].read()?;
3819 assert_eq!(orderbook.best_bid_tick, bid_tick_2);
3820 assert_eq!(orderbook.best_ask_tick, ask_tick_2);
3821
3822 exchange.cancel(alice, bid_order_3)?;
3824 let orderbook = exchange.books[book_key].read()?;
3826 assert_eq!(orderbook.best_bid_tick, i16::MIN);
3827 assert_eq!(orderbook.best_ask_tick, ask_tick_2);
3828
3829 exchange.cancel(alice, ask_order_2)?;
3831 let orderbook = exchange.books[book_key].read()?;
3833 assert_eq!(orderbook.best_bid_tick, i16::MIN);
3834 assert_eq!(orderbook.best_ask_tick, i16::MAX);
3835
3836 Ok(())
3837 })
3838 }
3839
3840 #[test]
3841 fn test_place() -> eyre::Result<()> {
3842 const AMOUNT: u128 = 1_000_000_000;
3843
3844 let mut storage = HashMapStorageProvider::new(1);
3845 StorageCtx::enter(&mut storage, || {
3846 let mut exchange = StablecoinDEX::new();
3847 exchange.initialize()?;
3848
3849 let alice = Address::random();
3850 let admin = Address::random();
3851
3852 let (base_token, _quote_token) =
3853 setup_test_tokens(admin, alice, exchange.address, AMOUNT)?;
3854 exchange.create_pair(base_token)?;
3855
3856 TIP20Setup::config(base_token)
3858 .with_mint(alice, U256::from(AMOUNT))
3859 .with_approval(alice, exchange.address, U256::from(AMOUNT))
3860 .apply()?;
3861
3862 let invalid_tick = 15i16;
3864 let result = exchange.place(alice, base_token, MIN_ORDER_AMOUNT, true, invalid_tick);
3865
3866 let error = result.unwrap_err();
3867 assert!(matches!(
3868 error,
3869 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidTick(_))
3870 ));
3871
3872 let valid_tick = -20i16;
3874 let result = exchange.place(alice, base_token, MIN_ORDER_AMOUNT, true, valid_tick);
3875 assert!(result.is_ok());
3876
3877 Ok(())
3878 })
3879 }
3880
3881 #[test]
3882 fn test_place_flip_checks() -> eyre::Result<()> {
3883 const AMOUNT: u128 = 1_000_000_000;
3884
3885 let mut storage = HashMapStorageProvider::new(1);
3886 StorageCtx::enter(&mut storage, || {
3887 let mut exchange = StablecoinDEX::new();
3888 exchange.initialize()?;
3889
3890 let alice = Address::random();
3891 let admin = Address::random();
3892
3893 let (base_token, _quote_token) =
3894 setup_test_tokens(admin, alice, exchange.address, AMOUNT)?;
3895 exchange.create_pair(base_token)?;
3896
3897 TIP20Setup::config(base_token)
3899 .with_mint(alice, U256::from(AMOUNT))
3900 .with_approval(alice, exchange.address, U256::from(AMOUNT))
3901 .apply()?;
3902
3903 let invalid_tick = 15i16;
3905 let invalid_flip_tick = 25i16;
3906 let result = exchange.place_flip(
3907 alice,
3908 base_token,
3909 MIN_ORDER_AMOUNT,
3910 true,
3911 invalid_tick,
3912 invalid_flip_tick,
3913 false,
3914 );
3915
3916 let error = result.unwrap_err();
3917 assert!(matches!(
3918 error,
3919 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidTick(_))
3920 ));
3921
3922 let valid_tick = 20i16;
3924 let invalid_flip_tick = 25i16;
3925 let result = exchange.place_flip(
3926 alice,
3927 base_token,
3928 MIN_ORDER_AMOUNT,
3929 true,
3930 valid_tick,
3931 invalid_flip_tick,
3932 false,
3933 );
3934
3935 let error = result.unwrap_err();
3936 assert!(matches!(
3937 error,
3938 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::InvalidFlipTick(_))
3939 ));
3940
3941 let valid_flip_tick = 30i16;
3942 let result = exchange.place_flip(
3943 alice,
3944 base_token,
3945 MIN_ORDER_AMOUNT,
3946 true,
3947 valid_tick,
3948 valid_flip_tick,
3949 false,
3950 );
3951 assert!(result.is_ok());
3952
3953 Ok(())
3954 })
3955 }
3956
3957 #[test]
3958 fn test_find_trade_path_rejects_non_tip20() -> eyre::Result<()> {
3959 let mut storage = HashMapStorageProvider::new(1);
3960 StorageCtx::enter(&mut storage, || {
3961 let mut exchange = StablecoinDEX::new();
3962 exchange.initialize()?;
3963
3964 let admin = Address::random();
3965 let user = Address::random();
3966
3967 let (_, quote_token) =
3968 setup_test_tokens(admin, user, exchange.address, MIN_ORDER_AMOUNT)?;
3969
3970 let non_tip20_address = Address::random();
3971 let result = exchange.find_trade_path(non_tip20_address, quote_token);
3972 assert!(
3973 matches!(
3974 result,
3975 Err(TempoPrecompileError::StablecoinDEX(
3976 StablecoinDEXError::InvalidToken(_)
3977 ))
3978 ),
3979 "Should return InvalidToken error for non-TIP20 token"
3980 );
3981
3982 Ok(())
3983 })
3984 }
3985
3986 #[test]
3987 fn test_quote_exact_in_handles_both_directions() -> eyre::Result<()> {
3988 let mut storage = HashMapStorageProvider::new(1);
3989 StorageCtx::enter(&mut storage, || {
3990 let mut exchange = StablecoinDEX::new();
3991 exchange.initialize()?;
3992
3993 let alice = Address::random();
3994 let admin = Address::random();
3995 let amount = MIN_ORDER_AMOUNT;
3996 let tick = 100_i16;
3997 let price = orderbook::tick_to_price(tick);
3998
3999 let bid_escrow = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
4001
4002 let (base_token, quote_token) =
4003 setup_test_tokens(admin, alice, exchange.address, bid_escrow)?;
4004
4005 TIP20Setup::config(base_token)
4006 .with_mint(alice, U256::from(amount))
4007 .with_approval(alice, exchange.address, U256::from(amount))
4008 .apply()?;
4009
4010 exchange.create_pair(base_token)?;
4011 let book_key = compute_book_key(base_token, quote_token);
4012
4013 exchange.place(alice, base_token, amount, true, tick)?;
4015
4016 let quoted_out_bid = exchange.quote_exact_in(book_key, amount, true)?;
4018 let expected_quote_out = amount
4019 .checked_mul(price as u128)
4020 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
4021 .expect("calculation");
4022 assert_eq!(
4023 quoted_out_bid, expected_quote_out,
4024 "quote_exact_in with is_bid=true should return quote amount"
4025 );
4026
4027 exchange.place(alice, base_token, amount, false, tick)?;
4029
4030 let quote_in = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
4032 let quoted_out_ask = exchange.quote_exact_in(book_key, quote_in, false)?;
4033 let expected_base_out = quote_in
4034 .checked_mul(orderbook::PRICE_SCALE as u128)
4035 .and_then(|v| v.checked_div(price as u128))
4036 .expect("calculation");
4037 assert_eq!(
4038 quoted_out_ask, expected_base_out,
4039 "quote_exact_in with is_bid=false should return base amount"
4040 );
4041
4042 Ok(())
4043 })
4044 }
4045
4046 #[test]
4047 fn test_place_auto_creates_pair() -> Result<()> {
4048 let mut storage = HashMapStorageProvider::new(1);
4049 StorageCtx::enter(&mut storage, || {
4050 let mut exchange = StablecoinDEX::new();
4051 exchange.initialize()?;
4052 let admin = Address::random();
4053 let user = Address::random();
4054
4055 let (base_token, quote_token) =
4057 setup_test_tokens(admin, user, exchange.address, 100_000_000)?;
4058
4059 let book_key = compute_book_key(base_token, quote_token);
4061 let book_before = exchange.books[book_key].read()?;
4062 assert!(book_before.base.is_zero(),);
4063
4064 let mut base = TIP20Token::from_address(base_token)?;
4066 base.transfer(
4067 user,
4068 ITIP20::transferCall {
4069 to: exchange.address,
4070 amount: U256::from(MIN_ORDER_AMOUNT),
4071 },
4072 )
4073 .expect("Base token transfer failed");
4074
4075 exchange.place(user, base_token, MIN_ORDER_AMOUNT, true, 0)?;
4077
4078 let book_after = exchange.books[book_key].read()?;
4079 assert_eq!(book_after.base, base_token);
4080
4081 let events = exchange.emitted_events();
4083 assert_eq!(events.len(), 2);
4084 assert_eq!(
4085 events[0],
4086 StablecoinDEXEvents::PairCreated(IStablecoinDEX::PairCreated {
4087 key: book_key,
4088 base: base_token,
4089 quote: quote_token,
4090 })
4091 .into_log_data()
4092 );
4093
4094 Ok(())
4095 })
4096 }
4097
4098 #[test]
4099 fn test_decrement_balance_preserves_balance() -> eyre::Result<()> {
4100 let mut storage = HashMapStorageProvider::new(1);
4101 StorageCtx::enter(&mut storage, || {
4102 let mut exchange = StablecoinDEX::new();
4103 exchange.initialize()?;
4104
4105 let admin = Address::random();
4106 let alice = Address::random();
4107
4108 let base = TIP20Setup::create("BASE", "BASE", admin).apply()?;
4109 let base_address = base.address();
4110
4111 exchange.create_pair(base_address)?;
4112
4113 let internal_balance = MIN_ORDER_AMOUNT / 2;
4114 exchange.set_balance(alice, base_address, internal_balance)?;
4115
4116 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
4117
4118 let tick = 0i16;
4119 let result = exchange.place(alice, base_address, MIN_ORDER_AMOUNT * 2, false, tick);
4120
4121 assert!(result.is_err());
4122 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
4123
4124 Ok(())
4125 })
4126 }
4127
4128 #[test]
4129 fn test_place_order_immediately_active() -> eyre::Result<()> {
4130 let mut storage = HashMapStorageProvider::new(1);
4131 StorageCtx::enter(&mut storage, || {
4132 let mut exchange = StablecoinDEX::new();
4133 exchange.initialize()?;
4134
4135 let admin = Address::random();
4136 let alice = Address::random();
4137 let min_order_amount = MIN_ORDER_AMOUNT;
4138 let tick = 100i16;
4139
4140 let price = orderbook::tick_to_price(tick);
4141 let expected_escrow =
4142 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
4143
4144 TIP20Setup::path_usd(admin)
4145 .with_issuer(admin)
4146 .with_mint(alice, U256::from(expected_escrow))
4147 .with_approval(alice, exchange.address, U256::from(expected_escrow))
4148 .apply()?;
4149
4150 let base = TIP20Setup::create("BASE", "BASE", admin).apply()?;
4151 let base_token = base.address();
4152 let quote_token = base.quote_token()?;
4153
4154 exchange.create_pair(base_token)?;
4155
4156 let order_id = exchange.place(alice, base_token, min_order_amount, true, tick)?;
4157
4158 assert_eq!(order_id, 1);
4159
4160 let book_key = compute_book_key(base_token, quote_token);
4161 let book_handler = &exchange.books[book_key];
4162 let level = book_handler.tick_level_handler(tick, true).read()?;
4163 assert_eq!(level.head, order_id, "Order should be head of tick level");
4164 assert_eq!(level.tail, order_id, "Order should be tail of tick level");
4165 assert_eq!(
4166 level.total_liquidity, min_order_amount,
4167 "Tick level should have order's liquidity"
4168 );
4169
4170 let orderbook = book_handler.read()?;
4171 assert_eq!(
4172 orderbook.best_bid_tick, tick,
4173 "Best bid tick should be updated"
4174 );
4175
4176 Ok(())
4177 })
4178 }
4179
4180 #[test]
4181 fn test_place_flip_order_immediately_active() -> eyre::Result<()> {
4182 let mut storage = HashMapStorageProvider::new(1);
4183 StorageCtx::enter(&mut storage, || {
4184 let mut exchange = StablecoinDEX::new();
4185 exchange.initialize()?;
4186
4187 let admin = Address::random();
4188 let alice = Address::random();
4189 let min_order_amount = MIN_ORDER_AMOUNT;
4190 let tick = 100i16;
4191 let flip_tick = 200i16;
4192
4193 let price = orderbook::tick_to_price(tick);
4194 let expected_escrow =
4195 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
4196
4197 TIP20Setup::path_usd(admin)
4198 .with_issuer(admin)
4199 .with_mint(alice, U256::from(expected_escrow))
4200 .with_approval(alice, exchange.address, U256::from(expected_escrow))
4201 .apply()?;
4202
4203 let base = TIP20Setup::create("BASE", "BASE", admin).apply()?;
4204 let base_token = base.address();
4205 let quote_token = base.quote_token()?;
4206
4207 exchange.create_pair(base_token)?;
4208
4209 let order_id = exchange.place_flip(
4210 alice,
4211 base_token,
4212 min_order_amount,
4213 true,
4214 tick,
4215 flip_tick,
4216 false,
4217 )?;
4218
4219 assert_eq!(order_id, 1);
4220
4221 let book_key = compute_book_key(base_token, quote_token);
4222 let book_handler = &exchange.books[book_key];
4223 let level = book_handler.tick_level_handler(tick, true).read()?;
4224 assert_eq!(level.head, order_id, "Order should be head of tick level");
4225 assert_eq!(level.tail, order_id, "Order should be tail of tick level");
4226 assert_eq!(
4227 level.total_liquidity, min_order_amount,
4228 "Tick level should have order's liquidity"
4229 );
4230
4231 let orderbook = book_handler.read()?;
4232 assert_eq!(
4233 orderbook.best_bid_tick, tick,
4234 "Best bid tick should be updated"
4235 );
4236
4237 let stored_order = exchange.orders[order_id].read()?;
4238 assert!(stored_order.is_flip(), "Order should be a flip order");
4239 assert_eq!(
4240 stored_order.flip_tick(),
4241 flip_tick,
4242 "Flip tick should match"
4243 );
4244
4245 Ok(())
4246 })
4247 }
4248
4249 #[test]
4250 fn test_place_post() -> eyre::Result<()> {
4251 let mut storage = HashMapStorageProvider::new(1);
4252 StorageCtx::enter(&mut storage, || {
4253 let mut exchange = StablecoinDEX::new();
4254 exchange.initialize()?;
4255
4256 let admin = Address::random();
4257 let alice = Address::random();
4258 let min_order_amount = MIN_ORDER_AMOUNT;
4259 let tick = 100i16;
4260
4261 let price = orderbook::tick_to_price(tick);
4262 let expected_escrow =
4263 (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
4264
4265 TIP20Setup::path_usd(admin)
4266 .with_issuer(admin)
4267 .with_mint(alice, U256::from(expected_escrow))
4268 .with_approval(alice, exchange.address, U256::from(expected_escrow))
4269 .apply()?;
4270
4271 let base = TIP20Setup::create("BASE", "BASE", admin).apply()?;
4272 let base_token = base.address();
4273 let quote_token = base.quote_token()?;
4274
4275 exchange.create_pair(base_token)?;
4276
4277 let order_id = exchange.place(alice, base_token, min_order_amount, true, tick)?;
4278
4279 let stored_order = exchange.orders[order_id].read()?;
4280 assert_eq!(stored_order.maker(), alice);
4281 assert_eq!(stored_order.remaining(), min_order_amount);
4282 assert_eq!(stored_order.tick(), tick);
4283 assert!(stored_order.is_bid());
4284
4285 let book_key = compute_book_key(base_token, quote_token);
4286 let level = exchange.books[book_key]
4287 .tick_level_handler(tick, true)
4288 .read()?;
4289 assert_eq!(level.head, order_id);
4290 assert_eq!(level.tail, order_id);
4291 assert_eq!(level.total_liquidity, min_order_amount);
4292
4293 let book = exchange.books[book_key].read()?;
4294 assert_eq!(book.best_bid_tick, tick);
4295
4296 assert_eq!(exchange.next_order_id()?, 2);
4297
4298 Ok(())
4299 })
4300 }
4301
4302 #[test]
4303 fn test_blacklisted_user_cannot_use_internal_balance() -> eyre::Result<()> {
4304 use crate::tip403_registry::{ITIP403Registry, TIP403Registry};
4305
4306 let mut storage = HashMapStorageProvider::new(1);
4307 StorageCtx::enter(&mut storage, || {
4308 let mut exchange = StablecoinDEX::new();
4309 exchange.initialize()?;
4310
4311 let alice = Address::random();
4312 let admin = Address::random();
4313
4314 let mut registry = TIP403Registry::new();
4316 let policy_id = registry.create_policy(
4317 admin,
4318 ITIP403Registry::createPolicyCall {
4319 admin,
4320 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4321 },
4322 )?;
4323
4324 let mut quote = TIP20Setup::path_usd(admin).with_issuer(admin).apply()?;
4326
4327 quote.change_transfer_policy_id(
4328 admin,
4329 ITIP20::changeTransferPolicyIdCall {
4330 newPolicyId: policy_id,
4331 },
4332 )?;
4333
4334 let mut base = TIP20Setup::create("BASE", "BASE", admin)
4336 .with_issuer(admin)
4337 .apply()?;
4338 let base_address = base.address();
4339
4340 base.change_transfer_policy_id(
4341 admin,
4342 ITIP20::changeTransferPolicyIdCall {
4343 newPolicyId: policy_id,
4344 },
4345 )?;
4346
4347 exchange.create_pair(base_address)?;
4348
4349 let internal_balance = MIN_ORDER_AMOUNT * 2;
4351 exchange.set_balance(alice, base_address, internal_balance)?;
4352 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
4353
4354 registry.modify_policy_blacklist(
4356 admin,
4357 ITIP403Registry::modifyPolicyBlacklistCall {
4358 policyId: policy_id,
4359 account: alice,
4360 restricted: true,
4361 },
4362 )?;
4363 assert!(!registry.is_authorized_as(policy_id, alice, AuthRole::sender())?);
4364
4365 let tick = 0i16;
4367 let result = exchange.place(alice, base_address, MIN_ORDER_AMOUNT, false, tick);
4368
4369 assert!(
4370 result.is_err(),
4371 "Blacklisted user should not be able to place orders using internal balance"
4372 );
4373 let err = result.unwrap_err();
4374 assert!(
4375 matches!(
4376 err,
4377 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4378 ),
4379 "Expected PolicyForbids error, got: {err:?}"
4380 );
4381 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
4382
4383 Ok(())
4384 })
4385 }
4386
4387 #[test]
4388 fn test_cancel_stale_order() -> eyre::Result<()> {
4389 let mut storage = HashMapStorageProvider::new(1);
4390 StorageCtx::enter(&mut storage, || {
4391 let mut exchange = StablecoinDEX::new();
4392 exchange.initialize()?;
4393
4394 let alice = Address::random();
4395 let admin = Address::random();
4396
4397 let mut registry = TIP403Registry::new();
4398 let policy_id = registry.create_policy(
4399 admin,
4400 ITIP403Registry::createPolicyCall {
4401 admin,
4402 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4403 },
4404 )?;
4405
4406 let mut base = TIP20Setup::create("USDC", "USDC", admin)
4407 .with_issuer(admin)
4408 .with_mint(alice, U256::from(MIN_ORDER_AMOUNT * 2))
4409 .with_approval(alice, exchange.address, U256::from(MIN_ORDER_AMOUNT * 2))
4410 .apply()?;
4411 base.change_transfer_policy_id(
4412 admin,
4413 ITIP20::changeTransferPolicyIdCall {
4414 newPolicyId: policy_id,
4415 },
4416 )?;
4417
4418 exchange.create_pair(base.address())?;
4419 let order_id = exchange.place(alice, base.address(), MIN_ORDER_AMOUNT, false, 0)?;
4420
4421 registry.modify_policy_blacklist(
4422 admin,
4423 ITIP403Registry::modifyPolicyBlacklistCall {
4424 policyId: policy_id,
4425 account: alice,
4426 restricted: true,
4427 },
4428 )?;
4429
4430 exchange.cancel_stale_order(order_id)?;
4431
4432 assert_eq!(
4433 exchange.balance_of(alice, base.address())?,
4434 MIN_ORDER_AMOUNT
4435 );
4436
4437 Ok(())
4438 })
4439 }
4440
4441 #[test]
4442 fn test_cancel_stale_not_stale() -> eyre::Result<()> {
4443 let mut storage = HashMapStorageProvider::new(1);
4444 StorageCtx::enter(&mut storage, || {
4445 let mut exchange = StablecoinDEX::new();
4446 exchange.initialize()?;
4447
4448 let alice = Address::random();
4449 let admin = Address::random();
4450
4451 let mut registry = TIP403Registry::new();
4452 let policy_id = registry.create_policy(
4453 admin,
4454 ITIP403Registry::createPolicyCall {
4455 admin,
4456 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4457 },
4458 )?;
4459
4460 let mut base = TIP20Setup::create("USDC", "USDC", admin)
4461 .with_issuer(admin)
4462 .with_mint(alice, U256::from(MIN_ORDER_AMOUNT * 2))
4463 .with_approval(alice, exchange.address, U256::from(MIN_ORDER_AMOUNT * 2))
4464 .apply()?;
4465 base.change_transfer_policy_id(
4466 admin,
4467 ITIP20::changeTransferPolicyIdCall {
4468 newPolicyId: policy_id,
4469 },
4470 )?;
4471
4472 exchange.create_pair(base.address())?;
4473 let order_id = exchange.place(alice, base.address(), MIN_ORDER_AMOUNT, false, 0)?;
4474
4475 let result = exchange.cancel_stale_order(order_id);
4476 assert!(result.is_err());
4477 assert!(matches!(
4478 result.unwrap_err(),
4479 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::OrderNotStale(_))
4480 ));
4481
4482 Ok(())
4483 })
4484 }
4485
4486 #[test]
4487 fn test_cancel_stale_order_with_invalid_policy_type() -> eyre::Result<()> {
4488 for spec in [TempoHardfork::T0, TempoHardfork::T1C, TempoHardfork::T2] {
4494 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
4495
4496 let alice = Address::random();
4497 let admin = Address::random();
4498
4499 let (order_id, base_token, invalid_policy_id) =
4500 StorageCtx::enter(&mut storage, || {
4501 let mut exchange = StablecoinDEX::new();
4502 exchange.initialize()?;
4503
4504 let mut base = TIP20Setup::create("USDC", "USDC", admin)
4505 .with_issuer(admin)
4506 .with_mint(alice, U256::from(MIN_ORDER_AMOUNT * 2))
4507 .with_approval(alice, exchange.address, U256::from(MIN_ORDER_AMOUNT * 2))
4508 .apply()?;
4509
4510 exchange.create_pair(base.address())?;
4511 let order_id =
4512 exchange.place(alice, base.address(), MIN_ORDER_AMOUNT, false, 0)?;
4513
4514 let mut registry = TIP403Registry::new();
4517 let invalid_policy_id = registry.create_policy(
4518 admin,
4519 ITIP403Registry::createPolicyCall {
4520 admin,
4521 policyType: ITIP403Registry::PolicyType::COMPOUND,
4522 },
4523 )?;
4524 base.change_transfer_policy_id(
4525 admin,
4526 ITIP20::changeTransferPolicyIdCall {
4527 newPolicyId: invalid_policy_id,
4528 },
4529 )?;
4530
4531 Ok::<_, TempoPrecompileError>((order_id, base.address(), invalid_policy_id))
4532 })?;
4533
4534 let mut storage = storage.with_spec(spec);
4536 StorageCtx::enter(&mut storage, || {
4537 let mut exchange = StablecoinDEX::new();
4538
4539 let registry = TIP403Registry::new();
4541 let auth_result =
4542 registry.is_authorized_as(invalid_policy_id, alice, AuthRole::sender());
4543 assert!(
4544 auth_result.is_err(),
4545 "[{spec:?}] is_authorized_as should fail for invalid policy type"
4546 );
4547
4548 exchange.cancel_stale_order(order_id)?;
4550
4551 assert_eq!(
4552 exchange.balance_of(alice, base_token)?,
4553 MIN_ORDER_AMOUNT,
4554 "[{spec:?}] alice should get her funds back"
4555 );
4556
4557 Ok::<_, eyre::Report>(())
4558 })?;
4559 }
4560 Ok(())
4561 }
4562
4563 #[test]
4564 fn test_cancel_stale_order_recipient_blacklisted_on_payout_token_pre_t4() -> eyre::Result<()> {
4565 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4566 StorageCtx::enter(&mut storage, || {
4567 let mut exchange = StablecoinDEX::new();
4568 exchange.initialize()?;
4569
4570 let alice = Address::random();
4571 let admin = Address::random();
4572
4573 let mut registry = TIP403Registry::new();
4574 let policy_id = registry.create_policy(
4575 admin,
4576 ITIP403Registry::createPolicyCall {
4577 admin,
4578 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4579 },
4580 )?;
4581
4582 let (base_addr, quote_addr) =
4583 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 2)?;
4584
4585 exchange.create_pair(base_addr)?;
4586 let order_id = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, false, 0)?;
4587
4588 let mut quote = TIP20Token::from_address(quote_addr)?;
4589 quote.change_transfer_policy_id(
4590 admin,
4591 ITIP20::changeTransferPolicyIdCall {
4592 newPolicyId: policy_id,
4593 },
4594 )?;
4595
4596 registry.modify_policy_blacklist(
4597 admin,
4598 ITIP403Registry::modifyPolicyBlacklistCall {
4599 policyId: policy_id,
4600 account: alice,
4601 restricted: true,
4602 },
4603 )?;
4604
4605 let result = exchange.cancel_stale_order(order_id);
4607 assert!(result.is_err());
4608 assert!(matches!(
4609 result.unwrap_err(),
4610 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::OrderNotStale(_))
4611 ));
4612
4613 Ok(())
4614 })
4615 }
4616
4617 #[test]
4618 fn test_cancel_stale_order_recipient_blacklisted_on_payout_token_t4() -> eyre::Result<()> {
4619 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T4);
4620 StorageCtx::enter(&mut storage, || {
4621 let mut exchange = StablecoinDEX::new();
4622 exchange.initialize()?;
4623
4624 let alice = Address::random();
4625 let admin = Address::random();
4626
4627 let mut registry = TIP403Registry::new();
4628 let policy_id = registry.create_policy(
4629 admin,
4630 ITIP403Registry::createPolicyCall {
4631 admin,
4632 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4633 },
4634 )?;
4635
4636 let (base_addr, quote_addr) =
4637 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 2)?;
4638
4639 exchange.create_pair(base_addr)?;
4640 let order_id = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, false, 0)?;
4641
4642 let mut quote = TIP20Token::from_address(quote_addr)?;
4643 quote.change_transfer_policy_id(
4644 admin,
4645 ITIP20::changeTransferPolicyIdCall {
4646 newPolicyId: policy_id,
4647 },
4648 )?;
4649
4650 registry.modify_policy_blacklist(
4651 admin,
4652 ITIP403Registry::modifyPolicyBlacklistCall {
4653 policyId: policy_id,
4654 account: alice,
4655 restricted: true,
4656 },
4657 )?;
4658
4659 exchange.cancel_stale_order(order_id)?;
4661
4662 assert_eq!(exchange.balance_of(alice, base_addr)?, MIN_ORDER_AMOUNT);
4663
4664 Ok(())
4665 })
4666 }
4667
4668 #[test]
4669 fn test_place_when_base_blacklisted() -> eyre::Result<()> {
4670 let mut storage = HashMapStorageProvider::new(1);
4671 StorageCtx::enter(&mut storage, || {
4672 let mut exchange = StablecoinDEX::new();
4673 exchange.initialize()?;
4674
4675 let alice = Address::random();
4676 let admin = Address::random();
4677
4678 let mut registry = TIP403Registry::new();
4680 let policy_id = registry.create_policy(
4681 admin,
4682 ITIP403Registry::createPolicyCall {
4683 admin,
4684 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4685 },
4686 )?;
4687
4688 let (base_addr, _quote_addr) =
4690 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 4)?;
4691
4692 let mut base = TIP20Token::from_address(base_addr)?;
4694 base.change_transfer_policy_id(
4695 admin,
4696 ITIP20::changeTransferPolicyIdCall {
4697 newPolicyId: policy_id,
4698 },
4699 )?;
4700
4701 registry.modify_policy_blacklist(
4703 admin,
4704 ITIP403Registry::modifyPolicyBlacklistCall {
4705 policyId: policy_id,
4706 account: alice,
4707 restricted: true,
4708 },
4709 )?;
4710
4711 exchange.create_pair(base_addr)?;
4712
4713 let result = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, true, 0);
4715 assert!(result.is_err());
4716 assert!(matches!(
4717 result.unwrap_err(),
4718 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4719 ));
4720
4721 let result =
4723 exchange.place_flip(alice, base_addr, MIN_ORDER_AMOUNT, true, 0, 100, false);
4724 assert!(result.is_err());
4725 assert!(matches!(
4726 result.unwrap_err(),
4727 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4728 ));
4729
4730 Ok(())
4731 })
4732 }
4733
4734 #[test]
4735 fn test_place_when_quote_blacklisted() -> eyre::Result<()> {
4736 let mut storage = HashMapStorageProvider::new(1);
4737 StorageCtx::enter(&mut storage, || {
4738 let mut exchange = StablecoinDEX::new();
4739 exchange.initialize()?;
4740
4741 let alice = Address::random();
4742 let admin = Address::random();
4743
4744 let mut registry = TIP403Registry::new();
4746 let policy_id = registry.create_policy(
4747 admin,
4748 ITIP403Registry::createPolicyCall {
4749 admin,
4750 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4751 },
4752 )?;
4753
4754 let (base_addr, quote_addr) =
4756 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 4)?;
4757
4758 let mut quote = TIP20Token::from_address(quote_addr)?;
4760 quote.change_transfer_policy_id(
4761 admin,
4762 ITIP20::changeTransferPolicyIdCall {
4763 newPolicyId: policy_id,
4764 },
4765 )?;
4766
4767 registry.modify_policy_blacklist(
4769 admin,
4770 ITIP403Registry::modifyPolicyBlacklistCall {
4771 policyId: policy_id,
4772 account: alice,
4773 restricted: true,
4774 },
4775 )?;
4776
4777 exchange.create_pair(base_addr)?;
4778
4779 let result = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, false, 0);
4781 assert!(result.is_err());
4782 assert!(matches!(
4783 result.unwrap_err(),
4784 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4785 ));
4786
4787 let result =
4789 exchange.place_flip(alice, base_addr, MIN_ORDER_AMOUNT, false, 100, 0, false);
4790 assert!(result.is_err());
4791 assert!(matches!(
4792 result.unwrap_err(),
4793 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4794 ));
4795
4796 Ok(())
4797 })
4798 }
4799
4800 #[test]
4801 fn test_compound_policy_non_escrow_token_direction() -> eyre::Result<()> {
4802 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
4803 StorageCtx::enter(&mut storage, || {
4804 let mut exchange = StablecoinDEX::new();
4805 exchange.initialize()?;
4806
4807 let (alice, admin) = (Address::random(), Address::random());
4808 let mut registry = TIP403Registry::new();
4809
4810 let recipient_policy = registry.create_policy(
4813 admin,
4814 ITIP403Registry::createPolicyCall {
4815 admin,
4816 policyType: ITIP403Registry::PolicyType::WHITELIST,
4817 },
4818 )?;
4819 let compound_id = registry.create_compound_policy(
4823 admin,
4824 ITIP403Registry::createCompoundPolicyCall {
4825 senderPolicyId: 1, recipientPolicyId: recipient_policy, mintRecipientPolicyId: 1, },
4829 )?;
4830
4831 let (base_addr, quote_addr) =
4833 setup_test_tokens(admin, alice, exchange.address, MIN_ORDER_AMOUNT * 4)?;
4834
4835 let mut quote = TIP20Token::from_address(quote_addr)?;
4837 quote.change_transfer_policy_id(
4838 admin,
4839 ITIP20::changeTransferPolicyIdCall {
4840 newPolicyId: compound_id,
4841 },
4842 )?;
4843
4844 exchange.create_pair(base_addr)?;
4845
4846 let res_ask = exchange.place(alice, base_addr, MIN_ORDER_AMOUNT, false, 0);
4850 let res_flip =
4852 exchange.place_flip(alice, base_addr, MIN_ORDER_AMOUNT, false, 100, 0, false);
4853
4854 for res in [res_ask, res_flip] {
4855 assert!(
4856 matches!(
4857 res.unwrap_err(),
4858 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
4859 ),
4860 "Order should fail: alice cannot receive quote token (non-escrow) per compound policy"
4861 );
4862 }
4863 Ok(())
4864 })
4865 }
4866
4867 #[test]
4868 fn test_swap_exact_amount_out_rounding() -> eyre::Result<()> {
4869 let mut storage = HashMapStorageProvider::new(1);
4870 StorageCtx::enter(&mut storage, || {
4871 let mut exchange = StablecoinDEX::new();
4872 exchange.initialize()?;
4873
4874 let alice = Address::random();
4875 let bob = Address::random();
4876 let admin = Address::random();
4877 let tick = 10;
4878
4879 let (base_token, quote_token) =
4880 setup_test_tokens(admin, alice, exchange.address, 200_000_000u128)?;
4881 exchange
4882 .create_pair(base_token)
4883 .expect("Could not create pair");
4884
4885 let order_amount = 100000000u128;
4886
4887 let tip20_quote_token = TIP20Token::from_address(quote_token)?;
4888 let alice_initial_balance =
4889 tip20_quote_token.balance_of(ITIP20::balanceOfCall { account: alice })?;
4890
4891 exchange
4892 .place(alice, base_token, order_amount, true, tick)
4893 .expect("Order should succeed");
4894
4895 let alice_balance_after_place =
4896 tip20_quote_token.balance_of(ITIP20::balanceOfCall { account: alice })?;
4897 let escrowed = alice_initial_balance - alice_balance_after_place;
4898 assert_eq!(escrowed, U256::from(100010000u128));
4899
4900 exchange
4901 .set_balance(bob, base_token, 200_000_000u128)
4902 .expect("Could not set balance");
4903
4904 exchange
4905 .swap_exact_amount_out(bob, base_token, quote_token, 100009999, u128::MAX)
4906 .expect("Swap should succeed");
4907
4908 Ok(())
4909 })
4910 }
4911
4912 #[test]
4913 fn test_stablecoin_dex_address_returns_correct_precompile() -> eyre::Result<()> {
4914 let mut storage = HashMapStorageProvider::new(1);
4915 StorageCtx::enter(&mut storage, || {
4916 let exchange = StablecoinDEX::new();
4917 assert_eq!(exchange.address(), STABLECOIN_DEX_ADDRESS);
4918 Ok(())
4919 })
4920 }
4921
4922 #[test]
4923 fn test_stablecoin_dex_initialize_sets_storage_state() -> eyre::Result<()> {
4924 let mut storage = HashMapStorageProvider::new(1);
4925 StorageCtx::enter(&mut storage, || {
4926 let mut exchange = StablecoinDEX::new();
4927
4928 assert!(!exchange.is_initialized()?);
4930
4931 exchange.initialize()?;
4933
4934 assert!(exchange.is_initialized()?);
4936
4937 let exchange2 = StablecoinDEX::new();
4939 assert!(exchange2.is_initialized()?);
4940
4941 Ok(())
4942 })
4943 }
4944
4945 #[test]
4946 fn test_get_order_validates_maker_and_order_id() -> eyre::Result<()> {
4947 let mut storage = HashMapStorageProvider::new(1);
4948 StorageCtx::enter(&mut storage, || {
4949 let mut exchange = StablecoinDEX::new();
4950 exchange.initialize()?;
4951
4952 let admin = Address::random();
4953 let alice = Address::random();
4954 let min_order_amount = MIN_ORDER_AMOUNT;
4955 let tick = 100i16;
4956
4957 let price = orderbook::tick_to_price(tick);
4958 let escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
4959
4960 let (base_token, _quote_token) =
4961 setup_test_tokens(admin, alice, exchange.address, escrow)?;
4962 exchange.create_pair(base_token)?;
4963
4964 let order_id = exchange.place(alice, base_token, min_order_amount, true, tick)?;
4965
4966 let order = exchange.get_order(order_id)?;
4968 assert_eq!(order.maker(), alice);
4969 assert!(!order.maker().is_zero());
4970 assert!(order.order_id() < exchange.next_order_id()?);
4971
4972 let result = exchange.get_order(999);
4974 assert!(result.is_err());
4975 assert_eq!(
4976 result.unwrap_err(),
4977 StablecoinDEXError::order_does_not_exist().into()
4978 );
4979
4980 let next_id = exchange.next_order_id()?;
4982 let result = exchange.get_order(next_id);
4983 assert!(result.is_err());
4984 assert_eq!(
4985 result.unwrap_err(),
4986 StablecoinDEXError::order_does_not_exist().into()
4987 );
4988
4989 Ok(())
4990 })
4991 }
4992
4993 struct FlipOrderTestCtx {
4995 exchange: StablecoinDEX,
4996 alice: Address,
4997 bob: Address,
4998 admin: Address,
4999 base_token: Address,
5000 quote_token: Address,
5001 book_key: B256,
5002 amount: u128,
5003 flip_tick: i16,
5004 }
5005
5006 fn setup_flip_order_test() -> eyre::Result<FlipOrderTestCtx> {
5008 let mut exchange = StablecoinDEX::new();
5009 exchange.initialize()?;
5010
5011 let alice = Address::random();
5012 let bob = Address::random();
5013 let admin = Address::random();
5014 let amount = MIN_ORDER_AMOUNT;
5015 let tick = 100i16;
5016 let flip_tick = 200i16;
5017
5018 let price = orderbook::tick_to_price(tick);
5019 let expected_escrow = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
5020
5021 let (base_token, quote_token) =
5022 setup_test_tokens(admin, alice, exchange.address, expected_escrow * 2)?;
5023 exchange.create_pair(base_token)?;
5024
5025 let book_key = compute_book_key(base_token, quote_token);
5026
5027 exchange.place_flip(alice, base_token, amount, true, tick, flip_tick, false)?;
5029
5030 Ok(FlipOrderTestCtx {
5031 exchange,
5032 alice,
5033 bob,
5034 admin,
5035 base_token,
5036 quote_token,
5037 book_key,
5038 amount,
5039 flip_tick,
5040 })
5041 }
5042
5043 #[test]
5044 fn test_flip_order_fill_ignores_business_logic_error() -> eyre::Result<()> {
5045 for spec in [TempoHardfork::T1, TempoHardfork::T1A, TempoHardfork::T2] {
5047 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
5048 StorageCtx::enter(&mut storage, || {
5049 let FlipOrderTestCtx {
5050 mut exchange,
5051 alice,
5052 bob,
5053 admin,
5054 base_token,
5055 quote_token,
5056 book_key,
5057 amount,
5058 flip_tick,
5059 } = setup_flip_order_test()?;
5060
5061 let mut registry = TIP403Registry::new();
5065 let policy_id = registry.create_policy(
5066 admin,
5067 ITIP403Registry::createPolicyCall {
5068 admin,
5069 policyType: ITIP403Registry::PolicyType::BLACKLIST,
5070 },
5071 )?;
5072
5073 let mut base = TIP20Token::from_address(base_token)?;
5074 base.change_transfer_policy_id(
5075 admin,
5076 ITIP20::changeTransferPolicyIdCall {
5077 newPolicyId: policy_id,
5078 },
5079 )?;
5080
5081 registry.modify_policy_blacklist(
5082 admin,
5083 ITIP403Registry::modifyPolicyBlacklistCall {
5084 policyId: policy_id,
5085 account: alice,
5086 restricted: true,
5087 },
5088 )?;
5089
5090 exchange.set_balance(bob, base_token, amount)?;
5092
5093 let result = exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0);
5095 assert!(
5096 result.is_ok(),
5097 "[{spec:?}] Swap should succeed when flip hits a business logic error"
5098 );
5099
5100 assert_eq!(exchange.balance_of(alice, base_token)?, amount);
5102
5103 let level = exchange.books[book_key]
5105 .tick_level_handler(flip_tick, false)
5106 .read()?;
5107 assert_eq!(
5108 level.total_liquidity, 0,
5109 "[{spec:?}] No flipped order should exist"
5110 );
5111
5112 Ok::<_, eyre::Report>(())
5113 })?;
5114 }
5115 Ok(())
5116 }
5117
5118 #[test]
5119 fn test_flip_order_fill_reverts_on_system_error_post_t1a() -> eyre::Result<()> {
5120 for spec in [TempoHardfork::T1, TempoHardfork::T1A, TempoHardfork::T2] {
5122 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
5123 StorageCtx::enter(&mut storage, || {
5124 let FlipOrderTestCtx {
5125 mut exchange,
5126 alice,
5127 bob,
5128 base_token,
5129 quote_token,
5130 book_key,
5131 amount,
5132 flip_tick,
5133 ..
5134 } = setup_flip_order_test()?;
5135
5136 let alice_quote_before = exchange.balance_of(alice, quote_token)?;
5137
5138 let poisoned_level = TickLevel::with_values(0, 0, u128::MAX);
5140 exchange.books[book_key]
5141 .tick_level_handler_mut(flip_tick, false)
5142 .write(poisoned_level)?;
5143
5144 exchange.set_balance(bob, base_token, amount)?;
5146
5147 let result = exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0);
5148
5149 if spec.is_t1a() {
5150 assert!(
5152 result.is_err(),
5153 "Swap should revert when flip hits a system error"
5154 );
5155 assert!(
5156 result.unwrap_err().is_system_error(),
5157 "Error must be classified as a system error",
5158 );
5159
5160 let alice_quote_after = exchange.balance_of(alice, quote_token)?;
5162 assert_eq!(alice_quote_before, alice_quote_after);
5163 } else {
5164 assert!(
5166 result.is_ok(),
5167 "[{spec:?}] Swap should succeed when system error is pre-T1A"
5168 );
5169 }
5170
5171 Ok::<_, eyre::Report>(())
5172 })?;
5173 }
5174 Ok(())
5175 }
5176
5177 #[test]
5178 fn test_orderbook_invariants_after_all_orders_filled() -> eyre::Result<()> {
5179 let mut storage = HashMapStorageProvider::new(1);
5180 StorageCtx::enter(&mut storage, || {
5181 let mut exchange = StablecoinDEX::new();
5182 exchange.initialize()?;
5183
5184 assert_eq!(exchange.next_order_id()?, 1);
5186
5187 let alice = Address::random();
5188 let bob = Address::random();
5189 let admin = Address::random();
5190 let amount = MIN_ORDER_AMOUNT;
5191 let tick = 100i16;
5192
5193 let price = orderbook::tick_to_price(tick) as u128;
5194 let quote_amount = (amount * price).div_ceil(orderbook::PRICE_SCALE as u128);
5195
5196 let base = TIP20Setup::create("BASE", "BASE", admin)
5197 .with_issuer(admin)
5198 .with_mint(alice, U256::from(amount * 4))
5199 .with_mint(bob, U256::from(amount * 4))
5200 .with_approval(alice, exchange.address, U256::MAX)
5201 .with_approval(bob, exchange.address, U256::MAX)
5202 .apply()?;
5203 let base_token = base.address();
5204 let quote_token = base.quote_token()?;
5205
5206 TIP20Setup::path_usd(admin)
5207 .with_issuer(admin)
5208 .with_mint(alice, U256::from(quote_amount * 4))
5209 .with_mint(bob, U256::from(quote_amount * 4))
5210 .with_approval(alice, exchange.address, U256::MAX)
5211 .with_approval(bob, exchange.address, U256::MAX)
5212 .apply()?;
5213
5214 let book_key = compute_book_key(base_token, quote_token);
5215 exchange.create_pair(base_token)?;
5216
5217 let bid_id = exchange.place(alice, base_token, amount, true, tick)?;
5219 assert_eq!(bid_id, 1);
5220 let ask_id = exchange.place(bob, base_token, amount, false, tick)?;
5221 assert_eq!(ask_id, 2);
5222
5223 let book = exchange.books[book_key].read()?;
5225 assert_eq!(book.best_bid_tick, tick);
5226 assert_eq!(book.best_ask_tick, tick);
5227
5228 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
5230
5231 exchange.swap_exact_amount_in(alice, quote_token, base_token, quote_amount, 0)?;
5233
5234 let book = exchange.books[book_key].read()?;
5236 assert_eq!(
5237 book.best_bid_tick,
5238 i16::MIN,
5239 "best_bid_tick must be sentinel after all bids filled"
5240 );
5241 assert_eq!(
5242 book.best_ask_tick,
5243 i16::MAX,
5244 "best_ask_tick must be sentinel after all asks filled"
5245 );
5246
5247 let bid_level = exchange.books[book_key]
5249 .tick_level_handler(tick, true)
5250 .read()?;
5251 assert_eq!(bid_level.head, 0, "bid level head must be 0 after drain");
5252 assert_eq!(bid_level.tail, 0, "bid level tail must be 0 after drain");
5253 assert_eq!(
5254 bid_level.total_liquidity, 0,
5255 "bid level liquidity must be 0 after drain"
5256 );
5257
5258 let ask_level = exchange.books[book_key]
5259 .tick_level_handler(tick, false)
5260 .read()?;
5261 assert_eq!(ask_level.head, 0, "ask level head must be 0 after drain");
5262 assert_eq!(ask_level.tail, 0, "ask level tail must be 0 after drain");
5263 assert_eq!(
5264 ask_level.total_liquidity, 0,
5265 "ask level liquidity must be 0 after drain"
5266 );
5267
5268 assert_eq!(
5270 exchange.next_order_id()?,
5271 3,
5272 "next_order_id must remain monotonic after drain"
5273 );
5274
5275 let result = exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0);
5278 assert_eq!(
5279 result,
5280 Err(StablecoinDEXError::insufficient_liquidity().into()),
5281 "swap against drained bid side must fail"
5282 );
5283 let result =
5285 exchange.swap_exact_amount_in(alice, quote_token, base_token, quote_amount, 0);
5286 assert_eq!(
5287 result,
5288 Err(StablecoinDEXError::insufficient_liquidity().into()),
5289 "swap against drained ask side must fail"
5290 );
5291
5292 Ok(())
5293 })
5294 }
5295
5296 #[test]
5297 fn test_orderbook_invariants_after_all_orders_cancelled() -> eyre::Result<()> {
5298 let mut storage = HashMapStorageProvider::new(1);
5299 StorageCtx::enter(&mut storage, || {
5300 let mut exchange = StablecoinDEX::new();
5301 exchange.initialize()?;
5302
5303 let alice = Address::random();
5304 let admin = Address::random();
5305 let amount = MIN_ORDER_AMOUNT;
5306 let tick = 100i16;
5307
5308 let price = orderbook::tick_to_price(tick) as u128;
5309 let quote_amount = (amount * price).div_ceil(orderbook::PRICE_SCALE as u128);
5310
5311 let base = TIP20Setup::create("BASE", "BASE", admin)
5312 .with_issuer(admin)
5313 .with_mint(alice, U256::from(amount * 2))
5314 .with_approval(alice, exchange.address, U256::MAX)
5315 .apply()?;
5316 let base_token = base.address();
5317 let quote_token = base.quote_token()?;
5318
5319 TIP20Setup::path_usd(admin)
5320 .with_issuer(admin)
5321 .with_mint(alice, U256::from(quote_amount * 2))
5322 .with_approval(alice, exchange.address, U256::MAX)
5323 .apply()?;
5324
5325 let book_key = compute_book_key(base_token, quote_token);
5326 exchange.create_pair(base_token)?;
5327
5328 let bid_id = exchange.place(alice, base_token, amount, true, tick)?;
5330 let ask_id = exchange.place(alice, base_token, amount, false, tick)?;
5331
5332 exchange.cancel(alice, bid_id)?;
5334 exchange.cancel(alice, ask_id)?;
5335
5336 let book = exchange.books[book_key].read()?;
5338 assert_eq!(
5339 book.best_bid_tick,
5340 i16::MIN,
5341 "best_bid_tick must be sentinel after all bids cancelled"
5342 );
5343 assert_eq!(
5344 book.best_ask_tick,
5345 i16::MAX,
5346 "best_ask_tick must be sentinel after all asks cancelled"
5347 );
5348
5349 let bid_level = exchange.books[book_key]
5351 .tick_level_handler(tick, true)
5352 .read()?;
5353 assert_eq!(bid_level.head, 0, "bid level head must be 0");
5354 assert_eq!(bid_level.tail, 0, "bid level tail must be 0");
5355 assert_eq!(bid_level.total_liquidity, 0, "bid liquidity must be 0");
5356
5357 let ask_level = exchange.books[book_key]
5358 .tick_level_handler(tick, false)
5359 .read()?;
5360 assert_eq!(ask_level.head, 0, "ask level head must be 0");
5361 assert_eq!(ask_level.tail, 0, "ask level tail must be 0");
5362 assert_eq!(ask_level.total_liquidity, 0, "ask liquidity must be 0");
5363
5364 let result = exchange.swap_exact_amount_in(alice, base_token, quote_token, amount, 0);
5366 assert_eq!(
5367 result,
5368 Err(StablecoinDEXError::insufficient_liquidity().into()),
5369 "swap against cancelled book must fail"
5370 );
5371
5372 Ok(())
5373 })
5374 }
5375
5376 #[test]
5377 fn test_sub_balance_errors_on_underflow() -> eyre::Result<()> {
5378 let mut storage = HashMapStorageProvider::new(1);
5379 StorageCtx::enter(&mut storage, || {
5380 let mut exchange = StablecoinDEX::new();
5381 exchange.initialize()?;
5382
5383 let user = Address::random();
5384 let admin = Address::random();
5385
5386 let base = TIP20Setup::create("BASE", "BASE", admin)
5387 .with_issuer(admin)
5388 .apply()?;
5389 let token = base.address();
5390
5391 exchange.set_balance(user, token, 100)?;
5393 assert_eq!(exchange.balance_of(user, token)?, 100);
5394
5395 let result = exchange.sub_balance(user, token, 101);
5397 assert_eq!(
5398 result,
5399 Err(TempoPrecompileError::under_overflow()),
5400 "sub_balance should error on underflow instead of saturating"
5401 );
5402
5403 assert_eq!(exchange.balance_of(user, token)?, 100);
5405
5406 Ok(())
5407 })
5408 }
5409
5410 #[test]
5411 fn test_flip_checkpoint_reverts_partial_state_post_t1c() -> eyre::Result<()> {
5412 for spec in [TempoHardfork::T1A, TempoHardfork::T1C] {
5418 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
5419 StorageCtx::enter(&mut storage, || {
5420 let FlipOrderTestCtx {
5421 mut exchange,
5422 alice,
5423 bob,
5424 base_token,
5425 quote_token,
5426 book_key,
5427 amount,
5428 flip_tick,
5429 ..
5430 } = setup_flip_order_test()?;
5431
5432 let next_id_before = exchange.next_order_id()?;
5433
5434 let poisoned = TickLevel::with_values(0, 0, u128::MAX);
5437 exchange.books[book_key]
5438 .tick_level_handler_mut(flip_tick, false)
5439 .write(poisoned)?;
5440
5441 exchange.set_balance(bob, base_token, amount)?;
5443
5444 let result = exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0);
5445 assert!(result.is_err(), "[{spec:?}] swap should fail");
5446
5447 let alice_base = exchange.balance_of(alice, base_token)?;
5451 let next_id_after = exchange.next_order_id()?;
5452
5453 if spec.is_t1c() {
5454 assert_eq!(alice_base, amount);
5456 assert_eq!(next_id_after, next_id_before);
5457 } else {
5458 assert_eq!(alice_base, 0);
5460 assert_eq!(next_id_after, next_id_before + 1);
5461 }
5462
5463 assert!(
5465 exchange.emitted_events().last().is_some_and(
5466 |e| e.topics()[0] != IStablecoinDEX::OrderPlaced::SIGNATURE_HASH
5467 )
5468 );
5469
5470 Ok::<_, eyre::Report>(())
5471 })?;
5472 }
5473 Ok(())
5474 }
5475
5476 #[test]
5477 fn test_swap_paused_token_allowed_pre_t3_blocked_on_t3() -> eyre::Result<()> {
5478 for spec in [TempoHardfork::T2, TempoHardfork::T3] {
5479 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
5480 StorageCtx::enter(&mut storage, || {
5481 let mut exchange = StablecoinDEX::new();
5482 exchange.initialize()?;
5483
5484 let (alice, bob, admin) = (Address::random(), Address::random(), Address::random());
5485 let amount_in = 500_000u128;
5486 let tick = 10;
5487
5488 let (base_token, quote_token) =
5489 setup_test_tokens(admin, alice, exchange.address, 500_000_000u128)?;
5490 exchange.create_pair(base_token)?;
5491
5492 exchange.place(alice, base_token, MIN_ORDER_AMOUNT * 2, true, tick)?;
5494
5495 exchange.set_balance(bob, base_token, amount_in * 2)?;
5497
5498 let mut base_tip20 = TIP20Token::from_address(base_token)?;
5500 base_tip20.grant_role_internal(admin, *PAUSE_ROLE)?;
5501 base_tip20.pause(admin, ITIP20::pauseCall {})?;
5502
5503 let res_in =
5504 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount_in, 0);
5505 let res_out = exchange.swap_exact_amount_out(
5506 bob,
5507 base_token,
5508 quote_token,
5509 amount_in,
5510 u128::MAX,
5511 );
5512
5513 if spec.is_t3() {
5514 assert_eq!(res_in, res_out);
5515 assert_eq!(res_in.unwrap_err(), TIP20Error::contract_paused().into());
5516 } else {
5517 assert!(res_in.is_ok());
5518 assert!(res_out.is_ok());
5519 }
5520
5521 Ok::<_, eyre::Report>(())
5522 })?;
5523 }
5524 Ok(())
5525 }
5526
5527 fn assert_paused_token_order<F>(
5531 pause_escrow_side: bool,
5532 internal_balance_amount: u128,
5533 is_bid: bool,
5534 mut place_order: F,
5535 ) -> eyre::Result<()>
5536 where
5537 F: FnMut(&mut StablecoinDEX, Address, Address, u128) -> Result<u128>,
5538 {
5539 for spec in [TempoHardfork::T3, TempoHardfork::T4] {
5540 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
5541 StorageCtx::enter(&mut storage, || {
5542 let mut exchange = StablecoinDEX::new();
5543 exchange.initialize()?;
5544
5545 let (alice, admin) = (Address::random(), Address::random());
5546 let amount = MIN_ORDER_AMOUNT;
5547
5548 let (base_token, quote_token) =
5549 setup_test_tokens(admin, alice, exchange.address, 500_000_000u128)?;
5550 exchange.create_pair(base_token)?;
5551
5552 let escrow_token = if is_bid { quote_token } else { base_token };
5553 let non_escrow_token = if is_bid { base_token } else { quote_token };
5554 exchange.set_balance(alice, escrow_token, internal_balance_amount)?;
5555
5556 let token_to_pause = if pause_escrow_side {
5557 escrow_token
5558 } else {
5559 non_escrow_token
5560 };
5561 let mut tip20 = TIP20Token::from_address(token_to_pause)?;
5562 tip20.grant_role_internal(admin, *PAUSE_ROLE)?;
5563 tip20.pause(admin, ITIP20::pauseCall {})?;
5564
5565 let next_order_id_before = exchange.next_order_id()?;
5566 let escrow_balance_before = exchange.balance_of(alice, escrow_token)?;
5567 let res = place_order(&mut exchange, alice, base_token, amount);
5568
5569 let should_succeed =
5575 !spec.is_t4() && (!pause_escrow_side || internal_balance_amount >= amount);
5576
5577 if should_succeed {
5578 let order_id = res?;
5579 assert_eq!(order_id, next_order_id_before);
5580 assert_eq!(exchange.next_order_id()?, next_order_id_before + 1);
5581 assert_eq!(
5582 exchange.balance_of(alice, escrow_token)?,
5583 escrow_balance_before.saturating_sub(amount)
5584 );
5585 } else {
5586 assert_eq!(res.unwrap_err(), TIP20Error::contract_paused().into());
5587 assert_eq!(exchange.next_order_id()?, next_order_id_before);
5588 assert_eq!(
5589 exchange.balance_of(alice, escrow_token)?,
5590 escrow_balance_before
5591 );
5592 }
5593
5594 Ok::<_, eyre::Report>(())
5595 })?;
5596 }
5597 Ok(())
5598 }
5599
5600 #[test]
5601 fn test_place_orders_on_paused_token_respects_internal_balance_path() -> eyre::Result<()> {
5602 let partial_internal_balance = MIN_ORDER_AMOUNT - 1;
5603
5604 assert_paused_token_order(
5607 true,
5608 MIN_ORDER_AMOUNT,
5609 false,
5610 |exchange, alice, base, amount| exchange.place(alice, base, amount, false, 0),
5611 )?;
5612 assert_paused_token_order(
5613 true,
5614 MIN_ORDER_AMOUNT,
5615 true,
5616 |exchange, alice, base, amount| exchange.place(alice, base, amount, true, 0),
5617 )?;
5618 assert_paused_token_order(
5619 true,
5620 MIN_ORDER_AMOUNT,
5621 false,
5622 |exchange, alice, base, amount| {
5623 exchange.place_flip(alice, base, amount, false, 100, 0, true)
5624 },
5625 )?;
5626 assert_paused_token_order(
5627 true,
5628 MIN_ORDER_AMOUNT,
5629 true,
5630 |exchange, alice, base, amount| {
5631 exchange.place_flip(alice, base, amount, true, 0, 100, true)
5632 },
5633 )?;
5634
5635 assert_paused_token_order(
5638 true,
5639 partial_internal_balance,
5640 false,
5641 |exchange, alice, base, amount| exchange.place(alice, base, amount, false, 0),
5642 )?;
5643 assert_paused_token_order(
5644 true,
5645 partial_internal_balance,
5646 true,
5647 |exchange, alice, base, amount| exchange.place(alice, base, amount, true, 0),
5648 )?;
5649 assert_paused_token_order(
5650 true,
5651 partial_internal_balance,
5652 false,
5653 |exchange, alice, base, amount| {
5654 exchange.place_flip(alice, base, amount, false, 100, 0, false)
5655 },
5656 )?;
5657 assert_paused_token_order(
5658 true,
5659 partial_internal_balance,
5660 true,
5661 |exchange, alice, base, amount| {
5662 exchange.place_flip(alice, base, amount, true, 0, 100, false)
5663 },
5664 )
5665 }
5666
5667 #[test]
5668 fn test_place_orders_on_paused_non_escrow_token_blocked_on_t4() -> eyre::Result<()> {
5669 assert_paused_token_order(false, 0, false, |exchange, alice, base, amount| {
5671 exchange.place(alice, base, amount, false, 0)
5672 })?;
5673 assert_paused_token_order(false, 0, true, |exchange, alice, base, amount| {
5674 exchange.place(alice, base, amount, true, 0)
5675 })?;
5676
5677 assert_paused_token_order(false, 0, false, |exchange, alice, base, amount| {
5679 exchange.place_flip(alice, base, amount, false, 100, 0, false)
5680 })?;
5681 assert_paused_token_order(false, 0, true, |exchange, alice, base, amount| {
5682 exchange.place_flip(alice, base, amount, true, 0, 100, false)
5683 })?;
5684
5685 assert_paused_token_order(
5687 false,
5688 MIN_ORDER_AMOUNT,
5689 false,
5690 |exchange, alice, base, amount| {
5691 exchange.place_flip(alice, base, amount, false, 100, 0, true)
5692 },
5693 )?;
5694 assert_paused_token_order(
5695 false,
5696 MIN_ORDER_AMOUNT,
5697 true,
5698 |exchange, alice, base, amount| {
5699 exchange.place_flip(alice, base, amount, true, 0, 100, true)
5700 },
5701 )
5702 }
5703
5704 #[test]
5705 fn test_swap_paused_intermediate_token_allowed_pre_t3_blocked_on_t3() -> eyre::Result<()> {
5706 for spec in [TempoHardfork::T2, TempoHardfork::T3] {
5707 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
5708 StorageCtx::enter(&mut storage, || {
5709 let mut exchange = StablecoinDEX::new();
5710 exchange.initialize()?;
5711
5712 let admin = Address::random();
5713 let alice = Address::random();
5714 let bob = Address::random();
5715
5716 let amount = MIN_ORDER_AMOUNT * 10;
5717 let amount_u256 = U256::from(amount);
5718
5719 let path_usd = TIP20Setup::path_usd(admin)
5721 .with_issuer(admin)
5722 .with_mint(alice, amount_u256)
5723 .with_approval(alice, exchange.address, amount_u256)
5724 .apply()?;
5725
5726 let usdc = TIP20Setup::create("USDC", "USDC", admin)
5727 .with_issuer(admin)
5728 .with_mint(alice, amount_u256)
5729 .with_approval(alice, exchange.address, amount_u256)
5730 .with_mint(bob, amount_u256)
5731 .with_approval(bob, exchange.address, amount_u256)
5732 .apply()?;
5733
5734 let eurc = TIP20Setup::create("EURC", "EURC", admin)
5735 .with_issuer(admin)
5736 .with_mint(alice, amount_u256)
5737 .with_approval(alice, exchange.address, amount_u256)
5738 .apply()?;
5739
5740 exchange.place(alice, usdc.address(), MIN_ORDER_AMOUNT * 5, true, 0)?;
5742 exchange.place(alice, eurc.address(), MIN_ORDER_AMOUNT * 5, false, 0)?;
5743
5744 let mut path_usd_tip20 = TIP20Token::from_address(path_usd.address())?;
5746 path_usd_tip20.grant_role_internal(admin, *PAUSE_ROLE)?;
5747 path_usd_tip20.pause(admin, ITIP20::pauseCall {})?;
5748
5749 let res_in = exchange.swap_exact_amount_in(
5751 bob,
5752 usdc.address(),
5753 eurc.address(),
5754 MIN_ORDER_AMOUNT,
5755 0,
5756 );
5757 let res_out = exchange.swap_exact_amount_out(
5758 bob,
5759 usdc.address(),
5760 eurc.address(),
5761 MIN_ORDER_AMOUNT,
5762 u128::MAX,
5763 );
5764
5765 if spec.is_t3() {
5766 assert_eq!(res_in, res_out);
5767 assert_eq!(res_in.unwrap_err(), TIP20Error::contract_paused().into());
5768 } else {
5769 assert!(res_in.is_ok());
5770 assert!(res_out.is_ok());
5771 }
5772
5773 Ok::<_, eyre::Report>(())
5774 })?;
5775 }
5776 Ok(())
5777 }
5778}