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