1pub mod amm;
6pub mod dispatch;
7
8use crate::{
9 error::{Result, TempoPrecompileError},
10 storage::{Handler, Mapping},
11 tip_fee_manager::amm::{Pool, PoolKey, compute_amount_out},
12 tip20::{ITIP20, TIP20Token, validate_usd_currency},
13 tip20_factory::TIP20Factory,
14};
15use alloy::primitives::B256;
16pub use tempo_contracts::precompiles::{
17 DEFAULT_FEE_TOKEN, FeeManagerError, FeeManagerEvent, IFeeManager, ITIPFeeAMM,
18 TIP_FEE_MANAGER_ADDRESS, TIPFeeAMMError, TIPFeeAMMEvent,
19};
20use alloy::primitives::{Address, U256, uint};
22use tempo_precompiles_macros::contract;
23
24#[contract(addr = TIP_FEE_MANAGER_ADDRESS)]
32pub struct TipFeeManager {
33 validator_tokens: Mapping<Address, Address>,
34 user_tokens: Mapping<Address, Address>,
35 collected_fees: Mapping<Address, Mapping<Address, U256>>,
36 pools: Mapping<B256, Pool>,
37 total_supply: Mapping<B256, U256>,
38 liquidity_balances: Mapping<B256, Mapping<Address, U256>>,
39
40 pending_fee_swap_reservation: Mapping<B256, u128>,
46}
47
48impl TipFeeManager {
49 pub const FEE_BPS: u64 = 25;
51 pub const BASIS_POINTS: u64 = 10000;
53 pub const MINIMUM_BALANCE: U256 = uint!(1_000_000_000_U256);
55
56 pub fn initialize(&mut self) -> Result<()> {
58 self.__initialize()
59 }
60
61 pub fn get_validator_token(&self, beneficiary: Address) -> Result<Address> {
63 let token = self.validator_tokens[beneficiary].read()?;
64
65 if token.is_zero() {
66 Ok(DEFAULT_FEE_TOKEN)
67 } else {
68 Ok(token)
69 }
70 }
71
72 pub fn set_validator_token(
83 &mut self,
84 sender: Address,
85 call: IFeeManager::setValidatorTokenCall,
86 beneficiary: Address,
87 ) -> Result<()> {
88 if !TIP20Factory::new().is_tip20(call.token)? {
90 return Err(FeeManagerError::invalid_token().into());
91 }
92
93 if sender == beneficiary {
95 return Err(FeeManagerError::cannot_change_within_block().into());
96 }
97
98 validate_usd_currency(call.token)?;
100
101 self.validator_tokens[sender].write(call.token)?;
102
103 self.emit_event(FeeManagerEvent::ValidatorTokenSet(
105 IFeeManager::ValidatorTokenSet {
106 validator: sender,
107 token: call.token,
108 },
109 ))
110 }
111
112 pub fn set_user_token(
119 &mut self,
120 sender: Address,
121 call: IFeeManager::setUserTokenCall,
122 ) -> Result<()> {
123 if !TIP20Factory::new().is_tip20(call.token)? {
125 return Err(FeeManagerError::invalid_token().into());
126 }
127
128 validate_usd_currency(call.token)?;
130
131 self.user_tokens[sender].write(call.token)?;
132
133 self.emit_event(FeeManagerEvent::UserTokenSet(IFeeManager::UserTokenSet {
135 user: sender,
136 token: call.token,
137 }))
138 }
139
140 pub fn collect_fee_pre_tx(
151 &mut self,
152 fee_payer: Address,
153 user_token: Address,
154 max_amount: U256,
155 beneficiary: Address,
156 ) -> Result<Address> {
157 let validator_token = self.get_validator_token(beneficiary)?;
159
160 let mut tip20_token = TIP20Token::from_address(user_token)?;
161
162 tip20_token.ensure_transfer_authorized(fee_payer, self.address)?;
164 tip20_token.transfer_fee_pre_tx(fee_payer, max_amount)?;
165
166 if user_token != validator_token {
167 let pool_id = PoolKey::new(user_token, validator_token).get_id();
168 let amount_out_needed = self.check_sufficient_liquidity(pool_id, max_amount)?;
169
170 if self.storage.spec().is_t1c() {
171 self.reserve_pool_liquidity(pool_id, amount_out_needed)?;
172 }
173 }
174
175 Ok(user_token)
177 }
178
179 pub fn collect_fee_post_tx(
189 &mut self,
190 fee_payer: Address,
191 actual_spending: U256,
192 refund_amount: U256,
193 fee_token: Address,
194 beneficiary: Address,
195 ) -> Result<()> {
196 let mut tip20_token = TIP20Token::from_address(fee_token)?;
198 tip20_token.transfer_fee_post_tx(fee_payer, refund_amount, actual_spending)?;
199
200 let validator_token = self.get_validator_token(beneficiary)?;
202
203 if fee_token != validator_token {
204 if !actual_spending.is_zero() {
206 self.execute_fee_swap(fee_token, validator_token, actual_spending)?;
208 }
209 }
210
211 let amount = if fee_token == validator_token {
212 actual_spending
213 } else {
214 compute_amount_out(actual_spending)?
215 };
216
217 self.increment_collected_fees(beneficiary, validator_token, amount)?;
218
219 Ok(())
220 }
221
222 fn increment_collected_fees(
224 &mut self,
225 validator: Address,
226 token: Address,
227 amount: U256,
228 ) -> Result<()> {
229 if amount.is_zero() {
230 return Ok(());
231 }
232
233 let collected_fees = self.collected_fees[validator][token].read()?;
234 self.collected_fees[validator][token].write(
235 collected_fees
236 .checked_add(amount)
237 .ok_or(TempoPrecompileError::under_overflow())?,
238 )?;
239
240 Ok(())
241 }
242
243 pub fn distribute_fees(&mut self, validator: Address, token: Address) -> Result<()> {
249 let amount = self.collected_fees[validator][token].read()?;
250 if amount.is_zero() {
251 return Ok(());
252 }
253 self.collected_fees[validator][token].write(U256::ZERO)?;
254
255 let mut tip20_token = TIP20Token::from_address(token)?;
257 tip20_token.transfer(
258 self.address,
259 ITIP20::transferCall {
260 to: validator,
261 amount,
262 },
263 )?;
264
265 self.emit_event(FeeManagerEvent::FeesDistributed(
267 IFeeManager::FeesDistributed {
268 validator,
269 token,
270 amount,
271 },
272 ))?;
273
274 Ok(())
275 }
276
277 pub fn user_tokens(&self, call: IFeeManager::userTokensCall) -> Result<Address> {
279 self.user_tokens[call.user].read()
280 }
281
282 pub fn validator_tokens(&self, call: IFeeManager::validatorTokensCall) -> Result<Address> {
284 let token = self.validator_tokens[call.validator].read()?;
285
286 if token.is_zero() {
287 Ok(DEFAULT_FEE_TOKEN)
288 } else {
289 Ok(token)
290 }
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use tempo_contracts::precompiles::TIP20Error;
297
298 use super::*;
299 use crate::{
300 TIP_FEE_MANAGER_ADDRESS,
301 error::TempoPrecompileError,
302 storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
303 test_util::TIP20Setup,
304 tip20::{ITIP20, TIP20Token},
305 };
306
307 #[test]
308 fn test_set_user_token() -> eyre::Result<()> {
309 let mut storage = HashMapStorageProvider::new(1);
310 let user = Address::random();
311 StorageCtx::enter(&mut storage, || {
312 let token = TIP20Setup::create("Test", "TST", user).apply()?;
313
314 let mut fee_manager = TipFeeManager::new();
317
318 let call = IFeeManager::setUserTokenCall {
319 token: token.address(),
320 };
321 let result = fee_manager.set_user_token(user, call);
322 assert!(result.is_ok());
323
324 let call = IFeeManager::userTokensCall { user };
325 assert_eq!(fee_manager.user_tokens(call)?, token.address());
326
327 Ok(())
328 })
329 }
330
331 #[test]
332 fn test_set_validator_token() -> eyre::Result<()> {
333 let mut storage = HashMapStorageProvider::new(1);
334 let validator = Address::random();
335 let admin = Address::random();
336 let beneficiary = Address::random();
337 StorageCtx::enter(&mut storage, || {
338 let token = TIP20Setup::create("Test", "TST", admin).apply()?;
339 let mut fee_manager = TipFeeManager::new();
340
341 let call = IFeeManager::setValidatorTokenCall {
342 token: token.address(),
343 };
344
345 let result = fee_manager.set_validator_token(validator, call.clone(), validator);
347 assert_eq!(
348 result,
349 Err(TempoPrecompileError::FeeManagerError(
350 FeeManagerError::cannot_change_within_block()
351 ))
352 );
353
354 let result = fee_manager.set_validator_token(validator, call, beneficiary);
356 assert!(result.is_ok());
357
358 let query_call = IFeeManager::validatorTokensCall { validator };
359 let returned_token = fee_manager.validator_tokens(query_call)?;
360 assert_eq!(returned_token, token.address());
361
362 Ok(())
363 })
364 }
365
366 #[test]
367 fn test_set_validator_token_cannot_change_within_block() -> eyre::Result<()> {
368 let mut storage = HashMapStorageProvider::new(1);
369 let validator = Address::random();
370 let beneficiary = Address::random();
371 let admin = Address::random();
372 StorageCtx::enter(&mut storage, || {
373 let token = TIP20Setup::create("Test", "TST", admin).apply()?;
374 let mut fee_manager = TipFeeManager::new();
375
376 let call = IFeeManager::setValidatorTokenCall {
377 token: token.address(),
378 };
379
380 let result = fee_manager.set_validator_token(validator, call.clone(), beneficiary);
382 assert!(result.is_ok());
383
384 let result = fee_manager.set_validator_token(validator, call, validator);
386 assert_eq!(
387 result,
388 Err(TempoPrecompileError::FeeManagerError(
389 FeeManagerError::cannot_change_within_block()
390 ))
391 );
392
393 Ok(())
394 })
395 }
396
397 #[test]
398 fn test_collect_fee_pre_tx() -> eyre::Result<()> {
399 let mut storage = HashMapStorageProvider::new(1);
400 let user = Address::random();
401 let validator = Address::random();
402 let beneficiary = Address::random();
403 StorageCtx::enter(&mut storage, || {
404 let max_amount = U256::from(10000);
405
406 let token = TIP20Setup::create("Test", "TST", user)
407 .with_issuer(user)
408 .with_mint(user, U256::from(u64::MAX))
409 .with_approval(user, TIP_FEE_MANAGER_ADDRESS, U256::MAX)
410 .apply()?;
411
412 let mut fee_manager = TipFeeManager::new();
413
414 fee_manager.set_validator_token(
416 validator,
417 IFeeManager::setValidatorTokenCall {
418 token: token.address(),
419 },
420 beneficiary,
421 )?;
422
423 fee_manager.set_user_token(
425 user,
426 IFeeManager::setUserTokenCall {
427 token: token.address(),
428 },
429 )?;
430
431 let result =
433 fee_manager.collect_fee_pre_tx(user, token.address(), max_amount, validator);
434 assert!(result.is_ok());
435 assert_eq!(result?, token.address());
436
437 Ok(())
438 })
439 }
440
441 #[test]
442 fn test_collect_fee_post_tx() -> eyre::Result<()> {
443 let mut storage = HashMapStorageProvider::new(1);
444 let user = Address::random();
445 let admin = Address::random();
446 let validator = Address::random();
447 let beneficiary = Address::random();
448 StorageCtx::enter(&mut storage, || {
449 let actual_used = U256::from(6000);
450 let refund_amount = U256::from(4000);
451
452 let token = TIP20Setup::create("Test", "TST", admin)
454 .with_issuer(admin)
455 .with_mint(TIP_FEE_MANAGER_ADDRESS, U256::from(100000000000000_u64))
456 .apply()?;
457
458 let mut fee_manager = TipFeeManager::new();
459
460 fee_manager.set_validator_token(
462 validator,
463 IFeeManager::setValidatorTokenCall {
464 token: token.address(),
465 },
466 beneficiary,
467 )?;
468
469 fee_manager.set_user_token(
471 user,
472 IFeeManager::setUserTokenCall {
473 token: token.address(),
474 },
475 )?;
476
477 let result = fee_manager.collect_fee_post_tx(
479 user,
480 actual_used,
481 refund_amount,
482 token.address(),
483 validator,
484 );
485 assert!(result.is_ok());
486
487 let tracked_amount = fee_manager.collected_fees[validator][token.address()].read()?;
489 assert_eq!(tracked_amount, actual_used);
490
491 let balance = token.balance_of(ITIP20::balanceOfCall { account: user })?;
493 assert_eq!(balance, refund_amount);
494
495 Ok(())
496 })
497 }
498
499 #[test]
500 fn test_rejects_non_usd() -> eyre::Result<()> {
501 let mut storage = HashMapStorageProvider::new(1);
502 let admin = Address::random();
503 let user = Address::random();
504 let validator = Address::random();
505 let beneficiary = Address::random();
506 StorageCtx::enter(&mut storage, || {
507 let non_usd_token = TIP20Setup::create("NonUSD", "EUR", admin)
509 .currency("EUR")
510 .apply()?;
511
512 let mut fee_manager = TipFeeManager::new();
513
514 let call = IFeeManager::setUserTokenCall {
516 token: non_usd_token.address(),
517 };
518 let result = fee_manager.set_user_token(user, call);
519 assert!(matches!(
520 result,
521 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
522 ));
523
524 let call = IFeeManager::setValidatorTokenCall {
526 token: non_usd_token.address(),
527 };
528 let result = fee_manager.set_validator_token(validator, call, beneficiary);
529 assert!(matches!(
530 result,
531 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
532 ));
533
534 Ok(())
535 })
536 }
537
538 #[test]
541 fn test_collect_fee_pre_tx_different_tokens() -> eyre::Result<()> {
542 let mut storage = HashMapStorageProvider::new(1);
543 let admin = Address::random();
544 let user = Address::random();
545 let validator = Address::random();
546
547 StorageCtx::enter(&mut storage, || {
548 let user_token = TIP20Setup::create("UserToken", "UTK", admin)
550 .with_issuer(admin)
551 .with_mint(user, U256::from(10000))
552 .with_approval(user, TIP_FEE_MANAGER_ADDRESS, U256::MAX)
553 .apply()?;
554
555 let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
556 .with_issuer(admin)
557 .with_mint(TIP_FEE_MANAGER_ADDRESS, U256::from(10000))
558 .apply()?;
559
560 let mut fee_manager = TipFeeManager::new();
561
562 let pool_id = fee_manager.pool_id(user_token.address(), validator_token.address());
564 fee_manager.pools[pool_id].write(crate::tip_fee_manager::amm::Pool {
565 reserve_user_token: 10000,
566 reserve_validator_token: 10000,
567 })?;
568
569 fee_manager.set_validator_token(
571 validator,
572 IFeeManager::setValidatorTokenCall {
573 token: validator_token.address(),
574 },
575 Address::random(),
576 )?;
577
578 let max_amount = U256::from(1000);
579
580 fee_manager.collect_fee_pre_tx(user, user_token.address(), max_amount, validator)?;
582
583 let collected =
588 fee_manager.collected_fees[validator][validator_token.address()].read()?;
589 assert_eq!(
590 collected,
591 U256::ZERO,
592 "Different tokens: no fees accumulated in pre_tx (swap happens in post_tx)"
593 );
594
595 let pool = fee_manager.pools[pool_id].read()?;
597 assert_eq!(
598 pool.reserve_user_token, 10000,
599 "Reserves unchanged in pre_tx"
600 );
601 assert_eq!(
602 pool.reserve_validator_token, 10000,
603 "Reserves unchanged in pre_tx"
604 );
605
606 Ok(())
607 })
608 }
609
610 #[test]
611 fn test_collect_fee_post_tx_immediate_swap() -> eyre::Result<()> {
612 let mut storage = HashMapStorageProvider::new(1);
613 let admin = Address::random();
614 let user = Address::random();
615 let validator = Address::random();
616
617 StorageCtx::enter(&mut storage, || {
618 let user_token = TIP20Setup::create("UserToken", "UTK", admin)
619 .with_issuer(admin)
620 .with_mint(user, U256::from(10000))
621 .with_mint(TIP_FEE_MANAGER_ADDRESS, U256::from(10000))
622 .with_approval(user, TIP_FEE_MANAGER_ADDRESS, U256::MAX)
623 .apply()?;
624
625 let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
626 .with_issuer(admin)
627 .with_mint(TIP_FEE_MANAGER_ADDRESS, U256::from(10000))
628 .apply()?;
629
630 let mut fee_manager = TipFeeManager::new();
631
632 let pool_id = fee_manager.pool_id(user_token.address(), validator_token.address());
633 fee_manager.pools[pool_id].write(crate::tip_fee_manager::amm::Pool {
634 reserve_user_token: 10000,
635 reserve_validator_token: 10000,
636 })?;
637
638 fee_manager.set_validator_token(
639 validator,
640 IFeeManager::setValidatorTokenCall {
641 token: validator_token.address(),
642 },
643 Address::random(),
644 )?;
645
646 let max_amount = U256::from(1000);
647 let actual_spending = U256::from(800);
648 let refund_amount = U256::from(200);
649
650 fee_manager.collect_fee_pre_tx(user, user_token.address(), max_amount, validator)?;
652
653 fee_manager.collect_fee_post_tx(
655 user,
656 actual_spending,
657 refund_amount,
658 user_token.address(),
659 validator,
660 )?;
661
662 let expected_fee_amount = (actual_spending * U256::from(9970)) / U256::from(10000);
664 let collected =
665 fee_manager.collected_fees[validator][validator_token.address()].read()?;
666 assert_eq!(collected, expected_fee_amount);
667
668 let pool = fee_manager.pools[pool_id].read()?;
670 assert_eq!(pool.reserve_user_token, 10000 + 800);
671 assert_eq!(pool.reserve_validator_token, 10000 - 797);
672
673 let tip20_token = TIP20Token::from_address(user_token.address())?;
675 let user_balance = tip20_token.balance_of(ITIP20::balanceOfCall { account: user })?;
676 assert_eq!(user_balance, U256::from(10000) - max_amount + refund_amount);
677
678 Ok(())
679 })
680 }
681
682 #[test]
684 fn test_collect_fee_pre_tx_insufficient_liquidity() -> eyre::Result<()> {
685 let mut storage = HashMapStorageProvider::new(1);
686 let admin = Address::random();
687 let user = Address::random();
688 let validator = Address::random();
689
690 StorageCtx::enter(&mut storage, || {
691 let user_token = TIP20Setup::create("UserToken", "UTK", admin)
692 .with_issuer(admin)
693 .with_mint(user, U256::from(10000))
694 .with_approval(user, TIP_FEE_MANAGER_ADDRESS, U256::MAX)
695 .apply()?;
696
697 let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
698 .with_issuer(admin)
699 .with_mint(TIP_FEE_MANAGER_ADDRESS, U256::from(100))
700 .apply()?;
701
702 let mut fee_manager = TipFeeManager::new();
703
704 let pool_id = fee_manager.pool_id(user_token.address(), validator_token.address());
705 fee_manager.pools[pool_id].write(crate::tip_fee_manager::amm::Pool {
707 reserve_user_token: 10000,
708 reserve_validator_token: 100,
709 })?;
710
711 fee_manager.set_validator_token(
712 validator,
713 IFeeManager::setValidatorTokenCall {
714 token: validator_token.address(),
715 },
716 Address::random(),
717 )?;
718
719 let max_amount = U256::from(1000);
722
723 let result =
724 fee_manager.collect_fee_pre_tx(user, user_token.address(), max_amount, validator);
725
726 assert!(result.is_err(), "Should fail with insufficient liquidity");
727
728 Ok(())
729 })
730 }
731
732 #[test]
734 fn test_distribute_fees_zero_balance() -> eyre::Result<()> {
735 let mut storage = HashMapStorageProvider::new(1);
736 let admin = Address::random();
737 let validator = Address::random();
738
739 StorageCtx::enter(&mut storage, || {
740 let token = TIP20Setup::create("TestToken", "TEST", admin)
741 .with_issuer(admin)
742 .apply()?;
743
744 let mut fee_manager = TipFeeManager::new();
745
746 fee_manager.set_validator_token(
747 validator,
748 IFeeManager::setValidatorTokenCall {
749 token: token.address(),
750 },
751 Address::random(),
752 )?;
753
754 let collected = fee_manager.collected_fees[validator][token.address()].read()?;
756 assert_eq!(collected, U256::ZERO);
757
758 let result = fee_manager.distribute_fees(validator, token.address());
760 assert!(result.is_ok(), "Should succeed even with zero balance");
761
762 let tip20_token = TIP20Token::from_address(token.address())?;
764 let balance = tip20_token.balance_of(ITIP20::balanceOfCall { account: validator })?;
765 assert_eq!(balance, U256::ZERO);
766
767 Ok(())
768 })
769 }
770
771 #[test]
773 fn test_distribute_fees() -> eyre::Result<()> {
774 let mut storage = HashMapStorageProvider::new(1);
775 let admin = Address::random();
776 let validator = Address::random();
777
778 StorageCtx::enter(&mut storage, || {
779 let token = TIP20Setup::create("TestToken", "TEST", admin)
781 .with_issuer(admin)
782 .with_mint(TIP_FEE_MANAGER_ADDRESS, U256::from(1000))
783 .apply()?;
784
785 let mut fee_manager = TipFeeManager::new();
786
787 fee_manager.set_validator_token(
789 validator,
790 IFeeManager::setValidatorTokenCall {
791 token: token.address(),
792 },
793 Address::random(), )?;
795
796 let fee_amount = U256::from(500);
798 fee_manager.collected_fees[validator][token.address()].write(fee_amount)?;
799
800 let tip20_token = TIP20Token::from_address(token.address())?;
802 let balance_before =
803 tip20_token.balance_of(ITIP20::balanceOfCall { account: validator })?;
804 assert_eq!(balance_before, U256::ZERO);
805
806 let mut fee_manager = TipFeeManager::new();
808 fee_manager.distribute_fees(validator, token.address())?;
809
810 let tip20_token = TIP20Token::from_address(token.address())?;
812 let balance_after =
813 tip20_token.balance_of(ITIP20::balanceOfCall { account: validator })?;
814 assert_eq!(balance_after, fee_amount);
815
816 let fee_manager = TipFeeManager::new();
818 let remaining = fee_manager.collected_fees[validator][token.address()].read()?;
819 assert_eq!(remaining, U256::ZERO);
820
821 Ok(())
822 })
823 }
824
825 #[test]
826 fn test_initialize_sets_storage_state() -> eyre::Result<()> {
827 let mut storage = HashMapStorageProvider::new(1);
828 StorageCtx::enter(&mut storage, || {
829 let mut fee_manager = TipFeeManager::new();
830
831 assert!(!fee_manager.is_initialized()?);
833
834 fee_manager.initialize()?;
836
837 assert!(fee_manager.is_initialized()?);
839
840 let fee_manager2 = TipFeeManager::new();
842 assert!(fee_manager2.is_initialized()?);
843
844 Ok(())
845 })
846 }
847}