tempo_precompiles/tip_fee_manager/
amm.rs

1use crate::{
2    error::{Result, TempoPrecompileError},
3    storage::{PrecompileStorageProvider, Storable},
4    tip_fee_manager::{ITIPFeeAMM, TIPFeeAMMError, TIPFeeAMMEvent, TipFeeManager},
5    tip20::{ITIP20, TIP20Token, validate_usd_currency},
6};
7use alloy::{
8    primitives::{Address, B256, IntoLogData, U256, keccak256, uint},
9    sol_types::SolValue,
10};
11use tempo_precompiles_macros::Storable;
12
13/// Constants from the Solidity reference implementation
14pub const M: U256 = uint!(9970_U256); // m = 0.9970 (scaled by 10000)
15pub const N: U256 = uint!(9985_U256);
16pub const SCALE: U256 = uint!(10000_U256);
17pub const SQRT_SCALE: U256 = uint!(100000_U256);
18pub const MIN_LIQUIDITY: U256 = uint!(1000_U256);
19
20/// Compute amount out for a fee swap
21#[inline]
22pub fn compute_amount_out(amount_in: U256) -> Result<U256> {
23    amount_in
24        .checked_mul(M)
25        .map(|product| product / SCALE)
26        .ok_or(TempoPrecompileError::under_overflow())
27}
28
29/// Pool structure matching the Solidity implementation
30#[derive(Debug, Clone, Default, Storable)]
31pub struct Pool {
32    pub reserve_user_token: u128,
33    pub reserve_validator_token: u128,
34}
35
36impl Pool {
37    pub fn from_slot(slot: U256) -> Self {
38        Self::from_evm_words([slot]).unwrap()
39    }
40}
41
42impl From<Pool> for ITIPFeeAMM::Pool {
43    fn from(value: Pool) -> Self {
44        Self {
45            reserveUserToken: value.reserve_user_token,
46            reserveValidatorToken: value.reserve_validator_token,
47        }
48    }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash, Storable)]
52pub struct PoolKey {
53    pub user_token: Address,
54    pub validator_token: Address,
55}
56
57impl PoolKey {
58    /// Creates a new pool key from user and validator token addresses.
59    /// This key uniquely identifies a trading pair in the AMM.
60    pub fn new(user_token: Address, validator_token: Address) -> Self {
61        Self {
62            user_token,
63            validator_token,
64        }
65    }
66
67    /// Generates a unique pool ID by hashing the token pair addresses.
68    /// Uses keccak256 to create a deterministic identifier for this pool.
69    pub fn get_id(&self) -> B256 {
70        keccak256((self.user_token, self.validator_token).abi_encode())
71    }
72}
73
74impl<'a, S: PrecompileStorageProvider> TipFeeManager<'a, S> {
75    /// Gets the pool id for a given set of tokens. Note that the pool id is dependent on the
76    /// ordering of the tokens ie. (token_a, token_b) results in a different pool id
77    /// than (token_b, token_a)
78    pub fn pool_id(&self, user_token: Address, validator_token: Address) -> B256 {
79        PoolKey::new(user_token, validator_token).get_id()
80    }
81
82    /// Retrieves a pool for a given `pool_id` from storage
83    pub fn get_pool(&mut self, call: ITIPFeeAMM::getPoolCall) -> Result<Pool> {
84        let pool_id = self.pool_id(call.userToken, call.validatorToken);
85        self.sload_pools(pool_id)
86    }
87
88    /// Ensures that pool has enough liquidity for a fee swap and reserve that liquidity in `pending_fee_swap_in`.
89    pub fn reserve_liquidity(
90        &mut self,
91        user_token: Address,
92        validator_token: Address,
93        max_amount: U256,
94    ) -> Result<()> {
95        let pool_id = PoolKey::new(user_token, validator_token).get_id();
96        let current_pending_fee_swap_in = self.get_pending_fee_swap_in(pool_id)?;
97
98        // Add the `max_amount` to the pending amount in and check that the resulting
99        // total output is within the pools current reserves
100        let new_total_pending = current_pending_fee_swap_in
101            .checked_add(
102                max_amount
103                    .try_into()
104                    .map_err(|_| TempoPrecompileError::under_overflow())?,
105            )
106            .ok_or(TempoPrecompileError::under_overflow())?;
107
108        let total_out_needed = compute_amount_out(U256::from(new_total_pending))?;
109
110        let pool = self.sload_pools(pool_id)?;
111        if total_out_needed > U256::from(pool.reserve_validator_token) {
112            return Err(TIPFeeAMMError::insufficient_liquidity().into());
113        }
114
115        self.set_pending_fee_swap_in(pool_id, new_total_pending)?;
116
117        Ok(())
118    }
119
120    /// Calculate validator token reserve minus pending swaps
121    fn get_effective_validator_reserve(&mut self, pool_id: B256) -> Result<U256> {
122        let pool = self.sload_pools(pool_id)?;
123        let pending_fee_swap_in = self.get_pending_fee_swap_in(pool_id)?;
124        let pending_out = compute_amount_out(U256::from(pending_fee_swap_in))?;
125
126        U256::from(pool.reserve_validator_token)
127            .checked_sub(pending_out)
128            .ok_or(TempoPrecompileError::under_overflow())
129    }
130
131    /// Calculate user token reserve plus pending swaps
132    fn get_effective_user_reserve(&mut self, pool_id: B256) -> Result<U256> {
133        let pool = self.sload_pools(pool_id)?;
134        let pending_fee_swap_in = U256::from(self.get_pending_fee_swap_in(pool_id)?);
135
136        U256::from(pool.reserve_user_token)
137            .checked_add(pending_fee_swap_in)
138            .ok_or(TempoPrecompileError::under_overflow())
139    }
140
141    /// Releases `refund_amount` of liquidity that was locked by `reserve_liquidity`
142    pub fn release_liquidity(
143        &mut self,
144        user_token: Address,
145        validator_token: Address,
146        refund_amount: U256,
147    ) -> Result<()> {
148        let pool_id = self.pool_id(user_token, validator_token);
149        let current_pending = self.get_pending_fee_swap_in(pool_id)?;
150        self.set_pending_fee_swap_in(
151            pool_id,
152            current_pending
153                .checked_sub(
154                    refund_amount
155                        .try_into()
156                        .map_err(|_| TempoPrecompileError::under_overflow())?,
157                )
158                .ok_or(TempoPrecompileError::under_overflow())?,
159        )?;
160
161        Ok(())
162    }
163
164    /// Swap to rebalance a fee token pool
165    pub fn rebalance_swap(
166        &mut self,
167        msg_sender: Address,
168        user_token: Address,
169        validator_token: Address,
170        amount_out: U256,
171        to: Address,
172    ) -> Result<U256> {
173        // Validate both tokens are USD currency
174        validate_usd_currency(user_token, self.storage)?;
175        validate_usd_currency(validator_token, self.storage)?;
176
177        let pool_id = self.pool_id(user_token, validator_token);
178        let mut pool = self.sload_pools(pool_id)?;
179
180        // Rebalancing swaps are always from validatorToken to userToken
181        // Calculate input and update reserves
182        let amount_in = amount_out
183            .checked_mul(N)
184            .and_then(|product| product.checked_div(SCALE))
185            .and_then(|result| result.checked_add(U256::ONE))
186            .ok_or(TempoPrecompileError::under_overflow())?;
187
188        let amount_in: u128 = amount_in
189            .try_into()
190            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
191        let amount_out: u128 = amount_out
192            .try_into()
193            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
194
195        pool.reserve_validator_token = pool
196            .reserve_validator_token
197            .checked_add(amount_in)
198            .ok_or(TIPFeeAMMError::insufficient_reserves())?;
199
200        pool.reserve_user_token = pool
201            .reserve_user_token
202            .checked_sub(amount_out)
203            .ok_or(TIPFeeAMMError::invalid_amount())?;
204
205        self.sstore_pools(pool_id, pool)?;
206
207        let amount_in = U256::from(amount_in);
208        let amount_out = U256::from(amount_out);
209        TIP20Token::from_address(validator_token, self.storage)?.system_transfer_from(
210            msg_sender,
211            self.address,
212            amount_in,
213        )?;
214
215        TIP20Token::from_address(user_token, self.storage)?.transfer(
216            self.address,
217            ITIP20::transferCall {
218                to,
219                amount: amount_out,
220            },
221        )?;
222
223        self.storage.emit_event(
224            self.address,
225            TIPFeeAMMEvent::RebalanceSwap(ITIPFeeAMM::RebalanceSwap {
226                userToken: user_token,
227                validatorToken: validator_token,
228                swapper: msg_sender,
229                amountIn: amount_in,
230                amountOut: amount_out,
231            })
232            .into_log_data(),
233        )?;
234
235        Ok(amount_in)
236    }
237
238    /// Mint LP tokens for a given pool
239    pub fn mint(
240        &mut self,
241        msg_sender: Address,
242        user_token: Address,
243        validator_token: Address,
244        amount_user_token: U256,
245        amount_validator_token: U256,
246        to: Address,
247    ) -> Result<U256> {
248        if user_token == validator_token {
249            return Err(TIPFeeAMMError::identical_addresses().into());
250        }
251
252        // Validate both tokens are USD currency
253        validate_usd_currency(user_token, self.storage)?;
254        validate_usd_currency(validator_token, self.storage)?;
255
256        let pool_id = self.pool_id(user_token, validator_token);
257        let mut pool = self.sload_pools(pool_id)?;
258        let total_supply = self.get_total_supply(pool_id)?;
259
260        let liquidity = if total_supply.is_zero() {
261            // Use checked math for multiplication and division
262            let mean = if self.storage.spec().is_moderato() {
263                amount_user_token
264                    .checked_add(amount_validator_token)
265                    .map(|product| product / uint!(2_U256))
266                    .ok_or(TIPFeeAMMError::invalid_amount())?
267            } else {
268                amount_user_token
269                    .checked_mul(amount_validator_token)
270                    .map(|product| product / uint!(2_U256))
271                    .ok_or(TIPFeeAMMError::invalid_amount())?
272            };
273            if mean <= MIN_LIQUIDITY {
274                return Err(TIPFeeAMMError::insufficient_liquidity().into());
275            }
276            self.set_total_supply(pool_id, MIN_LIQUIDITY)?;
277            mean.checked_sub(MIN_LIQUIDITY)
278                .ok_or(TIPFeeAMMError::insufficient_liquidity())?
279        } else {
280            let liquidity_user = if pool.reserve_user_token > 0 {
281                amount_user_token
282                    .checked_mul(total_supply)
283                    .and_then(|numerator| {
284                        numerator.checked_div(U256::from(pool.reserve_user_token))
285                    })
286                    .ok_or(TIPFeeAMMError::invalid_amount())?
287            } else {
288                U256::MAX
289            };
290
291            let liquidity_validator = if pool.reserve_validator_token > 0 {
292                amount_validator_token
293                    .checked_mul(total_supply)
294                    .and_then(|numerator| {
295                        numerator.checked_div(U256::from(pool.reserve_validator_token))
296                    })
297                    .ok_or(TIPFeeAMMError::invalid_amount())?
298            } else {
299                U256::MAX
300            };
301
302            liquidity_user.min(liquidity_validator)
303        };
304
305        if liquidity.is_zero() {
306            return Err(TIPFeeAMMError::insufficient_liquidity().into());
307        }
308
309        // Transfer tokens from user to contract
310        let _ = TIP20Token::from_address(user_token, self.storage)?.system_transfer_from(
311            msg_sender,
312            self.address,
313            amount_user_token,
314        )?;
315
316        let _ = TIP20Token::from_address(validator_token, self.storage)?.system_transfer_from(
317            msg_sender,
318            self.address,
319            amount_validator_token,
320        )?;
321
322        // Update reserves with overflow checks
323        let user_amount: u128 = amount_user_token
324            .try_into()
325            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
326        let validator_amount: u128 = amount_validator_token
327            .try_into()
328            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
329
330        pool.reserve_user_token = pool
331            .reserve_user_token
332            .checked_add(user_amount)
333            .ok_or(TIPFeeAMMError::invalid_amount())?;
334
335        pool.reserve_validator_token = pool
336            .reserve_validator_token
337            .checked_add(validator_amount)
338            .ok_or(TIPFeeAMMError::invalid_amount())?;
339        self.sstore_pools(pool_id, pool)?;
340
341        // Mint LP tokens
342        let current_total_supply = self.get_total_supply(pool_id)?;
343        self.set_total_supply(
344            pool_id,
345            current_total_supply
346                .checked_add(liquidity)
347                .ok_or(TempoPrecompileError::under_overflow())?,
348        )?;
349        let balance = self.get_liquidity_balances(pool_id, to)?;
350        self.set_liquidity_balances(
351            pool_id,
352            to,
353            balance
354                .checked_add(liquidity)
355                .ok_or(TempoPrecompileError::under_overflow())?,
356        )?;
357
358        // Emit Mint event
359        self.storage.emit_event(
360            self.address,
361            TIPFeeAMMEvent::Mint(ITIPFeeAMM::Mint {
362                sender: msg_sender,
363                userToken: user_token,
364                validatorToken: validator_token,
365                amountUserToken: amount_user_token,
366                amountValidatorToken: amount_validator_token,
367                liquidity,
368            })
369            .into_log_data(),
370        )?;
371
372        Ok(liquidity)
373    }
374
375    /// Mint LP tokens using only validator tokens
376    pub fn mint_with_validator_token(
377        &mut self,
378        msg_sender: Address,
379        user_token: Address,
380        validator_token: Address,
381        amount_validator_token: U256,
382        to: Address,
383    ) -> Result<U256> {
384        if user_token == validator_token {
385            return Err(TIPFeeAMMError::identical_addresses().into());
386        }
387
388        // Validate both tokens are USD currency
389        validate_usd_currency(user_token, self.storage)?;
390        validate_usd_currency(validator_token, self.storage)?;
391
392        let pool_id = self.pool_id(user_token, validator_token);
393        let mut pool = self.sload_pools(pool_id)?;
394        let mut total_supply = self.get_total_supply(pool_id)?;
395
396        let liquidity = if pool.reserve_user_token == 0 && pool.reserve_validator_token == 0 {
397            let half_amount = amount_validator_token
398                .checked_div(uint!(2_U256))
399                .ok_or(TempoPrecompileError::under_overflow())?;
400
401            if half_amount <= MIN_LIQUIDITY {
402                return Err(TIPFeeAMMError::insufficient_liquidity().into());
403            }
404
405            total_supply = total_supply
406                .checked_add(MIN_LIQUIDITY)
407                .ok_or(TempoPrecompileError::under_overflow())?;
408            self.set_total_supply(pool_id, total_supply)?;
409
410            half_amount
411                .checked_sub(MIN_LIQUIDITY)
412                .ok_or(TIPFeeAMMError::insufficient_liquidity())?
413        } else {
414            // Subsequent deposits: mint as if user called rebalanceSwap then minted with both
415            // liquidity = amountValidatorToken * _totalSupply / (V + n * U), with n = N / SCALE
416            let product = N
417                .checked_mul(U256::from(pool.reserve_user_token))
418                .and_then(|product| product.checked_div(SCALE))
419                .ok_or(TIPFeeAMMError::invalid_swap_calculation())?;
420
421            let denom = U256::from(pool.reserve_validator_token)
422                .checked_add(product)
423                .ok_or(TIPFeeAMMError::invalid_amount())?;
424
425            if denom.is_zero() {
426                return Err(TIPFeeAMMError::division_by_zero().into());
427            }
428
429            amount_validator_token
430                .checked_mul(total_supply)
431                .and_then(|numerator| numerator.checked_div(denom))
432                .ok_or(TIPFeeAMMError::invalid_swap_calculation())?
433        };
434
435        if liquidity.is_zero() {
436            return Err(TIPFeeAMMError::insufficient_liquidity().into());
437        }
438
439        // Transfer validator tokens from user
440        let _ = TIP20Token::from_address(validator_token, self.storage)?.system_transfer_from(
441            msg_sender,
442            self.address,
443            amount_validator_token,
444        )?;
445
446        // Update reserves
447        let validator_amount: u128 = amount_validator_token
448            .try_into()
449            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
450
451        pool.reserve_validator_token = pool
452            .reserve_validator_token
453            .checked_add(validator_amount)
454            .ok_or(TIPFeeAMMError::invalid_amount())?;
455
456        self.sstore_pools(pool_id, pool)?;
457
458        // Mint LP tokens
459        self.set_total_supply(
460            pool_id,
461            total_supply
462                .checked_add(liquidity)
463                .ok_or(TempoPrecompileError::under_overflow())?,
464        )?;
465
466        let balance = self.get_liquidity_balances(pool_id, to)?;
467        self.set_liquidity_balances(
468            pool_id,
469            to,
470            balance
471                .checked_add(liquidity)
472                .ok_or(TempoPrecompileError::under_overflow())?,
473        )?;
474
475        // Emit Mint event
476        self.storage.emit_event(
477            self.address,
478            TIPFeeAMMEvent::Mint(ITIPFeeAMM::Mint {
479                sender: msg_sender,
480                userToken: user_token,
481                validatorToken: validator_token,
482                amountUserToken: U256::ZERO,
483                amountValidatorToken: amount_validator_token,
484                liquidity,
485            })
486            .into_log_data(),
487        )?;
488
489        Ok(liquidity)
490    }
491
492    /// Burn LP tokens for a given pool
493    pub fn burn(
494        &mut self,
495        msg_sender: Address,
496        user_token: Address,
497        validator_token: Address,
498        liquidity: U256,
499        to: Address,
500    ) -> Result<(U256, U256)> {
501        if user_token == validator_token {
502            return Err(TIPFeeAMMError::identical_addresses().into());
503        }
504
505        // Validate both tokens are USD currency
506        validate_usd_currency(user_token, self.storage)?;
507        validate_usd_currency(validator_token, self.storage)?;
508
509        let pool_id = self.pool_id(user_token, validator_token);
510        // Check user has sufficient liquidity
511        let balance = self.get_liquidity_balances(pool_id, msg_sender)?;
512        if balance < liquidity {
513            return Err(TIPFeeAMMError::insufficient_liquidity().into());
514        }
515
516        let mut pool = self.sload_pools(pool_id)?;
517        // Calculate amounts to return
518        let (amount_user_token, amount_validator_token) =
519            self.calculate_burn_amounts(&pool, pool_id, liquidity)?;
520
521        // Burn LP tokens
522        self.set_liquidity_balances(
523            pool_id,
524            msg_sender,
525            balance
526                .checked_sub(liquidity)
527                .ok_or(TempoPrecompileError::under_overflow())?,
528        )?;
529        let total_supply = self.get_total_supply(pool_id)?;
530        self.set_total_supply(
531            pool_id,
532            total_supply
533                .checked_sub(liquidity)
534                .ok_or(TempoPrecompileError::under_overflow())?,
535        )?;
536
537        // Update reserves with underflow checks
538        let user_amount: u128 = amount_user_token
539            .try_into()
540            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
541        let validator_amount: u128 = amount_validator_token
542            .try_into()
543            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
544
545        pool.reserve_user_token = pool
546            .reserve_user_token
547            .checked_sub(user_amount)
548            .ok_or(TIPFeeAMMError::insufficient_reserves())?;
549        pool.reserve_validator_token = pool
550            .reserve_validator_token
551            .checked_sub(validator_amount)
552            .ok_or(TIPFeeAMMError::insufficient_reserves())?;
553        self.sstore_pools(pool_id, pool)?;
554
555        // Transfer tokens to user
556        let _ = TIP20Token::from_address(user_token, self.storage)?.transfer(
557            self.address,
558            ITIP20::transferCall {
559                to,
560                amount: amount_user_token,
561            },
562        )?;
563
564        let _ = TIP20Token::from_address(validator_token, self.storage)?.transfer(
565            self.address,
566            ITIP20::transferCall {
567                to,
568                amount: amount_validator_token,
569            },
570        )?;
571
572        // Emit Burn event
573        self.storage.emit_event(
574            self.address,
575            TIPFeeAMMEvent::Burn(ITIPFeeAMM::Burn {
576                sender: msg_sender,
577                userToken: user_token,
578                validatorToken: validator_token,
579                amountUserToken: amount_user_token,
580                amountValidatorToken: amount_validator_token,
581                liquidity,
582                to,
583            })
584            .into_log_data(),
585        )?;
586
587        Ok((amount_user_token, amount_validator_token))
588    }
589
590    /// Calculate burn amounts for liquidity withdrawal
591    fn calculate_burn_amounts(
592        &mut self,
593        pool: &Pool,
594        pool_id: B256,
595        liquidity: U256,
596    ) -> Result<(U256, U256)> {
597        let total_supply = self.get_total_supply(pool_id)?;
598        let amount_user_token = liquidity
599            .checked_mul(U256::from(pool.reserve_user_token))
600            .and_then(|product| product.checked_div(total_supply))
601            .ok_or(TempoPrecompileError::under_overflow())?;
602        let amount_validator_token = liquidity
603            .checked_mul(U256::from(pool.reserve_validator_token))
604            .and_then(|product| product.checked_div(total_supply))
605            .ok_or(TempoPrecompileError::under_overflow())?;
606
607        if !self.storage.spec().is_allegretto() {
608            if amount_user_token.is_zero() || amount_validator_token.is_zero() {
609                return Err(TIPFeeAMMError::insufficient_liquidity().into());
610            }
611
612            let available_user_token = self.get_effective_user_reserve(pool_id)?;
613            if amount_user_token > available_user_token {
614                return Err(TIPFeeAMMError::insufficient_reserves().into());
615            }
616        }
617
618        // Check that withdrawal does not violate pending swaps
619        let available_validator_token = self.get_effective_validator_reserve(pool_id)?;
620        if amount_validator_token > available_validator_token {
621            return Err(TIPFeeAMMError::insufficient_reserves().into());
622        }
623
624        Ok((amount_user_token, amount_validator_token))
625    }
626
627    /// Execute all pending fee swaps for a pool
628    pub fn execute_pending_fee_swaps(
629        &mut self,
630        user_token: Address,
631        validator_token: Address,
632    ) -> Result<U256> {
633        let pool_id = self.pool_id(user_token, validator_token);
634        let mut pool = self.sload_pools(pool_id)?;
635
636        let amount_in = U256::from(self.get_pending_fee_swap_in(pool_id)?);
637        let pending_out = compute_amount_out(amount_in)?;
638
639        // Use checked math for these operations
640        let new_user_reserve = U256::from(pool.reserve_user_token)
641            .checked_add(amount_in)
642            .ok_or(TempoPrecompileError::under_overflow())?;
643        let new_validator_reserve = U256::from(pool.reserve_validator_token)
644            .checked_sub(pending_out)
645            .ok_or(TempoPrecompileError::under_overflow())?;
646
647        pool.reserve_user_token = new_user_reserve
648            .try_into()
649            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
650        pool.reserve_validator_token = new_validator_reserve
651            .try_into()
652            .map_err(|_| TIPFeeAMMError::invalid_amount())?;
653
654        self.sstore_pools(pool_id, pool)?;
655        self.clear_pending_fee_swap_in(pool_id)?;
656
657        self.storage.emit_event(
658            self.address,
659            TIPFeeAMMEvent::FeeSwap(ITIPFeeAMM::FeeSwap {
660                userToken: user_token,
661                validatorToken: validator_token,
662                amountIn: amount_in,
663                amountOut: pending_out,
664            })
665            .into_log_data(),
666        )?;
667
668        Ok(pending_out)
669    }
670
671    /// Get total supply of LP tokens for a pool
672    pub fn get_total_supply(&mut self, pool_id: B256) -> Result<U256> {
673        self.sload_total_supply(pool_id)
674    }
675
676    /// Set total supply of LP tokens for a pool
677    fn set_total_supply(&mut self, pool_id: B256, total_supply: U256) -> Result<()> {
678        self.sstore_total_supply(pool_id, total_supply)
679    }
680
681    /// Get user's LP token balance
682    pub fn get_liquidity_balances(&mut self, pool_id: B256, user: Address) -> Result<U256> {
683        self.sload_liquidity_balances(pool_id, user)
684    }
685
686    /// Set user's LP token balance
687    fn set_liquidity_balances(
688        &mut self,
689        pool_id: B256,
690        user: Address,
691        balance: U256,
692    ) -> Result<()> {
693        self.sstore_liquidity_balances(pool_id, user, balance)
694    }
695
696    /// Get pending fee swap amount for a pool
697    pub fn get_pending_fee_swap_in(&mut self, pool_id: B256) -> Result<u128> {
698        self.sload_pending_fee_swap_in(pool_id)
699    }
700
701    /// Set pending fee swap amount for a pool
702    fn set_pending_fee_swap_in(&mut self, pool_id: B256, amount: u128) -> Result<()> {
703        self.sstore_pending_fee_swap_in(pool_id, amount)
704    }
705}
706
707/// Calculate integer square rootu
708pub fn sqrt(x: U256) -> U256 {
709    if x == U256::ZERO {
710        return U256::ZERO;
711    }
712    let mut z = (x + U256::ONE) / uint!(2_U256);
713    let mut y = x;
714    while z < y {
715        y = z;
716        z = (x / z + z) / uint!(2_U256);
717    }
718    y
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724    use crate::{
725        PATH_USD_ADDRESS,
726        error::TempoPrecompileError,
727        storage::{ContractStorage, hashmap::HashMapStorageProvider},
728        tip20::{ISSUER_ROLE, TIP20Token, tests::initialize_path_usd, token_id_to_address},
729    };
730    use alloy::primitives::{Address, uint};
731    use tempo_chainspec::hardfork::TempoHardfork;
732    use tempo_contracts::precompiles::TIP20Error;
733
734    #[test]
735    fn test_mint_identical_addresses() {
736        let mut storage = HashMapStorageProvider::new(1);
737        let mut amm = TipFeeManager::new(&mut storage);
738
739        let msg_sender = Address::random();
740        let token = Address::random();
741        let amount = U256::from(1000);
742        let to = Address::random();
743
744        let result = amm.mint(msg_sender, token, token, amount, amount, to);
745
746        assert!(matches!(
747            result,
748            Err(TempoPrecompileError::TIPFeeAMMError(
749                TIPFeeAMMError::IdenticalAddresses(_)
750            ))
751        ));
752    }
753
754    #[test]
755    fn test_burn_identical_addresses() {
756        let mut storage = HashMapStorageProvider::new(1);
757        let mut amm = TipFeeManager::new(&mut storage);
758
759        let msg_sender = Address::random();
760        let token = Address::random();
761        let liquidity = U256::from(1000);
762        let to = Address::random();
763
764        let result = amm.burn(msg_sender, token, token, liquidity, to);
765
766        assert!(matches!(
767            result,
768            Err(TempoPrecompileError::TIPFeeAMMError(
769                TIPFeeAMMError::IdenticalAddresses(_)
770            ))
771        ));
772    }
773
774    fn setup_test_amm() -> (
775        TipFeeManager<'static, HashMapStorageProvider>,
776        Address,
777        Address,
778        Address,
779    ) {
780        let storage = Box::leak(Box::new(HashMapStorageProvider::new(1)));
781        let admin = Address::random();
782
783        // Initialize PathUSD first
784        initialize_path_usd(storage, admin).unwrap();
785
786        // Create USD tokens for user and validator
787        let user_token = token_id_to_address(1);
788        let mut user_tip20 = TIP20Token::from_address(user_token, storage).unwrap();
789        user_tip20
790            .initialize(
791                "UserToken",
792                "UTK",
793                "USD",
794                PATH_USD_ADDRESS,
795                admin,
796                Address::ZERO,
797            )
798            .unwrap();
799
800        let validator_token = token_id_to_address(2);
801        let mut validator_tip20 = TIP20Token::from_address(validator_token, storage).unwrap();
802        validator_tip20
803            .initialize(
804                "ValidatorToken",
805                "VTK",
806                "USD",
807                PATH_USD_ADDRESS,
808                admin,
809                Address::ZERO,
810            )
811            .unwrap();
812
813        let amm = TipFeeManager::new(storage);
814        (amm, Address::ZERO, user_token, validator_token)
815    }
816
817    fn setup_pool_with_liquidity(
818        amm: &mut TipFeeManager<'_, impl PrecompileStorageProvider>,
819        user_token: Address,
820        validator_token: Address,
821        user_amount: U256,
822        validator_amount: U256,
823    ) -> Result<B256> {
824        let pool_id = amm.pool_id(user_token, validator_token);
825        let pool = Pool {
826            reserve_user_token: user_amount.to::<u128>(),
827            reserve_validator_token: validator_amount.to::<u128>(),
828        };
829        amm.sstore_pools(pool_id, pool)?;
830
831        // Set initial liquidity supply
832        let liquidity = if user_amount == validator_amount {
833            // Simplified: for equal amounts, liquidity ~= amount
834            user_amount
835        } else {
836            // Use geometric mean for unequal amounts
837            sqrt(user_amount * validator_amount)
838        };
839        amm.set_total_supply(pool_id, liquidity)?;
840
841        Ok(pool_id)
842    }
843
844    /// Test basic fee swap functionality
845    /// Corresponds to testFeeSwap in StableAMM.t.sol
846    #[test]
847    fn test_fee_swap() -> eyre::Result<()> {
848        let (mut amm, _, user_token, validator_token) = setup_test_amm();
849
850        // Setup pool with 100,000 tokens each
851        let liquidity_amount = uint!(100000_U256) * uint!(10_U256).pow(U256::from(6));
852        let pool_id = setup_pool_with_liquidity(
853            &mut amm,
854            user_token,
855            validator_token,
856            liquidity_amount,
857            liquidity_amount,
858        )?;
859
860        // Execute fee swap for 1000 tokens
861        let amount_in = uint!(1000_U256) * uint!(10_U256).pow(U256::from(6));
862
863        // Calculate expected output: amountIn * 0.9975
864        let expected_out = (amount_in * M) / SCALE;
865
866        // Execute fee swap
867        amm.reserve_liquidity(user_token, validator_token, amount_in)?;
868
869        // Check pending swaps updated
870        let pending_in = amm.get_pending_fee_swap_in(pool_id)?;
871        assert_eq!(
872            pending_in,
873            amount_in.to::<u128>(),
874            "Pending input should match amount in"
875        );
876
877        // Verify the expected output calculation
878        assert_eq!(expected_out, amount_in * M / SCALE);
879
880        Ok(())
881    }
882
883    /// Test fee swap with insufficient liquidity
884    /// Corresponds to testFeeSwapInsufficientLiquidity in StableAMM.t.sol
885    #[test]
886    fn test_fee_swap_insufficient_liquidity() {
887        let (mut amm, _, user_token, validator_token) = setup_test_amm();
888
889        // Setup pool with only 100 tokens each
890        let small_liquidity = uint!(100_U256) * uint!(10_U256).pow(U256::from(6));
891        setup_pool_with_liquidity(
892            &mut amm,
893            user_token,
894            validator_token,
895            small_liquidity,
896            small_liquidity,
897        )
898        .unwrap();
899
900        // Try to swap 201 tokens (would output ~200.7 tokens, but only 100 available)
901        let too_large_amount = uint!(201_U256) * uint!(10_U256).pow(U256::from(6));
902
903        // Execute fee swap - should fail
904        let result = amm.reserve_liquidity(user_token, validator_token, too_large_amount);
905
906        assert!(matches!(
907            result,
908            Err(TempoPrecompileError::TIPFeeAMMError(
909                TIPFeeAMMError::InsufficientLiquidity(_)
910            ))
911        ))
912    }
913
914    /// Test fee swap rounding consistency
915    /// Corresponds to testFeeSwapRoundingConsistency in StableAMM.t.sol
916    #[test]
917    fn test_fee_swap_rounding_consistency() -> eyre::Result<()> {
918        let (mut amm, _, user_token, validator_token) = setup_test_amm();
919
920        // Setup pool with 100,000 tokens each
921        let liquidity_amount = uint!(100000_U256) * uint!(10_U256).pow(U256::from(6));
922        let pool_id = setup_pool_with_liquidity(
923            &mut amm,
924            user_token,
925            validator_token,
926            liquidity_amount,
927            liquidity_amount,
928        )?;
929
930        // Test with a clean input amount
931        let amount_in = uint!(10000_U256) * uint!(10_U256).pow(U256::from(6));
932
933        // Execute fee swap
934        amm.reserve_liquidity(user_token, validator_token, amount_in)?;
935
936        // Calculate expected output using integer division (rounds down)
937        let expected_out = (amount_in * M) / SCALE;
938
939        // Execute pending swaps and verify reserves
940        let actual_out = amm.execute_pending_fee_swaps(user_token, validator_token)?;
941        assert_eq!(actual_out, expected_out, "Output should match expected");
942
943        // Check reserves updated correctly
944        let pool = amm.sload_pools(pool_id)?;
945        assert_eq!(
946            U256::from(pool.reserve_user_token),
947            liquidity_amount + amount_in,
948            "User token reserve should increase by input"
949        );
950        assert_eq!(
951            U256::from(pool.reserve_validator_token),
952            liquidity_amount - actual_out,
953            "Validator token reserve should decrease by output"
954        );
955
956        Ok(())
957    }
958
959    /// Test execute pending fee swaps
960    #[test]
961    fn test_execute_pending_fee_swaps() -> Result<()> {
962        let (mut amm, _, user_token, validator_token) = setup_test_amm();
963
964        // Setup pool
965        let initial_amount = uint!(100000_U256) * uint!(10_U256).pow(U256::from(6));
966        let pool_id = setup_pool_with_liquidity(
967            &mut amm,
968            user_token,
969            validator_token,
970            initial_amount,
971            initial_amount,
972        )?;
973
974        // Execute multiple fee swaps
975        let swap1 = uint!(1000_U256) * uint!(10_U256).pow(U256::from(6));
976        let swap2 = uint!(2000_U256) * uint!(10_U256).pow(U256::from(6));
977        let swap3 = uint!(3000_U256) * uint!(10_U256).pow(U256::from(6));
978
979        amm.reserve_liquidity(user_token, validator_token, swap1)?;
980        amm.reserve_liquidity(user_token, validator_token, swap2)?;
981        amm.reserve_liquidity(user_token, validator_token, swap3)?;
982
983        // Check total pending
984        let total_pending = swap1 + swap2 + swap3;
985        assert_eq!(
986            amm.get_pending_fee_swap_in(pool_id)
987                .expect("Could not get fee swap in"),
988            total_pending.to::<u128>()
989        );
990
991        // Execute all pending swaps
992        let total_out = amm.execute_pending_fee_swaps(user_token, validator_token)?;
993        let expected_total_out = (total_pending * M) / SCALE;
994        assert_eq!(total_out, expected_total_out);
995
996        // Verify pending cleared
997        assert_eq!(
998            amm.get_pending_fee_swap_in(pool_id)
999                .expect("Could not get fee swap in"),
1000            0
1001        );
1002
1003        // Verify reserves updated
1004        let pool = amm.sload_pools(pool_id)?;
1005        assert_eq!(
1006            U256::from(pool.reserve_user_token),
1007            initial_amount + total_pending
1008        );
1009        assert_eq!(
1010            U256::from(pool.reserve_validator_token),
1011            initial_amount - total_out
1012        );
1013
1014        Ok(())
1015    }
1016
1017    /// Test rebalance swap in correct direction
1018    /// Corresponds to disabled_testRebalanceSwapTowardBalance in StableAMM.t.sol
1019    #[test]
1020    #[ignore = "Overflow in calculateLiquidity when called during rebalanceSwap (same as Solidity disabled test)"]
1021    fn test_rebalance_swap() -> Result<()> {
1022        let (mut amm, _, user_token, validator_token) = setup_test_amm();
1023
1024        // Add balanced liquidity first (using same decimals as Solidity test)
1025        let initial_liquidity = uint!(100000_U256) * uint!(10_U256).pow(U256::from(6)); // 100000 * 1e6
1026        let pool_id = setup_pool_with_liquidity(
1027            &mut amm,
1028            user_token,
1029            validator_token,
1030            initial_liquidity,
1031            initial_liquidity,
1032        )?;
1033
1034        // Make the pool imbalanced by executing a fee swap
1035        let user_token_in = uint!(20000_U256) * uint!(10_U256).pow(U256::from(6)); // 20000 * 1e6
1036        amm.reserve_liquidity(user_token, validator_token, user_token_in)?;
1037        amm.execute_pending_fee_swaps(user_token, validator_token)?;
1038
1039        let pool_before = amm.sload_pools(pool_id)?;
1040        let x_before = U256::from(pool_before.reserve_user_token);
1041        let y_before = U256::from(pool_before.reserve_validator_token);
1042
1043        // Execute rebalancing swap using the actual function
1044        let swap_amount = uint!(1000_U256) * uint!(10_U256).pow(U256::from(6)); // 1000 * 1e6
1045        let msg_sender = Address::random();
1046        let to = Address::random();
1047        let amount_in =
1048            amm.rebalance_swap(msg_sender, user_token, validator_token, swap_amount, to)?;
1049
1050        // Verify the swap input
1051        assert!(amount_in > 0, "Should provide validator tokens");
1052
1053        // Get updated pool state
1054        let pool_after = amm.sload_pools(pool_id)?;
1055        let x_after = U256::from(pool_after.reserve_user_token);
1056        let y_after = U256::from(pool_after.reserve_validator_token);
1057
1058        // For rebalance swap: validator tokens go in, user tokens come out
1059        assert!(x_after < x_before, "User token reserve should decrease");
1060        assert!(
1061            y_after > y_before,
1062            "Validator token reserve should increase"
1063        );
1064
1065        // The amount_in returned is the validator tokens provided
1066        assert_eq!(
1067            y_after - y_before,
1068            amount_in,
1069            "Amount in should equal increase in validator reserve"
1070        );
1071
1072        // Verify the swap reduces imbalance
1073        let imbalance_before = if x_before > y_before {
1074            x_before - y_before
1075        } else {
1076            y_before - x_before
1077        };
1078        let imbalance_after = if x_after > y_after {
1079            x_after - y_after
1080        } else {
1081            y_after - x_after
1082        };
1083        assert!(
1084            imbalance_after < imbalance_before,
1085            "Swap should reduce imbalance"
1086        );
1087
1088        Ok(())
1089    }
1090
1091    /// Test rebalance swap with insufficient user funds
1092    #[test]
1093    fn test_rebalance_swap_insufficient_funds() -> eyre::Result<()> {
1094        let (mut amm, _, user_token, validator_token) = setup_test_amm();
1095
1096        // Setup balanced pool
1097        let amount = uint!(100000_U256) * uint!(10_U256).pow(U256::from(6));
1098        let pool_id =
1099            setup_pool_with_liquidity(&mut amm, user_token, validator_token, amount, amount)?;
1100
1101        let pool = amm.sload_pools(pool_id)?;
1102        assert_eq!(pool.reserve_user_token, pool.reserve_validator_token,);
1103
1104        let msg_sender = Address::random();
1105        let to = Address::random();
1106        let result = amm.rebalance_swap(
1107            msg_sender,
1108            user_token,
1109            validator_token,
1110            amount + U256::ONE,
1111            to,
1112        );
1113
1114        assert!(matches!(
1115            result,
1116            Err(TempoPrecompileError::TIPFeeAMMError(
1117                TIPFeeAMMError::InvalidAmount(_)
1118            )),
1119        ));
1120
1121        Ok(())
1122    }
1123
1124    /// Test has_liquidity function
1125    #[test]
1126    fn test_has_liquidity() -> eyre::Result<()> {
1127        let (mut amm, _, user_token, validator_token) = setup_test_amm();
1128
1129        // Setup pool with 100 tokens
1130        let liquidity = uint!(100_U256) * uint!(10_U256).pow(U256::from(6));
1131        setup_pool_with_liquidity(&mut amm, user_token, validator_token, liquidity, liquidity)?;
1132
1133        // Test with amount that would work
1134        let ok_amount = uint!(100_U256) * uint!(10_U256).pow(U256::from(6));
1135        assert!(
1136            amm.reserve_liquidity(user_token, validator_token, ok_amount)
1137                .is_ok(),
1138            "Should have liquidity for 100 tokens"
1139        );
1140
1141        // Test with amount that would fail
1142        let too_much = uint!(101_U256) * uint!(10_U256).pow(U256::from(6));
1143        assert!(
1144            amm.reserve_liquidity(user_token, validator_token, too_much)
1145                .is_err(),
1146            "Should not have liquidity for 101 tokens"
1147        );
1148
1149        Ok(())
1150    }
1151
1152    #[test]
1153    fn test_mint_rejects_non_usd_user_token() {
1154        let mut storage = HashMapStorageProvider::new(1);
1155        let amount = U256::from(1000);
1156
1157        let admin = Address::random();
1158        let msg_sender = Address::random();
1159        let to = Address::random();
1160
1161        // Init Linking USD, user token and validator tokens
1162        let mut path_usd = TIP20Token::from_address(PATH_USD_ADDRESS, &mut storage).unwrap();
1163        path_usd
1164            .initialize(
1165                "PathUSD",
1166                "LUSD",
1167                "USD",
1168                Address::ZERO,
1169                admin,
1170                Address::ZERO,
1171            )
1172            .unwrap();
1173
1174        let mut user_token = TIP20Token::new(1, &mut storage);
1175        user_token
1176            .initialize(
1177                "TestToken",
1178                "TEST",
1179                "EUR",
1180                PATH_USD_ADDRESS,
1181                admin,
1182                Address::ZERO,
1183            )
1184            .unwrap();
1185        let user_token_address = user_token.address();
1186
1187        let mut validator_token = TIP20Token::new(2, &mut storage);
1188        validator_token
1189            .initialize(
1190                "TestToken",
1191                "TEST",
1192                "USD",
1193                PATH_USD_ADDRESS,
1194                admin,
1195                Address::ZERO,
1196            )
1197            .unwrap();
1198        let validator_token_address = validator_token.address();
1199
1200        let mut amm = TipFeeManager::new(&mut storage);
1201        let result = amm.mint(
1202            msg_sender,
1203            user_token_address,
1204            validator_token_address,
1205            amount,
1206            amount,
1207            to,
1208        );
1209
1210        assert!(matches!(
1211            result,
1212            Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
1213        ));
1214
1215        // Test the inverse tokens
1216        let result = amm.mint(
1217            msg_sender,
1218            validator_token_address,
1219            user_token_address,
1220            amount,
1221            amount,
1222            to,
1223        );
1224        assert!(matches!(
1225            result,
1226            Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
1227        ));
1228    }
1229
1230    #[test]
1231    fn test_burn_rejects_non_usd_tokens() {
1232        let mut storage = HashMapStorageProvider::new(1);
1233        let liquidity = U256::from(1000);
1234
1235        let admin = Address::random();
1236        let msg_sender = Address::random();
1237        let to = Address::random();
1238
1239        // Init Linking USD, user token and validator tokens
1240        let mut path_usd = TIP20Token::from_address(PATH_USD_ADDRESS, &mut storage).unwrap();
1241        path_usd
1242            .initialize(
1243                "PathUSD",
1244                "LUSD",
1245                "USD",
1246                Address::ZERO,
1247                admin,
1248                Address::ZERO,
1249            )
1250            .unwrap();
1251
1252        let mut user_token = TIP20Token::new(1, &mut storage);
1253        user_token
1254            .initialize(
1255                "TestToken",
1256                "TEST",
1257                "EUR",
1258                PATH_USD_ADDRESS,
1259                admin,
1260                Address::ZERO,
1261            )
1262            .unwrap();
1263        let user_token_address = user_token.address();
1264
1265        let mut validator_token = TIP20Token::new(2, &mut storage);
1266        validator_token
1267            .initialize(
1268                "TestToken",
1269                "TEST",
1270                "USD",
1271                PATH_USD_ADDRESS,
1272                admin,
1273                Address::ZERO,
1274            )
1275            .unwrap();
1276        let validator_token_address = validator_token.address();
1277
1278        let mut amm = TipFeeManager::new(&mut storage);
1279        let result = amm.burn(
1280            msg_sender,
1281            user_token_address,
1282            validator_token_address,
1283            liquidity,
1284            to,
1285        );
1286
1287        assert!(matches!(
1288            result,
1289            Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
1290        ));
1291
1292        // Test the inverse tokens
1293        let result = amm.burn(
1294            msg_sender,
1295            validator_token_address,
1296            user_token_address,
1297            liquidity,
1298            to,
1299        );
1300
1301        assert!(matches!(
1302            result,
1303            Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
1304        ));
1305    }
1306    #[test]
1307    fn test_rebalance_swap_rejects_non_usd_tokens() {
1308        let mut storage = HashMapStorageProvider::new(1);
1309
1310        let admin = Address::random();
1311        let msg_sender = Address::random();
1312        let amount_out = U256::from(1000);
1313        let to = Address::random();
1314
1315        // Init Linking USD, user token and validator tokens
1316        let mut path_usd = TIP20Token::from_address(PATH_USD_ADDRESS, &mut storage).unwrap();
1317        path_usd
1318            .initialize(
1319                "PathUSD",
1320                "LUSD",
1321                "USD",
1322                Address::ZERO,
1323                admin,
1324                Address::ZERO,
1325            )
1326            .unwrap();
1327
1328        let mut user_token = TIP20Token::new(1, &mut storage);
1329        user_token
1330            .initialize(
1331                "TestToken",
1332                "TEST",
1333                "EUR",
1334                PATH_USD_ADDRESS,
1335                admin,
1336                Address::ZERO,
1337            )
1338            .unwrap();
1339        let user_token_address = user_token.address();
1340
1341        let mut validator_token = TIP20Token::new(2, &mut storage);
1342        validator_token
1343            .initialize(
1344                "TestToken",
1345                "TEST",
1346                "USD",
1347                PATH_USD_ADDRESS,
1348                admin,
1349                Address::ZERO,
1350            )
1351            .unwrap();
1352        let validator_token_address = validator_token.address();
1353
1354        let mut amm = TipFeeManager::new(&mut storage);
1355        let result = amm.rebalance_swap(
1356            msg_sender,
1357            user_token_address,
1358            validator_token_address,
1359            amount_out,
1360            to,
1361        );
1362
1363        assert!(matches!(
1364            result,
1365            Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
1366        ));
1367
1368        // Test the inverse tokens
1369        let mut amm = TipFeeManager::new(&mut storage);
1370        let result = amm.rebalance_swap(
1371            msg_sender,
1372            validator_token_address,
1373            user_token_address,
1374            amount_out,
1375            to,
1376        );
1377
1378        assert!(matches!(
1379            result,
1380            Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
1381        ));
1382    }
1383
1384    #[test]
1385    fn test_mint_with_validator_token_identical_addresses() {
1386        let (mut amm, _, user_token, _) = setup_test_amm();
1387        let msg_sender = Address::random();
1388        let to = Address::random();
1389        let amount = uint!(10000_U256);
1390
1391        // Try to mint with identical user and validator tokens
1392        let result = amm.mint_with_validator_token(
1393            msg_sender, user_token, user_token, // Same as user_token
1394            amount, to,
1395        );
1396
1397        assert!(matches!(
1398            result,
1399            Err(TempoPrecompileError::TIPFeeAMMError(
1400                TIPFeeAMMError::IdenticalAddresses(_)
1401            ))
1402        ));
1403    }
1404
1405    #[test]
1406    fn test_mint_with_validator_token_insufficient_amount() {
1407        let (mut amm, _, user_token, validator_token) = setup_test_amm();
1408        let msg_sender = Address::random();
1409        let to = Address::random();
1410
1411        // Try to mint with amount that would result in insufficient liquidity
1412        // MIN_LIQUIDITY is 1000, so amount/2 must be > 1000, meaning amount must be > 2000
1413        let insufficient_amount = uint!(2000_U256); // This equals MIN_LIQUIDITY when divided by 2
1414
1415        let result = amm.mint_with_validator_token(
1416            msg_sender,
1417            user_token,
1418            validator_token,
1419            insufficient_amount,
1420            to,
1421        );
1422
1423        assert!(matches!(
1424            result,
1425            Err(TempoPrecompileError::TIPFeeAMMError(
1426                TIPFeeAMMError::InsufficientLiquidity(_)
1427            ))
1428        ));
1429    }
1430
1431    #[test]
1432    fn test_mint_with_validator_token() -> eyre::Result<()> {
1433        let mut storage = HashMapStorageProvider::new(1);
1434        let admin = Address::random();
1435        let user = Address::random();
1436        initialize_path_usd(&mut storage, admin)?;
1437
1438        let mut user_token_tip20 = TIP20Token::new(1, &mut storage);
1439        user_token_tip20.initialize(
1440            "UserToken",
1441            "UTK",
1442            "USD",
1443            PATH_USD_ADDRESS,
1444            admin,
1445            Address::ZERO,
1446        )?;
1447        let user_token = user_token_tip20.address();
1448
1449        let mut validator_token_tip20 = TIP20Token::new(2, &mut storage);
1450        validator_token_tip20.initialize(
1451            "ValidatorToken",
1452            "VTK",
1453            "USD",
1454            PATH_USD_ADDRESS,
1455            admin,
1456            Address::ZERO,
1457        )?;
1458        validator_token_tip20.grant_role_internal(admin, *ISSUER_ROLE)?;
1459        let validator_token = validator_token_tip20.address();
1460
1461        // Mint tokens to user
1462        validator_token_tip20.mint(
1463            admin,
1464            ITIP20::mintCall {
1465                to: user,
1466                amount: uint!(1000000_U256),
1467            },
1468        )?;
1469
1470        // Test subsequent liquidity with a different user
1471        let user2 = Address::random();
1472        validator_token_tip20.mint(
1473            admin,
1474            ITIP20::mintCall {
1475                to: user2,
1476                amount: uint!(1000000_U256),
1477            },
1478        )?;
1479
1480        let mut amm = TipFeeManager::new(&mut storage);
1481        let pool_id = amm.pool_id(user_token, validator_token);
1482
1483        // Test initial liquidity
1484        let amount_validator_1 = uint!(10000_U256);
1485        let liquidity_1 = amm.mint_with_validator_token(
1486            user,
1487            user_token,
1488            validator_token,
1489            amount_validator_1,
1490            user,
1491        )?;
1492
1493        // For first mint iquidity = (amount / 2) - MIN_LIQUIDITY = 5000 - 1000 = 4000
1494        assert_eq!(liquidity_1, uint!(4000_U256));
1495
1496        // Verify pool state after first mint
1497        let pool_1 = amm.sload_pools(pool_id)?;
1498        assert_eq!(pool_1.reserve_user_token, 0);
1499        assert_eq!(pool_1.reserve_validator_token, 10000);
1500
1501        // Verify total supply after first mint
1502        let total_supply_1 = amm.get_total_supply(pool_id)?;
1503        assert_eq!(
1504            total_supply_1,
1505            uint!(5000_U256),
1506            "Total supply should be liquidity + MIN_LIQUIDITY"
1507        );
1508
1509        // Verify LP balance after first mint
1510        let lp_balance_1 = amm.sload_liquidity_balances(pool_id, user)?;
1511        assert_eq!(lp_balance_1, liquidity_1);
1512
1513        // Verify validator token balance transferred
1514        let validator_balance = TIP20Token::from_address(validator_token, amm.storage())?
1515            .balance_of(ITIP20::balanceOfCall { account: user })?;
1516        assert_eq!(
1517            validator_balance,
1518            uint!(990000_U256),
1519            "Validator tokens should be transferred"
1520        );
1521
1522        let amount_validator_2 = uint!(5000_U256);
1523        let liquidity_2 = amm.mint_with_validator_token(
1524            user2,
1525            user_token,
1526            validator_token,
1527            amount_validator_2,
1528            user2,
1529        )?;
1530
1531        // For second mint:
1532        // liquidity = amountValidatorToken * totalSupply / (reserveValidatorToken + N * reserveUserToken / SCALE)
1533        // reserveUserToken = 0, so term N*U/SCALE = 0
1534        // liquidity = 5000 * 5000 / 10000 = 2500
1535        assert_eq!(liquidity_2, uint!(2500_U256));
1536
1537        // Verify pool state after second mint
1538        let pool_2 = amm.sload_pools(pool_id)?;
1539        assert_eq!(pool_2.reserve_user_token, 0,);
1540        assert_eq!(
1541            pool_2.reserve_validator_token, 15000,
1542            "Validator reserve should be 10000 + 5000"
1543        );
1544
1545        // Verify total supply increased
1546        let total_supply_2 = amm.get_total_supply(pool_id)?;
1547        assert_eq!(
1548            total_supply_2,
1549            total_supply_1 + liquidity_2,
1550            "Total supply should increase by liquidity"
1551        );
1552
1553        // Verify first user's LP balance unchanged
1554        let lp_balance_1_after = amm.sload_liquidity_balances(pool_id, user)?;
1555        assert_eq!(lp_balance_1_after, liquidity_1,);
1556
1557        // Verify second user's LP balance
1558        let lp_balance_2 = amm.sload_liquidity_balances(pool_id, user2)?;
1559        assert_eq!(lp_balance_2, liquidity_2);
1560
1561        // Verify events emitted
1562        let events = amm.storage.events.get(&amm.address()).unwrap();
1563        assert_eq!(events.len(), 2);
1564        // First mint event
1565        assert_eq!(
1566            events[0],
1567            TIPFeeAMMEvent::Mint(ITIPFeeAMM::Mint {
1568                sender: user,
1569                userToken: user_token,
1570                validatorToken: validator_token,
1571                amountUserToken: U256::ZERO,
1572                amountValidatorToken: amount_validator_1,
1573                liquidity: lp_balance_1
1574            })
1575            .into_log_data()
1576        );
1577
1578        // Second mint event
1579        assert_eq!(
1580            events[1],
1581            TIPFeeAMMEvent::Mint(ITIPFeeAMM::Mint {
1582                sender: user2,
1583                userToken: user_token,
1584                validatorToken: validator_token,
1585                amountUserToken: U256::ZERO,
1586                amountValidatorToken: amount_validator_2,
1587                liquidity: lp_balance_2
1588            })
1589            .into_log_data()
1590        );
1591
1592        Ok(())
1593    }
1594
1595    /// Tests the mean calculation in add_liquidity for pre-Moderato hardfork
1596    /// Pre-Moderato: mean = (amount_user_token * amount_validator_token) / 2
1597    #[test]
1598    fn test_add_liquidity_pre_moderato() -> eyre::Result<()> {
1599        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1600        let admin = Address::random();
1601        initialize_path_usd(&mut storage, admin)?;
1602
1603        let mint_amount = uint!(10000000_U256);
1604        let token1 = token_id_to_address(1);
1605        let mut token1_tip20 = TIP20Token::from_address(token1, &mut storage)?;
1606        token1_tip20.initialize(
1607            "Token1",
1608            "TK1",
1609            "USD",
1610            PATH_USD_ADDRESS,
1611            admin,
1612            Address::ZERO,
1613        )?;
1614        token1_tip20.grant_role_internal(admin, *ISSUER_ROLE)?;
1615
1616        token1_tip20.mint(
1617            admin,
1618            crate::tip20::ITIP20::mintCall {
1619                to: admin,
1620                amount: mint_amount,
1621            },
1622        )?;
1623
1624        let token2 = token_id_to_address(2);
1625        let mut token2_tip20 = TIP20Token::from_address(token2, &mut storage)?;
1626        token2_tip20.initialize(
1627            "Token2",
1628            "TK2",
1629            "USD",
1630            PATH_USD_ADDRESS,
1631            admin,
1632            Address::ZERO,
1633        )?;
1634        token2_tip20.grant_role_internal(admin, *ISSUER_ROLE)?;
1635
1636        token2_tip20.mint(
1637            admin,
1638            crate::tip20::ITIP20::mintCall {
1639                to: admin,
1640                amount: mint_amount,
1641            },
1642        )?;
1643
1644        let mut amm = TipFeeManager::new(&mut storage);
1645
1646        let amount1 = uint!(2000_U256);
1647        let amount2 = uint!(3000_U256);
1648
1649        let result = amm.mint(admin, token1, token2, amount1, amount2, admin)?;
1650
1651        // Pre-Moderato: mean = (2000 * 3000) / 2 = 3,000,000
1652        // Expected liquidity = mean - MIN_LIQUIDITY = 3,000,000 - 1000 = 2,999,000
1653        let expected_mean = (amount1 * amount2) / uint!(2_U256);
1654        let expected_liquidity = expected_mean - MIN_LIQUIDITY;
1655
1656        assert_eq!(
1657            result, expected_liquidity,
1658            "Pre-Moderato should use multiplication: mean = (a * b) / 2"
1659        );
1660
1661        Ok(())
1662    }
1663
1664    /// Tests the mean calculation in add_liquidity for post-Moderato hardfork
1665    /// Post-Moderato: mean = (amount_user_token + amount_validator_token) / 2
1666    #[test]
1667    fn test_add_liquidity_post_moderato() -> eyre::Result<()> {
1668        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
1669        let admin = Address::random();
1670        initialize_path_usd(&mut storage, admin)?;
1671
1672        let mint_amount = uint!(10000_U256);
1673
1674        let token1 = token_id_to_address(1);
1675        let mut token1_tip20 = TIP20Token::from_address(token1, &mut storage)?;
1676        token1_tip20.initialize(
1677            "Token1",
1678            "TK1",
1679            "USD",
1680            PATH_USD_ADDRESS,
1681            admin,
1682            Address::ZERO,
1683        )?;
1684        token1_tip20.grant_role_internal(admin, *ISSUER_ROLE)?;
1685
1686        // Mint tokens to admin
1687        token1_tip20.mint(
1688            admin,
1689            crate::tip20::ITIP20::mintCall {
1690                to: admin,
1691                amount: mint_amount,
1692            },
1693        )?;
1694
1695        let token2 = token_id_to_address(2);
1696        let mut token2_tip20 = TIP20Token::from_address(token2, &mut storage)?;
1697        token2_tip20.initialize(
1698            "Token2",
1699            "TK2",
1700            "USD",
1701            PATH_USD_ADDRESS,
1702            admin,
1703            Address::ZERO,
1704        )?;
1705        token2_tip20.grant_role_internal(admin, *ISSUER_ROLE)?;
1706
1707        token2_tip20.mint(
1708            admin,
1709            crate::tip20::ITIP20::mintCall {
1710                to: admin,
1711                amount: mint_amount,
1712            },
1713        )?;
1714
1715        let mut amm = TipFeeManager::new(&mut storage);
1716
1717        let amount1 = uint!(2000_U256);
1718        let amount2 = uint!(3000_U256);
1719
1720        let result = amm.mint(admin, token1, token2, amount1, amount2, admin)?;
1721
1722        // Post-Moderato: mean = (2000 + 3000) / 2 = 2,500
1723        // Expected liquidity = mean - MIN_LIQUIDITY = 2,500 - 1000 = 1,500
1724        let expected_mean = (amount1 + amount2) / uint!(2_U256);
1725        let expected_liquidity = expected_mean - MIN_LIQUIDITY;
1726
1727        assert_eq!(
1728            result, expected_liquidity,
1729            "Post-Moderato should use addition: mean = (a + b) / 2"
1730        );
1731
1732        Ok(())
1733    }
1734
1735    /// Tests calculate_burn_amounts pre-Allegretto rejects zero amounts
1736    #[test]
1737    fn test_calculate_burn_amounts_pre_allegretto() -> eyre::Result<()> {
1738        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
1739        let mut amm = TipFeeManager::new(&mut storage);
1740
1741        // Create a pool with very large total supply to make burn amounts round to zero
1742        let pool = Pool {
1743            reserve_user_token: 1000,
1744            reserve_validator_token: 1000,
1745        };
1746        let pool_id = B256::ZERO;
1747        amm.set_total_supply(pool_id, uint!(1000000000000000_U256))?;
1748
1749        // Burning tiny liquidity should result in zero amounts
1750        let liquidity = uint!(1_U256);
1751        let result = amm.calculate_burn_amounts(&pool, pool_id, liquidity);
1752
1753        // Pre-Allegretto: should reject if amounts are zero
1754        assert!(result.is_err(),);
1755        assert!(matches!(
1756            result,
1757            Err(TempoPrecompileError::TIPFeeAMMError(
1758                TIPFeeAMMError::InsufficientLiquidity(_)
1759            ))
1760        ),);
1761
1762        Ok(())
1763    }
1764
1765    /// Tests calculate_burn_amounts post-Allegretto allows zero amounts
1766    #[test]
1767    fn test_calculate_burn_amounts_post_allegretto() -> eyre::Result<()> {
1768        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
1769        let mut amm = TipFeeManager::new(&mut storage);
1770
1771        let pool = Pool {
1772            reserve_user_token: 1000,
1773            reserve_validator_token: 1000,
1774        };
1775        let pool_id = B256::ZERO;
1776        amm.set_total_supply(pool_id, uint!(1000000000000000_U256))?;
1777
1778        let liquidity = uint!(1_U256);
1779        let result = amm.calculate_burn_amounts(&pool, pool_id, liquidity);
1780
1781        // Post-Allegretto should allow zero amounts
1782        assert!(result.is_ok(), "Post-Allegretto should allow zero amounts");
1783        let (amount_user, amount_validator) = result?;
1784        assert_eq!(amount_user, U256::ZERO);
1785        assert_eq!(amount_validator, U256::ZERO);
1786
1787        Ok(())
1788    }
1789
1790    #[test]
1791    fn test_reserve_liquidity_checks_total_pending() -> eyre::Result<()> {
1792        let reserve_validator_token = 627;
1793
1794        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::AllegroModerato);
1795        let admin = Address::random();
1796        initialize_path_usd(&mut storage, admin)?;
1797
1798        let user_token = token_id_to_address(3);
1799        {
1800            let mut user_tip20 = TIP20Token::from_address(user_token, &mut storage)?;
1801            user_tip20.initialize(
1802                "UserToken",
1803                "UTK",
1804                "USD",
1805                PATH_USD_ADDRESS,
1806                admin,
1807                Address::ZERO,
1808            )?;
1809        }
1810
1811        let validator_token = token_id_to_address(4);
1812        {
1813            let mut validator_tip20 = TIP20Token::from_address(validator_token, &mut storage)?;
1814            validator_tip20.initialize(
1815                "ValidatorToken",
1816                "VTK",
1817                "USD",
1818                PATH_USD_ADDRESS,
1819                admin,
1820                Address::ZERO,
1821            )?;
1822        }
1823
1824        let mut amm = TipFeeManager::new(&mut storage);
1825
1826        let pool_id = amm.pool_id(user_token, validator_token);
1827        let pool = Pool {
1828            reserve_user_token: 1000,
1829            reserve_validator_token,
1830        };
1831        amm.sstore_pools(pool_id, pool)?;
1832
1833        amm.reserve_liquidity(user_token, validator_token, U256::from(210))?;
1834        amm.reserve_liquidity(user_token, validator_token, U256::from(210))?;
1835
1836        let result = amm.reserve_liquidity(user_token, validator_token, U256::from(210));
1837        assert!(matches!(
1838            result,
1839            Err(TempoPrecompileError::TIPFeeAMMError(
1840                TIPFeeAMMError::InsufficientLiquidity(_)
1841            ))
1842        ));
1843
1844        assert_eq!(amm.get_pending_fee_swap_in(pool_id)?, 420);
1845
1846        let amount_out = amm.execute_pending_fee_swaps(user_token, validator_token)?;
1847        assert_eq!(amount_out, U256::from(418));
1848
1849        let pool_after = amm.sload_pools(pool_id)?;
1850        assert_eq!(
1851            pool_after.reserve_validator_token,
1852            reserve_validator_token - 418
1853        );
1854        assert_eq!(pool_after.reserve_user_token, 1000 + 420);
1855
1856        Ok(())
1857    }
1858}