Skip to main content

tempo_precompiles/tip_fee_manager/
amm.rs

1use crate::{
2    error::{Result, TempoPrecompileError},
3    storage::Handler,
4    tip_fee_manager::{ITIPFeeAMM, TIPFeeAMMError, TIPFeeAMMEvent, TipFeeManager},
5    tip20::{ITIP20, TIP20Token, validate_usd_currency},
6};
7use alloy::{
8    primitives::{Address, B256, U256, keccak256, uint},
9    sol_types::SolValue,
10};
11use tempo_precompiles_macros::Storable;
12
13/// Fee multiplier for fee swaps: 0.9970 scaled by 10000 (30 bps fee).
14pub const M: U256 = uint!(9970_U256);
15/// Fee multiplier for rebalance swaps: 0.9985 scaled by 10000.
16pub const N: U256 = uint!(9985_U256);
17/// Scale factor for fixed-point AMM arithmetic (10000).
18pub const SCALE: U256 = uint!(10000_U256);
19/// Minimum liquidity locked permanently when initializing a pool.
20pub const MIN_LIQUIDITY: U256 = uint!(1000_U256);
21
22/// Computes the output amount for a fee swap: `amount_in * M / SCALE`.
23///
24/// # Errors
25/// - `UnderOverflow` — multiplication of `amount_in * M` overflows
26#[inline]
27pub fn compute_amount_out(amount_in: U256) -> Result<U256> {
28    amount_in
29        .checked_mul(M)
30        .map(|product| product / SCALE)
31        .ok_or(TempoPrecompileError::under_overflow())
32}
33
34/// AMM pool reserves for a user-token / validator-token pair.
35#[derive(Debug, Clone, Default, Storable)]
36pub struct Pool {
37    /// Reserve of the user's fee token.
38    pub reserve_user_token: u128,
39    /// Reserve of the validator's fee token.
40    pub reserve_validator_token: u128,
41}
42
43impl From<Pool> for ITIPFeeAMM::Pool {
44    fn from(value: Pool) -> Self {
45        Self {
46            reserveUserToken: value.reserve_user_token,
47            reserveValidatorToken: value.reserve_validator_token,
48        }
49    }
50}
51
52/// Identifies a directional token pair in the fee AMM.
53#[derive(Debug, Clone, PartialEq, Eq, Hash, Storable)]
54pub struct PoolKey {
55    /// The fee token chosen by the user (transaction sender).
56    pub user_token: Address,
57    /// The fee token chosen by the validator (block producer).
58    pub validator_token: Address,
59}
60
61// TODO(rusowsky): remove this and create a read-only wrapper that is callable from read-only ctx with db access
62impl Pool {
63    /// Decodes a [`Pool`] directly from a raw EVM storage slot value.
64    pub fn decode_from_slot(slot_value: U256) -> Self {
65        use crate::storage::{LayoutCtx, Storable, packing::PackedSlot};
66
67        // NOTE: fine to expect, as `StorageOps` on `PackedSlot` are infallible
68        Self::load(&PackedSlot(slot_value), U256::ZERO, LayoutCtx::FULL)
69            .expect("unable to decode Pool from slot")
70    }
71}
72
73impl PoolKey {
74    /// Creates a new pool key from user and validator token addresses.
75    /// This key uniquely identifies a trading pair in the AMM.
76    pub fn new(user_token: Address, validator_token: Address) -> Self {
77        Self {
78            user_token,
79            validator_token,
80        }
81    }
82
83    /// Generates a unique pool ID by hashing the token pair addresses.
84    /// Uses keccak256 to create a deterministic identifier for this pool.
85    pub fn get_id(&self) -> B256 {
86        keccak256((self.user_token, self.validator_token).abi_encode())
87    }
88}
89
90impl TipFeeManager {
91    /// Returns the deterministic pool ID for a directional token pair. Note that the pool id is
92    /// order-dependent: `(A, B)` produces a different ID than `(B, A)`.
93    pub fn pool_id(&self, user_token: Address, validator_token: Address) -> B256 {
94        PoolKey::new(user_token, validator_token).get_id()
95    }
96
97    /// Returns the [`Pool`] reserves for the given user/validator token pair.
98    pub fn get_pool(&self, call: ITIPFeeAMM::getPoolCall) -> Result<Pool> {
99        let pool_id = self.pool_id(call.userToken, call.validatorToken);
100        self.pools[pool_id].read()
101    }
102
103    /// Checks that the pool identified by `pool_id` has enough validator-token reserves for the
104    /// fee swap of `max_amount` and returns the required output amount as `u128`.
105    ///
106    /// # Errors
107    /// - `InsufficientLiquidity` — pool validator-token reserve is below the required output
108    /// - `UnderOverflow` — output amount exceeds `u128`
109    pub fn check_sufficient_liquidity(&mut self, pool_id: B256, max_amount: U256) -> Result<u128> {
110        let amount_out_needed = compute_amount_out(max_amount)?;
111        let pool = self.pools[pool_id].read()?;
112
113        if amount_out_needed > U256::from(pool.reserve_validator_token) {
114            return Err(TIPFeeAMMError::insufficient_liquidity().into());
115        }
116
117        amount_out_needed
118            .try_into()
119            .map_err(|_| TempoPrecompileError::under_overflow())
120    }
121
122    /// Reserves pool liquidity in transient storage for a pending fee swap.
123    #[inline]
124    pub fn reserve_pool_liquidity(&mut self, pool_id: B256, amount: u128) -> Result<()> {
125        self.pending_fee_swap_reservation[pool_id].t_write(amount)
126    }
127
128    /// Executes a rebalance swap: sells `amount_out` of user-token from the pool in exchange for
129    /// validator-token at the rebalance rate (`N / SCALE`). Used by arbitrageurs to rebalance reserves.
130    ///
131    /// # Errors
132    /// - `InvalidAmount` — `amount_out` is zero or exceeds `u128`
133    /// - `InsufficientReserves` — adding `amount_in` overflows the validator reserve
134    /// - `InsufficientLiquidity` — remaining reserve would violate the pending reservation (T1C+)
135    /// - `UnderOverflow` — arithmetic overflow computing `amount_in`
136    pub fn rebalance_swap(
137        &mut self,
138        msg_sender: Address,
139        user_token: Address,
140        validator_token: Address,
141        amount_out: U256,
142        to: Address,
143    ) -> Result<U256> {
144        if amount_out.is_zero() {
145            return Err(TIPFeeAMMError::invalid_amount().into());
146        }
147
148        let pool_id = self.pool_id(user_token, validator_token);
149        let mut pool = self.pools[pool_id].read()?;
150
151        // Rebalancing swaps are always from validatorToken to userToken
152        // Calculate input and update reserves
153        let amount_in = amount_out
154            .checked_mul(N)
155            .and_then(|product| product.checked_div(SCALE))
156            .and_then(|result| result.checked_add(U256::ONE))
157            .ok_or(TempoPrecompileError::under_overflow())?;
158
159        let amount_in: u128 = amount_in
160            .try_into()
161            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
162        let amount_out: u128 = amount_out
163            .try_into()
164            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
165
166        pool.reserve_validator_token = pool
167            .reserve_validator_token
168            .checked_add(amount_in)
169            .ok_or(TIPFeeAMMError::insufficient_reserves())?;
170
171        pool.reserve_user_token = pool
172            .reserve_user_token
173            .checked_sub(amount_out)
174            .ok_or(TIPFeeAMMError::invalid_amount())?;
175
176        if self.storage.spec().is_t1c() {
177            let reserved = self.pending_fee_swap_reservation[pool_id].t_read()?;
178            if pool.reserve_validator_token < reserved {
179                return Err(TIPFeeAMMError::insufficient_liquidity().into());
180            }
181        }
182
183        self.pools[pool_id].write(pool)?;
184
185        let amount_in = U256::from(amount_in);
186        let amount_out = U256::from(amount_out);
187        TIP20Token::from_address(validator_token)?.system_transfer_from(
188            msg_sender,
189            self.address,
190            amount_in,
191        )?;
192
193        TIP20Token::from_address(user_token)?.transfer(
194            self.address,
195            ITIP20::transferCall {
196                to,
197                amount: amount_out,
198            },
199        )?;
200
201        self.emit_event(TIPFeeAMMEvent::RebalanceSwap(ITIPFeeAMM::RebalanceSwap {
202            userToken: user_token,
203            validatorToken: validator_token,
204            swapper: msg_sender,
205            amountIn: amount_in,
206            amountOut: amount_out,
207        }))?;
208
209        Ok(amount_in)
210    }
211
212    /// Mints LP tokens by depositing validator-token into a pool.
213    ///
214    /// On first deposit the pool is initialized with equal reserves and [`MIN_LIQUIDITY`] is
215    /// permanently locked. Subsequent deposits mint pro-rata to existing supply. Both tokens
216    /// must be distinct, USD-denominated TIP-20s.
217    ///
218    /// # Errors
219    /// - `IdenticalAddresses` — `user_token` equals `validator_token`
220    /// - `InvalidAmount` — `amount_validator_token` is zero or exceeds `u128`
221    /// - `InvalidCurrency` — either token is not USD-denominated
222    /// - `InsufficientLiquidity` — initial deposit ≤ `MIN_LIQUIDITY`, or zero liquidity minted
223    /// - `InvalidSwapCalculation` — pro-rata arithmetic fails
224    /// - `UnderOverflow` — supply or balance overflow
225    pub fn mint(
226        &mut self,
227        msg_sender: Address,
228        user_token: Address,
229        validator_token: Address,
230        amount_validator_token: U256,
231        to: Address,
232    ) -> Result<U256> {
233        if user_token == validator_token {
234            return Err(TIPFeeAMMError::identical_addresses().into());
235        }
236
237        if amount_validator_token.is_zero() {
238            return Err(TIPFeeAMMError::invalid_amount().into());
239        }
240
241        // Validate both tokens are USD currency
242        validate_usd_currency(user_token)?;
243        validate_usd_currency(validator_token)?;
244
245        let pool_id = self.pool_id(user_token, validator_token);
246        let mut pool = self.pools[pool_id].read()?;
247        let mut total_supply = self.get_total_supply(pool_id)?;
248
249        let liquidity = if pool.reserve_user_token == 0 && pool.reserve_validator_token == 0 {
250            let half_amount = amount_validator_token
251                .checked_div(uint!(2_U256))
252                .ok_or(TempoPrecompileError::under_overflow())?;
253
254            if half_amount <= MIN_LIQUIDITY {
255                return Err(TIPFeeAMMError::insufficient_liquidity().into());
256            }
257
258            total_supply = total_supply
259                .checked_add(MIN_LIQUIDITY)
260                .ok_or(TempoPrecompileError::under_overflow())?;
261            self.set_total_supply(pool_id, total_supply)?;
262
263            half_amount
264                .checked_sub(MIN_LIQUIDITY)
265                .ok_or(TIPFeeAMMError::insufficient_liquidity())?
266        } else {
267            // Subsequent deposits: mint as if user called rebalanceSwap then minted with both
268            // liquidity = amountValidatorToken * _totalSupply / (V + n * U), with n = N / SCALE
269            let product = N
270                .checked_mul(U256::from(pool.reserve_user_token))
271                .and_then(|product| product.checked_div(SCALE))
272                .ok_or(TIPFeeAMMError::invalid_swap_calculation())?;
273
274            let denom = U256::from(pool.reserve_validator_token)
275                .checked_add(product)
276                .ok_or(TIPFeeAMMError::invalid_amount())?;
277
278            if denom.is_zero() {
279                return Err(TIPFeeAMMError::division_by_zero().into());
280            }
281
282            amount_validator_token
283                .checked_mul(total_supply)
284                .and_then(|numerator| numerator.checked_div(denom))
285                .ok_or(TIPFeeAMMError::invalid_swap_calculation())?
286        };
287
288        if liquidity.is_zero() {
289            return Err(TIPFeeAMMError::insufficient_liquidity().into());
290        }
291
292        // Transfer validator tokens from user
293        let _ = TIP20Token::from_address(validator_token)?.system_transfer_from(
294            msg_sender,
295            self.address,
296            amount_validator_token,
297        )?;
298
299        // Update reserves
300        let validator_amount: u128 = amount_validator_token
301            .try_into()
302            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
303
304        pool.reserve_validator_token = pool
305            .reserve_validator_token
306            .checked_add(validator_amount)
307            .ok_or(TIPFeeAMMError::invalid_amount())?;
308
309        self.pools[pool_id].write(pool)?;
310
311        // Mint LP tokens
312        self.set_total_supply(
313            pool_id,
314            total_supply
315                .checked_add(liquidity)
316                .ok_or(TempoPrecompileError::under_overflow())?,
317        )?;
318
319        let balance = self.get_liquidity_balances(pool_id, to)?;
320        self.set_liquidity_balances(
321            pool_id,
322            to,
323            balance
324                .checked_add(liquidity)
325                .ok_or(TempoPrecompileError::under_overflow())?,
326        )?;
327
328        // Emit Mint event
329        self.emit_event(TIPFeeAMMEvent::Mint(ITIPFeeAMM::Mint {
330            sender: msg_sender,
331            to,
332            userToken: user_token,
333            validatorToken: validator_token,
334            amountValidatorToken: amount_validator_token,
335            liquidity,
336        }))?;
337
338        Ok(liquidity)
339    }
340
341    /// Burns LP tokens and returns the pro-rata share of both pool tokens to `to`.
342    ///
343    /// On T1C+ the burn is rejected if the remaining validator-token reserve would fall below
344    /// the pending fee-swap reservation set by [`TipFeeManager::reserve_pool_liquidity`].
345    ///
346    /// # Errors
347    /// - `IdenticalAddresses` — `user_token` equals `validator_token`
348    /// - `InvalidAmount` — `liquidity` is zero or amounts exceed `u128`
349    /// - `InvalidCurrency` — either token is not USD-denominated
350    /// - `InsufficientLiquidity` — caller's balance < `liquidity`, or remaining reserve would
351    ///   violate the pending reservation (T1C+)
352    /// - `InsufficientReserves` — pool reserves underflow after withdrawal
353    /// - `UnderOverflow` — supply or balance arithmetic overflows
354    pub fn burn(
355        &mut self,
356        msg_sender: Address,
357        user_token: Address,
358        validator_token: Address,
359        liquidity: U256,
360        to: Address,
361    ) -> Result<(U256, U256)> {
362        if user_token == validator_token {
363            return Err(TIPFeeAMMError::identical_addresses().into());
364        }
365
366        if liquidity.is_zero() {
367            return Err(TIPFeeAMMError::invalid_amount().into());
368        }
369
370        // Validate both tokens are USD currency
371        validate_usd_currency(user_token)?;
372        validate_usd_currency(validator_token)?;
373
374        let pool_id = self.pool_id(user_token, validator_token);
375        // Check user has sufficient liquidity
376        let balance = self.get_liquidity_balances(pool_id, msg_sender)?;
377        if balance < liquidity {
378            return Err(TIPFeeAMMError::insufficient_liquidity().into());
379        }
380
381        let mut pool = self.pools[pool_id].read()?;
382        // Calculate amounts to return
383        let (amount_user_token, amount_validator_token) =
384            self.calculate_burn_amounts(&pool, pool_id, liquidity)?;
385
386        // T1C+: Check that burn leaves enough liquidity for pending fee swaps
387        // Reservation is set by reserve_pool_liquidity() via check_sufficient_liquidity()
388        let validator_amount: u128 = amount_validator_token
389            .try_into()
390            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
391        let available_after_burn = pool
392            .reserve_validator_token
393            .checked_sub(validator_amount)
394            .ok_or(TIPFeeAMMError::insufficient_reserves())?;
395        if self.storage.spec().is_t1c() {
396            let reserved = self.pending_fee_swap_reservation[pool_id].t_read()?;
397            if available_after_burn < reserved {
398                return Err(TIPFeeAMMError::insufficient_liquidity().into());
399            }
400        }
401
402        // Burn LP tokens
403        self.set_liquidity_balances(
404            pool_id,
405            msg_sender,
406            balance
407                .checked_sub(liquidity)
408                .ok_or(TempoPrecompileError::under_overflow())?,
409        )?;
410        let total_supply = self.get_total_supply(pool_id)?;
411        self.set_total_supply(
412            pool_id,
413            total_supply
414                .checked_sub(liquidity)
415                .ok_or(TempoPrecompileError::under_overflow())?,
416        )?;
417
418        // Update reserves with underflow checks
419        let user_amount: u128 = amount_user_token
420            .try_into()
421            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
422        let validator_amount: u128 = amount_validator_token
423            .try_into()
424            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
425
426        pool.reserve_user_token = pool
427            .reserve_user_token
428            .checked_sub(user_amount)
429            .ok_or(TIPFeeAMMError::insufficient_reserves())?;
430        pool.reserve_validator_token = pool
431            .reserve_validator_token
432            .checked_sub(validator_amount)
433            .ok_or(TIPFeeAMMError::insufficient_reserves())?;
434        self.pools[pool_id].write(pool)?;
435
436        // Transfer tokens to user
437        let _ = TIP20Token::from_address(user_token)?.transfer(
438            self.address,
439            ITIP20::transferCall {
440                to,
441                amount: amount_user_token,
442            },
443        )?;
444
445        let _ = TIP20Token::from_address(validator_token)?.transfer(
446            self.address,
447            ITIP20::transferCall {
448                to,
449                amount: amount_validator_token,
450            },
451        )?;
452
453        // Emit Burn event
454        self.emit_event(TIPFeeAMMEvent::Burn(ITIPFeeAMM::Burn {
455            sender: msg_sender,
456            userToken: user_token,
457            validatorToken: validator_token,
458            amountUserToken: amount_user_token,
459            amountValidatorToken: amount_validator_token,
460            liquidity,
461            to,
462        }))?;
463
464        Ok((amount_user_token, amount_validator_token))
465    }
466
467    /// Calculate burn amounts for liquidity withdrawal
468    fn calculate_burn_amounts(
469        &self,
470        pool: &Pool,
471        pool_id: B256,
472        liquidity: U256,
473    ) -> Result<(U256, U256)> {
474        let total_supply = self.get_total_supply(pool_id)?;
475        let amount_user_token = liquidity
476            .checked_mul(U256::from(pool.reserve_user_token))
477            .and_then(|product| product.checked_div(total_supply))
478            .ok_or(TempoPrecompileError::under_overflow())?;
479        let amount_validator_token = liquidity
480            .checked_mul(U256::from(pool.reserve_validator_token))
481            .and_then(|product| product.checked_div(total_supply))
482            .ok_or(TempoPrecompileError::under_overflow())?;
483
484        Ok((amount_user_token, amount_validator_token))
485    }
486
487    /// Executes a fee swap, converting `user_token` to `validator_token` at a fixed rate m = 0.997
488    /// Called internally by [`TipFeeManager::collect_fee_post_tx`] during post-tx fee collection.
489    ///
490    /// # Errors
491    /// - `InsufficientLiquidity` — pool validator-token reserve is below the required output
492    /// - `UnderOverflow` — reserve arithmetic overflows or amounts exceed `u128`
493    pub fn execute_fee_swap(
494        &mut self,
495        user_token: Address,
496        validator_token: Address,
497        amount_in: U256,
498    ) -> Result<U256> {
499        let pool_id = self.pool_id(user_token, validator_token);
500        let mut pool = self.pools[pool_id].read()?;
501
502        // Calculate output at fixed price m = 0.9970
503        let amount_out = compute_amount_out(amount_in)?;
504
505        // Check if there's enough validatorToken available
506        if amount_out > U256::from(pool.reserve_validator_token) {
507            return Err(TIPFeeAMMError::insufficient_liquidity().into());
508        }
509
510        // Update reserves
511        let amount_in_u128: u128 = amount_in
512            .try_into()
513            .map_err(|_| TempoPrecompileError::under_overflow())?;
514        let amount_out_u128: u128 = amount_out
515            .try_into()
516            .map_err(|_| TempoPrecompileError::under_overflow())?;
517
518        pool.reserve_user_token = pool
519            .reserve_user_token
520            .checked_add(amount_in_u128)
521            .ok_or(TempoPrecompileError::under_overflow())?;
522        pool.reserve_validator_token = pool
523            .reserve_validator_token
524            .checked_sub(amount_out_u128)
525            .ok_or(TempoPrecompileError::under_overflow())?;
526
527        self.pools[pool_id].write(pool)?;
528
529        Ok(amount_out)
530    }
531
532    /// Returns the total supply of LP tokens for the given pool.
533    pub fn get_total_supply(&self, pool_id: B256) -> Result<U256> {
534        self.total_supply[pool_id].read()
535    }
536
537    /// Set total supply of LP tokens for a pool
538    fn set_total_supply(&mut self, pool_id: B256, total_supply: U256) -> Result<()> {
539        self.total_supply[pool_id].write(total_supply)
540    }
541
542    /// Returns the LP token balance for `user` in the given pool.
543    pub fn get_liquidity_balances(&self, pool_id: B256, user: Address) -> Result<U256> {
544        self.liquidity_balances[pool_id][user].read()
545    }
546
547    /// Set user's LP token balance
548    fn set_liquidity_balances(
549        &mut self,
550        pool_id: B256,
551        user: Address,
552        balance: U256,
553    ) -> Result<()> {
554        self.liquidity_balances[pool_id][user].write(balance)
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use alloy::primitives::Address;
561    use tempo_chainspec::hardfork::TempoHardfork;
562    use tempo_contracts::precompiles::TIP20Error;
563
564    use super::*;
565    use crate::{
566        error::TempoPrecompileError,
567        storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
568        test_util::TIP20Setup,
569        tip_fee_manager::TIPFeeAMMError,
570    };
571
572    /// Integer square root using the Babylonian method
573    fn sqrt(x: U256) -> U256 {
574        if x == U256::ZERO {
575            return U256::ZERO;
576        }
577        let mut z = (x + U256::ONE) / uint!(2_U256);
578        let mut y = x;
579        while z < y {
580            y = z;
581            z = (x / z + z) / uint!(2_U256);
582        }
583        y
584    }
585
586    /// Sets up a pool with initial liquidity for testing
587    fn setup_pool_with_liquidity(
588        amm: &mut TipFeeManager,
589        user_token: Address,
590        validator_token: Address,
591        user_amount: U256,
592        validator_amount: U256,
593    ) -> Result<B256> {
594        let pool_id = amm.pool_id(user_token, validator_token);
595        let pool = Pool {
596            reserve_user_token: user_amount.try_into().unwrap(),
597            reserve_validator_token: validator_amount.try_into().unwrap(),
598        };
599        amm.pools[pool_id].write(pool)?;
600        let liquidity = sqrt(user_amount * validator_amount);
601        amm.total_supply[pool_id].write(liquidity)?;
602        Ok(pool_id)
603    }
604
605    #[test]
606    fn test_mint_identical_addresses() -> eyre::Result<()> {
607        let mut storage = HashMapStorageProvider::new(1);
608        let admin = Address::random();
609        StorageCtx::enter(&mut storage, || {
610            let token = TIP20Setup::create("Test", "TST", admin).apply()?;
611            let mut amm = TipFeeManager::new();
612            let result = amm.mint(
613                admin,
614                token.address(),
615                token.address(),
616                U256::from(1000),
617                admin,
618            );
619            assert!(matches!(
620                result,
621                Err(TempoPrecompileError::TIPFeeAMMError(
622                    TIPFeeAMMError::IdenticalAddresses(_)
623                ))
624            ));
625            Ok(())
626        })
627    }
628
629    #[test]
630    fn test_burn_identical_addresses() -> eyre::Result<()> {
631        let mut storage = HashMapStorageProvider::new(1);
632        let admin = Address::random();
633        StorageCtx::enter(&mut storage, || {
634            let token = TIP20Setup::create("Test", "TST", admin).apply()?;
635            let mut amm = TipFeeManager::new();
636            let result = amm.burn(
637                admin,
638                token.address(),
639                token.address(),
640                U256::from(1000),
641                admin,
642            );
643            assert!(matches!(
644                result,
645                Err(TempoPrecompileError::TIPFeeAMMError(
646                    TIPFeeAMMError::IdenticalAddresses(_)
647                ))
648            ));
649            Ok(())
650        })
651    }
652
653    #[test]
654    fn test_rebalance_swap_insufficient_funds() -> eyre::Result<()> {
655        let mut storage = HashMapStorageProvider::new(1);
656        let admin = Address::random();
657        let to = Address::random();
658        StorageCtx::enter(&mut storage, || {
659            let user_token = TIP20Setup::create("UserToken", "UTK", admin).apply()?;
660            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin).apply()?;
661
662            let mut amm = TipFeeManager::new();
663            let amount = uint!(100000_U256) * uint!(10_U256).pow(U256::from(6));
664            setup_pool_with_liquidity(
665                &mut amm,
666                user_token.address(),
667                validator_token.address(),
668                amount,
669                amount,
670            )?;
671
672            let result = amm.rebalance_swap(
673                admin,
674                user_token.address(),
675                validator_token.address(),
676                amount + U256::ONE,
677                to,
678            );
679            assert!(matches!(
680                result,
681                Err(TempoPrecompileError::TIPFeeAMMError(
682                    TIPFeeAMMError::InvalidAmount(_)
683                ))
684            ));
685            Ok(())
686        })
687    }
688
689    #[test]
690    fn test_mint_rejects_non_usd_user_token() -> eyre::Result<()> {
691        let mut storage = HashMapStorageProvider::new(1);
692        let admin = Address::random();
693        StorageCtx::enter(&mut storage, || {
694            let eur_token = TIP20Setup::create("EuroToken", "EUR", admin)
695                .currency("EUR")
696                .apply()?;
697            let usd_token = TIP20Setup::create("USDToken", "USD", admin).apply()?;
698            let mut amm = TipFeeManager::new();
699
700            let result = amm.mint(
701                admin,
702                eur_token.address(),
703                usd_token.address(),
704                U256::from(1000),
705                admin,
706            );
707            assert!(matches!(
708                result,
709                Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
710            ));
711
712            let result = amm.mint(
713                admin,
714                usd_token.address(),
715                eur_token.address(),
716                U256::from(1000),
717                admin,
718            );
719            assert!(matches!(
720                result,
721                Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
722            ));
723            Ok(())
724        })
725    }
726
727    #[test]
728    fn test_burn_rejects_non_usd_tokens() -> eyre::Result<()> {
729        let mut storage = HashMapStorageProvider::new(1);
730        let admin = Address::random();
731        StorageCtx::enter(&mut storage, || {
732            let eur_token = TIP20Setup::create("EuroToken", "EUR", admin)
733                .currency("EUR")
734                .apply()?;
735            let usd_token = TIP20Setup::create("USDToken", "USD", admin).apply()?;
736            let mut amm = TipFeeManager::new();
737
738            let result = amm.burn(
739                admin,
740                eur_token.address(),
741                usd_token.address(),
742                U256::from(1000),
743                admin,
744            );
745            assert!(matches!(
746                result,
747                Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
748            ));
749
750            let result = amm.burn(
751                admin,
752                usd_token.address(),
753                eur_token.address(),
754                U256::from(1000),
755                admin,
756            );
757            assert!(matches!(
758                result,
759                Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
760            ));
761            Ok(())
762        })
763    }
764
765    #[test]
766    fn test_mint_insufficient_amount() -> eyre::Result<()> {
767        let mut storage = HashMapStorageProvider::new(1);
768        let admin = Address::random();
769        StorageCtx::enter(&mut storage, || {
770            let user_token = TIP20Setup::create("UserToken", "UTK", admin).apply()?;
771            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin).apply()?;
772            let mut amm = TipFeeManager::new();
773
774            // MIN_LIQUIDITY = 1000, amount/2 must be > 1000, so 2000 should fail
775            let insufficient = uint!(2000_U256);
776            let result = amm.mint(
777                admin,
778                user_token.address(),
779                validator_token.address(),
780                insufficient,
781                admin,
782            );
783            assert!(matches!(
784                result,
785                Err(TempoPrecompileError::TIPFeeAMMError(
786                    TIPFeeAMMError::InsufficientLiquidity(_)
787                ))
788            ));
789            Ok(())
790        })
791    }
792
793    #[test]
794    fn test_add_liquidity() -> eyre::Result<()> {
795        let mut storage = HashMapStorageProvider::new(1);
796        let admin = Address::random();
797
798        StorageCtx::enter(&mut storage, || {
799            let mint_amount = uint!(10000000_U256);
800            let token1 = TIP20Setup::create("Token1", "TK1", admin)
801                .with_issuer(admin)
802                .with_mint(admin, mint_amount)
803                .apply()?
804                .address();
805            let token2 = TIP20Setup::create("Token2", "TK2", admin)
806                .with_issuer(admin)
807                .with_mint(admin, mint_amount)
808                .apply()?
809                .address();
810
811            let mut amm = TipFeeManager::new();
812            let amount = uint!(10000_U256);
813            let result = amm.mint(admin, token1, token2, amount, admin)?;
814            let expected_mean = amount / uint!(2_U256);
815            let expected_liquidity = expected_mean - MIN_LIQUIDITY;
816
817            assert_eq!(result, expected_liquidity,);
818
819            Ok(())
820        })
821    }
822
823    #[test]
824    fn test_calculate_burn_amounts() -> eyre::Result<()> {
825        let mut storage = HashMapStorageProvider::new(1);
826
827        StorageCtx::enter(&mut storage, || {
828            let mut amm = TipFeeManager::new();
829
830            let pool = Pool {
831                reserve_user_token: 1000,
832                reserve_validator_token: 1000,
833            };
834            let pool_id = B256::ZERO;
835            amm.set_total_supply(pool_id, uint!(1000000000000000_U256))?;
836
837            let liquidity = uint!(1_U256);
838            let result = amm.calculate_burn_amounts(&pool, pool_id, liquidity);
839
840            assert!(result.is_ok());
841            let (amount_user, amount_validator) = result?;
842            assert_eq!(amount_user, U256::ZERO);
843            assert_eq!(amount_validator, U256::ZERO);
844
845            Ok(())
846        })
847    }
848
849    /// Test execute_fee_swap executes swap immediately and updates reserves
850    #[test]
851    fn test_execute_fee_swap_immediate() -> eyre::Result<()> {
852        let mut storage = HashMapStorageProvider::new(1);
853        let admin = Address::random();
854
855        StorageCtx::enter(&mut storage, || {
856            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
857                .apply()?
858                .address();
859            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
860                .apply()?
861                .address();
862
863            let mut amm = TipFeeManager::new();
864
865            // Setup pool with 1000 tokens each
866            let liquidity_amount = uint!(1000_U256);
867            let pool_id = setup_pool_with_liquidity(
868                &mut amm,
869                user_token,
870                validator_token,
871                liquidity_amount,
872                liquidity_amount,
873            )?;
874
875            // Execute fee swap for 100 tokens
876            let amount_in = uint!(100_U256);
877            let expected_out = (amount_in * M) / SCALE; // 100 * 9970 / 10000 = 99
878
879            let amount_out = amm.execute_fee_swap(user_token, validator_token, amount_in)?;
880
881            assert_eq!(amount_out, expected_out);
882
883            // Verify reserves updated immediately
884            let pool = amm.pools[pool_id].read()?;
885            assert_eq!(
886                U256::from(pool.reserve_user_token),
887                liquidity_amount + amount_in
888            );
889            assert_eq!(
890                U256::from(pool.reserve_validator_token),
891                liquidity_amount - expected_out
892            );
893
894            Ok(())
895        })
896    }
897
898    /// Test execute_fee_swap fails with insufficient liquidity
899    #[test]
900    fn test_execute_fee_swap_insufficient_liquidity() -> eyre::Result<()> {
901        let mut storage = HashMapStorageProvider::new(1);
902        let admin = Address::random();
903
904        StorageCtx::enter(&mut storage, || {
905            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
906                .apply()?
907                .address();
908            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
909                .apply()?
910                .address();
911
912            let mut amm = TipFeeManager::new();
913
914            // Setup pool with only 100 tokens each
915            let small_liquidity = uint!(100_U256);
916            setup_pool_with_liquidity(
917                &mut amm,
918                user_token,
919                validator_token,
920                small_liquidity,
921                small_liquidity,
922            )?;
923
924            // Try to swap 200 tokens (would need ~199 output, but only 100 available)
925            let too_large_amount = uint!(200_U256);
926
927            let result = amm.execute_fee_swap(user_token, validator_token, too_large_amount);
928
929            assert!(matches!(
930                result,
931                Err(TempoPrecompileError::TIPFeeAMMError(
932                    TIPFeeAMMError::InsufficientLiquidity(_)
933                ))
934            ));
935
936            Ok(())
937        })
938    }
939
940    /// Test fee swap rounding consistency across multiple swaps
941    #[test]
942    fn test_fee_swap_rounding_consistency() -> eyre::Result<()> {
943        let mut storage = HashMapStorageProvider::new(1);
944        let admin = Address::random();
945
946        StorageCtx::enter(&mut storage, || {
947            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
948                .apply()?
949                .address();
950            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
951                .apply()?
952                .address();
953
954            let mut amm = TipFeeManager::new();
955            let liquidity = uint!(100000_U256) * uint!(10_U256).pow(U256::from(6));
956            let pool_id = setup_pool_with_liquidity(
957                &mut amm,
958                user_token,
959                validator_token,
960                liquidity,
961                liquidity,
962            )?;
963
964            let amount_in = uint!(10000_U256) * uint!(10_U256).pow(U256::from(6));
965            let expected_out = (amount_in * M) / SCALE;
966
967            let actual_out = amm.execute_fee_swap(user_token, validator_token, amount_in)?;
968            assert_eq!(actual_out, expected_out, "Output should match expected");
969
970            let pool = amm.pools[pool_id].read()?;
971            assert_eq!(
972                U256::from(pool.reserve_user_token),
973                liquidity + amount_in,
974                "User reserve should increase"
975            );
976            assert_eq!(
977                U256::from(pool.reserve_validator_token),
978                liquidity - actual_out,
979                "Validator reserve should decrease"
980            );
981
982            Ok(())
983        })
984    }
985
986    /// Test multiple consecutive fee swaps update reserves correctly
987    #[test]
988    fn test_multiple_consecutive_fee_swaps() -> eyre::Result<()> {
989        let mut storage = HashMapStorageProvider::new(1);
990        let admin = Address::random();
991
992        StorageCtx::enter(&mut storage, || {
993            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
994                .apply()?
995                .address();
996            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
997                .apply()?
998                .address();
999
1000            let mut amm = TipFeeManager::new();
1001            let initial = uint!(100000_U256) * uint!(10_U256).pow(U256::from(6));
1002            let pool_id =
1003                setup_pool_with_liquidity(&mut amm, user_token, validator_token, initial, initial)?;
1004
1005            let swap1 = uint!(1000_U256) * uint!(10_U256).pow(U256::from(6));
1006            let swap2 = uint!(2000_U256) * uint!(10_U256).pow(U256::from(6));
1007            let swap3 = uint!(3000_U256) * uint!(10_U256).pow(U256::from(6));
1008
1009            let out1 = amm.execute_fee_swap(user_token, validator_token, swap1)?;
1010            let out2 = amm.execute_fee_swap(user_token, validator_token, swap2)?;
1011            let out3 = amm.execute_fee_swap(user_token, validator_token, swap3)?;
1012
1013            let total_in = swap1 + swap2 + swap3;
1014            let total_out = out1 + out2 + out3;
1015
1016            // Each swap output should be amount_in * M / SCALE
1017            assert_eq!(out1, (swap1 * M) / SCALE);
1018            assert_eq!(out2, (swap2 * M) / SCALE);
1019            assert_eq!(out3, (swap3 * M) / SCALE);
1020
1021            let pool = amm.pools[pool_id].read()?;
1022            assert_eq!(U256::from(pool.reserve_user_token), initial + total_in);
1023            assert_eq!(
1024                U256::from(pool.reserve_validator_token),
1025                initial - total_out
1026            );
1027
1028            Ok(())
1029        })
1030    }
1031
1032    /// Test check_sufficient_liquidity boundary condition
1033    #[test]
1034    fn test_check_sufficient_liquidity_boundary() -> eyre::Result<()> {
1035        let mut storage = HashMapStorageProvider::new(1);
1036        let admin = Address::random();
1037
1038        StorageCtx::enter(&mut storage, || {
1039            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1040                .apply()?
1041                .address();
1042            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1043                .apply()?
1044                .address();
1045
1046            let mut amm = TipFeeManager::new();
1047            let liquidity = uint!(100_U256) * uint!(10_U256).pow(U256::from(6));
1048            let pool_id = setup_pool_with_liquidity(
1049                &mut amm,
1050                user_token,
1051                validator_token,
1052                liquidity,
1053                liquidity,
1054            )?;
1055
1056            // Exactly at boundary should succeed (100 * 0.997 = 99.7, which is < 100)
1057            let ok_amount = uint!(100_U256) * uint!(10_U256).pow(U256::from(6));
1058            assert!(amm.check_sufficient_liquidity(pool_id, ok_amount).is_ok());
1059
1060            // Just over boundary should fail (101 * 0.997 = 100.697, which is > 100)
1061            let too_much = uint!(101_U256) * uint!(10_U256).pow(U256::from(6));
1062            assert!(amm.check_sufficient_liquidity(pool_id, too_much).is_err());
1063
1064            Ok(())
1065        })
1066    }
1067
1068    /// Test zero liquidity burn
1069    #[test]
1070    fn test_burn_zero_liquidity() -> eyre::Result<()> {
1071        let mut storage = HashMapStorageProvider::new(1);
1072        let admin = Address::random();
1073
1074        StorageCtx::enter(&mut storage, || {
1075            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1076                .apply()?
1077                .address();
1078            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1079                .apply()?
1080                .address();
1081
1082            let mut amm = TipFeeManager::new();
1083
1084            let result = amm.burn(admin, user_token, validator_token, U256::ZERO, admin);
1085
1086            assert!(matches!(
1087                result,
1088                Err(TempoPrecompileError::TIPFeeAMMError(
1089                    TIPFeeAMMError::InvalidAmount(_)
1090                ))
1091            ));
1092
1093            Ok(())
1094        })
1095    }
1096
1097    /// Test zero amount validator token
1098    #[test]
1099    fn test_mint_zero_amount_validator_token() -> eyre::Result<()> {
1100        let mut storage = HashMapStorageProvider::new(1);
1101        let admin = Address::random();
1102
1103        StorageCtx::enter(&mut storage, || {
1104            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1105                .apply()?
1106                .address();
1107            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1108                .apply()?
1109                .address();
1110
1111            let mut amm = TipFeeManager::new();
1112
1113            let result = amm.mint(admin, user_token, validator_token, U256::ZERO, admin);
1114
1115            assert!(matches!(
1116                result,
1117                Err(TempoPrecompileError::TIPFeeAMMError(
1118                    TIPFeeAMMError::InvalidAmount(_)
1119                ))
1120            ));
1121
1122            Ok(())
1123        })
1124    }
1125
1126    #[test]
1127    fn test_rebalance_swap() -> eyre::Result<()> {
1128        let mut storage = HashMapStorageProvider::new(1);
1129        let admin = Address::random();
1130        let recipient = Address::random();
1131
1132        StorageCtx::enter(&mut storage, || {
1133            let mint_amount = uint!(10000000_U256);
1134            let mut amm = TipFeeManager::new();
1135            let amm_address = amm.address;
1136
1137            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1138                .with_issuer(admin)
1139                .with_mint(admin, mint_amount)
1140                .with_mint(amm_address, mint_amount)
1141                .apply()?
1142                .address();
1143            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1144                .with_issuer(admin)
1145                .with_mint(admin, mint_amount)
1146                .apply()?
1147                .address();
1148
1149            let liquidity = uint!(100000_U256);
1150            let pool_id = setup_pool_with_liquidity(
1151                &mut amm,
1152                user_token,
1153                validator_token,
1154                liquidity,
1155                liquidity,
1156            )?;
1157
1158            let amount_out = uint!(1000_U256);
1159            let expected_in = (amount_out * N) / SCALE + U256::ONE;
1160
1161            let amount_in =
1162                amm.rebalance_swap(admin, user_token, validator_token, amount_out, recipient)?;
1163
1164            assert_eq!(amount_in, expected_in);
1165
1166            let pool = amm.pools[pool_id].read()?;
1167            assert_eq!(U256::from(pool.reserve_user_token), liquidity - amount_out);
1168            assert_eq!(
1169                U256::from(pool.reserve_validator_token),
1170                liquidity + amount_in
1171            );
1172
1173            Ok(())
1174        })
1175    }
1176
1177    #[test]
1178    fn test_mint_subsequent_deposit() -> eyre::Result<()> {
1179        let mut storage = HashMapStorageProvider::new(1);
1180        let admin = Address::random();
1181        let second_user = Address::random();
1182
1183        StorageCtx::enter(&mut storage, || {
1184            let mint_amount = uint!(100000000_U256);
1185            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1186                .with_issuer(admin)
1187                .with_mint(admin, mint_amount)
1188                .with_mint(second_user, mint_amount)
1189                .apply()?
1190                .address();
1191            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1192                .with_issuer(admin)
1193                .with_mint(admin, mint_amount)
1194                .with_mint(second_user, mint_amount)
1195                .apply()?
1196                .address();
1197
1198            let mut amm = TipFeeManager::new();
1199
1200            let initial_amount = uint!(100000_U256);
1201            let first_liquidity =
1202                amm.mint(admin, user_token, validator_token, initial_amount, admin)?;
1203
1204            let expected_first_liquidity = initial_amount / uint!(2_U256) - MIN_LIQUIDITY;
1205            assert_eq!(first_liquidity, expected_first_liquidity);
1206
1207            let pool_id = amm.pool_id(user_token, validator_token);
1208            let total_supply_after_first = amm.get_total_supply(pool_id)?;
1209            assert_eq!(total_supply_after_first, first_liquidity + MIN_LIQUIDITY);
1210
1211            let pool_after_first = amm.pools[pool_id].read()?;
1212            let reserve_val = U256::from(pool_after_first.reserve_validator_token);
1213
1214            let second_amount = uint!(50000_U256);
1215            let second_liquidity = amm.mint(
1216                second_user,
1217                user_token,
1218                validator_token,
1219                second_amount,
1220                second_user,
1221            )?;
1222
1223            let expected_second_liquidity = second_amount * total_supply_after_first / reserve_val;
1224            assert_eq!(second_liquidity, expected_second_liquidity);
1225
1226            let total_supply_after_second = amm.get_total_supply(pool_id)?;
1227            assert_eq!(
1228                total_supply_after_second,
1229                total_supply_after_first + second_liquidity
1230            );
1231
1232            let admin_balance = amm.get_liquidity_balances(pool_id, admin)?;
1233            let second_user_balance = amm.get_liquidity_balances(pool_id, second_user)?;
1234            assert_eq!(admin_balance, first_liquidity);
1235            assert_eq!(second_user_balance, second_liquidity);
1236
1237            Ok(())
1238        })
1239    }
1240
1241    #[test]
1242    fn test_burn() -> eyre::Result<()> {
1243        let mut storage = HashMapStorageProvider::new(1);
1244        let admin = Address::random();
1245        let recipient = Address::random();
1246
1247        StorageCtx::enter(&mut storage, || {
1248            let mint_amount = uint!(100000000_U256);
1249            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1250                .with_issuer(admin)
1251                .with_mint(admin, mint_amount)
1252                .apply()?
1253                .address();
1254            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1255                .with_issuer(admin)
1256                .with_mint(admin, mint_amount)
1257                .apply()?
1258                .address();
1259
1260            let mut amm = TipFeeManager::new();
1261
1262            let deposit_amount = uint!(100000_U256);
1263            let liquidity = amm.mint(admin, user_token, validator_token, deposit_amount, admin)?;
1264
1265            let expected_liquidity = deposit_amount / uint!(2_U256) - MIN_LIQUIDITY;
1266            assert_eq!(liquidity, expected_liquidity);
1267
1268            let pool_id = amm.pool_id(user_token, validator_token);
1269            let pool_before = amm.pools[pool_id].read()?;
1270            let total_supply_before = amm.get_total_supply(pool_id)?;
1271
1272            let burn_amount = liquidity / uint!(2_U256);
1273            let (amount_user, amount_validator) =
1274                amm.burn(admin, user_token, validator_token, burn_amount, recipient)?;
1275
1276            let expected_user =
1277                burn_amount * U256::from(pool_before.reserve_user_token) / total_supply_before;
1278            let expected_validator =
1279                burn_amount * U256::from(pool_before.reserve_validator_token) / total_supply_before;
1280            assert_eq!(amount_user, expected_user);
1281            assert_eq!(amount_validator, expected_validator);
1282
1283            let pool_after = amm.pools[pool_id].read()?;
1284            let total_supply_after = amm.get_total_supply(pool_id)?;
1285
1286            assert_eq!(total_supply_after, total_supply_before - burn_amount);
1287
1288            let admin_balance = amm.get_liquidity_balances(pool_id, admin)?;
1289            assert_eq!(admin_balance, liquidity - burn_amount);
1290
1291            assert_eq!(
1292                U256::from(pool_after.reserve_user_token),
1293                U256::from(pool_before.reserve_user_token) - amount_user
1294            );
1295            assert_eq!(
1296                U256::from(pool_after.reserve_validator_token),
1297                U256::from(pool_before.reserve_validator_token) - amount_validator
1298            );
1299
1300            Ok(())
1301        })
1302    }
1303
1304    #[test]
1305    fn test_burn_insufficient_balance() -> eyre::Result<()> {
1306        let mut storage = HashMapStorageProvider::new(1);
1307        let admin = Address::random();
1308        let other_user = Address::random();
1309
1310        StorageCtx::enter(&mut storage, || {
1311            let mint_amount = uint!(100000000_U256);
1312            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1313                .with_issuer(admin)
1314                .with_mint(admin, mint_amount)
1315                .apply()?
1316                .address();
1317            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1318                .with_issuer(admin)
1319                .with_mint(admin, mint_amount)
1320                .apply()?
1321                .address();
1322
1323            let mut amm = TipFeeManager::new();
1324
1325            let deposit_amount = uint!(100000_U256);
1326            let liquidity = amm.mint(admin, user_token, validator_token, deposit_amount, admin)?;
1327
1328            let result = amm.burn(
1329                other_user,
1330                user_token,
1331                validator_token,
1332                liquidity,
1333                other_user,
1334            );
1335
1336            assert!(matches!(
1337                result,
1338                Err(TempoPrecompileError::TIPFeeAMMError(
1339                    TIPFeeAMMError::InsufficientLiquidity(_)
1340                ))
1341            ));
1342
1343            Ok(())
1344        })
1345    }
1346
1347    // Test zero amount rebalance swap
1348    #[test]
1349    fn test_rebalance_swap_zero_amount_out() -> eyre::Result<()> {
1350        let mut storage = HashMapStorageProvider::new(1);
1351        let admin = Address::random();
1352        let to = Address::random();
1353
1354        StorageCtx::enter(&mut storage, || {
1355            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1356                .apply()?
1357                .address();
1358            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1359                .apply()?
1360                .address();
1361
1362            let mut amm = TipFeeManager::new();
1363
1364            let result = amm.rebalance_swap(admin, user_token, validator_token, U256::ZERO, to);
1365
1366            assert!(matches!(
1367                result,
1368                Err(TempoPrecompileError::TIPFeeAMMError(
1369                    TIPFeeAMMError::InvalidAmount(_)
1370                ))
1371            ));
1372
1373            Ok(())
1374        })
1375    }
1376
1377    #[test]
1378    fn test_t1c_reserve_pool_liquidity() -> eyre::Result<()> {
1379        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
1380        let admin = Address::random();
1381
1382        StorageCtx::enter(&mut storage, || {
1383            let mint_amount = uint!(10000000_U256);
1384            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1385                .with_issuer(admin)
1386                .with_mint(admin, mint_amount)
1387                .apply()?
1388                .address();
1389            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1390                .with_issuer(admin)
1391                .with_mint(admin, mint_amount)
1392                .apply()?
1393                .address();
1394
1395            let mut amm = TipFeeManager::new();
1396            let liquidity = uint!(100000_U256);
1397            let pool_id = setup_pool_with_liquidity(
1398                &mut amm,
1399                user_token,
1400                validator_token,
1401                liquidity,
1402                liquidity,
1403            )?;
1404
1405            let max_amount = uint!(10000_U256);
1406            let amount_out = amm.check_sufficient_liquidity(pool_id, max_amount)?;
1407            amm.reserve_pool_liquidity(pool_id, amount_out)?;
1408
1409            let reserved = amm.pending_fee_swap_reservation[pool_id].t_read()?;
1410            let expected_reserved: u128 = compute_amount_out(max_amount)?.try_into().unwrap();
1411            assert_eq!(reserved, expected_reserved);
1412
1413            Ok(())
1414        })
1415    }
1416
1417    #[test]
1418    fn test_t1c_burn_respects_reservation() -> eyre::Result<()> {
1419        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
1420        let admin = Address::random();
1421        let recipient = Address::random();
1422
1423        StorageCtx::enter(&mut storage, || {
1424            let mint_amount = uint!(100000000_U256);
1425            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1426                .with_issuer(admin)
1427                .with_mint(admin, mint_amount)
1428                .apply()?
1429                .address();
1430            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1431                .with_issuer(admin)
1432                .with_mint(admin, mint_amount)
1433                .apply()?
1434                .address();
1435
1436            let mut amm = TipFeeManager::new();
1437
1438            let deposit_amount = uint!(100000_U256);
1439            let liquidity = amm.mint(admin, user_token, validator_token, deposit_amount, admin)?;
1440
1441            let pool_id = amm.pool_id(user_token, validator_token);
1442            let pool = amm.pools[pool_id].read()?;
1443
1444            // Reserve most of the validator token liquidity
1445            let reserve_amount = U256::from(pool.reserve_validator_token) - uint!(100_U256);
1446            let amount_out = amm.check_sufficient_liquidity(pool_id, reserve_amount)?;
1447            amm.reserve_pool_liquidity(pool_id, amount_out)?;
1448
1449            let result = amm.burn(admin, user_token, validator_token, liquidity, recipient);
1450            assert!(matches!(
1451                result,
1452                Err(TempoPrecompileError::TIPFeeAMMError(
1453                    TIPFeeAMMError::InsufficientLiquidity(_)
1454                ))
1455            ));
1456
1457            Ok(())
1458        })
1459    }
1460
1461    #[test]
1462    fn test_t1c_partial_burn_with_reservation() -> eyre::Result<()> {
1463        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
1464        let admin = Address::random();
1465        let recipient = Address::random();
1466
1467        StorageCtx::enter(&mut storage, || {
1468            let mint_amount = uint!(100000000_U256);
1469            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1470                .with_issuer(admin)
1471                .with_mint(admin, mint_amount)
1472                .apply()?
1473                .address();
1474            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1475                .with_issuer(admin)
1476                .with_mint(admin, mint_amount)
1477                .apply()?
1478                .address();
1479
1480            let mut amm = TipFeeManager::new();
1481
1482            let deposit_amount = uint!(100000_U256);
1483            let liquidity = amm.mint(admin, user_token, validator_token, deposit_amount, admin)?;
1484
1485            let pool_id = amm.pool_id(user_token, validator_token);
1486            let small_reserve = uint!(1000_U256);
1487            let amount_out = amm.check_sufficient_liquidity(pool_id, small_reserve)?;
1488            amm.reserve_pool_liquidity(pool_id, amount_out)?;
1489
1490            let small_burn = liquidity / uint!(10_U256);
1491            let result = amm.burn(admin, user_token, validator_token, small_burn, recipient);
1492
1493            assert!(result.is_ok());
1494
1495            Ok(())
1496        })
1497    }
1498
1499    #[test]
1500    fn test_t1c_rebalance_swap_respects_reservation() -> eyre::Result<()> {
1501        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
1502        let admin = Address::random();
1503        let to = Address::random();
1504
1505        StorageCtx::enter(&mut storage, || {
1506            let mint_amount = uint!(100000000_U256);
1507            let mut amm = TipFeeManager::new();
1508            let amm_address = amm.address;
1509            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1510                .with_issuer(admin)
1511                .with_mint(admin, mint_amount)
1512                .with_mint(amm_address, mint_amount)
1513                .apply()?
1514                .address();
1515            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1516                .with_issuer(admin)
1517                .with_mint(admin, mint_amount)
1518                .apply()?
1519                .address();
1520
1521            let liq = uint!(100000_U256);
1522            let pool_id =
1523                setup_pool_with_liquidity(&mut amm, user_token, validator_token, liq, liq)?;
1524
1525            let amount_out = amm.check_sufficient_liquidity(pool_id, uint!(50000_U256))?;
1526            amm.reserve_pool_liquidity(pool_id, amount_out)?;
1527
1528            amm.rebalance_swap(admin, user_token, validator_token, uint!(5000_U256), to)?;
1529            let pool = amm.pools[pool_id].read()?;
1530            let reserved = amm.pending_fee_swap_reservation[pool_id].t_read()?;
1531            assert!(pool.reserve_validator_token >= reserved);
1532
1533            Ok(())
1534        })
1535    }
1536
1537    #[test]
1538    fn test_pre_t1c_rebalance_swap_skips_reservation() -> eyre::Result<()> {
1539        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1B);
1540        let admin = Address::random();
1541        let to = Address::random();
1542
1543        StorageCtx::enter(&mut storage, || {
1544            let mint_amount = uint!(100000000_U256);
1545            let mut amm = TipFeeManager::new();
1546            let amm_address = amm.address;
1547            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1548                .with_issuer(admin)
1549                .with_mint(admin, mint_amount)
1550                .with_mint(amm_address, mint_amount)
1551                .apply()?
1552                .address();
1553            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1554                .with_issuer(admin)
1555                .with_mint(admin, mint_amount)
1556                .apply()?
1557                .address();
1558
1559            let liq = uint!(100000_U256);
1560            let pool_id =
1561                setup_pool_with_liquidity(&mut amm, user_token, validator_token, liq, liq)?;
1562            amm.check_sufficient_liquidity(pool_id, uint!(90000_U256))?;
1563            assert!(
1564                amm.rebalance_swap(admin, user_token, validator_token, uint!(5000_U256), to)
1565                    .is_ok()
1566            );
1567
1568            Ok(())
1569        })
1570    }
1571
1572    #[test]
1573    fn test_pre_t1c_no_reservation() -> eyre::Result<()> {
1574        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1B);
1575        let admin = Address::random();
1576        let recipient = Address::random();
1577
1578        StorageCtx::enter(&mut storage, || {
1579            let mint_amount = uint!(100000000_U256);
1580            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
1581                .with_issuer(admin)
1582                .with_mint(admin, mint_amount)
1583                .apply()?
1584                .address();
1585            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
1586                .with_issuer(admin)
1587                .with_mint(admin, mint_amount)
1588                .apply()?
1589                .address();
1590
1591            let mut amm = TipFeeManager::new();
1592
1593            let deposit_amount = uint!(100000_U256);
1594            let liquidity = amm.mint(admin, user_token, validator_token, deposit_amount, admin)?;
1595
1596            let pool_id = amm.pool_id(user_token, validator_token);
1597            let pool = amm.pools[pool_id].read()?;
1598            let reserve_amount = U256::from(pool.reserve_validator_token) - uint!(100_U256);
1599            amm.check_sufficient_liquidity(pool_id, reserve_amount)?;
1600
1601            let result = amm.burn(admin, user_token, validator_token, liquidity, recipient);
1602            assert!(result.is_ok());
1603
1604            Ok(())
1605        })
1606    }
1607}