tempo_precompiles/stablecoin_exchange/
mod.rs

1//! Stablecoin DEX types and utilities.
2pub 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
30/// Minimum order size of $10 USD
31pub const MIN_ORDER_AMOUNT: u128 = 10_000_000;
32
33/// Allowed tick spacing for order placement
34pub const TICK_SPACING: i16 = 10;
35
36/// Calculate quote amount using floor division (rounds down)
37/// Pre-Moderato behavior
38fn 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
43/// Calculate quote amount using ceiling division (rounds up)
44/// Post-Moderato behavior
45fn 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
60/// Helper type to easily interact with the `stream_ending_at` array
61type 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    /// Stablecoin exchange address
69    pub fn address(&self) -> Address {
70        self.address
71    }
72
73    /// Initializes the contract
74    ///
75    /// This ensures the [`StablecoinExchange`] isn't empty and prevents state clear.
76    pub fn initialize(&mut self) -> Result<()> {
77        // must ensure the account is not empty, by setting some code
78        self.storage.set_code(
79            self.address,
80            Bytecode::new_legacy(Bytes::from_static(&[0xef])),
81        )
82    }
83
84    /// Read pending order ID
85    fn get_pending_order_id(&mut self) -> Result<u128> {
86        self.pending_order_id()
87    }
88
89    /// Set pending order ID
90    fn set_pending_order_id(&mut self, order_id: u128) -> Result<()> {
91        self.sstore_pending_order_id(order_id)
92    }
93
94    /// Read active order ID
95    fn get_active_order_id(&mut self) -> Result<u128> {
96        self.sload_active_order_id()
97    }
98
99    /// Set active order ID
100    fn set_active_order_id(&mut self, order_id: u128) -> Result<()> {
101        self.sstore_active_order_id(order_id)
102    }
103
104    /// Increment and return the pending order id
105    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    /// Get user's balance for a specific token
112    pub fn balance_of(&mut self, user: Address, token: Address) -> Result<u128> {
113        self.sload_balances(user, token)
114    }
115
116    /// Get MIN_PRICE value based on current hardfork
117    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    /// Get MAX_PRICE value based on current hardfork
126    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    /// Validates that a trading pair exists or creates the pair if
135    /// the chain height is post allegretto hardfork
136    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    /// Fetch order from storage. If the order is currently pending or filled, this function returns
148    /// `StablecoinExchangeError::OrderDoesNotExist`
149    pub fn get_order(&mut self, order_id: u128) -> Result<Order> {
150        let order = self.sload_orders(order_id)?;
151
152        // If the order is not filled and currently active
153        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    /// Set user's balance for a specific token
161    fn set_balance(&mut self, user: Address, token: Address, amount: u128) -> Result<()> {
162        self.sstore_balances(user, token, amount)
163    }
164
165    /// Add to user's balance
166    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    /// Subtract from user's balance
178    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    /// Emit the appropriate OrderFilled event based on hardfork
184    /// Pre-Allegretto: emits OrderFilled (without taker)
185    /// Post-Allegretto: emits OrderFilled (with taker)
186    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    /// Transfer tokens, accounting for pathUSD
222    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    /// Transfer tokens from user, accounting for pathUSD
244    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    /// Decrement user's internal balance or transfer from external wallet
268    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            // Post allegro-moderato hardfork, set balance after transfer from
283            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        // Find and validate the trade route (book keys + direction for each hop)
302        let route = self.find_trade_path(token_in, token_out)?;
303
304        // Execute quotes backwards from output to input
305        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        // Find and validate the trade route (book keys + direction for each hop)
320        let route = self.find_trade_path(token_in, token_out)?;
321
322        // Execute quotes for each hop using precomputed book keys and directions
323        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        // Find and validate the trade route (book keys + direction for each hop)
340        let route = self.find_trade_path(token_in, token_out)?;
341
342        // Deduct input tokens from sender (only once, at the start)
343        self.decrement_balance_or_transfer_from(sender, token_in, amount_in)?;
344
345        // Execute swaps for each hop - intermediate balances are transitory
346        let mut amount = amount_in;
347        for (book_key, base_for_quote) in route {
348            // Fill orders for this hop - no min check on intermediate hops
349            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        // Check final output meets minimum requirement
357        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        // Find and validate the trade route (book keys + direction for each hop)
375        let route = self.find_trade_path(token_in, token_out)?;
376
377        // Work backwards from output to calculate input needed - intermediate amounts are TRANSITORY
378        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        // Deduct input tokens ONCE at end
403        self.decrement_balance_or_transfer_from(sender, token_in, amount)?;
404
405        // Transfer only final output ONCE at end
406        self.transfer(token_out, sender, amount_out)?;
407
408        Ok(amount)
409    }
410
411    /// Generate deterministic key for token pair
412    pub fn pair_key(&self, token_a: Address, token_b: Address) -> B256 {
413        compute_book_key(token_a, token_b)
414    }
415
416    /// Get price level information
417    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    /// Get active order ID
424    pub fn active_order_id(&mut self) -> Result<u128> {
425        self.sload_active_order_id()
426    }
427
428    /// Get pending order ID
429    pub fn pending_order_id(&mut self) -> Result<u128> {
430        self.sload_pending_order_id()
431    }
432
433    /// Get orderbook by pair key
434    pub fn books(&mut self, pair_key: B256) -> Result<Orderbook> {
435        self.sload_books(pair_key)
436    }
437
438    /// Get all book keys
439    pub fn get_book_keys(&mut self) -> Result<Vec<B256>> {
440        self.sload_book_keys()
441    }
442
443    /// Convert scaled price to relative tick
444    /// Post-Moderato: validates price is within [MIN_PRICE, MAX_PRICE]
445    /// Pre-Moderato: no validation (legacy behavior)
446    pub fn price_to_tick(&self, price: u32) -> Result<i16> {
447        if self.storage.spec().is_moderato() {
448            // Post-Moderato: validate price bounds
449            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        // Validate that base is a TIP20 token (only after Moderato hardfork)
457        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        // Emit PairCreated event
476        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    /// Place a limit order on the orderbook
490    ///
491    /// Only supports placing an order on a pair between a token and its quote token.
492    /// The order is queued in the pending queue and will be processed at end of block.
493    ///
494    /// # Arguments
495    /// * `token` - The token to trade (not the quote token)
496    /// * `amount` - Order amount in the token
497    /// * `is_bid` - True for buy orders (using quote token to buy token), false for sell orders
498    /// * `tick` - Price tick: (price - 1) * 1000, where price is denominated in the quote token
499    ///
500    /// # Returns
501    /// The assigned order ID
502    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        // Compute book_key from token pair
513        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        // Validate tick is within bounds
519        if !(MIN_TICK..=MAX_TICK).contains(&tick) {
520            return Err(StablecoinExchangeError::tick_out_of_bounds(tick).into());
521        }
522
523        // Post allegretto, enforce that the tick adheres to tick spacing
524        if self.storage.spec().is_allegretto() && tick % TICK_SPACING != 0 {
525            return Err(StablecoinExchangeError::invalid_tick().into());
526        }
527
528        // Validate order amount meets minimum requirement
529        if amount < MIN_ORDER_AMOUNT {
530            return Err(StablecoinExchangeError::below_minimum_order_size(amount).into());
531        }
532
533        // Calculate escrow amount and token based on order side
534        let (escrow_token, escrow_amount) = if is_bid {
535            // For bids, escrow quote tokens based on price
536            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            // For asks, escrow base tokens
545            (token, amount)
546        };
547
548        // Debit from user's balance or transfer from wallet
549        self.decrement_balance_or_transfer_from(sender, escrow_token, escrow_amount)?;
550
551        // Create the order
552        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        // Store in pending queue. Orders are stored as a DLL at each tick level and are initially
560        // stored without a prev or next pointer. This is considered a "pending" order. Once `execute_block` is called, orders are
561        // linked and then considered "active"
562        self.sstore_orders(order_id, order)?;
563
564        // Emit OrderPlaced event
565        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    /// Place a flip order that auto-flips when filled
582    ///
583    /// Flip orders automatically create a new order on the opposite side when completely filled.
584    /// For bids: flip_tick must be > tick
585    /// For asks: flip_tick must be < tick
586    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        // Compute book_key from token pair
598        let book_key = compute_book_key(token, quote_token);
599
600        // Check book existence (only after Moderato hardfork)
601        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        // Validate tick and flip_tick are within bounds
607        if !(MIN_TICK..=MAX_TICK).contains(&tick) {
608            return Err(StablecoinExchangeError::tick_out_of_bounds(tick).into());
609        }
610
611        // Post allegretto, enforce that the tick adheres to tick spacing
612        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        // Post allegretto, enforce that the tick adheres to tick spacing
621        if self.storage.spec().is_allegretto() && flip_tick % TICK_SPACING != 0 {
622            return Err(StablecoinExchangeError::invalid_flip_tick().into());
623        }
624
625        // Validate flip_tick relationship to tick based on order side
626        if (is_bid && flip_tick <= tick) || (!is_bid && flip_tick >= tick) {
627            return Err(StablecoinExchangeError::invalid_flip_tick().into());
628        }
629
630        // Validate order amount meets minimum requirement
631        if amount < MIN_ORDER_AMOUNT {
632            return Err(StablecoinExchangeError::below_minimum_order_size(amount).into());
633        }
634
635        // Calculate escrow amount and token based on order side
636        let (escrow_token, escrow_amount) = if is_bid {
637            // For bids, escrow quote tokens based on price
638            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            // For asks, escrow base tokens
647            (token, amount)
648        };
649
650        // Debit from user's balance or transfer from wallet
651        self.decrement_balance_or_transfer_from(sender, escrow_token, escrow_amount)?;
652
653        // Create the flip order
654        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        // Store in pending queue
659        self.sstore_orders(order_id, order)?;
660
661        // Emit FlipOrderPlaced event
662        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    /// Process all pending orders into the active orderbook
680    ///
681    /// Only callable by the protocol via system transaction (sender must be Address::ZERO)
682    pub fn execute_block(&mut self, sender: Address) -> Result<()> {
683        // Only protocol can call this
684        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    /// Process a single pending order into the active orderbook
708    fn process_pending_order(&mut self, order_id: u128) -> Result<()> {
709        let order = self.sload_orders(order_id)?;
710
711        // If the order is already canceled, return early
712        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    /// Partially fill an order with the specified amount.
751    /// Fill amount is denominated in base token
752    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        // Update order remaining amount
763        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        // Update price level total liquidity
786        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        // Emit OrderFilled event for partial fill
795        self.emit_order_filled(order.order_id(), order.maker(), taker, fill_amount, true)?;
796
797        Ok(amount_out)
798    }
799
800    /// Fill an order and delete from storage. Returns the next best order and price level.
801    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        // Emit OrderFilled event for complete fill
829        self.emit_order_filled(order.order_id(), order.maker(), taker, fill_amount, false)?;
830
831        if order.is_flip() {
832            // Create a new flip order with flipped side and swapped ticks
833            // Bid becomes Ask, Ask becomes Bid
834            // The current tick becomes the new flip_tick, and flip_tick becomes the new tick
835            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        // Delete the filled order
846        self.clear_orders(order.order_id())?;
847
848        // Advance tick if liquidity is exhausted
849        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                // Update best_tick when tick is exhausted
865                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                // No more liquidity at better prices - return None to signal completion
876                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            // If there are subsequent orders at tick, advance to next order
885            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    /// Fill orders for exact output amount, post moderato hardfork
911    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                // Post-Moderato: set to 0 to avoid rounding errors
956                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    /// Fill orders for exact output amount, pre moderato hardfork
992    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            // Pre-Moderato: Check maxIn on each iteration for consensus compatibility
1018            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                // Pre-Moderato: always recalculate
1036                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    /// Fill orders with exact amount in, post Moderato hardfork
1056    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                // Post-Moderato: set to 0 to avoid rounding errors
1096                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    /// Fill orders with exact amount in, pre Moderato hardfork
1139    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            // Pre-Moderato: old behavior with unit mismatch
1153            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                // Pre-Moderato: always subtract order.remaining()
1170                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        // Pre-Moderato: Check min out after filling the full amount in
1184        if total_amount_out < min_amount_out {
1185            return Err(StablecoinExchangeError::insufficient_output().into());
1186        }
1187
1188        Ok(total_amount_out)
1189    }
1190
1191    /// Helper function to get best tick from orderbook
1192    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    /// Cancel an order and refund tokens to maker
1213    /// Only the order maker can cancel their own order
1214    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        // Check if the order is still pending (not yet in active orderbook)
1230        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    /// Cancel a pending order (not yet in the active orderbook)
1242    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        // For bids, calculate quote amount to refund; for asks, refund base amount
1251        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        // Credit remaining tokens to user's withdrawable balance
1259        self.increment_balance(order.maker(), token, refund_amount)?;
1260
1261        // Clear the order from storage
1262        self.clear_orders(order.order_id())?;
1263
1264        // Emit OrderCancelled event
1265        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    /// Cancel an active order (already in the orderbook)
1275    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        // Update linked list
1280        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        // Update level liquidity
1293        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 this was the last order at this tick, clear the bitmap bit
1300        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                // If this was the best tick, update it
1306                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        // Refund tokens to maker
1336        let orderbook = self.sload_books(order.book_key())?;
1337        if order.is_bid() {
1338            // Bid orders are in quote token, refund quote amount
1339            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            // Ask orders are in base token, refund base amount
1348            self.increment_balance(order.maker(), orderbook.base, order.remaining())?;
1349        }
1350
1351        // Clear the order from storage
1352        self.clear_orders(order.order_id())?;
1353
1354        // Emit OrderCancelled event
1355        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    /// Withdraw tokens from exchange balance
1365    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    /// Quote exact output amount without executing trades
1377    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        // Check for no liquidity: i16::MIN means no bids, i16::MAX means no asks
1388        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 no liquidity at this level, move to next tick
1398            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                // For bids: remaining_out is in quote, amount_in is in base
1418                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                // For asks: remaining_out is in base, amount_in is in quote
1430                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 we exhausted this level or filled our requirement, move to next tick
1459            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    /// Find the trade path between two tokens
1481    /// Returns a vector of (book_key, base_for_quote) tuples for each hop
1482    /// Also validates that all pairs exist
1483    fn find_trade_path(
1484        &mut self,
1485        token_in: Address,
1486        token_out: Address,
1487    ) -> Result<Vec<(B256, bool)>> {
1488        // Cannot trade same token
1489        if token_in == token_out {
1490            return Err(StablecoinExchangeError::identical_tokens().into());
1491        }
1492
1493        // Validate that both tokens are TIP20 tokens
1494        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        // Check if direct or reverse pair exists
1501        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        // Multi-hop: Find LCA and build path
1509        let path_in = self.find_path_to_root(token_in)?;
1510        let path_out = self.find_path_to_root(token_out)?;
1511
1512        // Find the lowest common ancestor (LCA)
1513        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        // Build the trade path: token_in -> ... -> LCA -> ... -> token_out
1524        let mut trade_path = Vec::new();
1525
1526        // Add path from token_in up to and including LCA
1527        for token in &path_in {
1528            trade_path.push(*token);
1529            if *token == lca {
1530                break;
1531            }
1532        }
1533
1534        // Add path from LCA down to token_out (excluding LCA itself)
1535        let lca_to_out: Vec<Address> = path_out
1536            .iter()
1537            .take_while(|&&t| t != lca)
1538            .copied()
1539            .collect();
1540
1541        // Reverse to get path from LCA to token_out
1542        trade_path.extend(lca_to_out.iter().rev());
1543
1544        self.validate_and_build_route(&trade_path)
1545    }
1546
1547    /// Validates that all pairs in the path exist and returns book keys with direction info
1548    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            // Validate pair exists
1559            if orderbook.base.is_zero() {
1560                return Err(StablecoinExchangeError::pair_does_not_exist().into());
1561            }
1562
1563            // Determine direction
1564            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    /// Find the path from a token to the root (PathUSD)
1573    /// Returns a vector of addresses starting with the token and ending with PathUSD
1574    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    /// Quote exact input amount without executing trades
1586    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        // Check for no liquidity: i16::MIN means no bids, i16::MAX means no asks
1598        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 no liquidity at this level, move to next tick
1608            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            // Compute (fill_amount, amount_out_tick, amount_consumed) based on hardfork
1627            let (fill_amount, amount_out_tick, amount_consumed) =
1628                if self.storage.spec().is_allegretto() {
1629                    // Post-allegretto: logic accounts for `is_bid`
1630                    if is_bid {
1631                        // For bids: remaining_in is base, amount_out is quote
1632                        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                        // For asks: remaining_in is quote, amount_out is base
1640                        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                    // Pre-allegretto: doesn't account for `is_bid`
1653                    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 we exhausted this level, move to next tick
1669            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        // Initialize quote token (PathUSD)
1757        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        // Grant issuer role to admin for quote token
1764        quote
1765            .token
1766            .grant_role_internal(admin, *ISSUER_ROLE)
1767            .unwrap();
1768
1769        // Initialize base token
1770        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 tokens for user
1777        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        // Post-Moderato: price validation should be enforced
1812        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
1813        let exchange = StablecoinExchange::new(&mut storage);
1814
1815        // Valid prices should succeed
1816        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        // Out of bounds prices should fail
1827        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        // Pre-Moderato: no price validation (legacy behavior)
1847        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1848        let exchange = StablecoinExchange::new(&mut storage);
1849
1850        // Valid prices should succeed
1851        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        // Out of bounds prices should also succeed (legacy behavior)
1862        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        // Floor division rounds DOWN
1880        // amount = 100, tick = 1 means price = 100001
1881        // 100 * 100001 / 100000 = 10000100 / 100000 = 100.001
1882        // Should round down to 100
1883        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        // Another test case
1890        let amount2 = 999u128;
1891        let tick2 = 5i16; // price = 100005
1892        let result2 = calculate_quote_amount_floor(amount2, tick2).unwrap();
1893        // 999 * 100005 / 100000 = 99904995 / 100000 = 999.04995 -> should be 999
1894        assert_eq!(result2, 999, "Expected 999 (rounded down from 999.04995)");
1895
1896        // Test with no remainder (should work the same)
1897        let amount3 = 100000u128;
1898        let tick3 = 0i16; // price = 100000
1899        let result3 = calculate_quote_amount_floor(amount3, tick3).unwrap();
1900        // 100000 * 100000 / 100000 = 100000 (exact, no rounding)
1901        assert_eq!(result3, 100000, "Exact division should remain exact");
1902    }
1903
1904    #[test]
1905    fn test_calculate_quote_amount_ceil() {
1906        // Ceiling division rounds UP
1907        // amount = 100, tick = 1 means price = 100001
1908        // 100 * 100001 / 100000 = 10000100 / 100000 = 100.001
1909        // Should round up to 101
1910        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        // Another test case
1917        let amount2 = 999u128;
1918        let tick2 = 5i16; // price = 100005
1919        let result2 = calculate_quote_amount_ceil(amount2, tick2).unwrap();
1920        // 999 * 100005 / 100000 = 99904995 / 100000 = 999.04995 -> should be 1000
1921        assert_eq!(result2, 1000, "Expected 1000 (rounded up from 999.04995)");
1922
1923        // Test with no remainder (should work the same)
1924        let amount3 = 100000u128;
1925        let tick3 = 0i16; // price = 100000
1926        let result3 = calculate_quote_amount_ceil(amount3, tick3).unwrap();
1927        // 100000 * 100000 / 100000 = 100000 (exact, no rounding needed)
1928        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        // Test with Moderato hardfork (validation should be enforced)
1934        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        // Test with Adagio (pre-Moderato) - validation is enforced in all hardforks
1966        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        // Try to place an order without creating the pair first
1987        // This validation is enforced both pre and post Moderato
1988        let result = exchange.place(alice, base_token, min_order_amount, true, tick);
1989
1990        // Should fail with pair_does_not_exist error
1991        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        // Create the pair
2023        exchange
2024            .create_pair(base_token)
2025            .expect("Could not create pair");
2026
2027        // Try to place an order below minimum amount
2028        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        // Setup tokens with enough balance for the escrow
2052        let (base_token, quote_token) = setup_test_tokens(
2053            exchange.storage,
2054            admin,
2055            alice,
2056            exchange.address,
2057            expected_escrow,
2058        );
2059
2060        // Create the pair before placing orders
2061        exchange
2062            .create_pair(base_token)
2063            .expect("Could not create pair");
2064
2065        // Place the bid order
2066        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        // Verify the order was stored correctly
2075        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        // Verify the order is not yet in the active orderbook
2086        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        // Verify balance was reduced by the escrow amount
2093        {
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            // Verify exchange received the tokens
2100            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; // Use positive tick to avoid conversion issues
2119
2120        // Setup tokens with enough base token balance for the order
2121        let (base_token, quote_token) = setup_test_tokens(
2122            exchange.storage,
2123            admin,
2124            alice,
2125            exchange.address,
2126            min_order_amount,
2127        );
2128        // Create the pair before placing orders
2129        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) // is_bid = false for ask
2135            .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        // Verify the order was stored correctly
2142        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        // Verify balance was reduced by the escrow amount
2159        {
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); // All tokens should be escrowed
2164
2165            // Verify exchange received the base tokens
2166            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        // Create the pair
2200        exchange
2201            .create_pair(base_token)
2202            .expect("Could not create pair");
2203
2204        // Try to place a flip order below minimum amount
2205        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        // Test with Moderato hardfork (validation should be enforced)
2217        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        // Try to place a flip order without creating the pair first
2239        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        // Test with Adagio (pre-Moderato) - validation should not be enforced
2252        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        // Try to place a flip order without creating the pair first
2274        // Pre-Moderato, the book existence check is skipped, so the order is accepted
2275        // (it would only fail later during execute_block when trying to process it)
2276        let result =
2277            exchange.place_flip(alice, base_token, min_order_amount, true, tick, flip_tick);
2278
2279        // Pre-Moderato: order should be accepted (placed in pending queue)
2280        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; // Must be > tick for bid flip orders
2296
2297        // Calculate escrow amount needed for bid
2298        let price = orderbook::tick_to_price(tick);
2299        let expected_escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2300
2301        // Setup tokens with enough balance for the escrow
2302        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        // Verify the order was stored correctly
2322        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        // Verify the order is not yet in the active orderbook
2334        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        // Verify balance was reduced by the escrow amount
2341        {
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            // Verify exchange received the tokens
2348            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        // Calculate escrow amount needed for bid
2369        let price = orderbook::tick_to_price(tick);
2370        let expected_escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2371
2372        // Setup tokens
2373        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        // Place the bid order
2386        let order_id = exchange
2387            .place(alice, base_token, min_order_amount, true, tick)
2388            .expect("Place bid order should succeed");
2389
2390        // Verify order was placed and tokens were escrowed
2391        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        // Cancel the pending order
2408        exchange
2409            .cancel(alice, order_id)
2410            .expect("Cancel pending order should succeed");
2411
2412        // Verify order was deleted
2413        let cancelled_order = exchange.sload_orders(order_id)?;
2414        assert_eq!(cancelled_order.maker(), Address::ZERO);
2415
2416        // Verify tokens were refunded to user's internal balance
2417        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        // Calculate escrow amount needed for both orders
2434        let price = orderbook::tick_to_price(tick);
2435        let expected_escrow = (min_order_amount * price as u128) / orderbook::PRICE_SCALE as u128;
2436
2437        // Setup tokens with enough balance for two orders
2438        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        // Create the pair
2447        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        // Verify orders are in pending state
2464        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        // Verify tick level is empty before execute_block
2472        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        // Execute block and assert that orders have been linked
2479        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        // Assert tick level is updated
2494        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        // Verify orderbook best bid tick is updated
2500        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        // Setup tokens
2530        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        // Place the bid order and cancel
2542        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        // Get balances before withdrawal
2553        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        // Verify wallet balances changed correctly
2559        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        // Alice has 0 balance on the exchange
2594        assert_eq!(exchange.balance_of(alice, quote_token)?, 0);
2595
2596        // Try to withdraw more than balance
2597        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        // Calculate expected amount_out based on tick price
2685        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        // Alice places a bid: willing to BUY base using quote
2714        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        // Quote: sell base to get quote
2724        // Should match against Alice's bid (buyer of base)
2725        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        // Place a flip bid order
2871        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        // Assert that the order has filled
2888        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        // Setup tokens
2916        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        // Create the pair
2925        let key = exchange
2926            .create_pair(base_token)
2927            .expect("Could not create pair");
2928
2929        // Verify PairCreated event was emitted
2930        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        // Setup tokens
2954        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    /// Helper to verify a single hop in a route
2974    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        // Setup: PathUSD <- USDC <- TokenA
3006        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        // Find path from TokenA to root
3030        let path = exchange
3031            .find_path_to_root(token_a_addr)
3032            .expect("Failed to find path");
3033
3034        // Expected: [TokenA, USDC, PathUSD]
3035        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        // Trading same token should error with IdenticalTokens
3062        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        // Setup: PathUSD <- Token (direct pair)
3083        let (token, path_usd) = setup_test_tokens(
3084            exchange.storage,
3085            admin,
3086            user,
3087            exchange.address,
3088            min_order_amount,
3089        );
3090
3091        // Create the pair first
3092        exchange.create_pair(token).expect("Failed to create pair");
3093
3094        // Trade token -> path_usd (direct pair)
3095        let route = exchange
3096            .find_trade_path(token, path_usd)
3097            .expect("Should find direct pair");
3098
3099        // Expected: 1 hop (token -> path_usd)
3100        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        // Setup: PathUSD <- Token
3123        let (token, path_usd) = setup_test_tokens(
3124            exchange.storage,
3125            admin,
3126            user,
3127            exchange.address,
3128            min_order_amount,
3129        );
3130
3131        // Create the pair first
3132        exchange.create_pair(token).expect("Failed to create pair");
3133
3134        // Trade path_usd -> token (reverse direction)
3135        let route = exchange
3136            .find_trade_path(path_usd, token)
3137            .expect("Should find reverse pair");
3138
3139        // Expected: 1 hop (path_usd -> token)
3140        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        // Setup: PathUSD <- USDC
3161        //        PathUSD <- EURC
3162        // (USDC and EURC are siblings, both have PathUSD as quote)
3163        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        // Create pairs first
3186        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        // Trade USDC -> EURC should go through PathUSD
3194        let route = exchange
3195            .find_trade_path(usdc_addr, eurc_addr)
3196            .expect("Should find path");
3197
3198        // Expected: 2 hops (USDC -> PathUSD, PathUSD -> EURC)
3199        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        // Setup: PathUSD <- USDC
3229        //        PathUSD <- EURC
3230        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        // Create pairs
3253        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        // Setup tokens and roles
3261        {
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        // Approve exchange
3303        {
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        // Place orders to provide liquidity at 1:1 rate (tick 0)
3342        // For trade USDC -> PathUSD -> EURC:
3343        // - First hop needs: bid on USDC (someone buying USDC with PathUSD)
3344        // - Second hop needs: ask on EURC (someone selling EURC for PathUSD)
3345
3346        // USDC bid: buy USDC with PathUSD
3347        exchange
3348            .place(alice, usdc_addr, min_order_amount * 5, true, 0)
3349            .expect("Failed to place USDC bid order");
3350
3351        // EURC ask: sell EURC for PathUSD
3352        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        // Quote multi-hop: USDC -> PathUSD -> EURC
3361        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        // With 1:1 rates at each hop, output should equal input
3367        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        // Setup: PathUSD <- USDC
3386        //        PathUSD <- EURC
3387        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        // Create pairs and setup (same as previous test)
3410        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        // Place orders at 1:1 rate
3485        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        // Quote multi-hop for exact output: USDC -> PathUSD -> EURC
3497        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        // With 1:1 rates at each hop, input should equal output
3503        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        // Setup: PathUSD <- USDC <- EURC
3523        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        // Setup alice as liquidity provider
3553        {
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        // Setup bob as trader
3616        {
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        // Place liquidity orders at 1:1
3636        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        // Check bob's balances before swap
3647        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        // Execute multi-hop swap: USDC -> PathUSD -> EURC
3657        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, // min_amount_out
3661            )
3662            .expect("Should execute multi-hop swap");
3663
3664        // Check bob's balances after swap
3665        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        // Verify bob spent USDC and received EURC
3675        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        // Verify bob has ZERO PathUSD (intermediate token should be transitory)
3687        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        // Setup: PathUSD <- USDC <- EURC
3722        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        // Setup alice as liquidity provider
3752        {
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        // Setup bob as trader
3820        {
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        // Place liquidity orders at 1:1
3841        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        // Check bob's balances before swap
3852        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        // Execute multi-hop swap: USDC -> PathUSD -> EURC (exact output)
3862        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, // max_amount_in
3869        )?;
3870
3871        // Check bob's balances after swap
3872        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        // Verify bob spent USDC and received exact EURC
3882        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        // Verify bob has ZERO PathUSD (intermediate token should be transitory)
3894        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        // Init path USD
3923        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        // Create EUR token with PATH USD as quote (valid non-USD token)
3936        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        // Test: create_pair should reject non-USD token (EUR token has EUR currency)
3953        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        // Test with Moderato hardfork (validation should be enforced)
4010        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
4011
4012        let admin = Address::random();
4013        // Init PATH USD
4014        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        // Test: create_pair should reject non-TIP20 address (random address without TIP20 prefix)
4030        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        // Test with Adagio (pre-Moderato) - validation should not be enforced
4092        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
4093
4094        let admin = Address::random();
4095        // Init Linking USD
4096        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        // Test: create_pair should not reject non-TIP20 address pre-Moderato
4112        // This will fail with a different error (trying to read quote_token from non-TIP20)
4113        // but NOT the InvalidQuoteToken error from the is_tip20 check
4114        let non_tip20_address = Address::random();
4115        let result = exchange.create_pair(non_tip20_address);
4116
4117        // Should fail but not with InvalidQuoteToken error (that check is skipped pre-Moderato)
4118        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        // Pre-Moderato: old behavior with unit mismatch causes MaxInputExceeded
4132        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        // Pre-Moderato: should fail with MaxInputExceeded due to unit mismatch
4163        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        // Post-Moderato: new behavior with correct unit conversion
4187        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        // Verify Bob got exactly the quote amount requested
4226        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        // Pre-Moderato: old behavior treats quote amount as base amount
4236        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        // Pre-Moderato: returns incorrect amount (treats quote as base)
4274        // It will return amount_in_quote (5M) instead of the correct converted amount
4275        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        // Post-Moderato: new behavior with correct unit conversion
4287        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        // Post-Moderato: returns correct converted amount
4325        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        // Test that fill_order properly clears the prev pointer when advancing to the next order
4336        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        // Give bob base tokens and carol quote tokens
4350        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        // Place two ask orders at the same tick: Order 1 (alice), Order 2 (bob)
4356        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        // Verify linked list is set up correctly
4364        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        // Swap to fill order1 completely
4370        let swap_amount = order1_amount;
4371        exchange.swap_exact_amount_out(carol, quote_token, base_token, swap_amount, u128::MAX)?;
4372
4373        // After filling order1, order2 should be the new head with prev = 0
4374        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        // Use different ticks for bids (100, 90) and asks (50, 60)
4396        let (bid_tick_1, bid_tick_2) = (100_i16, 90_i16); // (best, second best)
4397        let (ask_tick_1, ask_tick_2) = (50_i16, 60_i16); // (best, second best)
4398
4399        // Calculate escrow for all orders
4400        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        // Place bid orders at two different ticks
4417        exchange.place(alice, base_token, amount, true, bid_tick_1)?;
4418        exchange.place(alice, base_token, amount, true, bid_tick_2)?;
4419
4420        // Place ask orders at two different ticks
4421        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        // Verify initial best ticks
4435        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        // Fill all bids at tick 100 (bob sells base)
4440        exchange.set_balance(bob, base_token, amount)?;
4441        exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
4442        // Verify best_bid_tick moved to tick 90, best_ask_tick unchanged
4443        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        // Fill remaining bid at tick 90
4448        exchange.set_balance(bob, base_token, amount)?;
4449        exchange.swap_exact_amount_in(bob, base_token, quote_token, amount, 0)?;
4450        // Verify best_bid_tick is now i16::MIN, best_ask_tick unchanged
4451        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        // Fill all asks at tick 50 (bob buys base)
4456        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        // Verify best_ask_tick moved to tick 60, best_bid_tick unchanged
4461        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); // (best, second best)
4479        let (ask_tick_1, ask_tick_2) = (50_i16, 60_i16); // (best, second best)
4480
4481        // Calculate escrow for 3 bid orders (2 at tick 100, 1 at tick 90)
4482        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        // Place 2 bid orders at tick 100, 1 at tick 90
4499        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        // Place 2 ask orders at tick 50 and tick 60
4504        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        // Verify initial best ticks
4518        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        // Cancel one bid at tick 100
4523        exchange.cancel(alice, bid_order_1)?;
4524        // Verify best_bid_tick remains 100, best_ask_tick unchanged
4525        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        // Cancel remaining bid at tick 100
4530        exchange.cancel(alice, bid_order_2)?;
4531        // Verify best_bid_tick moved to 90, best_ask_tick unchanged
4532        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        // Cancel ask at tick 50
4537        exchange.cancel(alice, ask_order_1)?;
4538        // Verify best_ask_tick moved to 60, best_bid_tick unchanged
4539        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        // Cancel bid at tick 90
4544        exchange.cancel(alice, bid_order_3)?;
4545        // Verify best_bid_tick is now i16::MIN, best_ask_tick unchanged
4546        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        // Cancel ask at tick 60
4551        exchange.cancel(alice, ask_order_2)?;
4552        // Verify best_ask_tick is now i16::MAX, best_bid_tick unchanged
4553        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        // Give alice base tokens
4579        mint_and_approve_token(
4580            exchange.storage,
4581            1,
4582            admin,
4583            alice,
4584            exchange.address,
4585            1_000_000_000,
4586        );
4587
4588        // Test invalid tick spacing
4589        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        // Test valid tick spacing
4599        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        // Give alice base tokens
4625        mint_and_approve_token(
4626            exchange.storage,
4627            1,
4628            admin,
4629            alice,
4630            exchange.address,
4631            1_000_000_000,
4632        );
4633
4634        // Test invalid tick spacing
4635        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        // Test valid tick spacing
4653        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        // Calculate escrow for bid order (quote needed to buy `amount` base)
4729        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        // Place a bid order (alice wants to buy base with quote)
4738        exchange.place(alice, base_token, amount, true, tick)?;
4739        exchange.execute_block(Address::ZERO)?;
4740
4741        // Test is_bid == true: base -> quote
4742        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        // Place an ask order (alice wants to sell base for quote)
4753        exchange.place(alice, base_token, amount, false, tick)?;
4754        exchange.execute_block(Address::ZERO)?;
4755
4756        // Test is_bid == false: quote -> base
4757        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        // Setup tokens
4780        let (base_token, quote_token) =
4781            setup_test_tokens(exchange.storage, admin, user, exchange.address, 100_000_000);
4782
4783        // Before placing order, verify pair doesn't exist
4784        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        // Transfer tokens to exchange first
4789        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        // Place an order which should also create the pair
4800        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        // Verify PairCreated event was emitted (along with OrderPlaced)
4806        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        // Setup tokens
4831        let (base_token, quote_token) =
4832            setup_test_tokens(exchange.storage, admin, user, exchange.address, 100_000_000);
4833
4834        // Before placing flip order, verify pair doesn't exist
4835        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        // Transfer tokens to exchange first
4840        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        // Place a flip order which should also create the pair
4851        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        // Verify PairCreated event was emitted (along with FlipOrderPlaced)
4857        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        // Set token_id_counter to 2 so that token id 1 is considered valid
4921        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}