1pub mod dispatch;
3pub mod error;
4pub mod order;
5pub mod orderbook;
6
7pub use order::Order;
8pub use orderbook::{MAX_TICK, MIN_TICK, Orderbook, PRICE_SCALE, TickLevel, tick_to_price};
9use tempo_contracts::precompiles::PATH_USD_ADDRESS;
10pub use tempo_contracts::precompiles::{
11 IStablecoinExchange, StablecoinExchangeError, StablecoinExchangeEvents,
12};
13
14use crate::{
15 STABLECOIN_EXCHANGE_ADDRESS,
16 error::{Result, TempoPrecompileError},
17 path_usd::PathUSD,
18 stablecoin_exchange::orderbook::{
19 MAX_PRICE_POST_MODERATO, MAX_PRICE_PRE_MODERATO, MIN_PRICE_POST_MODERATO,
20 MIN_PRICE_PRE_MODERATO, compute_book_key,
21 },
22 storage::{Mapping, PrecompileStorageProvider, Slot, VecSlotExt},
23 tip20::{ITIP20, TIP20Token, is_tip20_prefix, validate_usd_currency},
24 tip20_factory::TIP20Factory,
25};
26use alloy::primitives::{Address, B256, Bytes, IntoLogData, U256};
27use revm::state::Bytecode;
28use tempo_precompiles_macros::contract;
29
30pub const MIN_ORDER_AMOUNT: u128 = 10_000_000;
32
33pub const TICK_SPACING: i16 = 10;
35
36fn calculate_quote_amount_floor(amount: u128, tick: i16) -> Option<u128> {
39 let price = tick_to_price(tick) as u128;
40 amount.checked_mul(price)?.checked_div(PRICE_SCALE as u128)
41}
42
43fn calculate_quote_amount_ceil(amount: u128, tick: i16) -> Option<u128> {
46 let price = tick_to_price(tick) as u128;
47 Some(amount.checked_mul(price)?.div_ceil(PRICE_SCALE as u128))
48}
49
50#[contract]
51pub struct StablecoinExchange {
52 books: Mapping<B256, Orderbook>,
53 orders: Mapping<u128, Order>,
54 balances: Mapping<Address, Mapping<Address, u128>>,
55 active_order_id: u128,
56 pending_order_id: u128,
57 book_keys: Vec<B256>,
58}
59
60type BookKeys = Slot<Vec<B256>>;
62
63impl<'a, S: PrecompileStorageProvider> StablecoinExchange<'a, S> {
64 pub fn new(storage: &'a mut S) -> Self {
65 Self::_new(STABLECOIN_EXCHANGE_ADDRESS, storage)
66 }
67
68 pub fn address(&self) -> Address {
70 self.address
71 }
72
73 pub fn initialize(&mut self) -> Result<()> {
77 self.storage.set_code(
79 self.address,
80 Bytecode::new_legacy(Bytes::from_static(&[0xef])),
81 )
82 }
83
84 fn get_pending_order_id(&mut self) -> Result<u128> {
86 self.pending_order_id()
87 }
88
89 fn set_pending_order_id(&mut self, order_id: u128) -> Result<()> {
91 self.sstore_pending_order_id(order_id)
92 }
93
94 fn get_active_order_id(&mut self) -> Result<u128> {
96 self.sload_active_order_id()
97 }
98
99 fn set_active_order_id(&mut self, order_id: u128) -> Result<()> {
101 self.sstore_active_order_id(order_id)
102 }
103
104 fn increment_pending_order_id(&mut self) -> Result<u128> {
106 let next_id = self.get_pending_order_id()? + 1;
107 self.set_pending_order_id(next_id)?;
108 Ok(next_id)
109 }
110
111 pub fn balance_of(&mut self, user: Address, token: Address) -> Result<u128> {
113 self.sload_balances(user, token)
114 }
115
116 pub fn min_price(&self) -> u32 {
118 if self.storage.spec().is_moderato() {
119 MIN_PRICE_POST_MODERATO
120 } else {
121 MIN_PRICE_PRE_MODERATO
122 }
123 }
124
125 pub fn max_price(&self) -> u32 {
127 if self.storage.spec().is_moderato() {
128 MAX_PRICE_POST_MODERATO
129 } else {
130 MAX_PRICE_PRE_MODERATO
131 }
132 }
133
134 fn validate_or_create_pair(&mut self, book: &Orderbook, token: Address) -> Result<()> {
137 if book.base.is_zero() {
138 if self.storage.spec().is_allegretto() {
139 self.create_pair(token)?;
140 } else {
141 return Err(StablecoinExchangeError::pair_does_not_exist().into());
142 }
143 }
144 Ok(())
145 }
146
147 pub fn get_order(&mut self, order_id: u128) -> Result<Order> {
150 let order = self.sload_orders(order_id)?;
151
152 if !order.maker().is_zero() && order.order_id() <= self.get_active_order_id()? {
154 Ok(order)
155 } else {
156 Err(StablecoinExchangeError::order_does_not_exist().into())
157 }
158 }
159
160 fn set_balance(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
162 self.sstore_balances(user, token, amount)
163 }
164
165 fn increment_balance(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
167 let current = self.balance_of(user, token)?;
168 self.set_balance(
169 user,
170 token,
171 current
172 .checked_add(amount)
173 .ok_or(TempoPrecompileError::under_overflow())?,
174 )
175 }
176
177 fn sub_balance(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
179 let current = self.balance_of(user, token)?;
180 self.set_balance(user, token, current.saturating_sub(amount))
181 }
182
183 fn emit_order_filled(
187 &mut self,
188 order_id: u128,
189 maker: Address,
190 taker: Address,
191 amount_filled: u128,
192 partial_fill: bool,
193 ) -> Result<()> {
194 if self.storage.spec().is_allegretto() {
195 self.storage.emit_event(
196 self.address,
197 StablecoinExchangeEvents::OrderFilled_1(IStablecoinExchange::OrderFilled_1 {
198 orderId: order_id,
199 maker,
200 taker,
201 amountFilled: amount_filled,
202 partialFill: partial_fill,
203 })
204 .into_log_data(),
205 )?;
206 } else {
207 self.storage.emit_event(
208 self.address,
209 StablecoinExchangeEvents::OrderFilled_0(IStablecoinExchange::OrderFilled_0 {
210 orderId: order_id,
211 maker,
212 amountFilled: amount_filled,
213 partialFill: partial_fill,
214 })
215 .into_log_data(),
216 )?;
217 }
218 Ok(())
219 }
220
221 fn transfer(&mut self, token: Address, to: Address, amount: u128) -> Result<()> {
223 if token == PATH_USD_ADDRESS {
224 PathUSD::new(self.storage).transfer(
225 self.address,
226 ITIP20::transferCall {
227 to,
228 amount: U256::from(amount),
229 },
230 )?;
231 } else {
232 TIP20Token::from_address(token, self.storage)?.transfer(
233 self.address,
234 ITIP20::transferCall {
235 to,
236 amount: U256::from(amount),
237 },
238 )?;
239 }
240 Ok(())
241 }
242
243 fn transfer_from(&mut self, token: Address, from: Address, amount: u128) -> Result<()> {
245 if token == PATH_USD_ADDRESS {
246 PathUSD::new(self.storage).transfer_from(
247 self.address,
248 ITIP20::transferFromCall {
249 from,
250 to: self.address,
251 amount: U256::from(amount),
252 },
253 )?;
254 } else {
255 TIP20Token::from_address(token, self.storage)?.transfer_from(
256 self.address,
257 ITIP20::transferFromCall {
258 from,
259 to: self.address,
260 amount: U256::from(amount),
261 },
262 )?;
263 }
264 Ok(())
265 }
266
267 fn decrement_balance_or_transfer_from(
269 &mut self,
270 user: Address,
271 token: Address,
272 amount: u128,
273 ) -> Result<()> {
274 let user_balance = self.balance_of(user, token)?;
275 if user_balance >= amount {
276 self.sub_balance(user, token, amount)
277 } else {
278 let remaining = amount
279 .checked_sub(user_balance)
280 .ok_or(TempoPrecompileError::under_overflow())?;
281
282 if self.storage.spec().is_allegro_moderato() {
284 self.transfer_from(token, user, remaining)?;
285 self.set_balance(user, token, 0)?;
286
287 Ok(())
288 } else {
289 self.set_balance(user, token, 0)?;
290 self.transfer_from(token, user, remaining)
291 }
292 }
293 }
294
295 pub fn quote_swap_exact_amount_out(
296 &mut self,
297 token_in: Address,
298 token_out: Address,
299 amount_out: u128,
300 ) -> Result<u128> {
301 let route = self.find_trade_path(token_in, token_out)?;
303
304 let mut current_amount = amount_out;
306 for (book_key, base_for_quote) in route.iter().rev() {
307 current_amount = self.quote_exact_out(*book_key, current_amount, *base_for_quote)?;
308 }
309
310 Ok(current_amount)
311 }
312
313 pub fn quote_swap_exact_amount_in(
314 &mut self,
315 token_in: Address,
316 token_out: Address,
317 amount_in: u128,
318 ) -> Result<u128> {
319 let route = self.find_trade_path(token_in, token_out)?;
321
322 let mut current_amount = amount_in;
324 for (book_key, base_for_quote) in route {
325 current_amount = self.quote_exact_in(book_key, current_amount, base_for_quote)?;
326 }
327
328 Ok(current_amount)
329 }
330
331 pub fn swap_exact_amount_in(
332 &mut self,
333 sender: Address,
334 token_in: Address,
335 token_out: Address,
336 amount_in: u128,
337 min_amount_out: u128,
338 ) -> Result<u128> {
339 let route = self.find_trade_path(token_in, token_out)?;
341
342 self.decrement_balance_or_transfer_from(sender, token_in, amount_in)?;
344
345 let mut amount = amount_in;
347 for (book_key, base_for_quote) in route {
348 amount = if self.storage.spec().is_moderato() {
350 self.fill_orders_exact_in_post_moderato(book_key, base_for_quote, amount, sender)?
351 } else {
352 self.fill_orders_exact_in_pre_moderato(book_key, base_for_quote, amount, 0, sender)?
353 };
354 }
355
356 if amount < min_amount_out {
358 return Err(StablecoinExchangeError::insufficient_output().into());
359 }
360
361 self.transfer(token_out, sender, amount)?;
362
363 Ok(amount)
364 }
365
366 pub fn swap_exact_amount_out(
367 &mut self,
368 sender: Address,
369 token_in: Address,
370 token_out: Address,
371 amount_out: u128,
372 max_amount_in: u128,
373 ) -> Result<u128> {
374 let route = self.find_trade_path(token_in, token_out)?;
376
377 let mut amount = amount_out;
379 for (book_key, base_for_quote) in route.iter().rev() {
380 amount = if self.storage.spec().is_moderato() {
381 self.fill_orders_exact_out_post_moderato(
382 *book_key,
383 *base_for_quote,
384 amount,
385 sender,
386 )?
387 } else {
388 self.fill_orders_exact_out_pre_moderato(
389 *book_key,
390 *base_for_quote,
391 amount,
392 max_amount_in,
393 sender,
394 )?
395 };
396 }
397
398 if amount > max_amount_in {
399 return Err(StablecoinExchangeError::max_input_exceeded().into());
400 }
401
402 self.decrement_balance_or_transfer_from(sender, token_in, amount)?;
404
405 self.transfer(token_out, sender, amount_out)?;
407
408 Ok(amount)
409 }
410
411 pub fn pair_key(&self, token_a: Address, token_b: Address) -> B256 {
413 compute_book_key(token_a, token_b)
414 }
415
416 pub fn get_price_level(&mut self, base: Address, tick: i16, is_bid: bool) -> Result<TickLevel> {
418 let quote = TIP20Token::from_address(base, self.storage)?.quote_token()?;
419 let key = compute_book_key(base, quote);
420 Orderbook::read_tick_level(self, key, is_bid, tick)
421 }
422
423 pub fn active_order_id(&mut self) -> Result<u128> {
425 self.sload_active_order_id()
426 }
427
428 pub fn pending_order_id(&mut self) -> Result<u128> {
430 self.sload_pending_order_id()
431 }
432
433 pub fn books(&mut self, pair_key: B256) -> Result<Orderbook> {
435 self.sload_books(pair_key)
436 }
437
438 pub fn get_book_keys(&mut self) -> Result<Vec<B256>> {
440 self.sload_book_keys()
441 }
442
443 pub fn price_to_tick(&self, price: u32) -> Result<i16> {
447 if self.storage.spec().is_moderato() {
448 orderbook::price_to_tick_post_moderato(price)
450 } else {
451 orderbook::price_to_tick_pre_moderato(price)
452 }
453 }
454
455 pub fn create_pair(&mut self, base: Address) -> Result<B256> {
456 if self.storage.spec().is_moderato() && !TIP20Factory::new(self.storage).is_tip20(base)? {
458 return Err(StablecoinExchangeError::invalid_base_token().into());
459 }
460
461 let quote = TIP20Token::from_address(base, self.storage)?.quote_token()?;
462 validate_usd_currency(base, self.storage)?;
463 validate_usd_currency(quote, self.storage)?;
464
465 let book_key = compute_book_key(base, quote);
466
467 if self.sload_books(book_key)?.is_initialized() {
468 return Err(StablecoinExchangeError::pair_already_exists().into());
469 }
470
471 let book = Orderbook::new(base, quote);
472 self.sstore_books(book_key, book)?;
473 BookKeys::new(slots::BOOK_KEYS).push(self, book_key)?;
474
475 self.storage.emit_event(
477 self.address,
478 StablecoinExchangeEvents::PairCreated(IStablecoinExchange::PairCreated {
479 key: book_key,
480 base,
481 quote,
482 })
483 .into_log_data(),
484 )?;
485
486 Ok(book_key)
487 }
488
489 pub fn place(
503 &mut self,
504 sender: Address,
505 token: Address,
506 amount: u128,
507 is_bid: bool,
508 tick: i16,
509 ) -> Result<u128> {
510 let quote_token = TIP20Token::from_address(token, self.storage)?.quote_token()?;
511
512 let book_key = compute_book_key(token, quote_token);
514
515 let book = self.sload_books(book_key)?;
516 self.validate_or_create_pair(&book, token)?;
517
518 if !(MIN_TICK..=MAX_TICK).contains(&tick) {
520 return Err(StablecoinExchangeError::tick_out_of_bounds(tick).into());
521 }
522
523 if self.storage.spec().is_allegretto() && tick % TICK_SPACING != 0 {
525 return Err(StablecoinExchangeError::invalid_tick().into());
526 }
527
528 if amount < MIN_ORDER_AMOUNT {
530 return Err(StablecoinExchangeError::below_minimum_order_size(amount).into());
531 }
532
533 let (escrow_token, escrow_amount) = if is_bid {
535 let quote_amount = if self.storage.spec().is_moderato() {
537 calculate_quote_amount_ceil(amount, tick)
538 } else {
539 calculate_quote_amount_floor(amount, tick)
540 }
541 .ok_or(StablecoinExchangeError::insufficient_balance())?;
542 (quote_token, quote_amount)
543 } else {
544 (token, amount)
546 };
547
548 self.decrement_balance_or_transfer_from(sender, escrow_token, escrow_amount)?;
550
551 let order_id = self.increment_pending_order_id()?;
553 let order = if is_bid {
554 Order::new_bid(order_id, sender, book_key, amount, tick)
555 } else {
556 Order::new_ask(order_id, sender, book_key, amount, tick)
557 };
558
559 self.sstore_orders(order_id, order)?;
563
564 self.storage.emit_event(
566 self.address,
567 StablecoinExchangeEvents::OrderPlaced(IStablecoinExchange::OrderPlaced {
568 orderId: order_id,
569 maker: sender,
570 token,
571 amount,
572 isBid: is_bid,
573 tick,
574 })
575 .into_log_data(),
576 )?;
577
578 Ok(order_id)
579 }
580
581 pub fn place_flip(
587 &mut self,
588 sender: Address,
589 token: Address,
590 amount: u128,
591 is_bid: bool,
592 tick: i16,
593 flip_tick: i16,
594 ) -> Result<u128> {
595 let quote_token = TIP20Token::from_address(token, self.storage)?.quote_token()?;
596
597 let book_key = compute_book_key(token, quote_token);
599
600 if self.storage.spec().is_moderato() {
602 let book = self.sload_books(book_key)?;
603 self.validate_or_create_pair(&book, token)?;
604 }
605
606 if !(MIN_TICK..=MAX_TICK).contains(&tick) {
608 return Err(StablecoinExchangeError::tick_out_of_bounds(tick).into());
609 }
610
611 if self.storage.spec().is_allegretto() && tick % TICK_SPACING != 0 {
613 return Err(StablecoinExchangeError::invalid_tick().into());
614 }
615
616 if !(MIN_TICK..=MAX_TICK).contains(&flip_tick) {
617 return Err(StablecoinExchangeError::tick_out_of_bounds(flip_tick).into());
618 }
619
620 if self.storage.spec().is_allegretto() && flip_tick % TICK_SPACING != 0 {
622 return Err(StablecoinExchangeError::invalid_flip_tick().into());
623 }
624
625 if (is_bid && flip_tick <= tick) || (!is_bid && flip_tick >= tick) {
627 return Err(StablecoinExchangeError::invalid_flip_tick().into());
628 }
629
630 if amount < MIN_ORDER_AMOUNT {
632 return Err(StablecoinExchangeError::below_minimum_order_size(amount).into());
633 }
634
635 let (escrow_token, escrow_amount) = if is_bid {
637 let quote_amount = if self.storage.spec().is_moderato() {
639 calculate_quote_amount_ceil(amount, tick)
640 } else {
641 calculate_quote_amount_floor(amount, tick)
642 }
643 .ok_or(StablecoinExchangeError::insufficient_balance())?;
644 (quote_token, quote_amount)
645 } else {
646 (token, amount)
648 };
649
650 self.decrement_balance_or_transfer_from(sender, escrow_token, escrow_amount)?;
652
653 let order_id = self.increment_pending_order_id()?;
655 let order = Order::new_flip(order_id, sender, book_key, amount, tick, is_bid, flip_tick)
656 .expect("Invalid flip tick");
657
658 self.sstore_orders(order_id, order)?;
660
661 self.storage.emit_event(
663 self.address,
664 StablecoinExchangeEvents::FlipOrderPlaced(IStablecoinExchange::FlipOrderPlaced {
665 orderId: order_id,
666 maker: sender,
667 token,
668 amount,
669 isBid: is_bid,
670 tick,
671 flipTick: flip_tick,
672 })
673 .into_log_data(),
674 )?;
675
676 Ok(order_id)
677 }
678
679 pub fn execute_block(&mut self, sender: Address) -> Result<()> {
683 if sender != Address::ZERO {
685 return Err(StablecoinExchangeError::unauthorized().into());
686 }
687
688 let next_order_id = self.get_active_order_id()?;
689
690 let pending_order_id = self.get_pending_order_id()?;
691
692 let mut current_order_id = next_order_id
693 .checked_add(1)
694 .ok_or(TempoPrecompileError::under_overflow())?;
695 while current_order_id <= pending_order_id {
696 self.process_pending_order(current_order_id)?;
697 current_order_id = current_order_id
698 .checked_add(1)
699 .ok_or(TempoPrecompileError::under_overflow())?;
700 }
701
702 self.set_active_order_id(pending_order_id)?;
703
704 Ok(())
705 }
706
707 fn process_pending_order(&mut self, order_id: u128) -> Result<()> {
709 let order = self.sload_orders(order_id)?;
710
711 if order.maker().is_zero() {
713 return Ok(());
714 }
715
716 let orderbook = self.sload_books(order.book_key())?;
717 let mut level =
718 Orderbook::read_tick_level(self, order.book_key(), order.is_bid(), order.tick())?;
719
720 let prev_tail = level.tail;
721 if prev_tail == 0 {
722 level.head = order_id;
723 level.tail = order_id;
724
725 Orderbook::set_tick_bit(self, order.book_key(), order.tick(), order.is_bid())
726 .expect("Tick is valid");
727
728 if order.is_bid() {
729 if order.tick() > orderbook.best_bid_tick {
730 Orderbook::update_best_bid_tick(self, order.book_key(), order.tick())?;
731 }
732 } else if order.tick() < orderbook.best_ask_tick {
733 Orderbook::update_best_ask_tick(self, order.book_key(), order.tick())?;
734 }
735 } else {
736 Order::update_next_order(self, prev_tail, order_id)?;
737 Order::update_prev_order(self, order_id, prev_tail)?;
738 level.tail = order_id;
739 }
740
741 let new_liquidity = level
742 .total_liquidity
743 .checked_add(order.remaining())
744 .ok_or(TempoPrecompileError::under_overflow())?;
745 level.total_liquidity = new_liquidity;
746
747 Orderbook::write_tick_level(self, order.book_key(), order.is_bid(), order.tick(), level)
748 }
749
750 fn partial_fill_order(
753 &mut self,
754 order: &mut Order,
755 level: &mut TickLevel,
756 fill_amount: u128,
757 taker: Address,
758 ) -> Result<u128> {
759 let orderbook = self.sload_books(order.book_key())?;
760 let price = tick_to_price(order.tick());
761
762 let new_remaining = order.remaining() - fill_amount;
764 Order::update_remaining(self, order.order_id(), new_remaining)?;
765
766 if order.is_bid() {
767 self.increment_balance(order.maker(), orderbook.base, fill_amount)?;
768 } else {
769 let quote_amount = fill_amount
770 .checked_mul(price as u128)
771 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
772 .ok_or(TempoPrecompileError::under_overflow())?;
773 self.increment_balance(order.maker(), orderbook.quote, quote_amount)?;
774 }
775
776 let amount_out = if order.is_bid() {
777 fill_amount
778 .checked_mul(price as u128)
779 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
780 .expect("Amount out calculation overflow")
781 } else {
782 fill_amount
783 };
784
785 let new_liquidity = level
787 .total_liquidity
788 .checked_sub(fill_amount)
789 .ok_or(TempoPrecompileError::under_overflow())?;
790 level.total_liquidity = new_liquidity;
791
792 Orderbook::write_tick_level(self, order.book_key(), order.is_bid(), order.tick(), *level)?;
793
794 self.emit_order_filled(order.order_id(), order.maker(), taker, fill_amount, true)?;
796
797 Ok(amount_out)
798 }
799
800 fn fill_order(
802 &mut self,
803 book_key: B256,
804 order: &mut Order,
805 mut level: TickLevel,
806 taker: Address,
807 ) -> Result<(u128, Option<(TickLevel, Order)>)> {
808 let orderbook = self.sload_books(order.book_key())?;
809 let price = tick_to_price(order.tick());
810 let fill_amount = order.remaining();
811
812 let amount_out = if order.is_bid() {
813 self.increment_balance(order.maker(), orderbook.base, fill_amount)?;
814 fill_amount
815 .checked_mul(price as u128)
816 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
817 .expect("Amount out calculation overflow")
818 } else {
819 let quote_amount = fill_amount
820 .checked_mul(price as u128)
821 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
822 .expect("Amount out calculation overflow");
823 self.increment_balance(order.maker(), orderbook.quote, quote_amount)?;
824
825 fill_amount
826 };
827
828 self.emit_order_filled(order.order_id(), order.maker(), taker, fill_amount, false)?;
830
831 if order.is_flip() {
832 let _ = self.place_flip(
836 order.maker(),
837 orderbook.base,
838 order.amount(),
839 !order.is_bid(),
840 order.flip_tick(),
841 order.tick(),
842 );
843 }
844
845 self.clear_orders(order.order_id())?;
847
848 let next_tick_info = if order.next() == 0 {
850 Orderbook::delete_tick_level(self, book_key, order.is_bid(), order.tick())?;
851
852 Orderbook::clear_tick_bit(self, order.book_key(), order.tick(), order.is_bid())
853 .expect("Tick is valid");
854
855 let (tick, has_liquidity) = Orderbook::next_initialized_tick(
856 self,
857 book_key,
858 order.is_bid(),
859 order.tick(),
860 self.storage.spec(),
861 );
862
863 if self.storage.spec().is_allegretto() {
864 if order.is_bid() {
866 let new_best = if has_liquidity { tick } else { i16::MIN };
867 Orderbook::update_best_bid_tick(self, book_key, new_best)?;
868 } else {
869 let new_best = if has_liquidity { tick } else { i16::MAX };
870 Orderbook::update_best_ask_tick(self, book_key, new_best)?;
871 }
872 }
873
874 if !has_liquidity {
875 None
877 } else {
878 let new_level = Orderbook::read_tick_level(self, book_key, order.is_bid(), tick)?;
879 let new_order = self.sload_orders(new_level.head)?;
880
881 Some((new_level, new_order))
882 }
883 } else {
884 level.head = order.next();
886 if self.storage.spec().is_allegretto() {
887 Order::update_prev_order(self, order.next(), 0)?;
888 }
889 let new_liquidity = level
890 .total_liquidity
891 .checked_sub(fill_amount)
892 .ok_or(TempoPrecompileError::under_overflow())?;
893 level.total_liquidity = new_liquidity;
894
895 Orderbook::write_tick_level(
896 self,
897 order.book_key(),
898 order.is_bid(),
899 order.tick(),
900 level,
901 )?;
902
903 let new_order = self.sload_orders(order.next())?;
904 Some((level, new_order))
905 };
906
907 Ok((amount_out, next_tick_info))
908 }
909
910 fn fill_orders_exact_out_post_moderato(
912 &mut self,
913 book_key: B256,
914 bid: bool,
915 mut amount_out: u128,
916 taker: Address,
917 ) -> Result<u128> {
918 let mut level = self.get_best_price_level(book_key, bid)?;
919 let mut order = self.sload_orders(level.head)?;
920
921 let mut total_amount_in: u128 = 0;
922
923 while amount_out > 0 {
924 let price = tick_to_price(order.tick());
925
926 let (fill_amount, amount_in) = if bid {
927 let base_needed = amount_out
928 .checked_mul(orderbook::PRICE_SCALE as u128)
929 .and_then(|v| v.checked_div(price as u128))
930 .ok_or(TempoPrecompileError::under_overflow())?;
931 let fill_amount = base_needed.min(order.remaining());
932 (fill_amount, fill_amount)
933 } else {
934 let fill_amount = amount_out.min(order.remaining());
935 let amount_in = fill_amount
936 .checked_mul(price as u128)
937 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
938 .ok_or(TempoPrecompileError::under_overflow())?;
939 (fill_amount, amount_in)
940 };
941
942 if fill_amount < order.remaining() {
943 self.partial_fill_order(&mut order, &mut level, fill_amount, taker)?;
944 total_amount_in = total_amount_in
945 .checked_add(amount_in)
946 .ok_or(TempoPrecompileError::under_overflow())?;
947 break;
948 } else {
949 let (amount_out_received, next_order_info) =
950 self.fill_order(book_key, &mut order, level, taker)?;
951 total_amount_in = total_amount_in
952 .checked_add(amount_in)
953 .ok_or(TempoPrecompileError::under_overflow())?;
954
955 if bid {
957 let base_needed = amount_out
958 .checked_mul(orderbook::PRICE_SCALE as u128)
959 .and_then(|v| v.checked_div(price as u128))
960 .ok_or(TempoPrecompileError::under_overflow())?;
961 if base_needed > order.remaining() {
962 amount_out = amount_out
963 .checked_sub(amount_out_received)
964 .ok_or(TempoPrecompileError::under_overflow())?;
965 } else {
966 amount_out = 0;
967 }
968 } else if amount_out > order.remaining() {
969 amount_out = amount_out
970 .checked_sub(amount_out_received)
971 .ok_or(TempoPrecompileError::under_overflow())?;
972 } else {
973 amount_out = 0;
974 }
975
976 if let Some((new_level, new_order)) = next_order_info {
977 level = new_level;
978 order = new_order;
979 } else {
980 if amount_out > 0 {
981 return Err(StablecoinExchangeError::insufficient_liquidity().into());
982 }
983 break;
984 }
985 }
986 }
987
988 Ok(total_amount_in)
989 }
990
991 fn fill_orders_exact_out_pre_moderato(
993 &mut self,
994 book_key: B256,
995 bid: bool,
996 mut amount_out: u128,
997 max_amount_in: u128,
998 taker: Address,
999 ) -> Result<u128> {
1000 let mut level = self.get_best_price_level(book_key, bid)?;
1001 let mut order = self.sload_orders(level.head)?;
1002
1003 let mut total_amount_in: u128 = 0;
1004
1005 while amount_out > 0 {
1006 let price = tick_to_price(order.tick());
1007 let fill_amount = amount_out.min(order.remaining());
1008 let amount_in = if bid {
1009 fill_amount
1010 } else {
1011 fill_amount
1012 .checked_mul(price as u128)
1013 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
1014 .ok_or(TempoPrecompileError::under_overflow())?
1015 };
1016
1017 if total_amount_in + amount_in > max_amount_in {
1019 return Err(StablecoinExchangeError::max_input_exceeded().into());
1020 }
1021
1022 if fill_amount < order.remaining() {
1023 self.partial_fill_order(&mut order, &mut level, fill_amount, taker)?;
1024 total_amount_in = total_amount_in
1025 .checked_add(amount_in)
1026 .ok_or(TempoPrecompileError::under_overflow())?;
1027 break;
1028 } else {
1029 let (amount_out_received, next_order_info) =
1030 self.fill_order(book_key, &mut order, level, taker)?;
1031 total_amount_in = total_amount_in
1032 .checked_add(amount_in)
1033 .ok_or(TempoPrecompileError::under_overflow())?;
1034
1035 amount_out = amount_out
1037 .checked_sub(amount_out_received)
1038 .ok_or(TempoPrecompileError::under_overflow())?;
1039
1040 if let Some((new_level, new_order)) = next_order_info {
1041 level = new_level;
1042 order = new_order;
1043 } else {
1044 if amount_out > 0 {
1045 return Err(StablecoinExchangeError::insufficient_liquidity().into());
1046 }
1047 break;
1048 }
1049 }
1050 }
1051
1052 Ok(total_amount_in)
1053 }
1054
1055 fn fill_orders_exact_in_post_moderato(
1057 &mut self,
1058 book_key: B256,
1059 bid: bool,
1060 mut amount_in: u128,
1061 taker: Address,
1062 ) -> Result<u128> {
1063 let mut level = self.get_best_price_level(book_key, bid)?;
1064 let mut order = self.sload_orders(level.head)?;
1065
1066 let mut total_amount_out: u128 = 0;
1067
1068 while amount_in > 0 {
1069 let price = tick_to_price(order.tick());
1070
1071 let fill_amount = if bid {
1072 amount_in.min(order.remaining())
1073 } else {
1074 let base_out = amount_in
1075 .checked_mul(orderbook::PRICE_SCALE as u128)
1076 .and_then(|v| v.checked_div(price as u128))
1077 .ok_or(TempoPrecompileError::under_overflow())?;
1078 base_out.min(order.remaining())
1079 };
1080
1081 if fill_amount < order.remaining() {
1082 let amount_out =
1083 self.partial_fill_order(&mut order, &mut level, fill_amount, taker)?;
1084 total_amount_out = total_amount_out
1085 .checked_add(amount_out)
1086 .ok_or(TempoPrecompileError::under_overflow())?;
1087 break;
1088 } else {
1089 let (amount_out, next_order_info) =
1090 self.fill_order(book_key, &mut order, level, taker)?;
1091 total_amount_out = total_amount_out
1092 .checked_add(amount_out)
1093 .ok_or(TempoPrecompileError::under_overflow())?;
1094
1095 if bid {
1097 if amount_in > order.remaining() {
1098 amount_in = amount_in
1099 .checked_sub(order.remaining())
1100 .ok_or(TempoPrecompileError::under_overflow())?;
1101 } else {
1102 amount_in = 0;
1103 }
1104 } else {
1105 let base_out = amount_in
1106 .checked_mul(orderbook::PRICE_SCALE as u128)
1107 .and_then(|v| v.checked_div(price as u128))
1108 .ok_or(TempoPrecompileError::under_overflow())?;
1109 if base_out > order.remaining() {
1110 let quote_needed = order
1111 .remaining()
1112 .checked_mul(price as u128)
1113 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
1114 .ok_or(TempoPrecompileError::under_overflow())?;
1115 amount_in = amount_in
1116 .checked_sub(quote_needed)
1117 .ok_or(TempoPrecompileError::under_overflow())?;
1118 } else {
1119 amount_in = 0;
1120 }
1121 }
1122
1123 if let Some((new_level, new_order)) = next_order_info {
1124 level = new_level;
1125 order = new_order;
1126 } else {
1127 if amount_in > 0 {
1128 return Err(StablecoinExchangeError::insufficient_liquidity().into());
1129 }
1130 break;
1131 }
1132 }
1133 }
1134
1135 Ok(total_amount_out)
1136 }
1137
1138 fn fill_orders_exact_in_pre_moderato(
1140 &mut self,
1141 book_key: B256,
1142 bid: bool,
1143 mut amount_in: u128,
1144 min_amount_out: u128,
1145 taker: Address,
1146 ) -> Result<u128> {
1147 let mut level = self.get_best_price_level(book_key, bid)?;
1148 let mut order = self.sload_orders(level.head)?;
1149
1150 let mut total_amount_out: u128 = 0;
1151 while amount_in > 0 {
1152 let fill_amount = amount_in.min(order.remaining());
1154
1155 if fill_amount < order.remaining() {
1156 let amount_out =
1157 self.partial_fill_order(&mut order, &mut level, fill_amount, taker)?;
1158 total_amount_out = total_amount_out
1159 .checked_add(amount_out)
1160 .ok_or(TempoPrecompileError::under_overflow())?;
1161 break;
1162 } else {
1163 let (amount_out, next_order_info) =
1164 self.fill_order(book_key, &mut order, level, taker)?;
1165 total_amount_out = total_amount_out
1166 .checked_add(amount_out)
1167 .ok_or(TempoPrecompileError::under_overflow())?;
1168
1169 amount_in = amount_in
1171 .checked_sub(order.remaining())
1172 .ok_or(TempoPrecompileError::under_overflow())?;
1173
1174 if let Some((new_level, new_order)) = next_order_info {
1175 level = new_level;
1176 order = new_order;
1177 } else {
1178 break;
1179 }
1180 }
1181 }
1182
1183 if total_amount_out < min_amount_out {
1185 return Err(StablecoinExchangeError::insufficient_output().into());
1186 }
1187
1188 Ok(total_amount_out)
1189 }
1190
1191 fn get_best_price_level(&mut self, book_key: B256, is_bid: bool) -> Result<TickLevel> {
1193 let orderbook = self.sload_books(book_key)?;
1194
1195 let current_tick = if is_bid {
1196 if orderbook.best_bid_tick == i16::MIN {
1197 return Err(StablecoinExchangeError::insufficient_liquidity().into());
1198 }
1199 orderbook.best_bid_tick
1200 } else {
1201 if orderbook.best_ask_tick == i16::MAX {
1202 return Err(StablecoinExchangeError::insufficient_liquidity().into());
1203 }
1204 orderbook.best_ask_tick
1205 };
1206
1207 let level = Orderbook::read_tick_level(self, book_key, is_bid, current_tick)?;
1208
1209 Ok(level)
1210 }
1211
1212 pub fn cancel(&mut self, sender: Address, order_id: u128) -> Result<()> {
1215 let order = self.sload_orders(order_id)?;
1216
1217 if order.maker().is_zero() {
1218 return Err(StablecoinExchangeError::order_does_not_exist().into());
1219 }
1220
1221 if order.maker() != sender {
1222 return Err(StablecoinExchangeError::unauthorized().into());
1223 }
1224
1225 if order.remaining() == 0 {
1226 return Err(StablecoinExchangeError::order_does_not_exist().into());
1227 }
1228
1229 let next_order_id = self.get_active_order_id()?;
1231
1232 if order.order_id() > next_order_id {
1233 self.cancel_pending_order(order)?;
1234 } else {
1235 self.cancel_active_order(order)?;
1236 }
1237
1238 Ok(())
1239 }
1240
1241 fn cancel_pending_order(&mut self, order: Order) -> Result<()> {
1243 let orderbook = self.sload_books(order.book_key())?;
1244 let token = if order.is_bid() {
1245 orderbook.quote
1246 } else {
1247 orderbook.base
1248 };
1249
1250 let refund_amount = if order.is_bid() {
1252 let price = orderbook::tick_to_price(order.tick());
1253 (order.remaining() * price as u128) / orderbook::PRICE_SCALE as u128
1254 } else {
1255 order.remaining()
1256 };
1257
1258 self.increment_balance(order.maker(), token, refund_amount)?;
1260
1261 self.clear_orders(order.order_id())?;
1263
1264 self.storage.emit_event(
1266 self.address,
1267 StablecoinExchangeEvents::OrderCancelled(IStablecoinExchange::OrderCancelled {
1268 orderId: order.order_id(),
1269 })
1270 .into_log_data(),
1271 )
1272 }
1273
1274 fn cancel_active_order(&mut self, order: Order) -> Result<()> {
1276 let mut level =
1277 Orderbook::read_tick_level(self, order.book_key(), order.is_bid(), order.tick())?;
1278
1279 if order.prev() != 0 {
1281 Order::update_next_order(self, order.prev(), order.next())?;
1282 } else {
1283 level.head = order.next();
1284 }
1285
1286 if order.next() != 0 {
1287 Order::update_prev_order(self, order.next(), order.prev())?;
1288 } else {
1289 level.tail = order.prev();
1290 }
1291
1292 let new_liquidity = level
1294 .total_liquidity
1295 .checked_sub(order.remaining())
1296 .ok_or(TempoPrecompileError::under_overflow())?;
1297 level.total_liquidity = new_liquidity;
1298
1299 if level.head == 0 {
1301 Orderbook::clear_tick_bit(self, order.book_key(), order.tick(), order.is_bid())
1302 .expect("Tick is valid");
1303
1304 if self.storage.spec().is_allegretto() {
1305 let orderbook = self.sload_books(order.book_key())?;
1307 let best_tick = if order.is_bid() {
1308 orderbook.best_bid_tick
1309 } else {
1310 orderbook.best_ask_tick
1311 };
1312
1313 if best_tick == order.tick() {
1314 let (next_tick, has_liquidity) = Orderbook::next_initialized_tick(
1315 self,
1316 order.book_key(),
1317 order.is_bid(),
1318 order.tick(),
1319 self.storage.spec(),
1320 );
1321
1322 if order.is_bid() {
1323 let new_best = if has_liquidity { next_tick } else { i16::MIN };
1324 Orderbook::update_best_bid_tick(self, order.book_key(), new_best)?;
1325 } else {
1326 let new_best = if has_liquidity { next_tick } else { i16::MAX };
1327 Orderbook::update_best_ask_tick(self, order.book_key(), new_best)?;
1328 }
1329 }
1330 }
1331 }
1332
1333 Orderbook::write_tick_level(self, order.book_key(), order.is_bid(), order.tick(), level)?;
1334
1335 let orderbook = self.sload_books(order.book_key())?;
1337 if order.is_bid() {
1338 let price = orderbook::tick_to_price(order.tick());
1340 let quote_amount = order
1341 .remaining()
1342 .checked_mul(price as u128)
1343 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
1344 .expect("Quote amount calculation overflow");
1345 self.increment_balance(order.maker(), orderbook.quote, quote_amount)?;
1346 } else {
1347 self.increment_balance(order.maker(), orderbook.base, order.remaining())?;
1349 }
1350
1351 self.clear_orders(order.order_id())?;
1353
1354 self.storage.emit_event(
1356 self.address,
1357 StablecoinExchangeEvents::OrderCancelled(IStablecoinExchange::OrderCancelled {
1358 orderId: order.order_id(),
1359 })
1360 .into_log_data(),
1361 )
1362 }
1363
1364 pub fn withdraw(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
1366 let current_balance = self.balance_of(user, token)?;
1367 if current_balance < amount {
1368 return Err(StablecoinExchangeError::insufficient_balance().into());
1369 }
1370 self.sub_balance(user, token, amount)?;
1371 self.transfer(token, user, amount)?;
1372
1373 Ok(())
1374 }
1375
1376 fn quote_exact_out(&mut self, book_key: B256, amount_out: u128, is_bid: bool) -> Result<u128> {
1378 let mut remaining_out = amount_out;
1379 let mut amount_in = 0u128;
1380 let orderbook = self.sload_books(book_key)?;
1381
1382 let mut current_tick = if is_bid {
1383 orderbook.best_bid_tick
1384 } else {
1385 orderbook.best_ask_tick
1386 };
1387 if current_tick == i16::MIN
1389 || self.storage.spec().is_allegretto() && current_tick == i16::MAX
1390 {
1391 return Err(StablecoinExchangeError::insufficient_liquidity().into());
1392 }
1393
1394 while remaining_out > 0 {
1395 let level = Orderbook::read_tick_level(self, book_key, is_bid, current_tick)?;
1396
1397 if level.total_liquidity == 0 {
1399 let (next_tick, initialized) = Orderbook::next_initialized_tick(
1400 self,
1401 book_key,
1402 is_bid,
1403 current_tick,
1404 self.storage.spec(),
1405 );
1406
1407 if !initialized {
1408 return Err(StablecoinExchangeError::insufficient_liquidity().into());
1409 }
1410 current_tick = next_tick;
1411 continue;
1412 }
1413
1414 let price = orderbook::tick_to_price(current_tick);
1415
1416 let (fill_amount, amount_in_tick) = if is_bid {
1417 let base_needed = remaining_out
1419 .checked_mul(orderbook::PRICE_SCALE as u128)
1420 .and_then(|v| v.checked_div(price as u128))
1421 .ok_or(TempoPrecompileError::under_overflow())?;
1422 let fill_amount = if base_needed > level.total_liquidity {
1423 level.total_liquidity
1424 } else {
1425 base_needed
1426 };
1427 (fill_amount, fill_amount)
1428 } else {
1429 let fill_amount = if remaining_out > level.total_liquidity {
1431 level.total_liquidity
1432 } else {
1433 remaining_out
1434 };
1435 let quote_needed = fill_amount
1436 .checked_mul(price as u128)
1437 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
1438 .ok_or(TempoPrecompileError::under_overflow())?;
1439 (fill_amount, quote_needed)
1440 };
1441
1442 let amount_out_tick = if is_bid {
1443 fill_amount
1444 .checked_mul(price as u128)
1445 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
1446 .ok_or(TempoPrecompileError::under_overflow())?
1447 } else {
1448 fill_amount
1449 };
1450
1451 remaining_out = remaining_out
1452 .checked_sub(amount_out_tick)
1453 .ok_or(TempoPrecompileError::under_overflow())?;
1454 amount_in = amount_in
1455 .checked_add(amount_in_tick)
1456 .ok_or(TempoPrecompileError::under_overflow())?;
1457
1458 if fill_amount == level.total_liquidity {
1460 let (next_tick, initialized) = Orderbook::next_initialized_tick(
1461 self,
1462 book_key,
1463 is_bid,
1464 current_tick,
1465 self.storage.spec(),
1466 );
1467
1468 if !initialized && remaining_out > 0 {
1469 return Err(StablecoinExchangeError::insufficient_liquidity().into());
1470 }
1471 current_tick = next_tick;
1472 } else {
1473 break;
1474 }
1475 }
1476
1477 Ok(amount_in)
1478 }
1479
1480 fn find_trade_path(
1484 &mut self,
1485 token_in: Address,
1486 token_out: Address,
1487 ) -> Result<Vec<(B256, bool)>> {
1488 if token_in == token_out {
1490 return Err(StablecoinExchangeError::identical_tokens().into());
1491 }
1492
1493 if self.storage.spec().is_allegretto()
1495 && (!is_tip20_prefix(token_in) || !is_tip20_prefix(token_out))
1496 {
1497 return Err(StablecoinExchangeError::invalid_token().into());
1498 }
1499
1500 let in_quote = TIP20Token::from_address(token_in, self.storage)?.quote_token()?;
1502 let out_quote = TIP20Token::from_address(token_out, self.storage)?.quote_token()?;
1503
1504 if in_quote == token_out || out_quote == token_in {
1505 return self.validate_and_build_route(&[token_in, token_out]);
1506 }
1507
1508 let path_in = self.find_path_to_root(token_in)?;
1510 let path_out = self.find_path_to_root(token_out)?;
1511
1512 let mut lca = None;
1514 for token_a in &path_in {
1515 if path_out.contains(token_a) {
1516 lca = Some(*token_a);
1517 break;
1518 }
1519 }
1520
1521 let lca = lca.ok_or_else(StablecoinExchangeError::pair_does_not_exist)?;
1522
1523 let mut trade_path = Vec::new();
1525
1526 for token in &path_in {
1528 trade_path.push(*token);
1529 if *token == lca {
1530 break;
1531 }
1532 }
1533
1534 let lca_to_out: Vec<Address> = path_out
1536 .iter()
1537 .take_while(|&&t| t != lca)
1538 .copied()
1539 .collect();
1540
1541 trade_path.extend(lca_to_out.iter().rev());
1543
1544 self.validate_and_build_route(&trade_path)
1545 }
1546
1547 fn validate_and_build_route(&mut self, path: &[Address]) -> Result<Vec<(B256, bool)>> {
1549 let mut route = Vec::new();
1550
1551 for i in 0..path.len() - 1 {
1552 let hop_token_in = path[i];
1553 let hop_token_out = path[i + 1];
1554
1555 let book_key = compute_book_key(hop_token_in, hop_token_out);
1556 let orderbook = self.sload_books(book_key)?;
1557
1558 if orderbook.base.is_zero() {
1560 return Err(StablecoinExchangeError::pair_does_not_exist().into());
1561 }
1562
1563 let base_for_quote = hop_token_in == orderbook.base;
1565
1566 route.push((book_key, base_for_quote));
1567 }
1568
1569 Ok(route)
1570 }
1571
1572 fn find_path_to_root(&mut self, mut token: Address) -> Result<Vec<Address>> {
1575 let mut path = vec![token];
1576
1577 while token != PATH_USD_ADDRESS {
1578 token = TIP20Token::from_address(token, self.storage)?.quote_token()?;
1579 path.push(token);
1580 }
1581
1582 Ok(path)
1583 }
1584
1585 fn quote_exact_in(&mut self, book_key: B256, amount_in: u128, is_bid: bool) -> Result<u128> {
1587 let mut remaining_in = amount_in;
1588 let mut amount_out = 0u128;
1589 let orderbook = self.sload_books(book_key)?;
1590
1591 let mut current_tick = if is_bid {
1592 orderbook.best_bid_tick
1593 } else {
1594 orderbook.best_ask_tick
1595 };
1596
1597 if current_tick == i16::MIN
1599 || self.storage.spec().is_allegretto() && current_tick == i16::MAX
1600 {
1601 return Err(StablecoinExchangeError::insufficient_liquidity().into());
1602 }
1603
1604 while remaining_in > 0 {
1605 let level = Orderbook::read_tick_level(self, book_key, is_bid, current_tick)?;
1606
1607 if level.total_liquidity == 0 {
1609 let (next_tick, initialized) = Orderbook::next_initialized_tick(
1610 self,
1611 book_key,
1612 is_bid,
1613 current_tick,
1614 self.storage.spec(),
1615 );
1616
1617 if !initialized {
1618 return Err(StablecoinExchangeError::insufficient_liquidity().into());
1619 }
1620 current_tick = next_tick;
1621 continue;
1622 }
1623
1624 let price = orderbook::tick_to_price(current_tick);
1625
1626 let (fill_amount, amount_out_tick, amount_consumed) =
1628 if self.storage.spec().is_allegretto() {
1629 if is_bid {
1631 let fill = remaining_in.min(level.total_liquidity);
1633 let quote_out = fill
1634 .checked_mul(price as u128)
1635 .ok_or(TempoPrecompileError::under_overflow())?
1636 / orderbook::PRICE_SCALE as u128;
1637 (fill, quote_out, fill)
1638 } else {
1639 let base_to_get = remaining_in
1641 .checked_mul(orderbook::PRICE_SCALE as u128)
1642 .and_then(|v| v.checked_div(price as u128))
1643 .ok_or(TempoPrecompileError::under_overflow())?;
1644 let fill = base_to_get.min(level.total_liquidity);
1645 let quote_consumed = fill
1646 .checked_mul(price as u128)
1647 .ok_or(TempoPrecompileError::under_overflow())?
1648 / orderbook::PRICE_SCALE as u128;
1649 (fill, fill, quote_consumed)
1650 }
1651 } else {
1652 let fill = remaining_in.min(level.total_liquidity);
1654 let amount_out_tick = fill
1655 .checked_mul(price as u128)
1656 .ok_or(TempoPrecompileError::under_overflow())?
1657 / orderbook::PRICE_SCALE as u128;
1658 (fill, amount_out_tick, fill)
1659 };
1660
1661 remaining_in = remaining_in
1662 .checked_sub(amount_consumed)
1663 .ok_or(TempoPrecompileError::under_overflow())?;
1664 amount_out = amount_out
1665 .checked_add(amount_out_tick)
1666 .ok_or(TempoPrecompileError::under_overflow())?;
1667
1668 if fill_amount == level.total_liquidity {
1670 let (next_tick, initialized) = Orderbook::next_initialized_tick(
1671 self,
1672 book_key,
1673 is_bid,
1674 current_tick,
1675 self.storage.spec(),
1676 );
1677
1678 if !initialized && remaining_in > 0 {
1679 return Err(StablecoinExchangeError::insufficient_liquidity().into());
1680 }
1681 current_tick = next_tick;
1682 } else {
1683 break;
1684 }
1685 }
1686
1687 Ok(amount_out)
1688 }
1689}
1690
1691#[cfg(test)]
1692mod tests {
1693 use tempo_chainspec::hardfork::TempoHardfork;
1694 use tempo_contracts::precompiles::TIP20Error;
1695
1696 use crate::{
1697 error::TempoPrecompileError,
1698 path_usd::TRANSFER_ROLE,
1699 storage::{ContractStorage, hashmap::HashMapStorageProvider},
1700 tip20::ISSUER_ROLE,
1701 };
1702
1703 use super::*;
1704
1705 fn mint_and_approve_token<S: PrecompileStorageProvider>(
1706 storage: &mut S,
1707 token_id: u64,
1708 admin: Address,
1709 user: Address,
1710 exchange_address: Address,
1711 amount: u128,
1712 ) {
1713 let mut token = TIP20Token::new(token_id, storage);
1714 token
1715 .mint(
1716 admin,
1717 ITIP20::mintCall {
1718 to: user,
1719 amount: U256::from(amount),
1720 },
1721 )
1722 .expect("Base mint failed");
1723 token
1724 .approve(
1725 user,
1726 ITIP20::approveCall {
1727 spender: exchange_address,
1728 amount: U256::from(amount),
1729 },
1730 )
1731 .expect("Base approve failed");
1732 }
1733
1734 fn mint_and_approve_quote<S: PrecompileStorageProvider>(
1735 storage: &mut S,
1736 admin: Address,
1737 user: Address,
1738 exchange_address: Address,
1739 amount: u128,
1740 ) {
1741 mint_and_approve_token(storage, 0, admin, user, exchange_address, amount);
1742 let mut quote = PathUSD::new(storage);
1743 quote
1744 .token
1745 .grant_role_internal(user, *TRANSFER_ROLE)
1746 .unwrap();
1747 }
1748
1749 fn setup_test_tokens<S: PrecompileStorageProvider>(
1750 storage: &mut S,
1751 admin: Address,
1752 user: Address,
1753 exchange_address: Address,
1754 amount: u128,
1755 ) -> (Address, Address) {
1756 let mut quote = PathUSD::new(storage);
1758 quote
1759 .initialize(admin)
1760 .expect("Quote token initialization failed");
1761 let quote_address = quote.token.address();
1762
1763 quote
1765 .token
1766 .grant_role_internal(admin, *ISSUER_ROLE)
1767 .unwrap();
1768
1769 let mut base = TIP20Token::new(1, quote.token.storage());
1771 base.initialize("BASE", "BASE", "USD", quote_address, admin, Address::ZERO)
1772 .expect("Base token initialization failed");
1773 base.grant_role_internal(admin, *ISSUER_ROLE).unwrap();
1774 let base_address = base.address();
1775
1776 mint_and_approve_quote(storage, admin, user, exchange_address, amount);
1778 mint_and_approve_token(storage, 1, admin, user, exchange_address, amount);
1779
1780 (base_address, quote_address)
1781 }
1782
1783 #[test]
1784 fn test_tick_to_price() {
1785 let test_ticks = [-2000i16, -1000, -100, -1, 0, 1, 100, 1000, 2000];
1786 for tick in test_ticks {
1787 let price = orderbook::tick_to_price(tick);
1788 let expected_price = (orderbook::PRICE_SCALE as i32 + tick as i32) as u32;
1789 assert_eq!(price, expected_price);
1790 }
1791 }
1792
1793 #[test]
1794 fn test_price_to_tick() {
1795 let test_prices = [
1796 98000u32, 99000, 99900, 99999, 100000, 100001, 100100, 101000, 102000,
1797 ];
1798
1799 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1800 let exchange = StablecoinExchange::new(&mut storage);
1801
1802 for price in test_prices {
1803 let tick = exchange.price_to_tick(price).unwrap();
1804 let expected_tick = (price as i32 - orderbook::PRICE_SCALE as i32) as i16;
1805 assert_eq!(tick, expected_tick);
1806 }
1807 }
1808
1809 #[test]
1810 fn test_price_to_tick_post_moderato() -> eyre::Result<()> {
1811 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
1813 let exchange = StablecoinExchange::new(&mut storage);
1814
1815 assert_eq!(exchange.price_to_tick(orderbook::PRICE_SCALE)?, 0);
1817 assert_eq!(
1818 exchange.price_to_tick(orderbook::MIN_PRICE_POST_MODERATO)?,
1819 MIN_TICK
1820 );
1821 assert_eq!(
1822 exchange.price_to_tick(orderbook::MAX_PRICE_POST_MODERATO)?,
1823 MAX_TICK
1824 );
1825
1826 let result = exchange.price_to_tick(orderbook::MIN_PRICE_POST_MODERATO - 1);
1828 assert!(result.is_err());
1829 assert!(matches!(
1830 result.unwrap_err(),
1831 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::TickOutOfBounds(_))
1832 ));
1833
1834 let result = exchange.price_to_tick(orderbook::MAX_PRICE_POST_MODERATO + 1);
1835 assert!(result.is_err());
1836 assert!(matches!(
1837 result.unwrap_err(),
1838 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::TickOutOfBounds(_))
1839 ));
1840
1841 Ok(())
1842 }
1843
1844 #[test]
1845 fn test_price_to_tick_pre_moderato() -> eyre::Result<()> {
1846 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1848 let exchange = StablecoinExchange::new(&mut storage);
1849
1850 assert_eq!(exchange.price_to_tick(orderbook::PRICE_SCALE)?, 0);
1852 assert_eq!(
1853 exchange.price_to_tick(orderbook::MIN_PRICE_PRE_MODERATO)?,
1854 i16::MIN
1855 );
1856 assert_eq!(
1857 exchange.price_to_tick(orderbook::MAX_PRICE_PRE_MODERATO)?,
1858 i16::MAX
1859 );
1860
1861 let tick = exchange.price_to_tick(orderbook::MIN_PRICE_PRE_MODERATO - 1)?;
1863 assert_eq!(
1864 tick,
1865 ((orderbook::MIN_PRICE_PRE_MODERATO - 1) as i32 - orderbook::PRICE_SCALE as i32) as i16
1866 );
1867
1868 let tick = exchange.price_to_tick(orderbook::MAX_PRICE_PRE_MODERATO + 1)?;
1869 assert_eq!(
1870 tick,
1871 ((orderbook::MAX_PRICE_PRE_MODERATO + 1) as i32 - orderbook::PRICE_SCALE as i32) as i16
1872 );
1873
1874 Ok(())
1875 }
1876
1877 #[test]
1878 fn test_calculate_quote_amount_floor() {
1879 let amount = 100u128;
1884 let tick = 1i16;
1885 let result = calculate_quote_amount_floor(amount, tick).unwrap();
1886
1887 assert_eq!(result, 100, "Expected 100 (rounded down from 100.001)");
1888
1889 let amount2 = 999u128;
1891 let tick2 = 5i16; let result2 = calculate_quote_amount_floor(amount2, tick2).unwrap();
1893 assert_eq!(result2, 999, "Expected 999 (rounded down from 999.04995)");
1895
1896 let amount3 = 100000u128;
1898 let tick3 = 0i16; let result3 = calculate_quote_amount_floor(amount3, tick3).unwrap();
1900 assert_eq!(result3, 100000, "Exact division should remain exact");
1902 }
1903
1904 #[test]
1905 fn test_calculate_quote_amount_ceil() {
1906 let amount = 100u128;
1911 let tick = 1i16;
1912 let result = calculate_quote_amount_ceil(amount, tick).unwrap();
1913
1914 assert_eq!(result, 101, "Expected 101 (rounded up from 100.001)");
1915
1916 let amount2 = 999u128;
1918 let tick2 = 5i16; let result2 = calculate_quote_amount_ceil(amount2, tick2).unwrap();
1920 assert_eq!(result2, 1000, "Expected 1000 (rounded up from 999.04995)");
1922
1923 let amount3 = 100000u128;
1925 let tick3 = 0i16; let result3 = calculate_quote_amount_ceil(amount3, tick3).unwrap();
1927 assert_eq!(result3, 100000, "Exact division should remain exact");
1929 }
1930
1931 #[test]
1932 fn test_place_order_pair_does_not_exist_post_moderato() -> eyre::Result<()> {
1933 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
1935 let mut exchange = StablecoinExchange::new(&mut storage);
1936 exchange.initialize()?;
1937
1938 let alice = Address::random();
1939 let admin = Address::random();
1940 let min_order_amount = MIN_ORDER_AMOUNT;
1941 let tick = 100i16;
1942
1943 let price = orderbook::tick_to_price(tick);
1944 let expected_escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
1945
1946 let (base_token, _quote_token) = setup_test_tokens(
1947 exchange.storage,
1948 admin,
1949 alice,
1950 exchange.address,
1951 expected_escrow,
1952 );
1953
1954 let result = exchange.place(alice, base_token, min_order_amount, true, tick);
1955 assert_eq!(
1956 result,
1957 Err(StablecoinExchangeError::pair_does_not_exist().into())
1958 );
1959
1960 Ok(())
1961 }
1962
1963 #[test]
1964 fn test_place_order_pair_does_not_exist_pre_moderato() -> eyre::Result<()> {
1965 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1967 let mut exchange = StablecoinExchange::new(&mut storage);
1968 exchange.initialize()?;
1969
1970 let alice = Address::random();
1971 let admin = Address::random();
1972 let min_order_amount = MIN_ORDER_AMOUNT;
1973 let tick = 100i16;
1974
1975 let price = orderbook::tick_to_price(tick);
1976 let expected_escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
1977
1978 let (base_token, _quote_token) = setup_test_tokens(
1979 exchange.storage,
1980 admin,
1981 alice,
1982 exchange.address,
1983 expected_escrow,
1984 );
1985
1986 let result = exchange.place(alice, base_token, min_order_amount, true, tick);
1989
1990 assert_eq!(
1992 result,
1993 Err(StablecoinExchangeError::pair_does_not_exist().into())
1994 );
1995
1996 Ok(())
1997 }
1998
1999 #[test]
2000 fn test_place_order_below_minimum_amount() -> eyre::Result<()> {
2001 let mut storage = HashMapStorageProvider::new(1);
2002 let mut exchange = StablecoinExchange::new(&mut storage);
2003 exchange.initialize()?;
2004
2005 let alice = Address::random();
2006 let admin = Address::random();
2007 let min_order_amount = MIN_ORDER_AMOUNT;
2008 let below_minimum = min_order_amount - 1;
2009 let tick = 100i16;
2010
2011 let price = orderbook::tick_to_price(tick);
2012 let escrow_amount = (below_minimum * price as u128) / orderbook::PRICE_SCALE as u128;
2013
2014 let (base_token, _quote_token) = setup_test_tokens(
2015 exchange.storage,
2016 admin,
2017 alice,
2018 exchange.address,
2019 escrow_amount,
2020 );
2021
2022 exchange
2024 .create_pair(base_token)
2025 .expect("Could not create pair");
2026
2027 let result = exchange.place(alice, base_token, below_minimum, true, tick);
2029 assert_eq!(
2030 result,
2031 Err(StablecoinExchangeError::below_minimum_order_size(below_minimum).into())
2032 );
2033
2034 Ok(())
2035 }
2036
2037 #[test]
2038 fn test_place_bid_order() -> eyre::Result<()> {
2039 let mut storage = HashMapStorageProvider::new(1);
2040 let mut exchange = StablecoinExchange::new(&mut storage);
2041 exchange.initialize()?;
2042
2043 let alice = Address::random();
2044 let admin = Address::random();
2045 let min_order_amount = MIN_ORDER_AMOUNT;
2046 let tick = 100i16;
2047
2048 let price = orderbook::tick_to_price(tick);
2049 let expected_escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2050
2051 let (base_token, quote_token) = setup_test_tokens(
2053 exchange.storage,
2054 admin,
2055 alice,
2056 exchange.address,
2057 expected_escrow,
2058 );
2059
2060 exchange
2062 .create_pair(base_token)
2063 .expect("Could not create pair");
2064
2065 let order_id = exchange
2067 .place(alice, base_token, min_order_amount, true, tick)
2068 .expect("Place bid order should succeed");
2069
2070 assert_eq!(order_id, 1);
2071 assert_eq!(exchange.active_order_id()?, 0);
2072 assert_eq!(exchange.pending_order_id()?, 1);
2073
2074 let stored_order = exchange.sload_orders(order_id)?;
2076 assert_eq!(stored_order.maker(), alice);
2077 assert_eq!(stored_order.amount(), min_order_amount);
2078 assert_eq!(stored_order.remaining(), min_order_amount);
2079 assert_eq!(stored_order.tick(), tick);
2080 assert!(stored_order.is_bid());
2081 assert!(!stored_order.is_flip());
2082 assert_eq!(stored_order.prev(), 0);
2083 assert_eq!(stored_order.next(), 0);
2084
2085 let book_key = compute_book_key(base_token, quote_token);
2087 let level = Orderbook::read_tick_level(&mut exchange, book_key, true, tick)?;
2088 assert_eq!(level.head, 0);
2089 assert_eq!(level.tail, 0);
2090 assert_eq!(level.total_liquidity, 0);
2091
2092 {
2094 let mut quote_tip20 = TIP20Token::from_address(quote_token, exchange.storage).unwrap();
2095 let remaining_balance =
2096 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?;
2097 assert_eq!(remaining_balance, U256::ZERO);
2098
2099 let exchange_balance = quote_tip20.balance_of(ITIP20::balanceOfCall {
2101 account: exchange.address,
2102 })?;
2103 assert_eq!(exchange_balance, U256::from(expected_escrow));
2104 }
2105
2106 Ok(())
2107 }
2108
2109 #[test]
2110 fn test_place_ask_order() -> eyre::Result<()> {
2111 let mut storage = HashMapStorageProvider::new(1);
2112 let mut exchange = StablecoinExchange::new(&mut storage);
2113 exchange.initialize().expect("Could not init exchange");
2114
2115 let alice = Address::random();
2116 let admin = Address::random();
2117 let min_order_amount = MIN_ORDER_AMOUNT;
2118 let tick = 50i16; let (base_token, quote_token) = setup_test_tokens(
2122 exchange.storage,
2123 admin,
2124 alice,
2125 exchange.address,
2126 min_order_amount,
2127 );
2128 exchange
2130 .create_pair(base_token)
2131 .expect("Could not create pair");
2132
2133 let order_id = exchange
2134 .place(alice, base_token, min_order_amount, false, tick) .expect("Place ask order should succeed");
2136
2137 assert_eq!(order_id, 1);
2138 assert_eq!(exchange.active_order_id()?, 0);
2139 assert_eq!(exchange.pending_order_id()?, 1);
2140
2141 let stored_order = exchange.sload_orders(order_id)?;
2143 assert_eq!(stored_order.maker(), alice);
2144 assert_eq!(stored_order.amount(), min_order_amount);
2145 assert_eq!(stored_order.remaining(), min_order_amount);
2146 assert_eq!(stored_order.tick(), tick);
2147 assert!(!stored_order.is_bid());
2148 assert!(!stored_order.is_flip());
2149 assert_eq!(stored_order.prev(), 0);
2150 assert_eq!(stored_order.next(), 0);
2151
2152 let book_key = compute_book_key(base_token, quote_token);
2153 let level = Orderbook::read_tick_level(&mut exchange, book_key, false, tick)?;
2154 assert_eq!(level.head, 0);
2155 assert_eq!(level.tail, 0);
2156 assert_eq!(level.total_liquidity, 0);
2157
2158 {
2160 let mut base_tip20 = TIP20Token::from_address(base_token, exchange.storage).unwrap();
2161 let remaining_balance =
2162 base_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?;
2163 assert_eq!(remaining_balance, U256::ZERO); let exchange_balance = base_tip20.balance_of(ITIP20::balanceOfCall {
2167 account: exchange.address,
2168 })?;
2169 assert_eq!(exchange_balance, U256::from(min_order_amount));
2170 }
2171
2172 Ok(())
2173 }
2174
2175 #[test]
2176 fn test_place_flip_order_below_minimum_amount() -> eyre::Result<()> {
2177 let mut storage = HashMapStorageProvider::new(1);
2178 let mut exchange = StablecoinExchange::new(&mut storage);
2179 exchange.initialize()?;
2180
2181 let alice = Address::random();
2182 let admin = Address::random();
2183 let min_order_amount = MIN_ORDER_AMOUNT;
2184 let below_minimum = min_order_amount - 1;
2185 let tick = 100i16;
2186 let flip_tick = 200i16;
2187
2188 let price = orderbook::tick_to_price(tick);
2189 let escrow_amount = (below_minimum * price as u128) / orderbook::PRICE_SCALE as u128;
2190
2191 let (base_token, _quote_token) = setup_test_tokens(
2192 exchange.storage,
2193 admin,
2194 alice,
2195 exchange.address,
2196 escrow_amount,
2197 );
2198
2199 exchange
2201 .create_pair(base_token)
2202 .expect("Could not create pair");
2203
2204 let result = exchange.place_flip(alice, base_token, below_minimum, true, tick, flip_tick);
2206 assert_eq!(
2207 result,
2208 Err(StablecoinExchangeError::below_minimum_order_size(below_minimum).into())
2209 );
2210
2211 Ok(())
2212 }
2213
2214 #[test]
2215 fn test_place_flip_order_pair_does_not_exist_post_moderato() -> eyre::Result<()> {
2216 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
2218 let mut exchange = StablecoinExchange::new(&mut storage);
2219 exchange.initialize()?;
2220
2221 let alice = Address::random();
2222 let admin = Address::random();
2223 let min_order_amount = MIN_ORDER_AMOUNT;
2224 let tick = 100i16;
2225 let flip_tick = 200i16;
2226
2227 let price = orderbook::tick_to_price(tick);
2228 let expected_escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2229
2230 let (base_token, _quote_token) = setup_test_tokens(
2231 exchange.storage,
2232 admin,
2233 alice,
2234 exchange.address,
2235 expected_escrow,
2236 );
2237
2238 let result =
2240 exchange.place_flip(alice, base_token, min_order_amount, true, tick, flip_tick);
2241 assert_eq!(
2242 result,
2243 Err(StablecoinExchangeError::pair_does_not_exist().into())
2244 );
2245
2246 Ok(())
2247 }
2248
2249 #[test]
2250 fn test_place_flip_order_pair_does_not_exist_pre_moderato() -> eyre::Result<()> {
2251 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
2253 let mut exchange = StablecoinExchange::new(&mut storage);
2254 exchange.initialize()?;
2255
2256 let alice = Address::random();
2257 let admin = Address::random();
2258 let min_order_amount = MIN_ORDER_AMOUNT;
2259 let tick = 100i16;
2260 let flip_tick = 200i16;
2261
2262 let price = orderbook::tick_to_price(tick);
2263 let expected_escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2264
2265 let (base_token, _quote_token) = setup_test_tokens(
2266 exchange.storage,
2267 admin,
2268 alice,
2269 exchange.address,
2270 expected_escrow,
2271 );
2272
2273 let result =
2277 exchange.place_flip(alice, base_token, min_order_amount, true, tick, flip_tick);
2278
2279 assert!(result.is_ok());
2281
2282 Ok(())
2283 }
2284
2285 #[test]
2286 fn test_place_flip_order() -> eyre::Result<()> {
2287 let mut storage = HashMapStorageProvider::new(1);
2288 let mut exchange = StablecoinExchange::new(&mut storage);
2289 exchange.initialize().expect("Could not init exchange");
2290
2291 let alice = Address::random();
2292 let admin = Address::random();
2293 let min_order_amount = MIN_ORDER_AMOUNT;
2294 let tick = 100i16;
2295 let flip_tick = 200i16; let price = orderbook::tick_to_price(tick);
2299 let expected_escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2300
2301 let (base_token, quote_token) = setup_test_tokens(
2303 exchange.storage,
2304 admin,
2305 alice,
2306 exchange.address,
2307 expected_escrow,
2308 );
2309 exchange
2310 .create_pair(base_token)
2311 .expect("Could not create pair");
2312
2313 let order_id = exchange
2314 .place_flip(alice, base_token, min_order_amount, true, tick, flip_tick)
2315 .expect("Place flip bid order should succeed");
2316
2317 assert_eq!(order_id, 1);
2318 assert_eq!(exchange.active_order_id()?, 0);
2319 assert_eq!(exchange.pending_order_id()?, 1);
2320
2321 let stored_order = exchange.sload_orders(order_id)?;
2323 assert_eq!(stored_order.maker(), alice);
2324 assert_eq!(stored_order.amount(), min_order_amount);
2325 assert_eq!(stored_order.remaining(), min_order_amount);
2326 assert_eq!(stored_order.tick(), tick);
2327 assert!(stored_order.is_bid());
2328 assert!(stored_order.is_flip());
2329 assert_eq!(stored_order.flip_tick(), flip_tick);
2330 assert_eq!(stored_order.prev(), 0);
2331 assert_eq!(stored_order.next(), 0);
2332
2333 let book_key = compute_book_key(base_token, quote_token);
2335 let level = Orderbook::read_tick_level(&mut exchange, book_key, true, tick)?;
2336 assert_eq!(level.head, 0);
2337 assert_eq!(level.tail, 0);
2338 assert_eq!(level.total_liquidity, 0);
2339
2340 {
2342 let mut quote_tip20 = TIP20Token::from_address(quote_token, exchange.storage).unwrap();
2343 let remaining_balance =
2344 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?;
2345 assert_eq!(remaining_balance, U256::ZERO);
2346
2347 let exchange_balance = quote_tip20.balance_of(ITIP20::balanceOfCall {
2349 account: exchange.address,
2350 })?;
2351 assert_eq!(exchange_balance, U256::from(expected_escrow));
2352 }
2353
2354 Ok(())
2355 }
2356
2357 #[test]
2358 fn test_cancel_pending_order() -> eyre::Result<()> {
2359 let mut storage = HashMapStorageProvider::new(1);
2360 let mut exchange = StablecoinExchange::new(&mut storage);
2361 exchange.initialize().expect("Could not init exchange");
2362
2363 let alice = Address::random();
2364 let admin = Address::random();
2365 let min_order_amount = MIN_ORDER_AMOUNT;
2366 let tick = 100i16;
2367
2368 let price = orderbook::tick_to_price(tick);
2370 let expected_escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2371
2372 let (base_token, quote_token) = setup_test_tokens(
2374 exchange.storage,
2375 admin,
2376 alice,
2377 exchange.address,
2378 expected_escrow,
2379 );
2380
2381 exchange
2382 .create_pair(base_token)
2383 .expect("Could not create pair");
2384
2385 let order_id = exchange
2387 .place(alice, base_token, min_order_amount, true, tick)
2388 .expect("Place bid order should succeed");
2389
2390 assert_eq!(exchange.balance_of(alice, quote_token)?, 0);
2392
2393 let (alice_balance_before, exchange_balance_before) = {
2394 let mut quote_tip20 = TIP20Token::from_address(quote_token, exchange.storage).unwrap();
2395
2396 (
2397 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?,
2398 quote_tip20.balance_of(ITIP20::balanceOfCall {
2399 account: exchange.address,
2400 })?,
2401 )
2402 };
2403
2404 assert_eq!(alice_balance_before, U256::ZERO);
2405 assert_eq!(exchange_balance_before, U256::from(expected_escrow));
2406
2407 exchange
2409 .cancel(alice, order_id)
2410 .expect("Cancel pending order should succeed");
2411
2412 let cancelled_order = exchange.sload_orders(order_id)?;
2414 assert_eq!(cancelled_order.maker(), Address::ZERO);
2415
2416 assert_eq!(exchange.balance_of(alice, quote_token)?, expected_escrow);
2418
2419 Ok(())
2420 }
2421
2422 #[test]
2423 fn test_execute_block() -> eyre::Result<()> {
2424 let mut storage = HashMapStorageProvider::new(1);
2425 let mut exchange = StablecoinExchange::new(&mut storage);
2426 exchange.initialize().expect("Could not init exchange");
2427
2428 let alice = Address::random();
2429 let admin = Address::random();
2430 let min_order_amount = MIN_ORDER_AMOUNT;
2431 let tick = 100i16;
2432
2433 let price = orderbook::tick_to_price(tick);
2435 let expected_escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2436
2437 let (base_token, quote_token) = setup_test_tokens(
2439 exchange.storage,
2440 admin,
2441 alice,
2442 exchange.address,
2443 expected_escrow * 2,
2444 );
2445
2446 exchange
2448 .create_pair(base_token)
2449 .expect("Could not create pair");
2450
2451 let order_id_0 = exchange
2452 .place(alice, base_token, min_order_amount, true, tick)
2453 .expect("Swap should succeed");
2454
2455 let order_id_1 = exchange
2456 .place(alice, base_token, min_order_amount, true, tick)
2457 .expect("Swap should succeed");
2458 assert_eq!(order_id_0, 1);
2459 assert_eq!(order_id_1, 2);
2460 assert_eq!(exchange.active_order_id()?, 0);
2461 assert_eq!(exchange.pending_order_id()?, 2);
2462
2463 let order_0 = exchange.sload_orders(order_id_0)?;
2465 let order_1 = exchange.sload_orders(order_id_1)?;
2466 assert_eq!(order_0.prev(), 0);
2467 assert_eq!(order_0.next(), 0);
2468 assert_eq!(order_1.prev(), 0);
2469 assert_eq!(order_1.next(), 0);
2470
2471 let book_key = compute_book_key(base_token, quote_token);
2473 let level_before = Orderbook::read_tick_level(&mut exchange, book_key, true, tick)?;
2474 assert_eq!(level_before.head, 0);
2475 assert_eq!(level_before.tail, 0);
2476 assert_eq!(level_before.total_liquidity, 0);
2477
2478 exchange
2480 .execute_block(Address::ZERO)
2481 .expect("Execute block should succeed");
2482
2483 assert_eq!(exchange.active_order_id()?, 2);
2484 assert_eq!(exchange.pending_order_id()?, 2);
2485
2486 let order_0 = exchange.sload_orders(order_id_0)?;
2487 let order_1 = exchange.sload_orders(order_id_1)?;
2488 assert_eq!(order_0.prev(), 0);
2489 assert_eq!(order_0.next(), order_1.order_id());
2490 assert_eq!(order_1.prev(), order_0.order_id());
2491 assert_eq!(order_1.next(), 0);
2492
2493 let level_after = Orderbook::read_tick_level(&mut exchange, book_key, true, tick)?;
2495 assert_eq!(level_after.head, order_0.order_id());
2496 assert_eq!(level_after.tail, order_1.order_id());
2497 assert_eq!(level_after.total_liquidity, min_order_amount * 2);
2498
2499 let orderbook = exchange.sload_books(book_key)?;
2501 assert_eq!(orderbook.best_bid_tick, tick);
2502
2503 Ok(())
2504 }
2505
2506 #[test]
2507 fn test_execute_block_unauthorized() {
2508 let mut storage = HashMapStorageProvider::new(1);
2509 let mut exchange = StablecoinExchange::new(&mut storage);
2510 exchange.initialize().expect("Could not init exchange");
2511
2512 let result = exchange.execute_block(Address::random());
2513 assert_eq!(result, Err(StablecoinExchangeError::unauthorized().into()));
2514 }
2515
2516 #[test]
2517 fn test_withdraw() -> eyre::Result<()> {
2518 let mut storage = HashMapStorageProvider::new(1);
2519 let mut exchange = StablecoinExchange::new(&mut storage);
2520 exchange.initialize().expect("Could not init exchange");
2521
2522 let alice = Address::random();
2523 let admin = Address::random();
2524 let min_order_amount = MIN_ORDER_AMOUNT;
2525 let tick = 100i16;
2526 let price = orderbook::tick_to_price(tick);
2527 let expected_escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2528
2529 let (base_token, quote_token) = setup_test_tokens(
2531 exchange.storage,
2532 admin,
2533 alice,
2534 exchange.address,
2535 expected_escrow,
2536 );
2537 exchange
2538 .create_pair(base_token)
2539 .expect("Could not create pair");
2540
2541 let order_id = exchange
2543 .place(alice, base_token, min_order_amount, true, tick)
2544 .expect("Place bid order should succeed");
2545
2546 exchange
2547 .cancel(alice, order_id)
2548 .expect("Cancel pending order should succeed");
2549
2550 assert_eq!(exchange.balance_of(alice, quote_token)?, expected_escrow);
2551
2552 exchange
2554 .withdraw(alice, quote_token, expected_escrow)
2555 .expect("Withdraw should succeed");
2556 assert_eq!(exchange.balance_of(alice, quote_token)?, 0);
2557
2558 let mut quote_tip20 = TIP20Token::from_address(quote_token, exchange.storage).unwrap();
2560
2561 assert_eq!(
2562 quote_tip20.balance_of(ITIP20::balanceOfCall { account: alice })?,
2563 expected_escrow
2564 );
2565 assert_eq!(
2566 quote_tip20.balance_of(ITIP20::balanceOfCall {
2567 account: exchange.address
2568 })?,
2569 0
2570 );
2571
2572 Ok(())
2573 }
2574
2575 #[test]
2576 fn test_withdraw_insufficient_balance() -> eyre::Result<()> {
2577 let mut storage = HashMapStorageProvider::new(1);
2578 let mut exchange = StablecoinExchange::new(&mut storage);
2579 exchange.initialize().expect("Could not init exchange");
2580
2581 let alice = Address::random();
2582 let admin = Address::random();
2583
2584 let min_order_amount = MIN_ORDER_AMOUNT;
2585 let (_base_token, quote_token) = setup_test_tokens(
2586 exchange.storage,
2587 admin,
2588 alice,
2589 exchange.address,
2590 min_order_amount,
2591 );
2592
2593 assert_eq!(exchange.balance_of(alice, quote_token)?, 0);
2595
2596 let result = exchange.withdraw(alice, quote_token, 100u128);
2598
2599 assert_eq!(
2600 result,
2601 Err(StablecoinExchangeError::insufficient_balance().into())
2602 );
2603
2604 Ok(())
2605 }
2606
2607 #[test]
2608 fn test_quote_swap_exact_amount_out() {
2609 let mut storage = HashMapStorageProvider::new(1);
2610 let mut exchange = StablecoinExchange::new(&mut storage);
2611 exchange.initialize().expect("Could not init exchange");
2612
2613 let alice = Address::random();
2614 let admin = Address::random();
2615 let min_order_amount = MIN_ORDER_AMOUNT;
2616 let amount_out = 500_000u128;
2617 let tick = 10;
2618
2619 let (base_token, quote_token) = setup_test_tokens(
2620 exchange.storage,
2621 admin,
2622 alice,
2623 exchange.address,
2624 200_000_000u128,
2625 );
2626 exchange
2627 .create_pair(base_token)
2628 .expect("Could not create pair");
2629
2630 let order_amount = min_order_amount;
2631 exchange
2632 .place(alice, base_token, order_amount, false, tick)
2633 .expect("Order should succeed");
2634
2635 exchange
2636 .execute_block(Address::ZERO)
2637 .expect("Execute block should succeed");
2638
2639 let amount_in = exchange
2640 .quote_swap_exact_amount_out(quote_token, base_token, amount_out)
2641 .expect("Swap should succeed");
2642
2643 let price = orderbook::tick_to_price(tick);
2644 let expected_amount_in = (amount_out * price as u128) / orderbook::PRICE_SCALE as u128;
2645 assert_eq!(amount_in, expected_amount_in);
2646 }
2647
2648 #[test]
2649 fn test_quote_swap_exact_amount_in() {
2650 let mut storage = HashMapStorageProvider::new(1);
2651 let mut exchange = StablecoinExchange::new(&mut storage);
2652 exchange.initialize().expect("Could not init exchange");
2653
2654 let alice = Address::random();
2655 let admin = Address::random();
2656 let min_order_amount = MIN_ORDER_AMOUNT;
2657 let amount_in = 500_000u128;
2658 let tick = 10;
2659
2660 let (base_token, quote_token) = setup_test_tokens(
2661 exchange.storage,
2662 admin,
2663 alice,
2664 exchange.address,
2665 200_000_000u128,
2666 );
2667 exchange
2668 .create_pair(base_token)
2669 .expect("Could not create pair");
2670
2671 let order_amount = min_order_amount;
2672 exchange
2673 .place(alice, base_token, order_amount, true, tick)
2674 .expect("Place bid order should succeed");
2675
2676 exchange
2677 .execute_block(Address::ZERO)
2678 .expect("Execute block should succeed");
2679
2680 let amount_out = exchange
2681 .quote_swap_exact_amount_in(base_token, quote_token, amount_in)
2682 .expect("Swap should succeed");
2683
2684 let price = orderbook::tick_to_price(tick);
2686 let expected_amount_out = (amount_in * price as u128) / orderbook::PRICE_SCALE as u128;
2687 assert_eq!(amount_out, expected_amount_out);
2688 }
2689
2690 #[test]
2691 fn test_quote_swap_exact_amount_out_base_for_quote() {
2692 let mut storage = HashMapStorageProvider::new(1);
2693 let mut exchange = StablecoinExchange::new(&mut storage);
2694 exchange.initialize().expect("Could not init exchange");
2695
2696 let alice = Address::random();
2697 let admin = Address::random();
2698 let min_order_amount = MIN_ORDER_AMOUNT;
2699 let amount_out = 500_000u128;
2700 let tick = 0;
2701
2702 let (base_token, quote_token) = setup_test_tokens(
2703 exchange.storage,
2704 admin,
2705 alice,
2706 exchange.address,
2707 200_000_000u128,
2708 );
2709 exchange
2710 .create_pair(base_token)
2711 .expect("Could not create pair");
2712
2713 let order_amount = min_order_amount;
2715 exchange
2716 .place(alice, base_token, order_amount, true, tick)
2717 .expect("Place bid order should succeed");
2718
2719 exchange
2720 .execute_block(Address::ZERO)
2721 .expect("Execute block should succeed");
2722
2723 let amount_in = exchange
2726 .quote_swap_exact_amount_out(base_token, quote_token, amount_out)
2727 .expect("Quote should succeed");
2728
2729 let price = orderbook::tick_to_price(tick);
2730 let expected_amount_in = (amount_out * price as u128) / orderbook::PRICE_SCALE as u128;
2731 assert_eq!(amount_in, expected_amount_in);
2732 }
2733
2734 #[test]
2735 fn test_swap_exact_amount_out() -> eyre::Result<()> {
2736 let mut storage = HashMapStorageProvider::new(1);
2737 let mut exchange = StablecoinExchange::new(&mut storage);
2738 exchange.initialize().expect("Could not init exchange");
2739
2740 let alice = Address::random();
2741 let bob = Address::random();
2742 let admin = Address::random();
2743 let min_order_amount = MIN_ORDER_AMOUNT;
2744 let amount_out = 500_000u128;
2745 let tick = 10;
2746
2747 let (base_token, quote_token) = setup_test_tokens(
2748 exchange.storage,
2749 admin,
2750 alice,
2751 exchange.address,
2752 200_000_000u128,
2753 );
2754 exchange
2755 .create_pair(base_token)
2756 .expect("Could not create pair");
2757
2758 let order_amount = min_order_amount;
2759 exchange
2760 .place(alice, base_token, order_amount, false, tick)
2761 .expect("Order should succeed");
2762
2763 exchange
2764 .execute_block(Address::ZERO)
2765 .expect("Execute block should succeed");
2766
2767 exchange
2768 .set_balance(bob, quote_token, 200_000_000u128)
2769 .expect("Could not set balance");
2770
2771 let price = orderbook::tick_to_price(tick);
2772 let max_amount_in = (amount_out * price as u128) / orderbook::PRICE_SCALE as u128;
2773
2774 let amount_in = exchange
2775 .swap_exact_amount_out(bob, quote_token, base_token, amount_out, max_amount_in)
2776 .expect("Swap should succeed");
2777
2778 let mut base_tip20 = TIP20Token::from_address(base_token, exchange.storage).unwrap();
2779 let bob_base_balance = base_tip20.balance_of(ITIP20::balanceOfCall { account: bob })?;
2780 assert_eq!(bob_base_balance, U256::from(amount_out));
2781
2782 let alice_quote_exchange_balance = exchange.balance_of(alice, quote_token)?;
2783 assert_eq!(alice_quote_exchange_balance, amount_in);
2784
2785 Ok(())
2786 }
2787
2788 #[test]
2789 fn test_swap_exact_amount_in() -> eyre::Result<()> {
2790 let mut storage = HashMapStorageProvider::new(1);
2791 let mut exchange = StablecoinExchange::new(&mut storage);
2792 exchange.initialize().expect("Could not init exchange");
2793
2794 let alice = Address::random();
2795 let bob = Address::random();
2796 let admin = Address::random();
2797 let min_order_amount = MIN_ORDER_AMOUNT;
2798 let amount_in = 500_000u128;
2799 let tick = 10;
2800
2801 let (base_token, quote_token) = setup_test_tokens(
2802 exchange.storage,
2803 admin,
2804 alice,
2805 exchange.address,
2806 200_000_000u128,
2807 );
2808 exchange
2809 .create_pair(base_token)
2810 .expect("Could not create pair");
2811
2812 let order_amount = min_order_amount;
2813 exchange
2814 .place(alice, base_token, order_amount, true, tick)
2815 .expect("Order should succeed");
2816
2817 exchange
2818 .execute_block(Address::ZERO)
2819 .expect("Execute block should succeed");
2820
2821 exchange
2822 .set_balance(bob, base_token, 200_000_000u128)
2823 .expect("Could not set balance");
2824
2825 let price = orderbook::tick_to_price(tick);
2826 let min_amount_out = (amount_in * price as u128) / orderbook::PRICE_SCALE as u128;
2827
2828 let amount_out = exchange
2829 .swap_exact_amount_in(bob, base_token, quote_token, amount_in, min_amount_out)
2830 .expect("Swap should succeed");
2831
2832 let mut quote_tip20 = TIP20Token::from_address(quote_token, exchange.storage).unwrap();
2833 let bob_quote_balance = quote_tip20.balance_of(ITIP20::balanceOfCall { account: bob })?;
2834 assert_eq!(bob_quote_balance, U256::from(amount_out));
2835
2836 let alice_base_exchange_balance = exchange.balance_of(alice, base_token)?;
2837 assert_eq!(alice_base_exchange_balance, amount_in);
2838
2839 Ok(())
2840 }
2841
2842 #[test]
2843 fn test_flip_order_execution() -> eyre::Result<()> {
2844 let mut storage = HashMapStorageProvider::new(1);
2845 let mut exchange = StablecoinExchange::new(&mut storage);
2846 exchange.initialize().expect("Could not init exchange");
2847
2848 let alice = Address::random();
2849 let bob = Address::random();
2850 let admin = Address::random();
2851 let min_order_amount = MIN_ORDER_AMOUNT;
2852 let amount = min_order_amount;
2853 let tick = 100i16;
2854 let flip_tick = 200i16;
2855
2856 let price = orderbook::tick_to_price(tick);
2857 let expected_escrow = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
2858
2859 let (base_token, quote_token) = setup_test_tokens(
2860 exchange.storage,
2861 admin,
2862 alice,
2863 exchange.address,
2864 expected_escrow * 2,
2865 );
2866 exchange
2867 .create_pair(base_token)
2868 .expect("Could not create pair");
2869
2870 let flip_order_id = exchange
2872 .place_flip(alice, base_token, amount, true, tick, flip_tick)
2873 .expect("Place flip order should succeed");
2874
2875 exchange
2876 .execute_block(Address::ZERO)
2877 .expect("Execute block should succeed");
2878
2879 exchange
2880 .set_balance(bob, base_token, amount)
2881 .expect("Could not set balance");
2882
2883 exchange
2884 .swap_exact_amount_in(bob, base_token, quote_token, amount, 0)
2885 .expect("Swap should succeed");
2886
2887 let filled_order = exchange.sload_orders(flip_order_id)?;
2889 assert_eq!(filled_order.maker(), Address::ZERO);
2890
2891 let new_order_id = exchange.pending_order_id()?;
2892 assert_eq!(new_order_id, flip_order_id + 1);
2893
2894 let new_order = exchange.sload_orders(new_order_id)?;
2895 assert_eq!(new_order.maker(), alice);
2896 assert_eq!(new_order.tick(), flip_tick);
2897 assert_eq!(new_order.flip_tick(), tick);
2898 assert!(new_order.is_ask());
2899 assert_eq!(new_order.amount(), amount);
2900 assert_eq!(new_order.remaining(), amount);
2901
2902 Ok(())
2903 }
2904
2905 #[test]
2906 fn test_pair_created() {
2907 let mut storage = HashMapStorageProvider::new(1);
2908 let mut exchange = StablecoinExchange::new(&mut storage);
2909 exchange.initialize().expect("Could not init exchange");
2910
2911 let admin = Address::random();
2912 let alice = Address::random();
2913
2914 let min_order_amount = MIN_ORDER_AMOUNT;
2915 let (base_token, quote_token) = setup_test_tokens(
2917 exchange.storage,
2918 admin,
2919 alice,
2920 exchange.address,
2921 min_order_amount,
2922 );
2923
2924 let key = exchange
2926 .create_pair(base_token)
2927 .expect("Could not create pair");
2928
2929 let events = &exchange.storage.events[&exchange.address];
2931 assert_eq!(events.len(), 1);
2932 assert_eq!(
2933 events[0],
2934 StablecoinExchangeEvents::PairCreated(IStablecoinExchange::PairCreated {
2935 key,
2936 base: base_token,
2937 quote: quote_token,
2938 })
2939 .into_log_data()
2940 );
2941 }
2942
2943 #[test]
2944 fn test_pair_already_created() {
2945 let mut storage = HashMapStorageProvider::new(1);
2946 let mut exchange = StablecoinExchange::new(&mut storage);
2947 exchange.initialize().expect("Could not init exchange");
2948
2949 let admin = Address::random();
2950 let alice = Address::random();
2951
2952 let min_order_amount = MIN_ORDER_AMOUNT;
2953 let (base_token, _) = setup_test_tokens(
2955 exchange.storage,
2956 admin,
2957 alice,
2958 exchange.address,
2959 min_order_amount,
2960 );
2961
2962 exchange
2963 .create_pair(base_token)
2964 .expect("Could not create pair");
2965
2966 let result = exchange.create_pair(base_token);
2967 assert_eq!(
2968 result,
2969 Err(StablecoinExchangeError::pair_already_exists().into())
2970 );
2971 }
2972
2973 fn verify_hop(
2975 storage: &mut impl PrecompileStorageProvider,
2976 exchange_addr: Address,
2977 hop: (B256, bool),
2978 token_in: Address,
2979 token_out: Address,
2980 ) -> eyre::Result<()> {
2981 let (book_key, base_for_quote) = hop;
2982 let expected_book_key = compute_book_key(token_in, token_out);
2983 assert_eq!(book_key, expected_book_key, "Book key should match");
2984
2985 let mut exchange = StablecoinExchange::_new(exchange_addr, storage);
2986 let orderbook = exchange.sload_books(book_key)?;
2987 let expected_direction = token_in == orderbook.base;
2988 assert_eq!(
2989 base_for_quote, expected_direction,
2990 "Direction should be correct: token_in={}, base={}, base_for_quote={}",
2991 token_in, orderbook.base, base_for_quote
2992 );
2993
2994 Ok(())
2995 }
2996
2997 #[test]
2998 fn test_find_path_to_root() -> eyre::Result<()> {
2999 let mut storage = HashMapStorageProvider::new(1);
3000 let mut exchange = StablecoinExchange::new(&mut storage);
3001 exchange.initialize()?;
3002
3003 let admin = Address::random();
3004
3005 let path_usd_addr = {
3007 let mut path_usd = PathUSD::new(exchange.storage);
3008 path_usd
3009 .initialize(admin)
3010 .expect("Failed to initialize PathUSD");
3011 path_usd.token.address()
3012 };
3013
3014 let usdc_addr = {
3015 let mut usdc = TIP20Token::new(2, exchange.storage);
3016 usdc.initialize("USDC", "USDC", "USD", path_usd_addr, admin, Address::ZERO)
3017 .expect("Failed to initialize USDC");
3018 usdc.address()
3019 };
3020
3021 let token_a_addr = {
3022 let mut token_a = TIP20Token::new(3, exchange.storage);
3023 token_a
3024 .initialize("TokenA", "TKA", "USD", usdc_addr, admin, Address::ZERO)
3025 .expect("Failed to initialize TokenA");
3026 token_a.address()
3027 };
3028
3029 let path = exchange
3031 .find_path_to_root(token_a_addr)
3032 .expect("Failed to find path");
3033
3034 assert_eq!(path.len(), 3);
3036 assert_eq!(path[0], token_a_addr);
3037 assert_eq!(path[1], usdc_addr);
3038 assert_eq!(path[2], path_usd_addr);
3039
3040 Ok(())
3041 }
3042
3043 #[test]
3044 fn test_find_trade_path_same_token_errors() -> eyre::Result<()> {
3045 let mut storage = HashMapStorageProvider::new(1);
3046 let mut exchange = StablecoinExchange::new(&mut storage);
3047 exchange.initialize()?;
3048
3049 let admin = Address::random();
3050 let user = Address::random();
3051
3052 let min_order_amount = MIN_ORDER_AMOUNT;
3053 let (token, _) = setup_test_tokens(
3054 exchange.storage,
3055 admin,
3056 user,
3057 exchange.address,
3058 min_order_amount,
3059 );
3060
3061 let result = exchange.find_trade_path(token, token);
3063 assert_eq!(
3064 result,
3065 Err(StablecoinExchangeError::identical_tokens().into()),
3066 "Should return IdenticalTokens error when token_in == token_out"
3067 );
3068
3069 Ok(())
3070 }
3071
3072 #[test]
3073 fn test_find_trade_path_direct_pair() -> eyre::Result<()> {
3074 let mut storage = HashMapStorageProvider::new(1);
3075 let mut exchange = StablecoinExchange::new(&mut storage);
3076 exchange.initialize()?;
3077
3078 let admin = Address::random();
3079 let user = Address::random();
3080
3081 let min_order_amount = MIN_ORDER_AMOUNT;
3082 let (token, path_usd) = setup_test_tokens(
3084 exchange.storage,
3085 admin,
3086 user,
3087 exchange.address,
3088 min_order_amount,
3089 );
3090
3091 exchange.create_pair(token).expect("Failed to create pair");
3093
3094 let route = exchange
3096 .find_trade_path(token, path_usd)
3097 .expect("Should find direct pair");
3098
3099 assert_eq!(route.len(), 1, "Should have 1 hop for direct pair");
3101 verify_hop(
3102 exchange.storage,
3103 exchange.address,
3104 route[0],
3105 token,
3106 path_usd,
3107 )?;
3108
3109 Ok(())
3110 }
3111
3112 #[test]
3113 fn test_find_trade_path_reverse_pair() -> eyre::Result<()> {
3114 let mut storage = HashMapStorageProvider::new(1);
3115 let mut exchange = StablecoinExchange::new(&mut storage);
3116 exchange.initialize()?;
3117
3118 let admin = Address::random();
3119 let user = Address::random();
3120
3121 let min_order_amount = MIN_ORDER_AMOUNT;
3122 let (token, path_usd) = setup_test_tokens(
3124 exchange.storage,
3125 admin,
3126 user,
3127 exchange.address,
3128 min_order_amount,
3129 );
3130
3131 exchange.create_pair(token).expect("Failed to create pair");
3133
3134 let route = exchange
3136 .find_trade_path(path_usd, token)
3137 .expect("Should find reverse pair");
3138
3139 assert_eq!(route.len(), 1, "Should have 1 hop for reverse pair");
3141 verify_hop(
3142 exchange.storage,
3143 exchange.address,
3144 route[0],
3145 path_usd,
3146 token,
3147 )?;
3148
3149 Ok(())
3150 }
3151
3152 #[test]
3153 fn test_find_trade_path_two_hop_siblings() -> eyre::Result<()> {
3154 let mut storage = HashMapStorageProvider::new(1);
3155 let mut exchange = StablecoinExchange::new(&mut storage);
3156 exchange.initialize()?;
3157
3158 let admin = Address::random();
3159
3160 let path_usd_addr = {
3164 let mut path_usd = PathUSD::new(exchange.storage);
3165 path_usd
3166 .initialize(admin)
3167 .expect("Failed to initialize PathUSD");
3168 path_usd.token.address()
3169 };
3170
3171 let usdc_addr = {
3172 let mut usdc = TIP20Token::new(2, exchange.storage);
3173 usdc.initialize("USDC", "USDC", "USD", path_usd_addr, admin, Address::ZERO)
3174 .expect("Failed to initialize USDC");
3175 usdc.address()
3176 };
3177
3178 let eurc_addr = {
3179 let mut eurc = TIP20Token::new(3, exchange.storage);
3180 eurc.initialize("EURC", "EURC", "USD", path_usd_addr, admin, Address::ZERO)
3181 .expect("Failed to initialize EURC");
3182 eurc.address()
3183 };
3184
3185 exchange
3187 .create_pair(usdc_addr)
3188 .expect("Failed to create USDC pair");
3189 exchange
3190 .create_pair(eurc_addr)
3191 .expect("Failed to create EURC pair");
3192
3193 let route = exchange
3195 .find_trade_path(usdc_addr, eurc_addr)
3196 .expect("Should find path");
3197
3198 assert_eq!(route.len(), 2, "Should have 2 hops for sibling tokens");
3200 verify_hop(
3201 exchange.storage,
3202 exchange.address,
3203 route[0],
3204 usdc_addr,
3205 path_usd_addr,
3206 )?;
3207 verify_hop(
3208 exchange.storage,
3209 exchange.address,
3210 route[1],
3211 path_usd_addr,
3212 eurc_addr,
3213 )?;
3214
3215 Ok(())
3216 }
3217
3218 #[test]
3219 fn test_quote_exact_in_multi_hop() -> eyre::Result<()> {
3220 let mut storage = HashMapStorageProvider::new(1);
3221 let mut exchange = StablecoinExchange::new(&mut storage);
3222 exchange.initialize()?;
3223
3224 let admin = Address::random();
3225 let alice = Address::random();
3226 let min_order_amount = MIN_ORDER_AMOUNT;
3227
3228 let path_usd_addr = {
3231 let mut path_usd = PathUSD::new(exchange.storage);
3232 path_usd
3233 .initialize(admin)
3234 .expect("Failed to initialize PathUSD");
3235 path_usd.token.address()
3236 };
3237
3238 let usdc_addr = {
3239 let mut usdc = TIP20Token::new(2, exchange.storage);
3240 usdc.initialize("USDC", "USDC", "USD", path_usd_addr, admin, Address::ZERO)
3241 .expect("Failed to initialize USDC");
3242 usdc.address()
3243 };
3244
3245 let eurc_addr = {
3246 let mut eurc = TIP20Token::new(3, exchange.storage);
3247 eurc.initialize("EURC", "EURC", "USD", path_usd_addr, admin, Address::ZERO)
3248 .expect("Failed to initialize EURC");
3249 eurc.address()
3250 };
3251
3252 exchange
3254 .create_pair(usdc_addr)
3255 .expect("Failed to create USDC pair");
3256 exchange
3257 .create_pair(eurc_addr)
3258 .expect("Failed to create EURC pair");
3259
3260 {
3262 let mut usdc = TIP20Token::new(2, exchange.storage);
3263 usdc.grant_role_internal(admin, *ISSUER_ROLE)?;
3264 usdc.mint(
3265 admin,
3266 ITIP20::mintCall {
3267 to: alice,
3268 amount: U256::from(min_order_amount * 10),
3269 },
3270 )
3271 .expect("Failed to mint USDC");
3272 }
3273
3274 {
3275 let mut eurc = TIP20Token::new(3, exchange.storage);
3276 eurc.grant_role_internal(admin, *ISSUER_ROLE)?;
3277 eurc.mint(
3278 admin,
3279 ITIP20::mintCall {
3280 to: alice,
3281 amount: U256::from(min_order_amount * 10),
3282 },
3283 )
3284 .expect("Failed to mint EURC");
3285 }
3286
3287 {
3288 let mut path_usd = PathUSD::new(exchange.storage);
3289 path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
3290 path_usd
3291 .token
3292 .mint(
3293 admin,
3294 ITIP20::mintCall {
3295 to: alice,
3296 amount: U256::from(min_order_amount * 10),
3297 },
3298 )
3299 .expect("Failed to mint PathUSD");
3300 }
3301
3302 {
3304 let mut usdc = TIP20Token::new(2, exchange.storage);
3305 usdc.approve(
3306 alice,
3307 ITIP20::approveCall {
3308 spender: exchange.address,
3309 amount: U256::from(min_order_amount * 10),
3310 },
3311 )
3312 .expect("Failed to approve USDC");
3313 }
3314
3315 {
3316 let mut eurc = TIP20Token::new(3, exchange.storage);
3317 eurc.approve(
3318 alice,
3319 ITIP20::approveCall {
3320 spender: exchange.address,
3321 amount: U256::from(min_order_amount * 10),
3322 },
3323 )
3324 .expect("Failed to approve EURC");
3325 }
3326
3327 {
3328 let mut path_usd = PathUSD::new(exchange.storage);
3329 path_usd
3330 .token
3331 .approve(
3332 alice,
3333 ITIP20::approveCall {
3334 spender: exchange.address,
3335 amount: U256::from(min_order_amount * 10),
3336 },
3337 )
3338 .expect("Failed to approve PathUSD");
3339 }
3340
3341 exchange
3348 .place(alice, usdc_addr, min_order_amount * 5, true, 0)
3349 .expect("Failed to place USDC bid order");
3350
3351 exchange
3353 .place(alice, eurc_addr, min_order_amount * 5, false, 0)
3354 .expect("Failed to place EURC ask order");
3355
3356 exchange
3357 .execute_block(Address::ZERO)
3358 .expect("Failed to execute block");
3359
3360 let amount_in = min_order_amount;
3362 let amount_out = exchange
3363 .quote_swap_exact_amount_in(usdc_addr, eurc_addr, amount_in)
3364 .expect("Should quote multi-hop trade");
3365
3366 assert_eq!(
3368 amount_out, amount_in,
3369 "With 1:1 rates, output should equal input"
3370 );
3371
3372 Ok(())
3373 }
3374
3375 #[test]
3376 fn test_quote_exact_out_multi_hop() -> eyre::Result<()> {
3377 let mut storage = HashMapStorageProvider::new(1);
3378 let mut exchange = StablecoinExchange::new(&mut storage);
3379 exchange.initialize()?;
3380
3381 let admin = Address::random();
3382 let alice = Address::random();
3383
3384 let min_order_amount = MIN_ORDER_AMOUNT;
3385 let path_usd_addr = {
3388 let mut path_usd = PathUSD::new(exchange.storage);
3389 path_usd
3390 .initialize(admin)
3391 .expect("Failed to initialize PathUSD");
3392 path_usd.token.address()
3393 };
3394
3395 let usdc_addr = {
3396 let mut usdc = TIP20Token::new(2, exchange.storage);
3397 usdc.initialize("USDC", "USDC", "USD", path_usd_addr, admin, Address::ZERO)
3398 .expect("Failed to initialize USDC");
3399 usdc.address()
3400 };
3401
3402 let eurc_addr = {
3403 let mut eurc = TIP20Token::new(3, exchange.storage);
3404 eurc.initialize("EURC", "EURC", "USD", path_usd_addr, admin, Address::ZERO)
3405 .expect("Failed to initialize EURC");
3406 eurc.address()
3407 };
3408
3409 exchange
3411 .create_pair(usdc_addr)
3412 .expect("Failed to create USDC pair");
3413 exchange
3414 .create_pair(eurc_addr)
3415 .expect("Failed to create EURC pair");
3416
3417 {
3418 let mut usdc = TIP20Token::new(2, exchange.storage);
3419 usdc.grant_role_internal(admin, *ISSUER_ROLE)?;
3420 usdc.mint(
3421 admin,
3422 ITIP20::mintCall {
3423 to: alice,
3424 amount: U256::from(min_order_amount * 10),
3425 },
3426 )
3427 .expect("Failed to mint USDC");
3428 usdc.approve(
3429 alice,
3430 ITIP20::approveCall {
3431 spender: exchange.address,
3432 amount: U256::from(min_order_amount * 10),
3433 },
3434 )
3435 .expect("Failed to approve USDC");
3436 }
3437
3438 {
3439 let mut eurc = TIP20Token::new(3, exchange.storage);
3440 eurc.grant_role_internal(admin, *ISSUER_ROLE)?;
3441 eurc.mint(
3442 admin,
3443 ITIP20::mintCall {
3444 to: alice,
3445 amount: U256::from(min_order_amount * 10),
3446 },
3447 )
3448 .expect("Failed to mint EURC");
3449 eurc.approve(
3450 alice,
3451 ITIP20::approveCall {
3452 spender: exchange.address,
3453 amount: U256::from(min_order_amount * 10),
3454 },
3455 )
3456 .expect("Failed to approve EURC");
3457 }
3458
3459 {
3460 let mut path_usd = PathUSD::new(exchange.storage);
3461 path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
3462 path_usd
3463 .token
3464 .mint(
3465 admin,
3466 ITIP20::mintCall {
3467 to: alice,
3468 amount: U256::from(min_order_amount * 10),
3469 },
3470 )
3471 .expect("Failed to mint PathUSD");
3472 path_usd
3473 .token
3474 .approve(
3475 alice,
3476 ITIP20::approveCall {
3477 spender: exchange.address,
3478 amount: U256::from(min_order_amount * 10),
3479 },
3480 )
3481 .expect("Failed to approve PathUSD");
3482 }
3483
3484 exchange
3486 .place(alice, usdc_addr, min_order_amount * 5, true, 0)
3487 .expect("Failed to place USDC bid order");
3488 exchange
3489 .place(alice, eurc_addr, min_order_amount * 5, false, 0)
3490 .expect("Failed to place EURC ask order");
3491
3492 exchange
3493 .execute_block(Address::ZERO)
3494 .expect("Failed to execute block");
3495
3496 let amount_out = min_order_amount;
3498 let amount_in = exchange
3499 .quote_swap_exact_amount_out(usdc_addr, eurc_addr, amount_out)
3500 .expect("Should quote multi-hop trade for exact output");
3501
3502 assert_eq!(
3504 amount_in, amount_out,
3505 "With 1:1 rates, input should equal output"
3506 );
3507
3508 Ok(())
3509 }
3510
3511 #[test]
3512 fn test_swap_exact_in_multi_hop_transitory_balances() -> eyre::Result<()> {
3513 let mut storage = HashMapStorageProvider::new(1);
3514 let mut exchange = StablecoinExchange::new(&mut storage);
3515 exchange.initialize()?;
3516
3517 let admin = Address::random();
3518 let alice = Address::random();
3519 let bob = Address::random();
3520
3521 let min_order_amount = MIN_ORDER_AMOUNT;
3522 let path_usd_addr = {
3524 let mut path_usd = PathUSD::new(exchange.storage);
3525 path_usd
3526 .initialize(admin)
3527 .expect("Failed to initialize PathUSD");
3528 path_usd.token.address()
3529 };
3530
3531 let usdc_addr = {
3532 let mut usdc = TIP20Token::new(2, exchange.storage);
3533 usdc.initialize("USDC", "USDC", "USD", path_usd_addr, admin, Address::ZERO)
3534 .expect("Failed to initialize USDC");
3535 usdc.address()
3536 };
3537
3538 let eurc_addr = {
3539 let mut eurc = TIP20Token::new(3, exchange.storage);
3540 eurc.initialize("EURC", "EURC", "USD", path_usd_addr, admin, Address::ZERO)
3541 .expect("Failed to initialize EURC");
3542 eurc.address()
3543 };
3544
3545 exchange
3546 .create_pair(usdc_addr)
3547 .expect("Failed to create USDC pair");
3548 exchange
3549 .create_pair(eurc_addr)
3550 .expect("Failed to create EURC pair");
3551
3552 {
3554 let mut usdc = TIP20Token::new(2, exchange.storage);
3555 usdc.grant_role_internal(admin, *ISSUER_ROLE)?;
3556 usdc.mint(
3557 admin,
3558 ITIP20::mintCall {
3559 to: alice,
3560 amount: U256::from(min_order_amount * 10),
3561 },
3562 )
3563 .expect("Failed to mint USDC");
3564 usdc.approve(
3565 alice,
3566 ITIP20::approveCall {
3567 spender: exchange.address,
3568 amount: U256::from(min_order_amount * 10),
3569 },
3570 )
3571 .expect("Failed to approve USDC");
3572 }
3573
3574 {
3575 let mut eurc = TIP20Token::new(3, exchange.storage);
3576 eurc.grant_role_internal(admin, *ISSUER_ROLE)?;
3577 eurc.mint(
3578 admin,
3579 ITIP20::mintCall {
3580 to: alice,
3581 amount: U256::from(min_order_amount * 10),
3582 },
3583 )
3584 .expect("Failed to mint EURC");
3585 eurc.approve(
3586 alice,
3587 ITIP20::approveCall {
3588 spender: exchange.address,
3589 amount: U256::from(min_order_amount * 10),
3590 },
3591 )
3592 .expect("Failed to approve EURC");
3593 }
3594
3595 {
3596 let mut path_usd = PathUSD::new(exchange.storage);
3597 path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
3598 path_usd.token.mint(
3599 admin,
3600 ITIP20::mintCall {
3601 to: alice,
3602 amount: U256::from(min_order_amount * 10),
3603 },
3604 )?;
3605
3606 path_usd.token.approve(
3607 alice,
3608 ITIP20::approveCall {
3609 spender: exchange.address,
3610 amount: U256::from(min_order_amount * 10),
3611 },
3612 )?;
3613 }
3614
3615 {
3617 let mut usdc = TIP20Token::new(2, exchange.storage);
3618 usdc.mint(
3619 admin,
3620 ITIP20::mintCall {
3621 to: bob,
3622 amount: U256::from(min_order_amount * 10),
3623 },
3624 )?;
3625
3626 usdc.approve(
3627 bob,
3628 ITIP20::approveCall {
3629 spender: exchange.address,
3630 amount: U256::from(min_order_amount * 10),
3631 },
3632 )?;
3633 }
3634
3635 exchange
3637 .place(alice, usdc_addr, min_order_amount * 5, true, 0)
3638 .expect("Failed to place USDC bid order");
3639 exchange
3640 .place(alice, eurc_addr, min_order_amount * 5, false, 0)
3641 .expect("Failed to place EURC ask order");
3642 exchange
3643 .execute_block(Address::ZERO)
3644 .expect("Failed to execute block");
3645
3646 let bob_usdc_before = {
3648 let mut usdc = TIP20Token::new(2, exchange.storage);
3649 usdc.balance_of(ITIP20::balanceOfCall { account: bob })?
3650 };
3651 let bob_eurc_before = {
3652 let mut eurc = TIP20Token::new(3, exchange.storage);
3653 eurc.balance_of(ITIP20::balanceOfCall { account: bob })?
3654 };
3655
3656 let amount_in = min_order_amount;
3658 let amount_out = exchange
3659 .swap_exact_amount_in(
3660 bob, usdc_addr, eurc_addr, amount_in, 0, )
3662 .expect("Should execute multi-hop swap");
3663
3664 let bob_usdc_after = {
3666 let mut usdc = TIP20Token::new(2, exchange.storage);
3667 usdc.balance_of(ITIP20::balanceOfCall { account: bob })?
3668 };
3669 let bob_eurc_after = {
3670 let mut eurc = TIP20Token::new(3, exchange.storage);
3671 eurc.balance_of(ITIP20::balanceOfCall { account: bob })?
3672 };
3673
3674 assert_eq!(
3676 bob_usdc_before - bob_usdc_after,
3677 U256::from(amount_in),
3678 "Bob should have spent exact amount_in USDC"
3679 );
3680 assert_eq!(
3681 bob_eurc_after - bob_eurc_before,
3682 U256::from(amount_out),
3683 "Bob should have received amount_out EURC"
3684 );
3685
3686 let bob_path_usd_wallet = {
3688 let mut path_usd = PathUSD::new(exchange.storage);
3689 path_usd
3690 .token
3691 .balance_of(ITIP20::balanceOfCall { account: bob })?
3692 };
3693 assert_eq!(
3694 bob_path_usd_wallet,
3695 U256::ZERO,
3696 "Bob should have ZERO PathUSD in wallet (transitory)"
3697 );
3698
3699 let bob_path_usd_exchange = exchange
3700 .balance_of(bob, path_usd_addr)
3701 .expect("Failed to get bob's PathUSD exchange balance");
3702 assert_eq!(
3703 bob_path_usd_exchange, 0,
3704 "Bob should have ZERO PathUSD on exchange (transitory)"
3705 );
3706
3707 Ok(())
3708 }
3709
3710 #[test]
3711 fn test_swap_exact_out_multi_hop_transitory_balances() -> eyre::Result<()> {
3712 let mut storage = HashMapStorageProvider::new(1);
3713 let mut exchange = StablecoinExchange::new(&mut storage);
3714 exchange.initialize()?;
3715
3716 let admin = Address::random();
3717 let alice = Address::random();
3718 let bob = Address::random();
3719
3720 let min_order_amount = MIN_ORDER_AMOUNT;
3721 let path_usd_addr = {
3723 let mut path_usd = PathUSD::new(exchange.storage);
3724 path_usd
3725 .initialize(admin)
3726 .expect("Failed to initialize PathUSD");
3727 path_usd.token.address()
3728 };
3729
3730 let usdc_addr = {
3731 let mut usdc = TIP20Token::new(2, exchange.storage);
3732 usdc.initialize("USDC", "USDC", "USD", path_usd_addr, admin, Address::ZERO)
3733 .expect("Failed to initialize USDC");
3734 usdc.address()
3735 };
3736
3737 let eurc_addr = {
3738 let mut eurc = TIP20Token::new(3, exchange.storage);
3739 eurc.initialize("EURC", "EURC", "USD", path_usd_addr, admin, Address::ZERO)
3740 .expect("Failed to initialize EURC");
3741 eurc.address()
3742 };
3743
3744 exchange
3745 .create_pair(usdc_addr)
3746 .expect("Failed to create USDC pair");
3747 exchange
3748 .create_pair(eurc_addr)
3749 .expect("Failed to create EURC pair");
3750
3751 {
3753 let mut usdc = TIP20Token::new(2, exchange.storage);
3754 usdc.grant_role_internal(admin, *ISSUER_ROLE)?;
3755 usdc.mint(
3756 admin,
3757 ITIP20::mintCall {
3758 to: alice,
3759 amount: U256::from(min_order_amount * 10),
3760 },
3761 )
3762 .expect("Failed to mint USDC");
3763 usdc.approve(
3764 alice,
3765 ITIP20::approveCall {
3766 spender: exchange.address,
3767 amount: U256::from(min_order_amount * 10),
3768 },
3769 )
3770 .expect("Failed to approve USDC");
3771 }
3772
3773 {
3774 let mut eurc = TIP20Token::new(3, exchange.storage);
3775 eurc.grant_role_internal(admin, *ISSUER_ROLE)?;
3776 eurc.mint(
3777 admin,
3778 ITIP20::mintCall {
3779 to: alice,
3780 amount: U256::from(min_order_amount * 10),
3781 },
3782 )
3783 .expect("Failed to mint EURC");
3784 eurc.approve(
3785 alice,
3786 ITIP20::approveCall {
3787 spender: exchange.address,
3788 amount: U256::from(min_order_amount * 10),
3789 },
3790 )
3791 .expect("Failed to approve EURC");
3792 }
3793
3794 {
3795 let mut path_usd = PathUSD::new(exchange.storage);
3796 path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
3797 path_usd
3798 .token
3799 .mint(
3800 admin,
3801 ITIP20::mintCall {
3802 to: alice,
3803 amount: U256::from(min_order_amount * 10),
3804 },
3805 )
3806 .expect("Failed to mint PathUSD");
3807 path_usd
3808 .token
3809 .approve(
3810 alice,
3811 ITIP20::approveCall {
3812 spender: exchange.address,
3813 amount: U256::from(min_order_amount * 10),
3814 },
3815 )
3816 .expect("Failed to approve PathUSD");
3817 }
3818
3819 {
3821 let mut usdc = TIP20Token::new(2, exchange.storage);
3822 usdc.mint(
3823 admin,
3824 ITIP20::mintCall {
3825 to: bob,
3826 amount: U256::from(min_order_amount * 10),
3827 },
3828 )
3829 .expect("Failed to mint USDC for bob");
3830 usdc.approve(
3831 bob,
3832 ITIP20::approveCall {
3833 spender: exchange.address,
3834 amount: U256::from(min_order_amount * 10),
3835 },
3836 )
3837 .expect("Failed to approve USDC for bob");
3838 }
3839
3840 exchange
3842 .place(alice, usdc_addr, min_order_amount * 5, true, 0)
3843 .expect("Failed to place USDC bid order");
3844 exchange
3845 .place(alice, eurc_addr, min_order_amount * 5, false, 0)
3846 .expect("Failed to place EURC ask order");
3847 exchange
3848 .execute_block(Address::ZERO)
3849 .expect("Failed to execute block");
3850
3851 let bob_usdc_before = {
3853 let mut usdc = TIP20Token::new(2, exchange.storage);
3854 usdc.balance_of(ITIP20::balanceOfCall { account: bob })?
3855 };
3856 let bob_eurc_before = {
3857 let mut eurc = TIP20Token::new(3, exchange.storage);
3858 eurc.balance_of(ITIP20::balanceOfCall { account: bob })?
3859 };
3860
3861 let amount_out = 90u128;
3863 let amount_in = exchange.swap_exact_amount_out(
3864 bob,
3865 usdc_addr,
3866 eurc_addr,
3867 amount_out,
3868 u128::MAX, )?;
3870
3871 let bob_usdc_after = {
3873 let mut usdc = TIP20Token::new(2, exchange.storage);
3874 usdc.balance_of(ITIP20::balanceOfCall { account: bob })?
3875 };
3876 let bob_eurc_after = {
3877 let mut eurc = TIP20Token::new(3, exchange.storage);
3878 eurc.balance_of(ITIP20::balanceOfCall { account: bob })?
3879 };
3880
3881 assert_eq!(
3883 bob_usdc_before - bob_usdc_after,
3884 U256::from(amount_in),
3885 "Bob should have spent amount_in USDC"
3886 );
3887 assert_eq!(
3888 bob_eurc_after - bob_eurc_before,
3889 U256::from(amount_out),
3890 "Bob should have received exact amount_out EURC"
3891 );
3892
3893 let bob_path_usd_wallet = {
3895 let mut path_usd = PathUSD::new(exchange.storage);
3896 path_usd
3897 .token
3898 .balance_of(ITIP20::balanceOfCall { account: bob })?
3899 };
3900 assert_eq!(
3901 bob_path_usd_wallet,
3902 U256::ZERO,
3903 "Bob should have ZERO PathUSD in wallet (transitory)"
3904 );
3905
3906 let bob_path_usd_exchange = exchange
3907 .balance_of(bob, path_usd_addr)
3908 .expect("Failed to get bob's PathUSD exchange balance");
3909 assert_eq!(
3910 bob_path_usd_exchange, 0,
3911 "Bob should have ZERO PathUSD on exchange (transitory)"
3912 );
3913
3914 Ok(())
3915 }
3916
3917 #[test]
3918 fn test_create_pair_invalid_currency() -> eyre::Result<()> {
3919 let mut storage = HashMapStorageProvider::new(1);
3920
3921 let admin = Address::random();
3922 let mut path_usd = TIP20Token::from_address(PATH_USD_ADDRESS, &mut storage).unwrap();
3924 path_usd
3925 .initialize(
3926 "PathUSD",
3927 "LUSD",
3928 "USD",
3929 Address::ZERO,
3930 admin,
3931 Address::ZERO,
3932 )
3933 .unwrap();
3934
3935 let mut token_0 = TIP20Token::new(1, path_usd.storage());
3937 token_0
3938 .initialize(
3939 "EuroToken",
3940 "EURO",
3941 "EUR",
3942 PATH_USD_ADDRESS,
3943 admin,
3944 Address::ZERO,
3945 )
3946 .unwrap();
3947 let token_0_address = token_0.address();
3948
3949 let mut exchange = StablecoinExchange::new(token_0.storage());
3950 exchange.initialize()?;
3951
3952 let result = exchange.create_pair(token_0_address);
3954 assert!(matches!(
3955 result,
3956 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
3957 ));
3958
3959 Ok(())
3960 }
3961
3962 #[test]
3963 fn test_max_in_check_pre_moderato() -> eyre::Result<()> {
3964 let mut storage = HashMapStorageProvider::new(1);
3965 let mut exchange = StablecoinExchange::new(&mut storage);
3966 exchange.initialize()?;
3967
3968 let alice = Address::random();
3969 let bob = Address::random();
3970 let admin = Address::random();
3971
3972 let (base_token, quote_token) = setup_test_tokens(
3973 exchange.storage,
3974 admin,
3975 alice,
3976 exchange.address,
3977 200_000_000u128,
3978 );
3979 exchange.create_pair(base_token)?;
3980
3981 let tick_50 = 50i16;
3982 let tick_100 = 100i16;
3983 let order_amount = MIN_ORDER_AMOUNT;
3984
3985 exchange.place(alice, base_token, order_amount, false, tick_50)?;
3986 exchange.place(alice, base_token, order_amount, false, tick_100)?;
3987 exchange.execute_block(Address::ZERO)?;
3988
3989 exchange.set_balance(bob, quote_token, 200_000_000u128)?;
3990
3991 let price_50 = orderbook::tick_to_price(tick_50);
3992 let quote_for_first = (order_amount * price_50 as u128) / orderbook::PRICE_SCALE as u128;
3993 let max_in_between = quote_for_first + 500;
3994
3995 let result = exchange.swap_exact_amount_out(
3996 bob,
3997 quote_token,
3998 base_token,
3999 order_amount + 999,
4000 max_in_between,
4001 );
4002 assert!(result.is_err());
4003
4004 Ok(())
4005 }
4006
4007 #[test]
4008 fn test_create_pair_rejects_non_tip20_base_post_moderato() -> eyre::Result<()> {
4009 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
4011
4012 let admin = Address::random();
4013 let mut path_usd = TIP20Token::from_address(PATH_USD_ADDRESS, &mut storage).unwrap();
4015 path_usd
4016 .initialize(
4017 "PathUSD",
4018 "LUSD",
4019 "USD",
4020 Address::ZERO,
4021 admin,
4022 Address::ZERO,
4023 )
4024 .unwrap();
4025
4026 let mut exchange = StablecoinExchange::new(path_usd.storage());
4027 exchange.initialize()?;
4028
4029 let non_tip20_address = Address::random();
4031 let result = exchange.create_pair(non_tip20_address);
4032 assert!(matches!(
4033 result,
4034 Err(TempoPrecompileError::StablecoinExchange(
4035 StablecoinExchangeError::InvalidBaseToken(_)
4036 ))
4037 ));
4038
4039 Ok(())
4040 }
4041
4042 #[test]
4043 fn test_max_in_check_post_moderato() -> eyre::Result<()> {
4044 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
4045 let mut exchange = StablecoinExchange::new(&mut storage);
4046 exchange.initialize()?;
4047
4048 let alice = Address::random();
4049 let bob = Address::random();
4050 let admin = Address::random();
4051
4052 let (base_token, quote_token) = setup_test_tokens(
4053 exchange.storage,
4054 admin,
4055 alice,
4056 exchange.address,
4057 200_000_000u128,
4058 );
4059 exchange.create_pair(base_token)?;
4060
4061 let tick_50 = 50i16;
4062 let tick_100 = 100i16;
4063 let order_amount = MIN_ORDER_AMOUNT;
4064
4065 exchange.place(alice, base_token, order_amount, false, tick_50)?;
4066 exchange.place(alice, base_token, order_amount, false, tick_100)?;
4067 exchange.execute_block(Address::ZERO)?;
4068
4069 exchange.set_balance(bob, quote_token, 200_000_000u128)?;
4070
4071 let price_50 = orderbook::tick_to_price(tick_50);
4072 let price_100 = orderbook::tick_to_price(tick_100);
4073 let quote_for_first = (order_amount * price_50 as u128) / orderbook::PRICE_SCALE as u128;
4074 let quote_for_partial_second = (999 * price_100 as u128) / orderbook::PRICE_SCALE as u128;
4075 let total_needed = quote_for_first + quote_for_partial_second;
4076
4077 let result = exchange.swap_exact_amount_out(
4078 bob,
4079 quote_token,
4080 base_token,
4081 order_amount + 999,
4082 total_needed,
4083 );
4084 assert!(result.is_ok());
4085
4086 Ok(())
4087 }
4088
4089 #[test]
4090 fn test_create_pair_allows_non_tip20_base_pre_moderato() -> eyre::Result<()> {
4091 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
4093
4094 let admin = Address::random();
4095 let mut path_usd = TIP20Token::from_address(PATH_USD_ADDRESS, &mut storage).unwrap();
4097 path_usd
4098 .initialize(
4099 "PathUSD",
4100 "LUSD",
4101 "USD",
4102 Address::ZERO,
4103 admin,
4104 Address::ZERO,
4105 )
4106 .unwrap();
4107
4108 let mut exchange = StablecoinExchange::new(path_usd.storage());
4109 exchange.initialize()?;
4110
4111 let non_tip20_address = Address::random();
4115 let result = exchange.create_pair(non_tip20_address);
4116
4117 assert!(result.is_err());
4119 assert!(!matches!(
4120 result,
4121 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
4122 _
4123 )))
4124 ));
4125
4126 Ok(())
4127 }
4128
4129 #[test]
4130 fn test_exact_out_bid_side_pre_moderato() -> eyre::Result<()> {
4131 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
4133 let mut exchange = StablecoinExchange::new(&mut storage);
4134 exchange.initialize()?;
4135
4136 let alice = Address::random();
4137 let bob = Address::random();
4138 let admin = Address::random();
4139
4140 let (base_token, quote_token) = setup_test_tokens(
4141 exchange.storage,
4142 admin,
4143 alice,
4144 exchange.address,
4145 1_000_000_000u128,
4146 );
4147 exchange.create_pair(base_token)?;
4148
4149 let tick = 1000i16;
4150 let price = tick_to_price(tick);
4151 let order_amount_base = MIN_ORDER_AMOUNT;
4152
4153 exchange.place(alice, base_token, order_amount_base, true, tick)?;
4154 exchange.execute_block(Address::ZERO)?;
4155
4156 let amount_out_quote = 5_000_000u128;
4157 let base_needed = (amount_out_quote * PRICE_SCALE as u128) / price as u128;
4158 let max_amount_in = base_needed + 10000;
4159
4160 exchange.set_balance(bob, base_token, max_amount_in * 2)?;
4161
4162 let result = exchange.swap_exact_amount_out(
4164 bob,
4165 base_token,
4166 quote_token,
4167 amount_out_quote,
4168 max_amount_in,
4169 );
4170
4171 assert!(
4172 matches!(
4173 result,
4174 Err(TempoPrecompileError::StablecoinExchange(
4175 StablecoinExchangeError::MaxInputExceeded(_)
4176 ))
4177 ),
4178 "Pre-Moderato should fail with MaxInputExceeded"
4179 );
4180
4181 Ok(())
4182 }
4183
4184 #[test]
4185 fn test_exact_out_bid_side_post_moderato() -> eyre::Result<()> {
4186 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
4188 let mut exchange = StablecoinExchange::new(&mut storage);
4189 exchange.initialize()?;
4190
4191 let alice = Address::random();
4192 let bob = Address::random();
4193 let admin = Address::random();
4194
4195 let (base_token, quote_token) = setup_test_tokens(
4196 exchange.storage,
4197 admin,
4198 alice,
4199 exchange.address,
4200 1_000_000_000u128,
4201 );
4202 exchange.create_pair(base_token)?;
4203
4204 let tick = 1000i16;
4205 let price = tick_to_price(tick);
4206 let order_amount_base = MIN_ORDER_AMOUNT;
4207
4208 exchange.place(alice, base_token, order_amount_base, true, tick)?;
4209 exchange.execute_block(Address::ZERO)?;
4210
4211 let amount_out_quote = 5_000_000u128;
4212 let base_needed = (amount_out_quote * PRICE_SCALE as u128) / price as u128;
4213 let max_amount_in = base_needed + 10000;
4214
4215 exchange.set_balance(bob, base_token, max_amount_in * 2)?;
4216
4217 let _amount_in = exchange.swap_exact_amount_out(
4218 bob,
4219 base_token,
4220 quote_token,
4221 amount_out_quote,
4222 max_amount_in,
4223 )?;
4224
4225 let mut quote_tip20 = TIP20Token::from_address(quote_token, exchange.storage).unwrap();
4227 let bob_quote_balance = quote_tip20.balance_of(ITIP20::balanceOfCall { account: bob })?;
4228 assert_eq!(bob_quote_balance, U256::from(amount_out_quote));
4229
4230 Ok(())
4231 }
4232
4233 #[test]
4234 fn test_exact_in_ask_side_pre_moderato() -> eyre::Result<()> {
4235 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
4237 let mut exchange = StablecoinExchange::new(&mut storage);
4238 exchange.initialize()?;
4239
4240 let alice = Address::random();
4241 let bob = Address::random();
4242 let admin = Address::random();
4243
4244 let (base_token, quote_token) = setup_test_tokens(
4245 exchange.storage,
4246 admin,
4247 alice,
4248 exchange.address,
4249 1_000_000_000u128,
4250 );
4251 exchange.create_pair(base_token)?;
4252
4253 let tick = 1000i16;
4254 let price = tick_to_price(tick);
4255 let order_amount_base = MIN_ORDER_AMOUNT;
4256
4257 exchange.place(alice, base_token, order_amount_base, false, tick)?;
4258 exchange.execute_block(Address::ZERO)?;
4259
4260 let amount_in_quote = 5_000_000u128;
4261 let min_amount_out = 0;
4262
4263 exchange.set_balance(bob, quote_token, amount_in_quote * 2)?;
4264
4265 let amount_out = exchange.swap_exact_amount_in(
4266 bob,
4267 quote_token,
4268 base_token,
4269 amount_in_quote,
4270 min_amount_out,
4271 )?;
4272
4273 assert_eq!(amount_out, amount_in_quote);
4276 assert_ne!(
4277 amount_out,
4278 (amount_in_quote * PRICE_SCALE as u128) / price as u128
4279 );
4280
4281 Ok(())
4282 }
4283
4284 #[test]
4285 fn test_exact_in_ask_side_post_moderato() -> eyre::Result<()> {
4286 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
4288 let mut exchange = StablecoinExchange::new(&mut storage);
4289 exchange.initialize()?;
4290
4291 let alice = Address::random();
4292 let bob = Address::random();
4293 let admin = Address::random();
4294
4295 let (base_token, quote_token) = setup_test_tokens(
4296 exchange.storage,
4297 admin,
4298 alice,
4299 exchange.address,
4300 1_000_000_000u128,
4301 );
4302 exchange.create_pair(base_token)?;
4303
4304 let tick = 1000i16;
4305 let price = tick_to_price(tick);
4306 let order_amount_base = MIN_ORDER_AMOUNT;
4307
4308 exchange.place(alice, base_token, order_amount_base, false, tick)?;
4309 exchange.execute_block(Address::ZERO)?;
4310
4311 let amount_in_quote = 5_000_000u128;
4312 let min_amount_out = 0;
4313
4314 exchange.set_balance(bob, quote_token, amount_in_quote * 2)?;
4315
4316 let amount_out = exchange.swap_exact_amount_in(
4317 bob,
4318 quote_token,
4319 base_token,
4320 amount_in_quote,
4321 min_amount_out,
4322 )?;
4323
4324 let expected_base = (amount_in_quote * PRICE_SCALE as u128) / price as u128;
4326 assert_eq!(amount_out, expected_base);
4327
4328 Ok(())
4329 }
4330
4331 #[test]
4332 fn test_clear_order_post_allegretto() -> eyre::Result<()> {
4333 const AMOUNT: u128 = 1_000_000_000;
4334
4335 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
4337 let mut exchange = StablecoinExchange::new(&mut storage);
4338 exchange.initialize()?;
4339
4340 let alice = Address::random();
4341 let bob = Address::random();
4342 let carol = Address::random();
4343 let admin = Address::random();
4344
4345 let (base_token, quote_token) =
4346 setup_test_tokens(exchange.storage, admin, alice, exchange.address, AMOUNT);
4347 exchange.create_pair(base_token)?;
4348
4349 mint_and_approve_token(exchange.storage, 1, admin, bob, exchange.address, AMOUNT);
4351 mint_and_approve_quote(exchange.storage, admin, carol, exchange.address, AMOUNT);
4352
4353 let tick = 100i16;
4354
4355 let order1_amount = MIN_ORDER_AMOUNT;
4357 let order2_amount = MIN_ORDER_AMOUNT;
4358
4359 let order1_id = exchange.place(alice, base_token, order1_amount, false, tick)?;
4360 let order2_id = exchange.place(bob, base_token, order2_amount, false, tick)?;
4361 exchange.execute_block(Address::ZERO)?;
4362
4363 let order1 = exchange.sload_orders(order1_id)?;
4365 let order2 = exchange.sload_orders(order2_id)?;
4366 assert_eq!(order1.next(), order2_id);
4367 assert_eq!(order2.prev(), order1_id);
4368
4369 let swap_amount = order1_amount;
4371 exchange.swap_exact_amount_out(carol, quote_token, base_token, swap_amount, u128::MAX)?;
4372
4373 let order2_after = exchange.sload_orders(order2_id)?;
4375 assert_eq!(
4376 order2_after.prev(),
4377 0,
4378 "New head order should have prev = 0 after previous head was filled"
4379 );
4380
4381 Ok(())
4382 }
4383
4384 #[test]
4385 fn test_best_tick_updates_on_fill() -> eyre::Result<()> {
4386 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
4387 let mut exchange = StablecoinExchange::new(&mut storage);
4388 exchange.initialize()?;
4389
4390 let alice = Address::random();
4391 let bob = Address::random();
4392 let admin = Address::random();
4393 let amount = MIN_ORDER_AMOUNT;
4394
4395 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);
4401 let bid_price_2 = orderbook::tick_to_price(bid_tick_2);
4402 let bid_escrow_1 = (amount * bid_price_1 as u128) / orderbook::PRICE_SCALE as u128;
4403 let bid_escrow_2 = (amount * bid_price_2 as u128) / orderbook::PRICE_SCALE as u128;
4404 let total_bid_escrow = bid_escrow_1 + bid_escrow_2;
4405
4406 let (base_token, quote_token) = setup_test_tokens(
4407 exchange.storage,
4408 admin,
4409 alice,
4410 exchange.address,
4411 total_bid_escrow,
4412 );
4413 exchange.create_pair(base_token)?;
4414 let book_key = compute_book_key(base_token, quote_token);
4415
4416 exchange.place(alice, base_token, amount, true, bid_tick_1)?;
4418 exchange.place(alice, base_token, amount, true, bid_tick_2)?;
4419
4420 mint_and_approve_token(
4422 exchange.storage,
4423 1,
4424 admin,
4425 alice,
4426 exchange.address,
4427 amount * 2,
4428 );
4429 exchange.place(alice, base_token, amount, false, ask_tick_1)?;
4430 exchange.place(alice, base_token, amount, false, ask_tick_2)?;
4431
4432 exchange.execute_block(Address::ZERO)?;
4433
4434 let orderbook = exchange.sload_books(book_key)?;
4436 assert_eq!(orderbook.best_bid_tick, bid_tick_1);
4437 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
4438
4439 exchange.set_balance(bob, base_token, amount)?;
4441 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
4442 let orderbook = exchange.sload_books(book_key)?;
4444 assert_eq!(orderbook.best_bid_tick, bid_tick_2);
4445 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
4446
4447 exchange.set_balance(bob, base_token, amount)?;
4449 exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
4450 let orderbook = exchange.sload_books(book_key)?;
4452 assert_eq!(orderbook.best_bid_tick, i16::MIN);
4453 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
4454
4455 let ask_price_1 = orderbook::tick_to_price(ask_tick_1);
4457 let quote_needed = (amount * ask_price_1 as u128) / orderbook::PRICE_SCALE as u128;
4458 exchange.set_balance(bob, quote_token, quote_needed)?;
4459 exchange.swap_exact_amount_in(bob, quote_token, base_token, quote_needed, 0)?;
4460 let orderbook = exchange.sload_books(book_key)?;
4462 assert_eq!(orderbook.best_ask_tick, ask_tick_2);
4463 assert_eq!(orderbook.best_bid_tick, i16::MIN);
4464
4465 Ok(())
4466 }
4467
4468 #[test]
4469 fn test_best_tick_updates_on_cancel() -> eyre::Result<()> {
4470 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
4471 let mut exchange = StablecoinExchange::new(&mut storage);
4472 exchange.initialize()?;
4473
4474 let alice = Address::random();
4475 let admin = Address::random();
4476 let amount = MIN_ORDER_AMOUNT;
4477
4478 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);
4483 let price_2 = orderbook::tick_to_price(bid_tick_2);
4484 let escrow_1 = (amount * price_1 as u128) / orderbook::PRICE_SCALE as u128;
4485 let escrow_2 = (amount * price_2 as u128) / orderbook::PRICE_SCALE as u128;
4486 let total_escrow = escrow_1 * 2 + escrow_2;
4487
4488 let (base_token, quote_token) = setup_test_tokens(
4489 exchange.storage,
4490 admin,
4491 alice,
4492 exchange.address,
4493 total_escrow,
4494 );
4495 exchange.create_pair(base_token)?;
4496 let book_key = compute_book_key(base_token, quote_token);
4497
4498 let bid_order_1 = exchange.place(alice, base_token, amount, true, bid_tick_1)?;
4500 let bid_order_2 = exchange.place(alice, base_token, amount, true, bid_tick_1)?;
4501 let bid_order_3 = exchange.place(alice, base_token, amount, true, bid_tick_2)?;
4502
4503 mint_and_approve_token(
4505 exchange.storage,
4506 1,
4507 admin,
4508 alice,
4509 exchange.address,
4510 amount * 2,
4511 );
4512 let ask_order_1 = exchange.place(alice, base_token, amount, false, ask_tick_1)?;
4513 let ask_order_2 = exchange.place(alice, base_token, amount, false, ask_tick_2)?;
4514
4515 exchange.execute_block(Address::ZERO)?;
4516
4517 let orderbook = exchange.sload_books(book_key)?;
4519 assert_eq!(orderbook.best_bid_tick, bid_tick_1);
4520 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
4521
4522 exchange.cancel(alice, bid_order_1)?;
4524 let orderbook = exchange.sload_books(book_key)?;
4526 assert_eq!(orderbook.best_bid_tick, bid_tick_1);
4527 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
4528
4529 exchange.cancel(alice, bid_order_2)?;
4531 let orderbook = exchange.sload_books(book_key)?;
4533 assert_eq!(orderbook.best_bid_tick, bid_tick_2);
4534 assert_eq!(orderbook.best_ask_tick, ask_tick_1);
4535
4536 exchange.cancel(alice, ask_order_1)?;
4538 let orderbook = exchange.sload_books(book_key)?;
4540 assert_eq!(orderbook.best_bid_tick, bid_tick_2);
4541 assert_eq!(orderbook.best_ask_tick, ask_tick_2);
4542
4543 exchange.cancel(alice, bid_order_3)?;
4545 let orderbook = exchange.sload_books(book_key)?;
4547 assert_eq!(orderbook.best_bid_tick, i16::MIN);
4548 assert_eq!(orderbook.best_ask_tick, ask_tick_2);
4549
4550 exchange.cancel(alice, ask_order_2)?;
4552 let orderbook = exchange.sload_books(book_key)?;
4554 assert_eq!(orderbook.best_bid_tick, i16::MIN);
4555 assert_eq!(orderbook.best_ask_tick, i16::MAX);
4556
4557 Ok(())
4558 }
4559
4560 #[test]
4561 fn test_place_post_allegretto() -> eyre::Result<()> {
4562 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
4563 let mut exchange = StablecoinExchange::new(&mut storage);
4564 exchange.initialize()?;
4565
4566 let alice = Address::random();
4567 let admin = Address::random();
4568
4569 let (base_token, _quote_token) = setup_test_tokens(
4570 exchange.storage,
4571 admin,
4572 alice,
4573 exchange.address,
4574 1_000_000_000,
4575 );
4576 exchange.create_pair(base_token)?;
4577
4578 mint_and_approve_token(
4580 exchange.storage,
4581 1,
4582 admin,
4583 alice,
4584 exchange.address,
4585 1_000_000_000,
4586 );
4587
4588 let invalid_tick = 15i16;
4590 let result = exchange.place(alice, base_token, MIN_ORDER_AMOUNT, true, invalid_tick);
4591
4592 let error = result.unwrap_err();
4593 assert!(matches!(
4594 error,
4595 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::InvalidTick(_))
4596 ));
4597
4598 let valid_tick = -20i16;
4600 let result = exchange.place(alice, base_token, MIN_ORDER_AMOUNT, true, valid_tick);
4601 assert!(result.is_ok());
4602
4603 Ok(())
4604 }
4605
4606 #[test]
4607 fn test_place_flip_post_allegretto() -> eyre::Result<()> {
4608 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
4609 let mut exchange = StablecoinExchange::new(&mut storage);
4610 exchange.initialize()?;
4611
4612 let alice = Address::random();
4613 let admin = Address::random();
4614
4615 let (base_token, _quote_token) = setup_test_tokens(
4616 exchange.storage,
4617 admin,
4618 alice,
4619 exchange.address,
4620 1_000_000_000,
4621 );
4622 exchange.create_pair(base_token)?;
4623
4624 mint_and_approve_token(
4626 exchange.storage,
4627 1,
4628 admin,
4629 alice,
4630 exchange.address,
4631 1_000_000_000,
4632 );
4633
4634 let invalid_tick = 15i16;
4636 let invalid_flip_tick = 25i16;
4637 let result = exchange.place_flip(
4638 alice,
4639 base_token,
4640 MIN_ORDER_AMOUNT,
4641 true,
4642 invalid_tick,
4643 invalid_flip_tick,
4644 );
4645
4646 let error = result.unwrap_err();
4647 assert!(matches!(
4648 error,
4649 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::InvalidTick(_))
4650 ));
4651
4652 let valid_tick = 20i16;
4654 let invalid_flip_tick = 25i16;
4655 let result = exchange.place_flip(
4656 alice,
4657 base_token,
4658 MIN_ORDER_AMOUNT,
4659 true,
4660 valid_tick,
4661 invalid_flip_tick,
4662 );
4663
4664 let error = result.unwrap_err();
4665 assert!(matches!(
4666 error,
4667 TempoPrecompileError::StablecoinExchange(StablecoinExchangeError::InvalidFlipTick(_))
4668 ));
4669
4670 let valid_flip_tick = 30i16;
4671 let result = exchange.place_flip(
4672 alice,
4673 base_token,
4674 MIN_ORDER_AMOUNT,
4675 true,
4676 valid_tick,
4677 valid_flip_tick,
4678 );
4679 assert!(result.is_ok());
4680
4681 Ok(())
4682 }
4683
4684 #[test]
4685 fn test_find_trade_path_rejects_non_tip20_post_allegretto() -> eyre::Result<()> {
4686 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
4687 let mut exchange = StablecoinExchange::new(&mut storage);
4688 exchange.initialize()?;
4689
4690 let admin = Address::random();
4691 let user = Address::random();
4692
4693 let (_, quote_token) = setup_test_tokens(
4694 exchange.storage,
4695 admin,
4696 user,
4697 exchange.address,
4698 MIN_ORDER_AMOUNT,
4699 );
4700
4701 let non_tip20_address = Address::random();
4702 let result = exchange.find_trade_path(non_tip20_address, quote_token);
4703 assert!(
4704 matches!(
4705 result,
4706 Err(TempoPrecompileError::StablecoinExchange(
4707 StablecoinExchangeError::InvalidToken(_)
4708 ))
4709 ),
4710 "Should return InvalidToken error for non-TIP20 token post-Allegretto"
4711 );
4712
4713 Ok(())
4714 }
4715
4716 #[test]
4717 fn test_quote_exact_in_handles_both_directions() -> eyre::Result<()> {
4718 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
4719 let mut exchange = StablecoinExchange::new(&mut storage);
4720 exchange.initialize()?;
4721
4722 let alice = Address::random();
4723 let admin = Address::random();
4724 let amount = MIN_ORDER_AMOUNT;
4725 let tick = 100_i16;
4726 let price = orderbook::tick_to_price(tick);
4727
4728 let bid_escrow = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
4730
4731 let (base_token, quote_token) =
4732 setup_test_tokens(exchange.storage, admin, alice, exchange.address, bid_escrow);
4733 exchange.create_pair(base_token)?;
4734 let book_key = compute_book_key(base_token, quote_token);
4735 mint_and_approve_token(exchange.storage, 1, admin, alice, exchange.address, amount);
4736
4737 exchange.place(alice, base_token, amount, true, tick)?;
4739 exchange.execute_block(Address::ZERO)?;
4740
4741 let quoted_out_bid = exchange.quote_exact_in(book_key, amount, true)?;
4743 let expected_quote_out = amount
4744 .checked_mul(price as u128)
4745 .and_then(|v| v.checked_div(orderbook::PRICE_SCALE as u128))
4746 .expect("calculation");
4747 assert_eq!(
4748 quoted_out_bid, expected_quote_out,
4749 "quote_exact_in with is_bid=true should return quote amount"
4750 );
4751
4752 exchange.place(alice, base_token, amount, false, tick)?;
4754 exchange.execute_block(Address::ZERO)?;
4755
4756 let quote_in = (amount * price as u128) / orderbook::PRICE_SCALE as u128;
4758 let quoted_out_ask = exchange.quote_exact_in(book_key, quote_in, false)?;
4759 let expected_base_out = quote_in
4760 .checked_mul(orderbook::PRICE_SCALE as u128)
4761 .and_then(|v| v.checked_div(price as u128))
4762 .expect("calculation");
4763 assert_eq!(
4764 quoted_out_ask, expected_base_out,
4765 "quote_exact_in with is_bid=false should return base amount"
4766 );
4767
4768 Ok(())
4769 }
4770
4771 #[test]
4772 fn test_place_auto_creates_pair_post_allegretto() -> Result<()> {
4773 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
4774 let mut exchange = StablecoinExchange::new(&mut storage);
4775 exchange.initialize()?;
4776 let admin = Address::random();
4777 let user = Address::random();
4778
4779 let (base_token, quote_token) =
4781 setup_test_tokens(exchange.storage, admin, user, exchange.address, 100_000_000);
4782
4783 let book_key = compute_book_key(base_token, quote_token);
4785 let book_before = exchange.sload_books(book_key)?;
4786 assert!(book_before.base.is_zero(),);
4787
4788 let mut base = TIP20Token::new(1, exchange.storage);
4790 base.transfer(
4791 user,
4792 ITIP20::transferCall {
4793 to: exchange.address,
4794 amount: U256::from(MIN_ORDER_AMOUNT),
4795 },
4796 )
4797 .expect("Base token transfer failed");
4798
4799 exchange.place(user, base_token, MIN_ORDER_AMOUNT, true, 0)?;
4801
4802 let book_after = exchange.sload_books(book_key)?;
4803 assert_eq!(book_after.base, base_token);
4804
4805 let events = &exchange.storage.events[&exchange.address];
4807 assert_eq!(events.len(), 2);
4808 assert_eq!(
4809 events[0],
4810 StablecoinExchangeEvents::PairCreated(IStablecoinExchange::PairCreated {
4811 key: book_key,
4812 base: base_token,
4813 quote: quote_token,
4814 })
4815 .into_log_data()
4816 );
4817
4818 Ok(())
4819 }
4820
4821 #[test]
4822 fn test_place_flip_auto_creates_pair_post_allegretto() -> Result<()> {
4823 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
4824 let mut exchange = StablecoinExchange::new(&mut storage);
4825 exchange.initialize()?;
4826
4827 let admin = Address::random();
4828 let user = Address::random();
4829
4830 let (base_token, quote_token) =
4832 setup_test_tokens(exchange.storage, admin, user, exchange.address, 100_000_000);
4833
4834 let book_key = compute_book_key(base_token, quote_token);
4836 let book_before = exchange.sload_books(book_key)?;
4837 assert!(book_before.base.is_zero(),);
4838
4839 let mut base = TIP20Token::new(1, exchange.storage);
4841 base.transfer(
4842 user,
4843 ITIP20::transferCall {
4844 to: exchange.address,
4845 amount: U256::from(MIN_ORDER_AMOUNT),
4846 },
4847 )
4848 .expect("Base token transfer failed");
4849
4850 exchange.place_flip(user, base_token, MIN_ORDER_AMOUNT, true, 0, 10)?;
4852
4853 let book_after = exchange.sload_books(book_key)?;
4854 assert_eq!(book_after.base, base_token);
4855
4856 let events = &exchange.storage.events[&exchange.address];
4858 assert_eq!(events.len(), 2);
4859 assert_eq!(
4860 events[0],
4861 StablecoinExchangeEvents::PairCreated(IStablecoinExchange::PairCreated {
4862 key: book_key,
4863 base: base_token,
4864 quote: quote_token,
4865 })
4866 .into_log_data()
4867 );
4868
4869 Ok(())
4870 }
4871
4872 #[test]
4873 fn test_decrement_balance_zeroes_balance_pre_allegro_moderato() -> eyre::Result<()> {
4874 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
4875 let mut exchange = StablecoinExchange::new(&mut storage);
4876
4877 exchange.initialize()?;
4878
4879 let alice = Address::random();
4880 let admin = Address::random();
4881
4882 let mut quote = PathUSD::new(exchange.storage);
4883 quote.initialize(admin)?;
4884 let quote_address = quote.token.address();
4885
4886 let mut base = TIP20Token::new(1, quote.token.storage());
4887 base.initialize("BASE", "BASE", "USD", quote_address, admin, Address::ZERO)?;
4888 base.grant_role_internal(admin, *ISSUER_ROLE)?;
4889 let base_address = base.address();
4890
4891 exchange.create_pair(base_address)?;
4892
4893 let internal_balance = MIN_ORDER_AMOUNT / 2;
4894 exchange.sstore_balances(alice, base_address, internal_balance)?;
4895
4896 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
4897
4898 let tick = 0i16;
4899 let result = exchange.place(alice, base_address, MIN_ORDER_AMOUNT, false, tick);
4900
4901 assert!(result.is_err());
4902 assert_eq!(exchange.balance_of(alice, base_address)?, 0);
4903
4904 Ok(())
4905 }
4906
4907 #[test]
4908 fn test_decrement_balance_preserves_balance_post_allegro_moderato() -> eyre::Result<()> {
4909 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::AllegroModerato);
4910 let mut exchange = StablecoinExchange::new(&mut storage);
4911 exchange.initialize()?;
4912
4913 let alice = Address::random();
4914 let admin = Address::random();
4915
4916 let mut quote = PathUSD::new(exchange.storage);
4917 quote.initialize(admin)?;
4918 let quote_address = quote.token.address();
4919
4920 TIP20Factory::new(quote.token.storage()).set_token_id_counter(U256::from(2))?;
4922
4923 let mut base = TIP20Token::new(1, quote.token.storage());
4924 base.initialize("BASE", "BASE", "USD", quote_address, admin, Address::ZERO)?;
4925 base.grant_role_internal(admin, *ISSUER_ROLE)?;
4926 let base_address = base.address();
4927
4928 exchange.create_pair(base_address)?;
4929
4930 let internal_balance = MIN_ORDER_AMOUNT / 2;
4931 exchange.sstore_balances(alice, base_address, internal_balance)?;
4932
4933 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
4934
4935 let tick = 0i16;
4936 let result = exchange.place(alice, base_address, MIN_ORDER_AMOUNT * 2, false, tick);
4937
4938 assert!(result.is_err());
4939 assert_eq!(exchange.balance_of(alice, base_address)?, internal_balance);
4940
4941 Ok(())
4942 }
4943}