tempo_precompiles/tip_fee_manager/
mod.rs

1pub mod amm;
2pub mod dispatch;
3
4use alloy::primitives::B256;
5use tempo_contracts::precompiles::TIP_FEE_MANAGER_ADDRESS;
6pub use tempo_contracts::precompiles::{
7    FeeManagerError, FeeManagerEvent, IFeeManager, ITIPFeeAMM, TIPFeeAMMError, TIPFeeAMMEvent,
8};
9
10use crate::{
11    DEFAULT_FEE_TOKEN_POST_ALLEGRETTO, DEFAULT_FEE_TOKEN_PRE_ALLEGRETTO, PATH_USD_ADDRESS,
12    error::{Result, TempoPrecompileError},
13    storage::{Mapping, PrecompileStorageProvider, Slot, StorageKey, VecSlotExt},
14    tip_fee_manager::amm::{Pool, compute_amount_out},
15    tip20::{
16        ITIP20, TIP20Token, address_to_token_id_unchecked, is_tip20_prefix, token_id_to_address,
17        validate_usd_currency,
18    },
19};
20
21// Re-export PoolKey for backward compatibility with tests
22use alloy::primitives::{Address, Bytes, IntoLogData, U256, uint};
23use revm::state::Bytecode;
24use tempo_precompiles_macros::{Storable, contract};
25
26/// Helper type to easily interact with the `tokens_with_fees` array
27type TokensWithFees = Slot<Vec<Address>>;
28
29/// Helper type to easily interact with the `pools_with_fees` array
30type PoolsWithFees = Slot<Vec<TokenPair>>;
31
32/// Helper type to easily interact with the `validators_with_fees` array
33type ValidatorsWithFees = Slot<Vec<Address>>;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Storable)]
36pub struct TokenPair {
37    pub user_token: u64,
38    pub validator_token: u64,
39}
40
41impl StorageKey for TokenPair {
42    fn as_storage_bytes(&self) -> impl AsRef<[u8]> {
43        let mut bytes = [0u8; 16];
44        bytes[..8].copy_from_slice(&self.user_token.to_be_bytes());
45        bytes[8..16].copy_from_slice(&self.validator_token.to_be_bytes());
46        bytes
47    }
48}
49
50#[contract]
51pub struct TipFeeManager {
52    validator_tokens: Mapping<Address, Address>,
53    user_tokens: Mapping<Address, Address>,
54    collected_fees: Mapping<Address, U256>,
55    tokens_with_fees: Vec<Address>,
56    token_in_fees_array: Mapping<Address, bool>,
57    pools: Mapping<B256, Pool>,
58    pending_fee_swap_in: Mapping<B256, u128>,
59    total_supply: Mapping<B256, U256>,
60    liquidity_balances: Mapping<B256, Mapping<Address, U256>>,
61    pools_with_fees: Vec<TokenPair>,
62    pool_in_fees_array: Mapping<TokenPair, bool>,
63    validators_with_fees: Vec<Address>,
64    validator_in_fees_array: Mapping<Address, bool>,
65}
66
67impl<'a, S: PrecompileStorageProvider> TipFeeManager<'a, S> {
68    // Constants
69    pub const FEE_BPS: u64 = 25; // 0.25% fee
70    pub const BASIS_POINTS: u64 = 10000;
71    pub const MINIMUM_BALANCE: U256 = uint!(1_000_000_000_U256); // 1e9
72
73    /// Creates an instance of the precompile.
74    ///
75    /// Caution: This does not initialize the account, see [`Self::initialize`].
76    pub fn new(storage: &'a mut S) -> Self {
77        Self::_new(TIP_FEE_MANAGER_ADDRESS, storage)
78    }
79
80    /// Initializes the contract
81    ///
82    /// This ensures the [`TipFeeManager`] isn't empty and prevents state clear.
83    pub fn initialize(&mut self) -> Result<()> {
84        // must ensure the account is not empty, by setting some code
85        self.storage.set_code(
86            self.address,
87            Bytecode::new_legacy(Bytes::from_static(&[0xef])),
88        )
89    }
90
91    /// Returns the default fee token based on the current hardfork.
92    /// Post-Allegretto returns PathUSD, pre-Allegretto returns the first TIP20 after PathUSD.
93    fn default_fee_token(&self) -> Address {
94        if self.storage.spec().is_allegretto() {
95            DEFAULT_FEE_TOKEN_POST_ALLEGRETTO
96        } else {
97            DEFAULT_FEE_TOKEN_PRE_ALLEGRETTO
98        }
99    }
100
101    pub fn get_validator_token(&mut self, beneficiary: Address) -> Result<Address> {
102        let token = self.sload_validator_tokens(beneficiary)?;
103
104        if token.is_zero() {
105            Ok(self.default_fee_token())
106        } else {
107            Ok(token)
108        }
109    }
110
111    pub fn set_validator_token(
112        &mut self,
113        sender: Address,
114        call: IFeeManager::setValidatorTokenCall,
115        beneficiary: Address,
116    ) -> Result<()> {
117        if !is_tip20_prefix(call.token) {
118            return Err(FeeManagerError::invalid_token().into());
119        }
120
121        // Prevent changing if validator already has collected fees (post-Allegretto)
122        if self.storage.spec().is_allegretto() && self.sload_validator_in_fees_array(sender)? {
123            return Err(FeeManagerError::cannot_change_with_pending_fees().into());
124        }
125
126        // Prevent changing within the validator's own block
127        if sender == beneficiary {
128            return Err(FeeManagerError::cannot_change_within_block().into());
129        }
130
131        // Validate that the fee token is USD
132        validate_usd_currency(call.token, self.storage)?;
133
134        self.sstore_validator_tokens(sender, call.token)?;
135
136        // Emit ValidatorTokenSet event
137        self.storage.emit_event(
138            self.address,
139            FeeManagerEvent::ValidatorTokenSet(IFeeManager::ValidatorTokenSet {
140                validator: sender,
141                token: call.token,
142            })
143            .into_log_data(),
144        )
145    }
146
147    pub fn set_user_token(
148        &mut self,
149        sender: Address,
150        call: IFeeManager::setUserTokenCall,
151    ) -> Result<()> {
152        if !is_tip20_prefix(call.token) {
153            return Err(FeeManagerError::invalid_token().into());
154        }
155
156        // Depending on the hardfork, allow/disallow PathUSD to be set as the fee token
157        // Pre moderato: Allow
158        // Post moderato: Disallow
159        // Post allegro moderato: Allow
160        if self.storage.spec().is_moderato()
161            && !self.storage.spec().is_allegro_moderato()
162            && call.token == PATH_USD_ADDRESS
163        {
164            return Err(FeeManagerError::invalid_token().into());
165        }
166
167        // Validate that the fee token is USD
168        validate_usd_currency(call.token, self.storage)?;
169
170        self.sstore_user_tokens(sender, call.token)?;
171
172        // Emit UserTokenSet event
173        self.storage.emit_event(
174            self.address,
175            FeeManagerEvent::UserTokenSet(IFeeManager::UserTokenSet {
176                user: sender,
177                token: call.token,
178            })
179            .into_log_data(),
180        )
181    }
182
183    /// Collects fees from user before transaction execution.
184    ///
185    /// Determines fee token, verifies pool liquidity for swaps if needed, reserves liquidity
186    /// for the max fee amount and transfers it to the fee manager.
187    /// Unused gas is later returned via collect_fee_post_tx
188    pub fn collect_fee_pre_tx(
189        &mut self,
190        fee_payer: Address,
191        user_token: Address,
192        max_amount: U256,
193        beneficiary: Address,
194    ) -> Result<Address> {
195        // Get the validator's token preference
196        let validator_token = self.get_validator_token(beneficiary)?;
197
198        // Verify pool liquidity if user token differs from validator token
199        if user_token != validator_token {
200            self.reserve_liquidity(user_token, validator_token, max_amount)?;
201        }
202
203        let mut tip20_token = TIP20Token::from_address(user_token, self.storage)?;
204
205        // Ensure that user and FeeManager are authorized to interact with the token
206        tip20_token.ensure_transfer_authorized(fee_payer, self.address)?;
207        tip20_token.transfer_fee_pre_tx(fee_payer, max_amount)?;
208
209        // Return the user's token preference
210        Ok(user_token)
211    }
212
213    /// Finalizes fee collection after transaction execution.
214    ///
215    /// Refunds unused tokens to user and tracks actual fee amount for swapping in `execute_block`
216    /// Called after transaction to settle the difference between max fee and actual usage.
217    pub fn collect_fee_post_tx(
218        &mut self,
219        fee_payer: Address,
220        actual_spending: U256,
221        refund_amount: U256,
222        fee_token: Address,
223        beneficiary: Address,
224    ) -> Result<()> {
225        // Refund unused tokens to user
226        let mut tip20_token = TIP20Token::from_address(fee_token, self.storage)?;
227        tip20_token.transfer_fee_post_tx(fee_payer, refund_amount, actual_spending)?;
228
229        // Execute fee swap and track collected fees
230        let validator_token = self.get_validator_token(beneficiary)?;
231
232        if fee_token != validator_token {
233            // Release Fee AMM liquidity
234            self.release_liquidity(fee_token, validator_token, refund_amount)?;
235
236            // Record the pool if there was a non-zero swap
237            if !actual_spending.is_zero() {
238                if !self.storage.spec().is_allegretto() {
239                    // Pre-Allegretto: track in buggy token_in_fees_array
240                    if !self.sload_token_in_fees_array(fee_token)? {
241                        TokensWithFees::new(slots::TOKENS_WITH_FEES).push(self, fee_token)?;
242                        self.sstore_token_in_fees_array(fee_token, true)?;
243                    }
244                } else {
245                    self.add_pair_to_fees_array(fee_token, validator_token)?;
246                }
247            }
248        }
249
250        if !self.storage.spec().is_allegretto() {
251            // Pre-Allegretto: increment collected fees if no AMM swap
252            if fee_token == validator_token {
253                self.increment_collected_fees(beneficiary, actual_spending)?;
254            }
255        } else {
256            // Post-Allegretto: calculate the actual fee amount and save it in per-validator collected fees
257            let amount = if fee_token == validator_token {
258                actual_spending
259            } else {
260                compute_amount_out(actual_spending)?
261            };
262
263            self.increment_collected_fees(beneficiary, amount)?;
264        }
265
266        Ok(())
267    }
268
269    pub fn execute_block(&mut self, sender: Address, beneficiary: Address) -> Result<()> {
270        // Only protocol can call this
271        if sender != Address::ZERO {
272            return Err(FeeManagerError::only_system_contract().into());
273        }
274
275        let mut total_amount_out = U256::ZERO;
276        let pools = if !self.storage.spec().is_allegretto() {
277            let validator_token = self.get_validator_token(beneficiary)?;
278            self.drain_tokens_with_fees()?
279                .into_iter()
280                .map(|token| (token, validator_token))
281                .collect::<Vec<_>>()
282        } else {
283            self.drain_pools_with_fees()?
284                .into_iter()
285                .map(|pair| {
286                    (
287                        token_id_to_address(pair.user_token),
288                        token_id_to_address(pair.validator_token),
289                    )
290                })
291                .collect()
292        };
293        for (user_token, validator_token) in pools {
294            total_amount_out += self.execute_pending_fee_swaps(user_token, validator_token)?;
295        }
296
297        // Pre-Allegretto: increment beneficiary's collected fees if there was a non-zero swap
298        if !self.storage.spec().is_allegretto() && !total_amount_out.is_zero() {
299            self.increment_collected_fees(beneficiary, total_amount_out)?;
300        }
301
302        for validator in self.drain_validators_with_fees()? {
303            let collected_fees = self.sload_collected_fees(validator)?;
304
305            if collected_fees.is_zero() {
306                continue;
307            }
308
309            let validator_token = self.get_validator_token(validator)?;
310            let mut token = TIP20Token::from_address(validator_token, self.storage)?;
311
312            // If FeeManager or validator are blacklisted, we are not transferring any fees
313            if token.is_transfer_authorized(self.address, beneficiary)? {
314                // Bound fee transfer to contract balance
315                let balance = token.balance_of(ITIP20::balanceOfCall {
316                    account: self.address,
317                })?;
318
319                if !balance.is_zero() {
320                    token
321                        .transfer(
322                            self.address,
323                            ITIP20::transferCall {
324                                to: beneficiary,
325                                amount: collected_fees.min(balance),
326                            },
327                        )
328                        .map_err(|_| {
329                            IFeeManager::IFeeManagerErrors::InsufficientFeeTokenBalance(
330                                IFeeManager::InsufficientFeeTokenBalance {},
331                            )
332                        })?;
333                }
334            }
335
336            // Clear collected fees for the validator
337            self.sstore_collected_fees(validator, U256::ZERO)?;
338        }
339
340        Ok(())
341    }
342
343    /// Add a token to the tokens with fees array
344    fn add_pair_to_fees_array(
345        &mut self,
346        user_token: Address,
347        validator_token: Address,
348    ) -> Result<()> {
349        let pair = TokenPair {
350            user_token: address_to_token_id_unchecked(user_token),
351            validator_token: address_to_token_id_unchecked(validator_token),
352        };
353        if !self.sload_pool_in_fees_array(pair)? {
354            self.sstore_pool_in_fees_array(pair, true)?;
355            PoolsWithFees::new(slots::POOLS_WITH_FEES).push(self, pair)?;
356        }
357        Ok(())
358    }
359
360    /// Drain all tokens with fees by popping from the back until empty
361    /// Returns a `Vec<Address>` with all the tokens that were in storage
362    /// Also sets token_in_fees_array to false for each token
363    fn drain_tokens_with_fees(&mut self) -> Result<Vec<Address>> {
364        let mut tokens = Vec::new();
365        let tokens_with_fees = TokensWithFees::new(slots::TOKENS_WITH_FEES);
366        while let Some(token) = tokens_with_fees.pop(self)? {
367            tokens.push(token);
368            if self.storage.spec().is_moderato() {
369                self.sstore_token_in_fees_array(token, false)?;
370            }
371        }
372
373        Ok(tokens)
374    }
375
376    /// Drain all validators with fees by popping from the back until empty
377    fn drain_validators_with_fees(&mut self) -> Result<Vec<Address>> {
378        let mut validators = Vec::new();
379        let validator_with_fees = ValidatorsWithFees::new(slots::VALIDATORS_WITH_FEES);
380        while let Some(validator) = validator_with_fees.pop(self)? {
381            validators.push(validator);
382            self.sstore_validator_in_fees_array(validator, false)?;
383        }
384        Ok(validators)
385    }
386
387    /// Drain all pools with fees by popping from the back until empty
388    fn drain_pools_with_fees(&mut self) -> Result<Vec<TokenPair>> {
389        let mut pools = Vec::new();
390        let pools_with_fees = PoolsWithFees::new(slots::POOLS_WITH_FEES);
391        while let Some(pool) = pools_with_fees.pop(self)? {
392            pools.push(pool);
393            self.sstore_pool_in_fees_array(pool, false)?;
394        }
395        Ok(pools)
396    }
397
398    /// Increment collected fees for the validator token
399    fn increment_collected_fees(&mut self, validator: Address, amount: U256) -> Result<()> {
400        if amount.is_zero() {
401            return Ok(());
402        }
403
404        let collected_fees = self.sload_collected_fees(validator)?;
405        self.sstore_collected_fees(
406            validator,
407            collected_fees
408                .checked_add(amount)
409                .ok_or(TempoPrecompileError::under_overflow())?,
410        )?;
411
412        // If this is the first fee for the validator, record it in validators with fees
413        if collected_fees.is_zero() {
414            self.sstore_validator_in_fees_array(validator, true)?;
415            ValidatorsWithFees::new(slots::VALIDATORS_WITH_FEES).push(self, validator)?;
416        }
417
418        Ok(())
419    }
420
421    pub fn user_tokens(&mut self, call: IFeeManager::userTokensCall) -> Result<Address> {
422        self.sload_user_tokens(call.user)
423    }
424
425    pub fn validator_tokens(&mut self, call: IFeeManager::validatorTokensCall) -> Result<Address> {
426        let token = self.sload_validator_tokens(call.validator)?;
427
428        if token.is_zero() {
429            Ok(self.default_fee_token())
430        } else {
431            Ok(token)
432        }
433    }
434
435    pub fn get_fee_token_balance(
436        &mut self,
437        call: IFeeManager::getFeeTokenBalanceCall,
438    ) -> Result<IFeeManager::getFeeTokenBalanceReturn> {
439        let mut token = self.sload_user_tokens(call.sender)?;
440
441        if token.is_zero() {
442            let validator_token = self.sload_validator_tokens(call.validator)?;
443
444            if validator_token.is_zero() {
445                return Ok(IFeeManager::getFeeTokenBalanceReturn {
446                    _0: Address::ZERO,
447                    _1: U256::ZERO,
448                });
449            } else {
450                token = validator_token;
451            }
452        }
453
454        let mut tip20_token = TIP20Token::from_address(token, self.storage)?;
455        let token_balance = tip20_token.balance_of(ITIP20::balanceOfCall {
456            account: call.sender,
457        })?;
458
459        Ok(IFeeManager::getFeeTokenBalanceReturn {
460            _0: token,
461            _1: token_balance,
462        })
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use tempo_chainspec::hardfork::TempoHardfork;
469    use tempo_contracts::precompiles::TIP20Error;
470
471    use super::*;
472    use crate::{
473        PATH_USD_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
474        error::TempoPrecompileError,
475        storage::hashmap::HashMapStorageProvider,
476        tip20::{ISSUER_ROLE, ITIP20, TIP20Token, tests::initialize_path_usd, token_id_to_address},
477    };
478
479    fn deploy_token_with_balance(
480        storage: &mut HashMapStorageProvider,
481        token: Address,
482        user: Address,
483        amount: U256,
484    ) {
485        initialize_path_usd(storage, user).unwrap();
486        let mut tip20_token = TIP20Token::from_address(token, storage).unwrap();
487
488        // Initialize token
489        tip20_token
490            .initialize(
491                "TestToken",
492                "TEST",
493                "USD",
494                PATH_USD_ADDRESS,
495                user,
496                Address::ZERO,
497            )
498            .unwrap();
499
500        // Grant issuer role to user and mint tokens
501        tip20_token.grant_role_internal(user, *ISSUER_ROLE).unwrap();
502
503        tip20_token
504            .mint(user, ITIP20::mintCall { to: user, amount })
505            .unwrap();
506
507        // Approve fee manager
508        tip20_token
509            .approve(
510                user,
511                ITIP20::approveCall {
512                    spender: TIP_FEE_MANAGER_ADDRESS,
513                    amount: U256::MAX,
514                },
515            )
516            .unwrap();
517    }
518
519    #[test]
520    fn test_set_user_token() -> eyre::Result<()> {
521        let mut storage = HashMapStorageProvider::new(1);
522        let user = Address::random();
523
524        // Initialize PathUSD first
525        initialize_path_usd(&mut storage, user).unwrap();
526
527        // Create a USD token to use as fee token
528        let token = token_id_to_address(1);
529        let mut tip20_token = TIP20Token::from_address(token, &mut storage).unwrap();
530        tip20_token
531            .initialize(
532                "TestToken",
533                "TEST",
534                "USD",
535                PATH_USD_ADDRESS,
536                user,
537                Address::ZERO,
538            )
539            .unwrap();
540
541        let mut fee_manager = TipFeeManager::new(&mut storage);
542
543        let call = IFeeManager::setUserTokenCall { token };
544        let result = fee_manager.set_user_token(user, call);
545        assert!(result.is_ok());
546
547        let call = IFeeManager::userTokensCall { user };
548        assert_eq!(fee_manager.user_tokens(call)?, token);
549        Ok(())
550    }
551
552    #[test]
553    fn test_set_user_token_cannot_be_path_usd_post_moderato() -> eyre::Result<()> {
554        // Test with Moderato hardfork (validation should be enforced)
555        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
556        let user = Address::random();
557
558        // Initialize PathUSD first
559        initialize_path_usd(&mut storage, user).unwrap();
560
561        let mut fee_manager = TipFeeManager::new(&mut storage);
562
563        // Try to set PathUSD as user token - should fail
564        let call = IFeeManager::setUserTokenCall {
565            token: PATH_USD_ADDRESS,
566        };
567        let result = fee_manager.set_user_token(user, call);
568
569        assert!(matches!(
570            result,
571            Err(TempoPrecompileError::FeeManagerError(
572                FeeManagerError::InvalidToken(_)
573            ))
574        ));
575
576        Ok(())
577    }
578
579    #[test]
580    fn test_set_user_token_allows_path_usd_pre_moderato() -> eyre::Result<()> {
581        // Test with Adagio (pre-Moderato) - validation should not be enforced
582        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
583        let user = Address::random();
584
585        // Initialize PathUSD first
586        initialize_path_usd(&mut storage, user).unwrap();
587
588        let mut fee_manager = TipFeeManager::new(&mut storage);
589
590        // Try to set PathUSD as user token - should succeed pre-Moderato
591        let call = IFeeManager::setUserTokenCall {
592            token: PATH_USD_ADDRESS,
593        };
594        let result = fee_manager.set_user_token(user, call);
595
596        // Pre-Moderato: should be allowed to set PathUSD as user token
597        assert!(result.is_ok());
598
599        Ok(())
600    }
601
602    #[test]
603    fn test_set_user_token_allows_path_usd_post_allegro_moderato() -> eyre::Result<()> {
604        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::AllegroModerato);
605        let user = Address::random();
606        initialize_path_usd(&mut storage, user).unwrap();
607
608        let mut fee_manager = TipFeeManager::new(&mut storage);
609
610        let call = IFeeManager::setUserTokenCall {
611            token: PATH_USD_ADDRESS,
612        };
613        let result = fee_manager.set_user_token(user, call);
614
615        // Post Allegro Moderato: should be allowed to set PathUSD as user token
616        assert!(result.is_ok());
617
618        Ok(())
619    }
620
621    #[test]
622    fn test_set_validator_token() -> eyre::Result<()> {
623        let mut storage = HashMapStorageProvider::new(1);
624        let validator = Address::random();
625        let admin = Address::random();
626
627        // Initialize PathUSD first
628        initialize_path_usd(&mut storage, admin).unwrap();
629
630        // Create a USD token to use as fee token
631        let token = token_id_to_address(1);
632        let mut tip20_token = TIP20Token::from_address(token, &mut storage).unwrap();
633        tip20_token
634            .initialize(
635                "TestToken",
636                "TEST",
637                "USD",
638                PATH_USD_ADDRESS,
639                admin,
640                Address::ZERO,
641            )
642            .unwrap();
643
644        let mut fee_manager = TipFeeManager::new(&mut storage);
645
646        let call = IFeeManager::setValidatorTokenCall { token };
647        let result = fee_manager.set_validator_token(validator, call.clone(), validator);
648        assert_eq!(
649            result,
650            Err(TempoPrecompileError::FeeManagerError(
651                FeeManagerError::cannot_change_within_block()
652            ))
653        );
654
655        // Now set beneficiary to a random address to avoid `CannotChangeWithinBlock` error
656        let beneficiary = Address::random();
657        let result = fee_manager.set_validator_token(validator, call, beneficiary);
658        assert!(result.is_ok());
659
660        let query_call = IFeeManager::validatorTokensCall { validator };
661        let returned_token = fee_manager.validator_tokens(query_call)?;
662        assert_eq!(returned_token, token);
663
664        Ok(())
665    }
666
667    #[test]
668    fn test_set_validator_token_cannot_change_with_pending_fees() -> eyre::Result<()> {
669        use tempo_chainspec::hardfork::TempoHardfork;
670
671        // Use Allegretto hardfork since the pending fees check is post-Allegretto
672        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::Allegretto);
673        let validator = Address::random();
674        let beneficiary = Address::random(); // Different from validator
675        let admin = Address::random();
676
677        // Initialize PathUSD first
678        initialize_path_usd(&mut storage, admin).unwrap();
679
680        // Create a USD token to use as fee token
681        let token = token_id_to_address(1);
682        let mut tip20_token = TIP20Token::from_address(token, &mut storage).unwrap();
683        tip20_token
684            .initialize(
685                "TestToken",
686                "TEST",
687                "USD",
688                PATH_USD_ADDRESS,
689                admin,
690                Address::ZERO,
691            )
692            .unwrap();
693
694        let mut fee_manager = TipFeeManager::new(&mut storage);
695
696        // Simulate validator having pending fees by setting validator_in_fees_array
697        fee_manager.sstore_validator_in_fees_array(validator, true)?;
698
699        // Try to set validator token when validator has pending fees (but is not the beneficiary)
700        let call = IFeeManager::setValidatorTokenCall { token };
701        let result = fee_manager.set_validator_token(validator, call.clone(), beneficiary);
702
703        // Should fail with CannotChangeWithPendingFees
704        assert_eq!(
705            result,
706            Err(TempoPrecompileError::FeeManagerError(
707                FeeManagerError::cannot_change_with_pending_fees()
708            ))
709        );
710
711        // Now clear the pending fees flag and try again - should succeed since validator != beneficiary
712        fee_manager.sstore_validator_in_fees_array(validator, false)?;
713        let result = fee_manager.set_validator_token(validator, call.clone(), beneficiary);
714        assert!(result.is_ok());
715
716        // But if validator is the beneficiary, should fail with CannotChangeWithinBlock
717        let result = fee_manager.set_validator_token(validator, call, validator);
718        assert_eq!(
719            result,
720            Err(TempoPrecompileError::FeeManagerError(
721                FeeManagerError::cannot_change_within_block()
722            ))
723        );
724
725        Ok(())
726    }
727
728    #[test]
729    fn test_is_tip20_prefix() {
730        let token_id = rand::random::<u64>();
731        let token = token_id_to_address(token_id);
732        assert!(is_tip20_prefix(token));
733
734        let token = Address::random();
735        assert!(!is_tip20_prefix(token));
736    }
737
738    #[test]
739    fn test_collect_fee_pre_tx() {
740        let mut storage = HashMapStorageProvider::new(1);
741        let user = Address::random();
742        let validator = Address::random();
743        let token = token_id_to_address(rand::random::<u64>());
744        let max_amount = U256::from(10000);
745
746        // Setup token with balance and approval
747        deploy_token_with_balance(&mut storage, token, user, U256::from(u64::MAX));
748
749        let mut fee_manager = TipFeeManager::new(&mut storage);
750
751        // Set validator token
752        // Set beneficiary to a random address to avoid `CannotChangeWithinBlock` error
753        let beneficiary = Address::random();
754        fee_manager
755            .set_validator_token(
756                validator,
757                IFeeManager::setValidatorTokenCall { token },
758                beneficiary,
759            )
760            .unwrap();
761
762        // Set user token
763        fee_manager
764            .set_user_token(user, IFeeManager::setUserTokenCall { token })
765            .unwrap();
766
767        // Call collect_fee_pre_tx directly
768        let result = fee_manager.collect_fee_pre_tx(user, token, max_amount, validator);
769        assert!(result.is_ok());
770        assert_eq!(result.unwrap(), token);
771    }
772
773    #[test]
774    fn test_collect_fee_post_tx() -> eyre::Result<()> {
775        let mut storage = HashMapStorageProvider::new(1);
776        let user = Address::random();
777        let token = token_id_to_address(rand::random::<u64>());
778        let actual_used = U256::from(6000);
779        let refund_amount = U256::from(4000);
780
781        // Setup token with balance for fee manager
782        let admin = Address::random();
783
784        // Initialize token and give fee manager tokens (simulating that collect_fee_pre_tx already happened)
785        {
786            initialize_path_usd(&mut storage, admin).unwrap();
787            let mut tip20_token = TIP20Token::from_address(token, &mut storage).unwrap();
788            tip20_token
789                .initialize(
790                    "TestToken",
791                    "TEST",
792                    "USD",
793                    PATH_USD_ADDRESS,
794                    admin,
795                    Address::ZERO,
796                )
797                .unwrap();
798
799            tip20_token.grant_role_internal(admin, *ISSUER_ROLE)?;
800            tip20_token
801                .mint(
802                    admin,
803                    ITIP20::mintCall {
804                        to: TIP_FEE_MANAGER_ADDRESS,
805                        amount: U256::from(100000000000000_u64),
806                    },
807                )
808                .unwrap();
809        }
810
811        let validator = Address::random();
812        let mut fee_manager = TipFeeManager::new(&mut storage);
813
814        // Set validator token
815        // Set beneficiary to a random address to avoid `CannotChangeWithinBlock` error
816        fee_manager
817            .set_validator_token(
818                validator,
819                IFeeManager::setValidatorTokenCall { token },
820                Address::random(),
821            )
822            .unwrap();
823
824        // Set user token
825        fee_manager
826            .set_user_token(user, IFeeManager::setUserTokenCall { token })
827            .unwrap();
828
829        // Call collect_fee_post_tx directly
830        let result =
831            fee_manager.collect_fee_post_tx(user, actual_used, refund_amount, token, validator);
832        assert!(result.is_ok());
833
834        // Verify fees were tracked
835        let tracked_amount = fee_manager.sload_collected_fees(validator)?;
836        assert_eq!(tracked_amount, actual_used);
837
838        // Verify user got the refund
839        let mut tip20_token = TIP20Token::from_address(token, &mut storage).unwrap();
840        let balance = tip20_token.balance_of(ITIP20::balanceOfCall { account: user })?;
841        assert_eq!(balance, refund_amount);
842
843        Ok(())
844    }
845
846    #[test]
847    fn test_rejects_non_usd() -> eyre::Result<()> {
848        let mut storage = HashMapStorageProvider::new(1);
849
850        let admin = Address::random();
851        let token = token_id_to_address(rand::random::<u64>());
852        let mut tip20_token = TIP20Token::from_address(token, &mut storage).unwrap();
853        tip20_token
854            .initialize(
855                "NonUSD",
856                "NonUSD",
857                "NonUSD",
858                PATH_USD_ADDRESS,
859                admin,
860                Address::ZERO,
861            )
862            .unwrap();
863
864        let validator = Address::random();
865        let mut fee_manager = TipFeeManager::new(&mut storage);
866
867        let user = Address::random();
868
869        let call = IFeeManager::setUserTokenCall { token };
870        let result = fee_manager.set_user_token(user, call);
871
872        assert!(matches!(
873            result,
874            Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
875        ));
876
877        // Set beneficiary to a random address to avoid `CannotChangeWithinBlock` error
878        let call = IFeeManager::setValidatorTokenCall { token };
879        let result = fee_manager.set_validator_token(validator, call, Address::random());
880
881        assert!(matches!(
882            result,
883            Err(TempoPrecompileError::TIP20(TIP20Error::InvalidCurrency(_)))
884        ));
885
886        Ok(())
887    }
888
889    #[test]
890    fn test_prevent_insufficient_balance_transfer() -> eyre::Result<()> {
891        let mut storage = HashMapStorageProvider::new(1);
892        let admin = Address::random();
893        let validator = Address::random();
894        let token = token_id_to_address(rand::random::<u64>());
895
896        // Manually set collected fees to 1000 and actual balance to 500 to simulate the attack.
897        let collected_fees = U256::from(1000);
898        let balance = U256::from(500);
899
900        {
901            // Initialize token
902            initialize_path_usd(&mut storage, admin)?;
903            let mut tip20_token = TIP20Token::from_address(token, &mut storage).unwrap();
904            tip20_token.initialize(
905                "TestToken",
906                "TEST",
907                "USD",
908                PATH_USD_ADDRESS,
909                admin,
910                Address::ZERO,
911            )?;
912            tip20_token.grant_role_internal(admin, *ISSUER_ROLE)?;
913
914            // Mint tokens simulating `collected fees - attack burn`
915            tip20_token.mint(
916                admin,
917                ITIP20::mintCall {
918                    to: TIP_FEE_MANAGER_ADDRESS,
919                    amount: balance,
920                },
921            )?;
922        }
923
924        {
925            // Set validator token
926            let mut fee_manager = TipFeeManager::new(&mut storage);
927            fee_manager.set_validator_token(
928                validator,
929                IFeeManager::setValidatorTokenCall { token },
930                Address::random(),
931            )?;
932
933            // Simulate collected fees
934            fee_manager.increment_collected_fees(validator, collected_fees)?;
935
936            // Execute block
937            let result = fee_manager.execute_block(Address::ZERO, validator);
938            assert!(result.is_ok());
939
940            // Verify collected fees are cleared
941            let remaining_fees = fee_manager.sload_collected_fees(validator)?;
942            assert_eq!(remaining_fees, U256::ZERO);
943        }
944
945        // Verify validator got the available balance
946        let mut tip20_token = TIP20Token::from_address(token, &mut storage).unwrap();
947        let validator_balance =
948            tip20_token.balance_of(ITIP20::balanceOfCall { account: validator })?;
949        assert_eq!(validator_balance, balance);
950
951        let fee_manager_balance = tip20_token.balance_of(ITIP20::balanceOfCall {
952            account: TIP_FEE_MANAGER_ADDRESS,
953        })?;
954        assert_eq!(fee_manager_balance, U256::ZERO);
955
956        Ok(())
957    }
958}