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
13pub const M: U256 = uint!(9970_U256); pub 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#[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#[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 pub fn new(user_token: Address, validator_token: Address) -> Self {
61 Self {
62 user_token,
63 validator_token,
64 }
65 }
66
67 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 pub fn pool_id(&self, user_token: Address, validator_token: Address) -> B256 {
79 PoolKey::new(user_token, validator_token).get_id()
80 }
81
82 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 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 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 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 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 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 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_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 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 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_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 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 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 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 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 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 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_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 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 let _ = TIP20Token::from_address(validator_token, self.storage)?.system_transfer_from(
441 msg_sender,
442 self.address,
443 amount_validator_token,
444 )?;
445
446 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 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 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 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_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 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 let (amount_user_token, amount_validator_token) =
519 self.calculate_burn_amounts(&pool, pool_id, liquidity)?;
520
521 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 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 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 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 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 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 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 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 pub fn get_total_supply(&mut self, pool_id: B256) -> Result<U256> {
673 self.sload_total_supply(pool_id)
674 }
675
676 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 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 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 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 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
707pub 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_path_usd(storage, admin).unwrap();
785
786 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 let liquidity = if user_amount == validator_amount {
833 user_amount
835 } else {
836 sqrt(user_amount * validator_amount)
838 };
839 amm.set_total_supply(pool_id, liquidity)?;
840
841 Ok(pool_id)
842 }
843
844 #[test]
847 fn test_fee_swap() -> eyre::Result<()> {
848 let (mut amm, _, user_token, validator_token) = setup_test_amm();
849
850 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 let amount_in = uint!(1000_U256) * uint!(10_U256).pow(U256::from(6));
862
863 let expected_out = (amount_in * M) / SCALE;
865
866 amm.reserve_liquidity(user_token, validator_token, amount_in)?;
868
869 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 assert_eq!(expected_out, amount_in * M / SCALE);
879
880 Ok(())
881 }
882
883 #[test]
886 fn test_fee_swap_insufficient_liquidity() {
887 let (mut amm, _, user_token, validator_token) = setup_test_amm();
888
889 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 let too_large_amount = uint!(201_U256) * uint!(10_U256).pow(U256::from(6));
902
903 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]
917 fn test_fee_swap_rounding_consistency() -> eyre::Result<()> {
918 let (mut amm, _, user_token, validator_token) = setup_test_amm();
919
920 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 let amount_in = uint!(10000_U256) * uint!(10_U256).pow(U256::from(6));
932
933 amm.reserve_liquidity(user_token, validator_token, amount_in)?;
935
936 let expected_out = (amount_in * M) / SCALE;
938
939 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 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]
961 fn test_execute_pending_fee_swaps() -> Result<()> {
962 let (mut amm, _, user_token, validator_token) = setup_test_amm();
963
964 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 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 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 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 assert_eq!(
998 amm.get_pending_fee_swap_in(pool_id)
999 .expect("Could not get fee swap in"),
1000 0
1001 );
1002
1003 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]
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 let initial_liquidity = uint!(100000_U256) * uint!(10_U256).pow(U256::from(6)); 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 let user_token_in = uint!(20000_U256) * uint!(10_U256).pow(U256::from(6)); 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 let swap_amount = uint!(1000_U256) * uint!(10_U256).pow(U256::from(6)); 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 assert!(amount_in > 0, "Should provide validator tokens");
1052
1053 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 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 assert_eq!(
1067 y_after - y_before,
1068 amount_in,
1069 "Amount in should equal increase in validator reserve"
1070 );
1071
1072 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]
1093 fn test_rebalance_swap_insufficient_funds() -> eyre::Result<()> {
1094 let (mut amm, _, user_token, validator_token) = setup_test_amm();
1095
1096 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]
1126 fn test_has_liquidity() -> eyre::Result<()> {
1127 let (mut amm, _, user_token, validator_token) = setup_test_amm();
1128
1129 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 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 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 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 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 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 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 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 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 let result = amm.mint_with_validator_token(
1393 msg_sender, user_token, user_token, 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 let insufficient_amount = uint!(2000_U256); 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 validator_token_tip20.mint(
1463 admin,
1464 ITIP20::mintCall {
1465 to: user,
1466 amount: uint!(1000000_U256),
1467 },
1468 )?;
1469
1470 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 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 assert_eq!(liquidity_1, uint!(4000_U256));
1495
1496 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 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 let lp_balance_1 = amm.sload_liquidity_balances(pool_id, user)?;
1511 assert_eq!(lp_balance_1, liquidity_1);
1512
1513 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 assert_eq!(liquidity_2, uint!(2500_U256));
1536
1537 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 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 let lp_balance_1_after = amm.sload_liquidity_balances(pool_id, user)?;
1555 assert_eq!(lp_balance_1_after, liquidity_1,);
1556
1557 let lp_balance_2 = amm.sload_liquidity_balances(pool_id, user2)?;
1559 assert_eq!(lp_balance_2, liquidity_2);
1560
1561 let events = amm.storage.events.get(&amm.address()).unwrap();
1563 assert_eq!(events.len(), 2);
1564 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 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 #[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 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 #[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 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 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 #[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 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 let liquidity = uint!(1_U256);
1751 let result = amm.calculate_burn_amounts(&pool, pool_id, liquidity);
1752
1753 assert!(result.is_err(),);
1755 assert!(matches!(
1756 result,
1757 Err(TempoPrecompileError::TIPFeeAMMError(
1758 TIPFeeAMMError::InsufficientLiquidity(_)
1759 ))
1760 ),);
1761
1762 Ok(())
1763 }
1764
1765 #[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 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}