tempo_precompiles/tip20/
mod.rs

1pub mod dispatch;
2pub mod rewards;
3pub mod roles;
4
5use tempo_contracts::precompiles::{FeeManagerError, STABLECOIN_EXCHANGE_ADDRESS};
6pub use tempo_contracts::precompiles::{
7    IRolesAuth, ITIP20, RolesAuthError, RolesAuthEvent, TIP20Error, TIP20Event,
8};
9
10use crate::{
11    PATH_USD_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
12    account_keychain::AccountKeychain,
13    error::{Result, TempoPrecompileError},
14    storage::{Handler, Mapping, StorageCtx},
15    tip20::{
16        rewards::{RewardStream, UserRewardInfo},
17        roles::DEFAULT_ADMIN_ROLE,
18    },
19    tip20_factory::TIP20Factory,
20    tip403_registry::{ITIP403Registry, TIP403Registry},
21};
22use alloy::{
23    hex,
24    primitives::{Address, B256, U256, keccak256, uint},
25};
26use std::sync::LazyLock;
27use tempo_precompiles_macros::contract;
28use tracing::trace;
29
30/// u128::MAX as U256
31pub const U128_MAX: U256 = uint!(0xffffffffffffffffffffffffffffffff_U256);
32
33/// Decimal precision for TIP-20 tokens
34const TIP20_DECIMALS: u8 = 6;
35
36/// USD currency string constant
37pub const USD_CURRENCY: &str = "USD";
38
39/// TIP20 token address prefix (12 bytes for token ID encoding)
40const TIP20_TOKEN_PREFIX: [u8; 12] = hex!("20C000000000000000000000");
41
42/// Returns true if the address has the TIP20 prefix.
43///
44/// Note: This only checks the prefix, not whether the token was actually created.
45/// Use `TIP20Factory::is_tip20()` for full validation (post-AllegroModerato).
46pub fn is_tip20_prefix(token: Address) -> bool {
47    token.as_slice().starts_with(&TIP20_TOKEN_PREFIX)
48}
49
50/// Converts a token ID to its corresponding contract address
51/// Uses the pattern: TIP20_TOKEN_PREFIX ++ token_id
52pub fn token_id_to_address(token_id: u64) -> Address {
53    let mut address_bytes = [0u8; 20];
54    address_bytes[..12].copy_from_slice(&TIP20_TOKEN_PREFIX);
55    address_bytes[12..20].copy_from_slice(&token_id.to_be_bytes());
56    Address::from(address_bytes)
57}
58
59pub fn address_to_token_id_unchecked(address: Address) -> u64 {
60    u64::from_be_bytes(address.as_slice()[12..20].try_into().unwrap())
61}
62
63#[contract]
64pub struct TIP20Token {
65    // RolesAuth
66    roles: Mapping<Address, Mapping<B256, bool>>,
67    role_admins: Mapping<B256, B256>,
68
69    // TIP20 Metadata
70    name: String,
71    symbol: String,
72    currency: String,
73    domain_separator: B256,
74    quote_token: Address,
75    next_quote_token: Address,
76    transfer_policy_id: u64,
77
78    // TIP20 Token
79    total_supply: U256,
80    balances: Mapping<Address, U256>,
81    allowances: Mapping<Address, Mapping<Address, U256>>,
82    nonces: Mapping<Address, U256>,
83    paused: bool,
84    supply_cap: U256,
85    salts: Mapping<B256, bool>,
86
87    // TIP20 Rewards
88    global_reward_per_token: U256,
89    last_update_time: u64,
90    total_reward_per_second: U256,
91    opted_in_supply: u128,
92    next_stream_id: u64,
93    streams: Mapping<u64, RewardStream>,
94    scheduled_rate_decrease: Mapping<u128, U256>,
95    user_reward_info: Mapping<Address, UserRewardInfo>,
96
97    // Fee recipient
98    fee_recipient: Address,
99}
100
101pub static PAUSE_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"PAUSE_ROLE"));
102pub static UNPAUSE_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"UNPAUSE_ROLE"));
103pub static ISSUER_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"ISSUER_ROLE"));
104pub static BURN_BLOCKED_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"BURN_BLOCKED_ROLE"));
105
106/// Validates that a token has USD currency
107pub fn validate_usd_currency(token: Address, storage: StorageCtx) -> Result<()> {
108    if storage.spec().is_moderato() && !is_tip20_prefix(token) {
109        return Err(FeeManagerError::invalid_token().into());
110    }
111
112    let tip20_token = TIP20Token::from_address(token)?;
113    let currency = tip20_token.currency()?;
114    if currency != USD_CURRENCY {
115        return Err(TIP20Error::invalid_currency().into());
116    }
117    Ok(())
118}
119
120impl TIP20Token {
121    pub fn name(&self) -> Result<String> {
122        self.name.read()
123    }
124
125    pub fn symbol(&self) -> Result<String> {
126        self.symbol.read()
127    }
128
129    pub fn decimals(&self) -> Result<u8> {
130        Ok(TIP20_DECIMALS)
131    }
132
133    pub fn currency(&self) -> Result<String> {
134        self.currency.read()
135    }
136
137    pub fn total_supply(&self) -> Result<U256> {
138        self.total_supply.read()
139    }
140
141    pub fn quote_token(&self) -> Result<Address> {
142        self.quote_token.read()
143    }
144
145    pub fn next_quote_token(&self) -> Result<Address> {
146        self.next_quote_token.read()
147    }
148
149    pub fn supply_cap(&self) -> Result<U256> {
150        self.supply_cap.read()
151    }
152
153    pub fn paused(&self) -> Result<bool> {
154        self.paused.read()
155    }
156
157    pub fn transfer_policy_id(&self) -> Result<u64> {
158        self.transfer_policy_id.read()
159    }
160
161    /// Returns the PAUSE_ROLE constant
162    ///
163    /// This role identifier grants permission to pause the token contract.
164    /// The role is computed as `keccak256("PAUSE_ROLE")`.
165    pub fn pause_role() -> B256 {
166        *PAUSE_ROLE
167    }
168
169    /// Returns the UNPAUSE_ROLE constant
170    ///
171    /// This role identifier grants permission to unpause the token contract.
172    /// The role is computed as `keccak256("UNPAUSE_ROLE")`.
173    pub fn unpause_role() -> B256 {
174        *UNPAUSE_ROLE
175    }
176
177    /// Returns the ISSUER_ROLE constant
178    ///
179    /// This role identifier grants permission to mint and burn tokens.
180    /// The role is computed as `keccak256("ISSUER_ROLE")`.
181    pub fn issuer_role() -> B256 {
182        *ISSUER_ROLE
183    }
184
185    /// Returns the BURN_BLOCKED_ROLE constant
186    ///
187    /// This role identifier grants permission to burn tokens from blocked accounts.
188    /// The role is computed as `keccak256("BURN_BLOCKED_ROLE")`.
189    pub fn burn_blocked_role() -> B256 {
190        *BURN_BLOCKED_ROLE
191    }
192
193    // View functions
194    pub fn balance_of(&self, call: ITIP20::balanceOfCall) -> Result<U256> {
195        self.balances.at(call.account).read()
196    }
197
198    pub fn allowance(&self, call: ITIP20::allowanceCall) -> Result<U256> {
199        self.allowances.at(call.owner).at(call.spender).read()
200    }
201
202    // Admin functions
203    pub fn change_transfer_policy_id(
204        &mut self,
205        msg_sender: Address,
206        call: ITIP20::changeTransferPolicyIdCall,
207    ) -> Result<()> {
208        self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
209        self.transfer_policy_id.write(call.newPolicyId)?;
210
211        self.emit_event(TIP20Event::TransferPolicyUpdate(
212            ITIP20::TransferPolicyUpdate {
213                updater: msg_sender,
214                newPolicyId: call.newPolicyId,
215            },
216        ))
217    }
218
219    pub fn set_supply_cap(
220        &mut self,
221        msg_sender: Address,
222        call: ITIP20::setSupplyCapCall,
223    ) -> Result<()> {
224        self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
225        if call.newSupplyCap < self.total_supply()? {
226            return Err(TIP20Error::invalid_supply_cap().into());
227        }
228
229        if call.newSupplyCap > U128_MAX {
230            return Err(TIP20Error::supply_cap_exceeded().into());
231        }
232
233        self.supply_cap.write(call.newSupplyCap)?;
234
235        self.emit_event(TIP20Event::SupplyCapUpdate(ITIP20::SupplyCapUpdate {
236            updater: msg_sender,
237            newSupplyCap: call.newSupplyCap,
238        }))
239    }
240
241    pub fn pause(&mut self, msg_sender: Address, _call: ITIP20::pauseCall) -> Result<()> {
242        self.check_role(msg_sender, *PAUSE_ROLE)?;
243        self.paused.write(true)?;
244
245        self.emit_event(TIP20Event::PauseStateUpdate(ITIP20::PauseStateUpdate {
246            updater: msg_sender,
247            isPaused: true,
248        }))
249    }
250
251    pub fn unpause(&mut self, msg_sender: Address, _call: ITIP20::unpauseCall) -> Result<()> {
252        self.check_role(msg_sender, *UNPAUSE_ROLE)?;
253        self.paused.write(false)?;
254
255        self.emit_event(TIP20Event::PauseStateUpdate(ITIP20::PauseStateUpdate {
256            updater: msg_sender,
257            isPaused: false,
258        }))
259    }
260
261    pub fn set_next_quote_token(
262        &mut self,
263        msg_sender: Address,
264        call: ITIP20::setNextQuoteTokenCall,
265    ) -> Result<()> {
266        self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
267
268        // Verify the new quote token is a valid TIP20 token that has been deployed
269        if self.storage.spec().is_allegro_moderato() {
270            // Post-AllegroModerato: use factory's is_tip20 which checks both prefix and counter
271            if !TIP20Factory::new().is_tip20(call.newQuoteToken)? {
272                return Err(TIP20Error::invalid_quote_token().into());
273            }
274        } else {
275            // Pre-AllegroModerato: use original logic (prefix check + separate counter check)
276            if !is_tip20_prefix(call.newQuoteToken) {
277                return Err(TIP20Error::invalid_quote_token().into());
278            }
279
280            let new_token_id = address_to_token_id_unchecked(call.newQuoteToken);
281            let factory_token_id_counter = TIP20Factory::new().token_id_counter()?.to::<u64>();
282
283            // Ensure the quote token has been deployed (token_id < counter)
284            if new_token_id >= factory_token_id_counter {
285                return Err(TIP20Error::invalid_quote_token().into());
286            }
287        }
288
289        // Check if the currency is USD, if so then the quote token's currency MUST also be USD
290        let currency = self.currency()?;
291        if currency == USD_CURRENCY {
292            let quote_token_currency = Self::from_address(call.newQuoteToken)?.currency()?;
293            if quote_token_currency != USD_CURRENCY {
294                return Err(TIP20Error::invalid_quote_token().into());
295            }
296        }
297
298        self.next_quote_token.write(call.newQuoteToken)?;
299
300        self.emit_event(TIP20Event::NextQuoteTokenSet(ITIP20::NextQuoteTokenSet {
301            updater: msg_sender,
302            nextQuoteToken: call.newQuoteToken,
303        }))
304    }
305
306    pub fn complete_quote_token_update(
307        &mut self,
308        msg_sender: Address,
309        _call: ITIP20::completeQuoteTokenUpdateCall,
310    ) -> Result<()> {
311        self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
312
313        let next_quote_token = self.next_quote_token()?;
314
315        // Check that this does not create a loop
316        // Loop through quote tokens until we reach the root (PathUSD)
317        let mut current = next_quote_token;
318        while current != PATH_USD_ADDRESS {
319            if current == self.address {
320                return Err(TIP20Error::invalid_quote_token().into());
321            }
322
323            current = Self::from_address(current)?.quote_token()?;
324        }
325
326        // Update the quote token
327        self.quote_token.write(next_quote_token)?;
328
329        self.emit_event(TIP20Event::QuoteTokenUpdate(ITIP20::QuoteTokenUpdate {
330            updater: msg_sender,
331            newQuoteToken: next_quote_token,
332        }))
333    }
334
335    /// Sets a new fee recipient
336    pub fn set_fee_recipient(&mut self, msg_sender: Address, new_recipient: Address) -> Result<()> {
337        self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
338        self.fee_recipient.write(new_recipient)?;
339
340        self.emit_event(TIP20Event::FeeRecipientUpdated(
341            ITIP20::FeeRecipientUpdated {
342                updater: msg_sender,
343                newRecipient: new_recipient,
344            },
345        ))?;
346
347        Ok(())
348    }
349
350    /// Gets the current fee recipient
351    pub fn get_fee_recipient(&self, _msg_sender: Address) -> Result<Address> {
352        self.fee_recipient.read()
353    }
354
355    // Token operations
356    /// Mints new tokens to specified address
357    pub fn mint(&mut self, msg_sender: Address, call: ITIP20::mintCall) -> Result<()> {
358        self._mint(msg_sender, call.to, call.amount)?;
359        if self.storage.spec().is_allegro_moderato() {
360            self.emit_event(TIP20Event::Mint(ITIP20::Mint {
361                to: call.to,
362                amount: call.amount,
363            }))?;
364        }
365        Ok(())
366    }
367
368    /// Mints new tokens to specified address with memo attached
369    pub fn mint_with_memo(
370        &mut self,
371        msg_sender: Address,
372        call: ITIP20::mintWithMemoCall,
373    ) -> Result<()> {
374        self._mint(msg_sender, call.to, call.amount)?;
375
376        // Post-Moderato: emit events where sender is Address::ZERO for mint operations
377        let from = if self.storage.spec().is_moderato() {
378            Address::ZERO
379        } else {
380            msg_sender
381        };
382
383        self.emit_event(TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
384            from,
385            to: call.to,
386            amount: call.amount,
387            memo: call.memo,
388        }))?;
389        if self.storage.spec().is_allegro_moderato() {
390            self.emit_event(TIP20Event::Mint(ITIP20::Mint {
391                to: call.to,
392                amount: call.amount,
393            }))?;
394        }
395        Ok(())
396    }
397
398    /// Internal helper to mint new tokens and update balances
399    fn _mint(&mut self, msg_sender: Address, to: Address, amount: U256) -> Result<()> {
400        self.check_role(msg_sender, *ISSUER_ROLE)?;
401        let total_supply = self.total_supply()?;
402
403        // Check if the `to` address is authorized to receive tokens
404        if self.storage.spec().is_allegretto() {
405            let transfer_policy_id = self.transfer_policy_id()?;
406            let registry = TIP403Registry::new();
407            if !registry.is_authorized(ITIP403Registry::isAuthorizedCall {
408                policyId: transfer_policy_id,
409                user: to,
410            })? {
411                return Err(TIP20Error::policy_forbids().into());
412            }
413        }
414
415        let new_supply = total_supply
416            .checked_add(amount)
417            .ok_or(TempoPrecompileError::under_overflow())?;
418
419        let supply_cap = self.supply_cap()?;
420        if new_supply > supply_cap {
421            return Err(TIP20Error::supply_cap_exceeded().into());
422        }
423
424        let timestamp = self.storage.timestamp();
425        self.accrue(timestamp)?;
426
427        self.handle_rewards_on_mint(to, amount)?;
428
429        self.set_total_supply(new_supply)?;
430        let to_balance = self.get_balance(to)?;
431        let new_to_balance: alloy::primitives::Uint<256, 4> = to_balance
432            .checked_add(amount)
433            .ok_or(TempoPrecompileError::under_overflow())?;
434        self.set_balance(to, new_to_balance)?;
435
436        self.emit_event(TIP20Event::Transfer(ITIP20::Transfer {
437            from: Address::ZERO,
438            to,
439            amount,
440        }))?;
441
442        // Pre-Allegro Moderato: emit Mint event here
443        if !self.storage.spec().is_allegro_moderato() {
444            self.emit_event(TIP20Event::Mint(ITIP20::Mint { to, amount }))?;
445        }
446        Ok(())
447    }
448
449    /// Burns tokens from sender's balance and reduces total supply
450    pub fn burn(&mut self, msg_sender: Address, call: ITIP20::burnCall) -> Result<()> {
451        self._burn(msg_sender, call.amount)?;
452        if self.storage.spec().is_allegro_moderato() {
453            self.emit_event(TIP20Event::Burn(ITIP20::Burn {
454                from: msg_sender,
455                amount: call.amount,
456            }))?;
457        }
458        Ok(())
459    }
460
461    /// Burns tokens from sender's balance with memo attached
462    pub fn burn_with_memo(
463        &mut self,
464        msg_sender: Address,
465        call: ITIP20::burnWithMemoCall,
466    ) -> Result<()> {
467        self._burn(msg_sender, call.amount)?;
468
469        self.emit_event(TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
470            from: msg_sender,
471            to: Address::ZERO,
472            amount: call.amount,
473            memo: call.memo,
474        }))?;
475        if self.storage.spec().is_allegro_moderato() {
476            self.emit_event(TIP20Event::Burn(ITIP20::Burn {
477                from: msg_sender,
478                amount: call.amount,
479            }))?;
480        }
481        Ok(())
482    }
483
484    /// Burns tokens from blocked addresses that cannot transfer
485    pub fn burn_blocked(
486        &mut self,
487        msg_sender: Address,
488        call: ITIP20::burnBlockedCall,
489    ) -> Result<()> {
490        self.check_role(msg_sender, *BURN_BLOCKED_ROLE)?;
491
492        // Prevent burning from `FeeManager` and `StablecoinExchange` to protect accounting invariants
493        if self.storage.spec().is_allegretto()
494            && matches!(
495                call.from,
496                TIP_FEE_MANAGER_ADDRESS | STABLECOIN_EXCHANGE_ADDRESS
497            )
498        {
499            return Err(TIP20Error::protected_address().into());
500        }
501
502        // Check if the address is blocked from transferring
503        let transfer_policy_id = self.transfer_policy_id()?;
504        let registry = TIP403Registry::new();
505        if registry.is_authorized(ITIP403Registry::isAuthorizedCall {
506            policyId: transfer_policy_id,
507            user: call.from,
508        })? {
509            // Only allow burning from addresses that are blocked from transferring
510            return Err(TIP20Error::policy_forbids().into());
511        }
512
513        self._transfer(call.from, Address::ZERO, call.amount)?;
514
515        let total_supply = self.total_supply()?;
516        let new_supply =
517            total_supply
518                .checked_sub(call.amount)
519                .ok_or(TIP20Error::insufficient_balance(
520                    total_supply,
521                    call.amount,
522                    self.address,
523                ))?;
524        self.set_total_supply(new_supply)?;
525
526        self.emit_event(TIP20Event::BurnBlocked(ITIP20::BurnBlocked {
527            from: call.from,
528            amount: call.amount,
529        }))
530    }
531
532    fn _burn(&mut self, msg_sender: Address, amount: U256) -> Result<()> {
533        self.check_role(msg_sender, *ISSUER_ROLE)?;
534
535        self._transfer(msg_sender, Address::ZERO, amount)?;
536
537        let total_supply = self.total_supply()?;
538        let new_supply =
539            total_supply
540                .checked_sub(amount)
541                .ok_or(TIP20Error::insufficient_balance(
542                    total_supply,
543                    amount,
544                    self.address,
545                ))?;
546        self.set_total_supply(new_supply)?;
547
548        // Pre-Allegro Moderato: emit Burn event here
549        if !self.storage.spec().is_allegro_moderato() {
550            self.emit_event(TIP20Event::Burn(ITIP20::Burn {
551                from: msg_sender,
552                amount,
553            }))?;
554        }
555        Ok(())
556    }
557
558    // Standard token functions
559    pub fn approve(&mut self, msg_sender: Address, call: ITIP20::approveCall) -> Result<bool> {
560        // Only check access keys after Allegretto hardfork
561        if self.storage.spec().is_allegretto() {
562            // Get the old allowance
563            let old_allowance = self.get_allowance(msg_sender, call.spender)?;
564
565            // Check and update spending limits for access keys
566            let mut keychain = AccountKeychain::new();
567            keychain.authorize_approve(msg_sender, self.address, old_allowance, call.amount)?;
568        }
569
570        // Set the new allowance
571        self.set_allowance(msg_sender, call.spender, call.amount)?;
572
573        self.emit_event(TIP20Event::Approval(ITIP20::Approval {
574            owner: msg_sender,
575            spender: call.spender,
576            amount: call.amount,
577        }))?;
578
579        Ok(true)
580    }
581
582    pub fn transfer(&mut self, msg_sender: Address, call: ITIP20::transferCall) -> Result<bool> {
583        trace!(%msg_sender, ?call, "transferring TIP20");
584        self.check_not_paused()?;
585        self.check_not_token_address(call.to)?;
586        self.ensure_transfer_authorized(msg_sender, call.to)?;
587
588        // Only check access keys after Allegretto hardfork
589        if self.storage.spec().is_allegretto() {
590            // Check and update spending limits for access keys
591            let mut keychain = AccountKeychain::new();
592            keychain.authorize_transfer(msg_sender, self.address, call.amount)?;
593        }
594
595        self._transfer(msg_sender, call.to, call.amount)?;
596        Ok(true)
597    }
598
599    pub fn transfer_from(
600        &mut self,
601        msg_sender: Address,
602        call: ITIP20::transferFromCall,
603    ) -> Result<bool> {
604        self._transfer_from(msg_sender, call.from, call.to, call.amount)
605    }
606
607    /// Transfer from `from` to `to` address with memo attached
608    pub fn transfer_from_with_memo(
609        &mut self,
610        msg_sender: Address,
611        call: ITIP20::transferFromWithMemoCall,
612    ) -> Result<bool> {
613        self._transfer_from(msg_sender, call.from, call.to, call.amount)?;
614
615        // Post-Moderato: call.from address in events, pre-Moderato uses msg_sender
616        let from = if self.storage.spec().is_moderato() {
617            call.from
618        } else {
619            msg_sender
620        };
621
622        self.emit_event(TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
623            from,
624            to: call.to,
625            amount: call.amount,
626            memo: call.memo,
627        }))?;
628
629        Ok(true)
630    }
631
632    /// Transfer from `from` to `to` address without approval requirement
633    /// This function is not exposed via the public interface and should only be invoked by precompiles
634    pub fn system_transfer_from(
635        &mut self,
636        from: Address,
637        to: Address,
638        amount: U256,
639    ) -> Result<bool> {
640        self.check_not_paused()?;
641        self.check_not_token_address(to)?;
642        self.ensure_transfer_authorized(from, to)?;
643
644        self._transfer(from, to, amount)?;
645
646        Ok(true)
647    }
648
649    fn _transfer_from(
650        &mut self,
651        msg_sender: Address,
652        from: Address,
653        to: Address,
654        amount: U256,
655    ) -> Result<bool> {
656        self.check_not_paused()?;
657        self.check_not_token_address(to)?;
658        self.ensure_transfer_authorized(from, to)?;
659
660        let allowed = self.get_allowance(from, msg_sender)?;
661        if amount > allowed {
662            return Err(TIP20Error::insufficient_allowance().into());
663        }
664
665        if allowed != U256::MAX {
666            let new_allowance = allowed
667                .checked_sub(amount)
668                .ok_or(TIP20Error::insufficient_allowance())?;
669            self.set_allowance(from, msg_sender, new_allowance)?;
670        }
671
672        self._transfer(from, to, amount)?;
673
674        Ok(true)
675    }
676
677    // TIP20 extension functions
678    pub fn transfer_with_memo(
679        &mut self,
680        msg_sender: Address,
681        call: ITIP20::transferWithMemoCall,
682    ) -> Result<()> {
683        self.check_not_paused()?;
684        self.check_not_token_address(call.to)?;
685        self.ensure_transfer_authorized(msg_sender, call.to)?;
686
687        self._transfer(msg_sender, call.to, call.amount)?;
688
689        self.emit_event(TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
690            from: msg_sender,
691            to: call.to,
692            amount: call.amount,
693            memo: call.memo,
694        }))
695    }
696}
697
698// Utility functions
699impl TIP20Token {
700    pub fn new(token_id: u64) -> Self {
701        let token_address = token_id_to_address(token_id);
702        Self::__new(token_address)
703    }
704
705    /// Create a TIP20Token from an address.
706    /// Returns an error if the address is not a valid TIP20 token (post-AllegroModerato).
707    pub fn from_address(address: Address) -> Result<Self> {
708        if StorageCtx.spec().is_allegro_moderato() && !is_tip20_prefix(address) {
709            return Err(TIP20Error::invalid_token().into());
710        }
711        let token_id = address_to_token_id_unchecked(address);
712        Ok(Self::new(token_id))
713    }
714
715    /// Only called internally from the factory, which won't try to re-initialize a token.
716    pub fn initialize(
717        &mut self,
718        name: &str,
719        symbol: &str,
720        currency: &str,
721        quote_token: Address,
722        admin: Address,
723        fee_recipient: Address,
724    ) -> Result<()> {
725        trace!(%name, address=%self.address, "Initializing token");
726
727        // must ensure the account is not empty, by setting some code
728        self.__initialize()?;
729
730        self.name.write(name.to_string())?;
731        self.symbol.write(symbol.to_string())?;
732        self.currency.write(currency.to_string())?;
733
734        // If the currency is USD, the quote token must also be USD.
735        // Skip this check in AllegroModerato+ when quote_token is Address::ZERO (first token case).
736        if currency == USD_CURRENCY {
737            let skip_check = self.storage.spec().is_allegro_moderato() && quote_token.is_zero();
738            if !skip_check {
739                let quote_token_currency = Self::from_address(quote_token)?.currency()?;
740                if quote_token_currency != USD_CURRENCY {
741                    return Err(TIP20Error::invalid_quote_token().into());
742                }
743            }
744        }
745
746        self.quote_token.write(quote_token)?;
747        // Initialize nextQuoteToken to the same value as quoteToken
748        self.next_quote_token.write(quote_token)?;
749
750        // Set default values
751        if self.storage.spec().is_moderato() {
752            self.supply_cap.write(U256::from(u128::MAX))?;
753        } else {
754            self.supply_cap.write(U256::MAX)?;
755        }
756        self.transfer_policy_id.write(1)?;
757
758        // Gate to avoid consensus-breaking gas usage
759        if self.storage.spec().is_allegretto() {
760            self.fee_recipient.write(fee_recipient)?;
761        }
762
763        // Initialize roles system and grant admin role
764        self.initialize_roles()?;
765        self.grant_default_admin(admin)
766    }
767
768    fn get_balance(&self, account: Address) -> Result<U256> {
769        self.balances.at(account).read()
770    }
771
772    fn set_balance(&mut self, account: Address, amount: U256) -> Result<()> {
773        self.balances.at(account).write(amount)
774    }
775
776    fn get_allowance(&self, owner: Address, spender: Address) -> Result<U256> {
777        self.allowances.at(owner).at(spender).read()
778    }
779
780    fn set_allowance(&mut self, owner: Address, spender: Address, amount: U256) -> Result<()> {
781        self.allowances.at(owner).at(spender).write(amount)
782    }
783
784    fn set_total_supply(&mut self, amount: U256) -> Result<()> {
785        self.total_supply.write(amount)
786    }
787
788    fn check_not_paused(&self) -> Result<()> {
789        if self.paused()? {
790            return Err(TIP20Error::contract_paused().into());
791        }
792        Ok(())
793    }
794
795    fn check_not_token_address(&self, to: Address) -> Result<()> {
796        // Don't allow sending to other precompiled tokens
797        if is_tip20_prefix(to) {
798            return Err(TIP20Error::invalid_recipient().into());
799        }
800        Ok(())
801    }
802
803    /// Checks if the transfer is authorized.
804    pub fn is_transfer_authorized(&self, from: Address, to: Address) -> Result<bool> {
805        let transfer_policy_id = self.transfer_policy_id()?;
806        let registry = TIP403Registry::new();
807
808        // Check if 'from' address is authorized
809        let from_authorized = registry.is_authorized(ITIP403Registry::isAuthorizedCall {
810            policyId: transfer_policy_id,
811            user: from,
812        })?;
813
814        // Check if 'to' address is authorized
815        let to_authorized = registry.is_authorized(ITIP403Registry::isAuthorizedCall {
816            policyId: transfer_policy_id,
817            user: to,
818        })?;
819
820        Ok(from_authorized && to_authorized)
821    }
822
823    /// Ensures the transfer is authorized.
824    pub fn ensure_transfer_authorized(&self, from: Address, to: Address) -> Result<()> {
825        if !self.is_transfer_authorized(from, to)? {
826            return Err(TIP20Error::policy_forbids().into());
827        }
828
829        Ok(())
830    }
831
832    fn _transfer(&mut self, from: Address, to: Address, amount: U256) -> Result<()> {
833        let from_balance = self.get_balance(from)?;
834        if amount > from_balance {
835            return Err(
836                TIP20Error::insufficient_balance(from_balance, amount, self.address).into(),
837            );
838        }
839
840        // Accrue before balance changes
841        let timestamp = self.storage.timestamp();
842        self.accrue(timestamp)?;
843
844        self.handle_rewards_on_transfer(from, to, amount)?;
845
846        // Adjust balances
847        let from_balance = self.get_balance(from)?;
848        let new_from_balance = from_balance
849            .checked_sub(amount)
850            .ok_or(TempoPrecompileError::under_overflow())?;
851
852        self.set_balance(from, new_from_balance)?;
853
854        if to != Address::ZERO {
855            let to_balance = self.get_balance(to)?;
856            let new_to_balance = to_balance
857                .checked_add(amount)
858                .ok_or(TempoPrecompileError::under_overflow())?;
859
860            self.set_balance(to, new_to_balance)?;
861        }
862
863        self.emit_event(TIP20Event::Transfer(ITIP20::Transfer { from, to, amount }))
864    }
865
866    /// Transfers fee tokens from user to fee manager before transaction execution
867    pub fn transfer_fee_pre_tx(&mut self, from: Address, amount: U256) -> Result<()> {
868        let from_balance = self.get_balance(from)?;
869        if amount > from_balance {
870            return Err(
871                TIP20Error::insufficient_balance(from_balance, amount, self.address).into(),
872            );
873        }
874
875        // Handle rewards (only after Moderato hardfork)
876        if self.storage.spec().is_moderato() {
877            // Accrue rewards up to current timestamp
878            let current_timestamp = self.storage.timestamp();
879            self.accrue(current_timestamp)?;
880
881            // Update rewards for the sender and get their reward recipient
882            let from_reward_recipient = self.update_rewards(from)?;
883
884            // If user is opted into rewards, decrease opted-in supply
885            if from_reward_recipient != Address::ZERO {
886                let opted_in_supply = U256::from(self.get_opted_in_supply()?)
887                    .checked_sub(amount)
888                    .ok_or(TempoPrecompileError::under_overflow())?;
889                self.set_opted_in_supply(
890                    opted_in_supply
891                        .try_into()
892                        .map_err(|_| TempoPrecompileError::under_overflow())?,
893                )?;
894            }
895        }
896
897        let new_from_balance =
898            from_balance
899                .checked_sub(amount)
900                .ok_or(TIP20Error::insufficient_balance(
901                    from_balance,
902                    amount,
903                    self.address,
904                ))?;
905
906        self.set_balance(from, new_from_balance)?;
907
908        let to_balance = self.get_balance(TIP_FEE_MANAGER_ADDRESS)?;
909        let new_to_balance = to_balance
910            .checked_add(amount)
911            .ok_or(TIP20Error::supply_cap_exceeded())?;
912        self.set_balance(TIP_FEE_MANAGER_ADDRESS, new_to_balance)?;
913
914        Ok(())
915    }
916
917    /// Refunds unused fee tokens to user and emits transfer event for gas amount used
918    pub fn transfer_fee_post_tx(
919        &mut self,
920        to: Address,
921        refund: U256,
922        actual_spending: U256,
923    ) -> Result<()> {
924        self.emit_event(TIP20Event::Transfer(ITIP20::Transfer {
925            from: to,
926            to: TIP_FEE_MANAGER_ADDRESS,
927            amount: actual_spending,
928        }))?;
929
930        // Exit early if there is no refund
931        if refund.is_zero() {
932            return Ok(());
933        }
934
935        // Handle rewards (only after Moderato hardfork)
936        if self.storage.spec().is_moderato() {
937            // Note: We assume that transferFeePreTx is always called first, so _accrue has already been called
938            // Update rewards for the recipient and get their reward recipient
939            let to_reward_recipient = self.update_rewards(to)?;
940
941            // If user is opted into rewards, increase opted-in supply by refund amount
942            if to_reward_recipient != Address::ZERO {
943                let opted_in_supply = U256::from(self.get_opted_in_supply()?)
944                    .checked_add(refund)
945                    .ok_or(TempoPrecompileError::under_overflow())?;
946                self.set_opted_in_supply(
947                    opted_in_supply
948                        .try_into()
949                        .map_err(|_| TempoPrecompileError::under_overflow())?,
950                )?;
951            }
952        }
953
954        let from_balance = self.get_balance(TIP_FEE_MANAGER_ADDRESS)?;
955        if refund > from_balance {
956            return Err(
957                TIP20Error::insufficient_balance(from_balance, refund, self.address).into(),
958            );
959        }
960
961        let new_from_balance =
962            from_balance
963                .checked_sub(refund)
964                .ok_or(TIP20Error::insufficient_balance(
965                    from_balance,
966                    refund,
967                    self.address,
968                ))?;
969
970        self.set_balance(TIP_FEE_MANAGER_ADDRESS, new_from_balance)?;
971
972        let to_balance = self.get_balance(to)?;
973        let new_to_balance = to_balance
974            .checked_add(refund)
975            .ok_or(TIP20Error::supply_cap_exceeded())?;
976        self.set_balance(to, new_to_balance)
977    }
978}
979
980#[cfg(test)]
981pub(crate) mod tests {
982    use alloy::primitives::{Address, FixedBytes, IntoLogData, U256};
983    use tempo_chainspec::hardfork::TempoHardfork;
984    use tempo_contracts::precompiles::{DEFAULT_FEE_TOKEN_POST_ALLEGRETTO, ITIP20Factory};
985
986    use super::*;
987    use crate::{
988        PATH_USD_ADDRESS, error::TempoPrecompileError, storage::hashmap::HashMapStorageProvider,
989        test_util::setup_storage,
990    };
991    use rand::{Rng, distributions::Alphanumeric, random, thread_rng};
992
993    /// Initialize PathUSD token. For AllegroModerato+, uses the factory flow.
994    /// For older specs, initializes directly.
995    pub(crate) fn initialize_path_usd(admin: Address) -> Result<()> {
996        if !StorageCtx.spec().is_allegretto() {
997            let mut path_usd = TIP20Token::from_address(PATH_USD_ADDRESS)?;
998            path_usd.initialize(
999                "PathUSD",
1000                "PUSD",
1001                "USD",
1002                Address::ZERO,
1003                admin,
1004                Address::ZERO,
1005            )
1006        } else {
1007            let mut factory = TIP20Factory::new();
1008            factory.initialize()?;
1009            deploy_path_usd(&mut factory, admin)?;
1010
1011            Ok(())
1012        }
1013    }
1014
1015    /// Deploy PathUSD via the factory. Requires AllegroModerato+ spec and no tokens deployed yet.
1016    pub(crate) fn deploy_path_usd(factory: &mut TIP20Factory, admin: Address) -> Result<Address> {
1017        let token_id = factory.token_id_counter()?;
1018
1019        if !token_id.is_zero() {
1020            return Err(TempoPrecompileError::Fatal(
1021                "PathUSD is not the first deployed token".to_string(),
1022            ));
1023        }
1024
1025        factory.create_token(
1026            admin,
1027            ITIP20Factory::createTokenCall {
1028                name: "PathUSD".to_string(),
1029                symbol: "PUSD".to_string(),
1030                currency: "USD".to_string(),
1031                quoteToken: Address::ZERO,
1032                admin,
1033            },
1034        )
1035    }
1036
1037    /// Helper to setup a token with rewards for testing fee transfer functions
1038    /// Returns (token_id, initial_opted_in_supply)
1039    fn setup_token_with_rewards(
1040        admin: Address,
1041        user: Address,
1042        mint_amount: U256,
1043        reward_amount: U256,
1044    ) -> Result<(u64, u128)> {
1045        initialize_path_usd(admin)?;
1046        let token_id = setup_factory_with_token(admin, "Test", "TST")?;
1047
1048        let mut token = TIP20Token::new(token_id);
1049        token.grant_role_internal(admin, *ISSUER_ROLE)?;
1050
1051        // Mint tokens to admin (for reward stream)
1052        token.mint(
1053            admin,
1054            ITIP20::mintCall {
1055                to: admin,
1056                amount: reward_amount,
1057            },
1058        )?;
1059
1060        // Mint tokens to user
1061        token.mint(
1062            admin,
1063            ITIP20::mintCall {
1064                to: user,
1065                amount: mint_amount,
1066            },
1067        )?;
1068
1069        // User opts into rewards
1070        token.set_reward_recipient(user, ITIP20::setRewardRecipientCall { recipient: user })?;
1071
1072        // Verify initial opted-in supply
1073        let initial_opted_in = token.get_opted_in_supply()?;
1074        assert_eq!(initial_opted_in, mint_amount.to::<u128>());
1075
1076        // Start a reward stream
1077        token.start_reward(
1078            admin,
1079            ITIP20::startRewardCall {
1080                amount: reward_amount,
1081                secs: 100,
1082            },
1083        )?;
1084
1085        // Advance time to accrue rewards
1086        let initial_time = StorageCtx.timestamp();
1087        StorageCtx.set_timestamp(initial_time + U256::from(50));
1088
1089        Ok((token_id, initial_opted_in))
1090    }
1091
1092    /// Initialize a factory and create a single token
1093    fn setup_factory_with_token(admin: Address, name: &str, symbol: &str) -> Result<u64> {
1094        initialize_path_usd(admin)?;
1095
1096        let mut factory = TIP20Factory::new();
1097        let token_address = factory.create_token(
1098            admin,
1099            ITIP20Factory::createTokenCall {
1100                name: name.to_string(),
1101                symbol: symbol.to_string(),
1102                currency: "USD".to_string(),
1103                quoteToken: PATH_USD_ADDRESS,
1104                admin,
1105            },
1106        )?;
1107
1108        Ok(address_to_token_id_unchecked(token_address))
1109    }
1110
1111    /// Create a token via an already-initialized factory
1112    fn create_token_via_factory(
1113        factory: &mut TIP20Factory,
1114        admin: Address,
1115        name: &str,
1116        symbol: &str,
1117        quote_token: Address,
1118    ) -> Result<u64> {
1119        let token_address = factory.create_token(
1120            admin,
1121            ITIP20Factory::createTokenCall {
1122                name: name.to_string(),
1123                symbol: symbol.to_string(),
1124                currency: "USD".to_string(),
1125                quoteToken: quote_token,
1126                admin,
1127            },
1128        )?;
1129
1130        Ok(address_to_token_id_unchecked(token_address))
1131    }
1132
1133    /// Setup factory and create a token with a separate quote token (both linking to LINKING_USD)
1134    fn setup_token_with_custom_quote_token(admin: Address) -> Result<(u64, u64)> {
1135        initialize_path_usd(admin)?;
1136        let mut factory = TIP20Factory::new();
1137        factory.initialize()?;
1138
1139        let token_id =
1140            create_token_via_factory(&mut factory, admin, "Test", "TST", PATH_USD_ADDRESS)?;
1141        let quote_token_id =
1142            create_token_via_factory(&mut factory, admin, "Quote", "QUOTE", PATH_USD_ADDRESS)?;
1143
1144        Ok((token_id, quote_token_id))
1145    }
1146
1147    #[test]
1148    fn test_mint_increases_balance_and_supply() -> eyre::Result<()> {
1149        let (mut storage, admin) = setup_storage();
1150        let addr = Address::random();
1151        let token_id = 1;
1152
1153        StorageCtx::enter(&mut storage, || {
1154            initialize_path_usd(admin)?;
1155            let mut token = TIP20Token::new(token_id);
1156            // Initialize with admin
1157            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1158
1159            // Grant issuer role to admin
1160            token.grant_role_internal(admin, *ISSUER_ROLE)?;
1161
1162            let amount = U256::random().min(U256::from(u128::MAX)) % token.supply_cap()?;
1163            token.mint(admin, ITIP20::mintCall { to: addr, amount })?;
1164
1165            assert_eq!(token.get_balance(addr)?, amount);
1166            assert_eq!(token.total_supply()?, amount);
1167
1168            token.assert_emitted_events(vec![
1169                TIP20Event::Transfer(ITIP20::Transfer {
1170                    from: Address::ZERO,
1171                    to: addr,
1172                    amount,
1173                }),
1174                TIP20Event::Mint(ITIP20::Mint { to: addr, amount }),
1175            ]);
1176
1177            Ok(())
1178        })
1179    }
1180
1181    #[test]
1182    fn test_transfer_moves_balance() -> eyre::Result<()> {
1183        let (mut storage, admin) = setup_storage();
1184        let from = Address::random();
1185        let to = Address::random();
1186        let token_id = 1;
1187
1188        StorageCtx::enter(&mut storage, || {
1189            initialize_path_usd(admin)?;
1190            let mut token = TIP20Token::new(token_id);
1191            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1192            token.grant_role_internal(admin, *ISSUER_ROLE)?;
1193
1194            let amount = U256::random().min(U256::from(u128::MAX)) % token.supply_cap()?;
1195            token.mint(admin, ITIP20::mintCall { to: from, amount })?;
1196            token.transfer(from, ITIP20::transferCall { to, amount })?;
1197
1198            assert_eq!(token.get_balance(from)?, U256::ZERO);
1199            assert_eq!(token.get_balance(to)?, amount);
1200            assert_eq!(token.total_supply()?, amount); // Supply unchanged
1201
1202            token.assert_emitted_events(vec![
1203                TIP20Event::Transfer(ITIP20::Transfer {
1204                    from: Address::ZERO,
1205                    to: from,
1206                    amount,
1207                }),
1208                TIP20Event::Mint(ITIP20::Mint { to: from, amount }),
1209                TIP20Event::Transfer(ITIP20::Transfer { from, to, amount }),
1210            ]);
1211
1212            Ok(())
1213        })
1214    }
1215
1216    #[test]
1217    fn test_transfer_insufficient_balance_fails() -> eyre::Result<()> {
1218        let (mut storage, admin) = setup_storage();
1219        let from = Address::random();
1220        let to = Address::random();
1221
1222        StorageCtx::enter(&mut storage, || {
1223            initialize_path_usd(admin)?;
1224            let mut token = TIP20Token::new(1);
1225            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1226
1227            let amount =
1228                U256::random().min(U256::from(u128::MAX)) % token.supply_cap()? + U256::ONE;
1229            let result = token.transfer(from, ITIP20::transferCall { to, amount });
1230            assert!(matches!(
1231                result,
1232                Err(TempoPrecompileError::TIP20(
1233                    TIP20Error::InsufficientBalance(_)
1234                ))
1235            ));
1236
1237            Ok(())
1238        })
1239    }
1240
1241    #[test]
1242    fn test_mint_with_memo_post_moderato() -> eyre::Result<()> {
1243        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
1244        let admin = Address::random();
1245        let token_id = 1;
1246
1247        StorageCtx::enter(&mut storage, || {
1248            initialize_path_usd(admin)?;
1249            let mut token = TIP20Token::new(token_id);
1250            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1251            token.grant_role_internal(admin, *ISSUER_ROLE)?;
1252
1253            let to = Address::random();
1254            let amount = U256::random() % token.supply_cap()?;
1255            let memo = FixedBytes::random();
1256
1257            token.mint_with_memo(admin, ITIP20::mintWithMemoCall { to, amount, memo })?;
1258
1259            // TransferWithMemo event should have Address::ZERO as from for post-Moderato
1260            assert_eq!(
1261                token.emitted_events()[2],
1262                TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
1263                    from: Address::ZERO,
1264                    to,
1265                    amount,
1266                    memo
1267                })
1268                .into_log_data()
1269            );
1270
1271            Ok(())
1272        })
1273    }
1274
1275    #[test]
1276    fn test_mint_with_memo_pre_moderato() -> eyre::Result<()> {
1277        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1278        let admin = Address::random();
1279        let token_id = 1;
1280
1281        StorageCtx::enter(&mut storage, || {
1282            initialize_path_usd(admin)?;
1283            let mut token = TIP20Token::new(token_id);
1284            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1285            token.grant_role_internal(admin, *ISSUER_ROLE)?;
1286
1287            let to = Address::random();
1288            let amount = U256::random();
1289            let memo = FixedBytes::random();
1290
1291            token.mint_with_memo(admin, ITIP20::mintWithMemoCall { to, amount, memo })?;
1292
1293            // TransferWithMemo event should have msg_sender as from for pre-Moderato
1294            assert_eq!(
1295                token.emitted_events()[2],
1296                TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
1297                    from: admin,
1298                    to,
1299                    amount,
1300                    memo
1301                })
1302                .into_log_data()
1303            );
1304
1305            Ok(())
1306        })
1307    }
1308
1309    #[test]
1310    fn test_burn_with_memo() -> eyre::Result<()> {
1311        let mut storage = HashMapStorageProvider::new(1);
1312        let admin = Address::random();
1313        let token_id = 1;
1314
1315        StorageCtx::enter(&mut storage, || {
1316            initialize_path_usd(admin)?;
1317            let mut token = TIP20Token::new(token_id);
1318            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1319            token.grant_role_internal(admin, *ISSUER_ROLE)?;
1320
1321            let amount = U256::from(random::<u128>());
1322            let memo = FixedBytes::random();
1323
1324            token.mint(admin, ITIP20::mintCall { to: admin, amount })?;
1325            token.burn_with_memo(admin, ITIP20::burnWithMemoCall { amount, memo })?;
1326
1327            assert_eq!(
1328                token.emitted_events()[2],
1329                TIP20Event::Transfer(ITIP20::Transfer {
1330                    from: admin,
1331                    to: Address::ZERO,
1332                    amount
1333                })
1334                .into_log_data()
1335            );
1336
1337            assert_eq!(
1338                token.emitted_events()[3],
1339                TIP20Event::Burn(ITIP20::Burn {
1340                    from: admin,
1341                    amount
1342                })
1343                .into_log_data()
1344            );
1345
1346            assert_eq!(
1347                token.emitted_events()[4],
1348                TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
1349                    from: admin,
1350                    to: Address::ZERO,
1351                    amount,
1352                    memo
1353                })
1354                .into_log_data()
1355            );
1356
1357            Ok(())
1358        })
1359    }
1360
1361    #[test]
1362    fn test_transfer_from_with_memo_pre_moderato() -> eyre::Result<()> {
1363        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1364        let admin = Address::random();
1365        let token_id = 1;
1366
1367        StorageCtx::enter(&mut storage, || {
1368            initialize_path_usd(admin)?;
1369            let mut token = TIP20Token::new(token_id);
1370            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1371
1372            token.grant_role_internal(admin, *ISSUER_ROLE)?;
1373
1374            let owner = Address::random();
1375            let spender = Address::random();
1376            let to = Address::random();
1377            let amount = U256::random();
1378            let memo = FixedBytes::random();
1379
1380            token.mint(admin, ITIP20::mintCall { to: owner, amount })?;
1381            token.approve(owner, ITIP20::approveCall { spender, amount })?;
1382            assert!(token.transfer_from_with_memo(
1383                spender,
1384                ITIP20::transferFromWithMemoCall {
1385                    from: owner,
1386                    to,
1387                    amount,
1388                    memo,
1389                },
1390            )?);
1391
1392            assert_eq!(
1393                token.emitted_events()[3],
1394                TIP20Event::Transfer(ITIP20::Transfer {
1395                    from: owner,
1396                    to,
1397                    amount
1398                })
1399                .into_log_data()
1400            );
1401
1402            assert_eq!(
1403                token.emitted_events()[4],
1404                TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
1405                    from: spender,
1406                    to,
1407                    amount,
1408                    memo
1409                })
1410                .into_log_data()
1411            );
1412
1413            Ok(())
1414        })
1415    }
1416
1417    #[test]
1418    fn test_transfer_from_with_memo_from_address_post_moderato() -> eyre::Result<()> {
1419        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
1420        let admin = Address::random();
1421        let token_id = 1;
1422
1423        StorageCtx::enter(&mut storage, || {
1424            initialize_path_usd(admin)?;
1425            let mut token = TIP20Token::new(token_id);
1426            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1427
1428            token.grant_role_internal(admin, *ISSUER_ROLE)?;
1429
1430            let owner = Address::random();
1431            let spender = Address::random();
1432            let to = Address::random();
1433            let amount = U256::random() % token.supply_cap()?;
1434            let memo = FixedBytes::random();
1435
1436            token.mint(admin, ITIP20::mintCall { to: owner, amount })?;
1437            token.approve(owner, ITIP20::approveCall { spender, amount })?;
1438            token.transfer_from_with_memo(
1439                spender,
1440                ITIP20::transferFromWithMemoCall {
1441                    from: owner,
1442                    to,
1443                    amount,
1444                    memo,
1445                },
1446            )?;
1447
1448            // TransferWithMemo event should have use call.from in transfer event
1449            assert_eq!(
1450                token.emitted_events()[4],
1451                TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
1452                    from: owner,
1453                    to,
1454                    amount,
1455                    memo
1456                })
1457                .into_log_data()
1458            );
1459
1460            Ok(())
1461        })
1462    }
1463
1464    #[test]
1465    fn test_transfer_from_with_memo_from_address_pre_moderato() -> eyre::Result<()> {
1466        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1467        let admin = Address::random();
1468        let token_id = 1;
1469
1470        StorageCtx::enter(&mut storage, || {
1471            initialize_path_usd(admin)?;
1472            let mut token = TIP20Token::new(token_id);
1473            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1474
1475            token.grant_role_internal(admin, *ISSUER_ROLE)?;
1476
1477            let owner = Address::random();
1478            let spender = Address::random();
1479            let to = Address::random();
1480            let amount = U256::random();
1481            let memo = FixedBytes::random();
1482
1483            token.mint(admin, ITIP20::mintCall { to: owner, amount })?;
1484            token.approve(owner, ITIP20::approveCall { spender, amount })?;
1485            token.transfer_from_with_memo(
1486                spender,
1487                ITIP20::transferFromWithMemoCall {
1488                    from: owner,
1489                    to,
1490                    amount,
1491                    memo,
1492                },
1493            )?;
1494
1495            // TransferWithMemo event should user msg_sender in transfer event
1496            assert_eq!(
1497                token.emitted_events()[4],
1498                TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
1499                    from: spender,
1500                    to,
1501                    amount,
1502                    memo
1503                })
1504                .into_log_data()
1505            );
1506
1507            Ok(())
1508        })
1509    }
1510
1511    #[test]
1512    fn test_transfer_fee_pre_tx() -> eyre::Result<()> {
1513        let mut storage = HashMapStorageProvider::new(1);
1514        let admin = Address::random();
1515        let user = Address::random();
1516        let token_id = 1;
1517
1518        StorageCtx::enter(&mut storage, || {
1519            initialize_path_usd(admin)?;
1520            let mut token = TIP20Token::new(token_id);
1521            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1522
1523            token.grant_role_internal(admin, *ISSUER_ROLE)?;
1524
1525            let amount = U256::from(100);
1526            token.mint(admin, ITIP20::mintCall { to: user, amount })?;
1527
1528            let fee_amount = U256::from(50);
1529            token
1530                .transfer_fee_pre_tx(user, fee_amount)
1531                .expect("transfer failed");
1532
1533            assert_eq!(token.get_balance(user)?, U256::from(50));
1534            assert_eq!(token.get_balance(TIP_FEE_MANAGER_ADDRESS)?, fee_amount);
1535
1536            Ok(())
1537        })
1538    }
1539
1540    #[test]
1541    fn test_transfer_fee_pre_tx_insufficient_balance() -> eyre::Result<()> {
1542        let mut storage = HashMapStorageProvider::new(1);
1543        let admin = Address::random();
1544        let user = Address::random();
1545        let token_id = 1;
1546
1547        StorageCtx::enter(&mut storage, || {
1548            initialize_path_usd(admin)?;
1549            let mut token = TIP20Token::new(token_id);
1550            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1551
1552            let fee_amount = U256::from(50);
1553            assert_eq!(
1554                token.transfer_fee_pre_tx(user, fee_amount),
1555                Err(TempoPrecompileError::TIP20(
1556                    TIP20Error::insufficient_balance(U256::ZERO, fee_amount, token.address)
1557                ))
1558            );
1559            Ok(())
1560        })
1561    }
1562
1563    #[test]
1564    fn test_transfer_fee_post_tx() -> eyre::Result<()> {
1565        let mut storage = HashMapStorageProvider::new(1);
1566        let admin = Address::random();
1567        let user = Address::random();
1568        let token_id = 1;
1569
1570        StorageCtx::enter(&mut storage, || {
1571            initialize_path_usd(admin)?;
1572            let mut token = TIP20Token::new(token_id);
1573            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1574
1575            let initial_fee = U256::from(100);
1576            token.set_balance(TIP_FEE_MANAGER_ADDRESS, initial_fee)?;
1577
1578            let refund_amount = U256::from(30);
1579            let gas_used = U256::from(10);
1580            token.transfer_fee_post_tx(user, refund_amount, gas_used)?;
1581
1582            assert_eq!(token.get_balance(user)?, refund_amount);
1583            assert_eq!(token.get_balance(TIP_FEE_MANAGER_ADDRESS)?, U256::from(70));
1584
1585            assert_eq!(
1586                token.emitted_events().last().unwrap(),
1587                &TIP20Event::Transfer(ITIP20::Transfer {
1588                    from: user,
1589                    to: TIP_FEE_MANAGER_ADDRESS,
1590                    amount: gas_used
1591                })
1592                .into_log_data()
1593            );
1594
1595            Ok(())
1596        })
1597    }
1598
1599    #[test]
1600    fn test_transfer_from_insufficient_allowance() -> eyre::Result<()> {
1601        let mut storage = HashMapStorageProvider::new(1);
1602        let admin = Address::random();
1603        let from = Address::random();
1604        let spender = Address::random();
1605        let to = Address::random();
1606        let amount = U256::from(100);
1607        let token_id = 1;
1608
1609        StorageCtx::enter(&mut storage, || {
1610            initialize_path_usd(admin)?;
1611            let mut token = TIP20Token::new(token_id);
1612            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1613            token.grant_role_internal(admin, *ISSUER_ROLE)?;
1614            token.mint(admin, ITIP20::mintCall { to: from, amount })?;
1615
1616            assert!(matches!(
1617                token.transfer_from(spender, ITIP20::transferFromCall { from, to, amount }),
1618                Err(TempoPrecompileError::TIP20(
1619                    TIP20Error::InsufficientAllowance(_)
1620                ))
1621            ));
1622
1623            Ok(())
1624        })
1625    }
1626
1627    #[test]
1628    fn test_system_transfer_from() -> eyre::Result<()> {
1629        let mut storage = HashMapStorageProvider::new(1);
1630        let admin = Address::random();
1631        let from = Address::random();
1632        let to = Address::random();
1633        let amount = U256::from(100);
1634        let token_id = 1;
1635
1636        StorageCtx::enter(&mut storage, || {
1637            initialize_path_usd(admin)?;
1638            let mut token = TIP20Token::new(token_id);
1639            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1640
1641            token.grant_role_internal(admin, *ISSUER_ROLE)?;
1642
1643            token.mint(admin, ITIP20::mintCall { to: from, amount })?;
1644
1645            assert!(token.system_transfer_from(from, to, amount).is_ok());
1646            assert_eq!(
1647                token.emitted_events().last().unwrap(),
1648                &TIP20Event::Transfer(ITIP20::Transfer { from, to, amount }).into_log_data()
1649            );
1650
1651            Ok(())
1652        })
1653    }
1654
1655    #[test]
1656    fn test_initialize_sets_next_quote_token() -> eyre::Result<()> {
1657        let mut storage = HashMapStorageProvider::new(1);
1658        let admin = Address::random();
1659
1660        StorageCtx::enter(&mut storage, || {
1661            let token_id = setup_factory_with_token(admin, "Test", "TST")?;
1662            let token = TIP20Token::new(token_id);
1663
1664            // Verify both quoteToken and nextQuoteToken are set to the same value
1665            assert_eq!(token.quote_token()?, PATH_USD_ADDRESS);
1666            assert_eq!(token.next_quote_token()?, PATH_USD_ADDRESS);
1667
1668            Ok(())
1669        })
1670    }
1671
1672    #[test]
1673    fn test_update_quote_token() -> eyre::Result<()> {
1674        let mut storage = HashMapStorageProvider::new(1);
1675        let admin = Address::random();
1676
1677        StorageCtx::enter(&mut storage, || {
1678            let (token_id, quote_token_id) = setup_token_with_custom_quote_token(admin)?;
1679            let quote_token_address = token_id_to_address(quote_token_id);
1680
1681            let mut token = TIP20Token::new(token_id);
1682
1683            // Set next quote token
1684            token.set_next_quote_token(
1685                admin,
1686                ITIP20::setNextQuoteTokenCall {
1687                    newQuoteToken: quote_token_address,
1688                },
1689            )?;
1690
1691            // Verify next quote token was set
1692            assert_eq!(token.next_quote_token()?, quote_token_address);
1693
1694            // Verify event was emitted
1695            assert_eq!(
1696                token.emitted_events().last().unwrap(),
1697                &TIP20Event::NextQuoteTokenSet(ITIP20::NextQuoteTokenSet {
1698                    updater: admin,
1699                    nextQuoteToken: quote_token_address,
1700                })
1701                .into_log_data()
1702            );
1703
1704            Ok(())
1705        })
1706    }
1707
1708    #[test]
1709    fn test_update_quote_token_requires_admin() -> eyre::Result<()> {
1710        let mut storage = HashMapStorageProvider::new(1);
1711        let admin = Address::random();
1712        let non_admin = Address::random();
1713        let token_id = 1;
1714
1715        StorageCtx::enter(&mut storage, || {
1716            initialize_path_usd(admin)?;
1717            let mut token = TIP20Token::new(token_id);
1718            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1719
1720            let quote_token_address = token_id_to_address(2);
1721
1722            // Try to set next quote token as non-admin
1723            let result = token.set_next_quote_token(
1724                non_admin,
1725                ITIP20::setNextQuoteTokenCall {
1726                    newQuoteToken: quote_token_address,
1727                },
1728            );
1729
1730            assert!(matches!(
1731                result,
1732                Err(TempoPrecompileError::RolesAuthError(
1733                    RolesAuthError::Unauthorized(_)
1734                ))
1735            ));
1736
1737            Ok(())
1738        })
1739    }
1740
1741    #[test]
1742    fn test_update_quote_token_rejects_non_tip20() -> eyre::Result<()> {
1743        let mut storage = HashMapStorageProvider::new(1);
1744        let admin = Address::random();
1745
1746        StorageCtx::enter(&mut storage, || {
1747            let token_id = setup_factory_with_token(admin, "Test", "TST")?;
1748            let mut token = TIP20Token::new(token_id);
1749
1750            // Try to set a non-TIP20 address (random address that doesn't match TIP20 pattern)
1751            let non_tip20_address = Address::random();
1752            let result = token.set_next_quote_token(
1753                admin,
1754                ITIP20::setNextQuoteTokenCall {
1755                    newQuoteToken: non_tip20_address,
1756                },
1757            );
1758
1759            assert!(matches!(
1760                result,
1761                Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
1762                    _
1763                )))
1764            ));
1765
1766            Ok(())
1767        })
1768    }
1769
1770    #[test]
1771    fn test_update_quote_token_rejects_undeployed_token() -> eyre::Result<()> {
1772        let mut storage = HashMapStorageProvider::new(1);
1773        let admin = Address::random();
1774
1775        StorageCtx::enter(&mut storage, || {
1776            let token_id = setup_factory_with_token(admin, "Test", "TST")?;
1777            let mut token = TIP20Token::new(token_id);
1778
1779            // Try to set a TIP20 address that hasn't been deployed yet (token_id = 999)
1780            // This has the correct TIP20 address pattern but hasn't been created
1781            let undeployed_token_address = token_id_to_address(999);
1782            let result = token.set_next_quote_token(
1783                admin,
1784                ITIP20::setNextQuoteTokenCall {
1785                    newQuoteToken: undeployed_token_address,
1786                },
1787            );
1788
1789            assert!(matches!(
1790                result,
1791                Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
1792                    _
1793                )))
1794            ));
1795
1796            Ok(())
1797        })
1798    }
1799
1800    #[test]
1801    fn test_finalize_quote_token_update() -> eyre::Result<()> {
1802        let mut storage = HashMapStorageProvider::new(1);
1803        let admin = Address::random();
1804
1805        StorageCtx::enter(&mut storage, || {
1806            let (token_id, quote_token_id) = setup_token_with_custom_quote_token(admin)?;
1807            let quote_token_address = token_id_to_address(quote_token_id);
1808
1809            let mut token = TIP20Token::new(token_id);
1810
1811            // Set next quote token
1812            token.set_next_quote_token(
1813                admin,
1814                ITIP20::setNextQuoteTokenCall {
1815                    newQuoteToken: quote_token_address,
1816                },
1817            )?;
1818
1819            // Complete the update
1820            token.complete_quote_token_update(admin, ITIP20::completeQuoteTokenUpdateCall {})?;
1821
1822            // Verify quote token was updated
1823            assert_eq!(token.quote_token()?, quote_token_address);
1824
1825            // Verify event was emitted
1826            assert_eq!(
1827                token.emitted_events().last().unwrap(),
1828                &TIP20Event::QuoteTokenUpdate(ITIP20::QuoteTokenUpdate {
1829                    updater: admin,
1830                    newQuoteToken: quote_token_address,
1831                })
1832                .into_log_data()
1833            );
1834
1835            Ok(())
1836        })
1837    }
1838
1839    #[test]
1840    fn test_finalize_quote_token_update_detects_loop() -> eyre::Result<()> {
1841        let mut storage = HashMapStorageProvider::new(1);
1842        let admin = Address::random();
1843
1844        StorageCtx::enter(&mut storage, || {
1845            initialize_path_usd(admin)?;
1846            let mut factory = TIP20Factory::new();
1847
1848            // Create token_b first (links to LINKING_USD)
1849            let token_b_id =
1850                create_token_via_factory(&mut factory, admin, "Token B", "TKB", PATH_USD_ADDRESS)?;
1851            let token_b_address = token_id_to_address(token_b_id);
1852
1853            // Create token_a (links to token_b)
1854            let token_a_id =
1855                create_token_via_factory(&mut factory, admin, "Token A", "TKA", token_b_address)?;
1856            let token_a_address = token_id_to_address(token_a_id);
1857
1858            // Now try to set token_a as the next quote token for token_b (would create A -> B -> A loop)
1859            let mut token_b = TIP20Token::new(token_b_id);
1860            token_b.set_next_quote_token(
1861                admin,
1862                ITIP20::setNextQuoteTokenCall {
1863                    newQuoteToken: token_a_address,
1864                },
1865            )?;
1866
1867            // Try to complete the update - should fail due to loop detection
1868            let result =
1869                token_b.complete_quote_token_update(admin, ITIP20::completeQuoteTokenUpdateCall {});
1870
1871            assert!(matches!(
1872                result,
1873                Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
1874                    _
1875                )))
1876            ));
1877
1878            Ok(())
1879        })
1880    }
1881
1882    #[test]
1883    fn test_finalize_quote_token_update_requires_admin() -> eyre::Result<()> {
1884        let mut storage = HashMapStorageProvider::new(1);
1885        let admin = Address::random();
1886        let non_admin = Address::random();
1887
1888        StorageCtx::enter(&mut storage, || {
1889            let (token_id, quote_token_id) = setup_token_with_custom_quote_token(admin)?;
1890            let quote_token_address = token_id_to_address(quote_token_id);
1891
1892            let mut token = TIP20Token::new(token_id);
1893
1894            // Set next quote token as admin
1895            token.set_next_quote_token(
1896                admin,
1897                ITIP20::setNextQuoteTokenCall {
1898                    newQuoteToken: quote_token_address,
1899                },
1900            )?;
1901
1902            // Try to complete update as non-admin
1903            let result = token
1904                .complete_quote_token_update(non_admin, ITIP20::completeQuoteTokenUpdateCall {});
1905
1906            assert!(matches!(
1907                result,
1908                Err(TempoPrecompileError::RolesAuthError(
1909                    RolesAuthError::Unauthorized(_)
1910                ))
1911            ));
1912
1913            Ok(())
1914        })
1915    }
1916
1917    #[test]
1918    fn test_tip20_token_prefix() {
1919        assert_eq!(
1920            TIP20_TOKEN_PREFIX,
1921            [
1922                0x20, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
1923            ]
1924        );
1925        assert_eq!(
1926            &DEFAULT_FEE_TOKEN_POST_ALLEGRETTO.as_slice()[..12],
1927            &TIP20_TOKEN_PREFIX
1928        );
1929    }
1930
1931    #[test]
1932    fn test_arbitrary_currency() -> eyre::Result<()> {
1933        let mut storage = HashMapStorageProvider::new(1);
1934        let admin = Address::random();
1935
1936        StorageCtx::enter(&mut storage, || {
1937            for _ in 0..50 {
1938                let mut token = TIP20Token::new(1);
1939
1940                let currency: String = thread_rng()
1941                    .sample_iter(&Alphanumeric)
1942                    .take(31)
1943                    .map(char::from)
1944                    .collect();
1945
1946                // Initialize token with the random currency
1947                token.initialize(
1948                    "Test",
1949                    "TST",
1950                    &currency,
1951                    PATH_USD_ADDRESS,
1952                    admin,
1953                    Address::ZERO,
1954                )?;
1955
1956                // Verify the currency was stored and can be retrieved correctly
1957                let stored_currency = token.currency()?;
1958                assert_eq!(stored_currency, currency,);
1959            }
1960
1961            Ok(())
1962        })
1963    }
1964
1965    #[test]
1966    #[ignore = "NOTE(rusowsky): this doesn't panic anymore, as storage primitives can handle long strings now"]
1967    fn test_invalid_currency() -> eyre::Result<()> {
1968        let mut storage = HashMapStorageProvider::new(1);
1969        let admin = Address::random();
1970
1971        StorageCtx::enter(&mut storage, || {
1972            for _ in 0..10 {
1973                let mut token = TIP20Token::new(1);
1974
1975                let currency: String = thread_rng()
1976                    .sample_iter(&Alphanumeric)
1977                    .take(32)
1978                    .map(char::from)
1979                    .collect();
1980
1981                let result = token.initialize(
1982                    "Test",
1983                    "TST",
1984                    &currency,
1985                    PATH_USD_ADDRESS,
1986                    admin,
1987                    Address::ZERO,
1988                );
1989                assert!(matches!(
1990                    result,
1991                    Err(TempoPrecompileError::TIP20(TIP20Error::StringTooLong(_)))
1992                ),);
1993            }
1994
1995            Ok(())
1996        })
1997    }
1998
1999    #[test]
2000    fn test_from_address() -> eyre::Result<()> {
2001        let mut storage = HashMapStorageProvider::new(1);
2002        let admin = Address::random();
2003
2004        StorageCtx::enter(&mut storage, || {
2005            // Create a token to get a valid address
2006            let token_id = setup_factory_with_token(admin, "TEST", "TST")?;
2007            let token_address = token_id_to_address(token_id);
2008
2009            // Test from_address creates same instance as new()
2010            let token = TIP20Token::new(token_id);
2011            let addr_via_new = token.address;
2012
2013            let token = TIP20Token::from_address(token_address)?;
2014            let addr_via_from_address = token.address;
2015
2016            assert_eq!(
2017                addr_via_new, addr_via_from_address,
2018                "Both methods should create token with same address"
2019            );
2020            assert_eq!(
2021                addr_via_from_address, token_address,
2022                "from_address should use the provided address"
2023            );
2024
2025            Ok(())
2026        })
2027    }
2028
2029    #[test]
2030    fn test_new_invalid_quote_token() -> eyre::Result<()> {
2031        let mut storage = HashMapStorageProvider::new(1);
2032        let admin = Address::random();
2033
2034        StorageCtx::enter(&mut storage, || {
2035            let currency: String = thread_rng()
2036                .sample_iter(&Alphanumeric)
2037                .take(31)
2038                .map(char::from)
2039                .collect();
2040
2041            let mut token = TIP20Token::new(1);
2042            token.initialize(
2043                "Token",
2044                "T",
2045                &currency,
2046                PATH_USD_ADDRESS,
2047                admin,
2048                Address::ZERO,
2049            )?;
2050
2051            // Try to create a new USD token with the arbitrary token as the quote token, this should fail
2052            let token_address = token.address;
2053            let mut usd_token = TIP20Token::new(2);
2054            let result = usd_token.initialize(
2055                "USD Token",
2056                "USDT",
2057                USD_CURRENCY,
2058                token_address,
2059                admin,
2060                Address::ZERO,
2061            );
2062
2063            assert!(matches!(
2064                result,
2065                Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
2066                    _
2067                )))
2068            ));
2069
2070            Ok(())
2071        })
2072    }
2073
2074    #[test]
2075    fn test_new_valid_quote_token() -> eyre::Result<()> {
2076        let mut storage = HashMapStorageProvider::new(1);
2077        let admin = Address::random();
2078
2079        StorageCtx::enter(&mut storage, || {
2080            initialize_path_usd(admin)?;
2081            let mut usd_token1 = TIP20Token::new(1);
2082            usd_token1.initialize(
2083                "USD Token",
2084                "USDT",
2085                USD_CURRENCY,
2086                PATH_USD_ADDRESS,
2087                admin,
2088                Address::ZERO,
2089            )?;
2090
2091            // USD token with USD token as quote
2092            let usd_token1_address = token_id_to_address(1);
2093            let mut usd_token2 = TIP20Token::new(2);
2094            let result = usd_token2.initialize(
2095                "USD Token 2",
2096                "USD2",
2097                USD_CURRENCY,
2098                usd_token1_address,
2099                admin,
2100                Address::ZERO,
2101            );
2102            assert!(result.is_ok());
2103
2104            // Create non USD token
2105            let currency_1: String = thread_rng()
2106                .sample_iter(&Alphanumeric)
2107                .take(31)
2108                .map(char::from)
2109                .collect();
2110
2111            let mut token_1 = TIP20Token::new(3);
2112            token_1.initialize(
2113                "Token 1",
2114                "TK1",
2115                &currency_1,
2116                PATH_USD_ADDRESS,
2117                admin,
2118                Address::ZERO,
2119            )?;
2120
2121            // Create a non USD token with non USD quote token
2122            let currency_2: String = thread_rng()
2123                .sample_iter(&Alphanumeric)
2124                .take(31)
2125                .map(char::from)
2126                .collect();
2127
2128            let token_1_address = token_id_to_address(3);
2129            let mut token_2 = TIP20Token::new(4);
2130            let result = token_2.initialize(
2131                "Token 2",
2132                "TK2",
2133                &currency_2,
2134                token_1_address,
2135                admin,
2136                Address::ZERO,
2137            );
2138            assert!(result.is_ok());
2139
2140            Ok(())
2141        })
2142    }
2143
2144    #[test]
2145    fn test_update_quote_token_invalid_token() -> eyre::Result<()> {
2146        let mut storage = HashMapStorageProvider::new(1);
2147        let admin = Address::random();
2148
2149        StorageCtx::enter(&mut storage, || {
2150            initialize_path_usd(admin)?;
2151
2152            let currency: String = thread_rng()
2153                .sample_iter(&Alphanumeric)
2154                .take(31)
2155                .map(char::from)
2156                .collect();
2157
2158            let mut token_1 = TIP20Token::new(1);
2159            token_1.initialize(
2160                "Token 1",
2161                "TK1",
2162                &currency,
2163                PATH_USD_ADDRESS,
2164                admin,
2165                Address::ZERO,
2166            )?;
2167
2168            // Create a new USD token
2169            let mut usd_token = TIP20Token::new(2);
2170            usd_token.initialize(
2171                "USD Token",
2172                "USDT",
2173                USD_CURRENCY,
2174                PATH_USD_ADDRESS,
2175                admin,
2176                Address::ZERO,
2177            )?;
2178
2179            // Try to update the USD token's quote token to the arbitrary currency token, this should fail
2180            let token_1_address = token_id_to_address(1);
2181            let result = usd_token.set_next_quote_token(
2182                admin,
2183                ITIP20::setNextQuoteTokenCall {
2184                    newQuoteToken: token_1_address,
2185                },
2186            );
2187
2188            assert!(matches!(
2189                result,
2190                Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
2191                    _
2192                )))
2193            ));
2194
2195            Ok(())
2196        })
2197    }
2198
2199    #[test]
2200    fn test_is_tip20_prefix() -> eyre::Result<()> {
2201        let mut storage = HashMapStorageProvider::new(1);
2202        let sender = Address::random();
2203
2204        StorageCtx::enter(&mut storage, || {
2205            initialize_path_usd(sender)?;
2206
2207            let mut factory = TIP20Factory::new();
2208            factory.initialize()?;
2209
2210            let created_tip20 = factory.create_token(
2211                sender,
2212                ITIP20Factory::createTokenCall {
2213                    name: "Test Token".to_string(),
2214                    symbol: "TEST".to_string(),
2215                    currency: "USD".to_string(),
2216                    quoteToken: crate::PATH_USD_ADDRESS,
2217                    admin: sender,
2218                },
2219            )?;
2220            let non_tip20 = Address::random();
2221
2222            assert!(is_tip20_prefix(PATH_USD_ADDRESS));
2223            assert!(is_tip20_prefix(created_tip20));
2224            assert!(!is_tip20_prefix(non_tip20));
2225            Ok(())
2226        })
2227    }
2228
2229    #[test]
2230    fn test_transfer_fee_pre_tx_handles_rewards_post_moderato() -> eyre::Result<()> {
2231        // Test with Moderato hardfork (rewards should be handled)
2232        // Note that we initially create storage at the Adagio hardfork so that scheduled rewards
2233        // are enabled for the test setup
2234        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
2235        let admin = Address::random();
2236        let user = Address::random();
2237
2238        StorageCtx::enter(&mut storage, || {
2239            let mint_amount = U256::from(1000e18);
2240            let reward_amount = U256::from(100e18);
2241
2242            // Setup token with rewards enabled
2243            let (token_id, initial_opted_in) =
2244                setup_token_with_rewards(admin, user, mint_amount, reward_amount)?;
2245
2246            // Update the hardfork to Moderato to ensure rewards are handled post hardfork
2247            StorageCtx.set_spec(TempoHardfork::Moderato);
2248
2249            // Transfer fee from user
2250            let fee_amount = U256::from(100e18);
2251            let mut token = TIP20Token::new(token_id);
2252            token.transfer_fee_pre_tx(user, fee_amount)?;
2253
2254            // After transfer_fee_pre_tx, the opted-in supply should be decreased
2255            let final_opted_in = token.get_opted_in_supply()?;
2256            assert_eq!(
2257                final_opted_in,
2258                initial_opted_in - fee_amount.to::<u128>(),
2259                "opted-in supply should decrease by fee amount"
2260            );
2261
2262            // User should have accumulated rewards (verify rewards were updated)
2263            let user_info = token.get_user_reward_info(user)?;
2264            assert!(
2265                user_info.reward_balance > U256::ZERO,
2266                "user should have accumulated rewards"
2267            );
2268
2269            Ok(())
2270        })
2271    }
2272
2273    #[test]
2274    fn test_transfer_fee_pre_tx_no_rewards_pre_moderato() -> eyre::Result<()> {
2275        // Test with Adagio (pre-Moderato) - rewards should NOT be handled
2276        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
2277        let admin = Address::random();
2278        let user = Address::random();
2279
2280        StorageCtx::enter(&mut storage, || {
2281            let mint_amount = U256::from(1000e18);
2282            let reward_amount = U256::from(100e18);
2283
2284            // Setup token with rewards enabled
2285            let (token_id, initial_opted_in) =
2286                setup_token_with_rewards(admin, user, mint_amount, reward_amount)?;
2287
2288            // Transfer fee from user
2289            let fee_amount = U256::from(100e18);
2290            let mut token = TIP20Token::new(token_id);
2291            token.transfer_fee_pre_tx(user, fee_amount)?;
2292
2293            // Pre-Moderato: opted-in supply should NOT be decreased (rewards not handled)
2294            let final_opted_in = token.get_opted_in_supply()?;
2295            assert_eq!(
2296                final_opted_in, initial_opted_in,
2297                "opted-in supply should NOT change pre-Moderato"
2298            );
2299
2300            // User should NOT have accumulated rewards (rewards not handled)
2301            let user_info = token.get_user_reward_info(user)?;
2302            assert_eq!(
2303                user_info.reward_balance,
2304                U256::ZERO,
2305                "user should NOT have accumulated rewards pre-Moderato"
2306            );
2307
2308            Ok(())
2309        })
2310    }
2311
2312    #[test]
2313    fn test_transfer_fee_post_tx_handles_rewards_post_moderato() -> eyre::Result<()> {
2314        // Test with Moderato hardfork (rewards should be handled)
2315        // Note that we initially create storage at the Adagio hardfork so that scheduled rewards
2316        // are enabled for the test setup
2317        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
2318        let admin = Address::random();
2319        let user = Address::random();
2320
2321        StorageCtx::enter(&mut storage, || {
2322            let mint_amount = U256::from(1000e18);
2323            let reward_amount = U256::from(100e18);
2324
2325            // Setup token with rewards enabled
2326            let (token_id, _initial_opted_in) =
2327                setup_token_with_rewards(admin, user, mint_amount, reward_amount)?;
2328
2329            // Update the hardfork to Moderato to ensure rewards are handled post hardfork
2330            StorageCtx.set_spec(TempoHardfork::Moderato);
2331            // Simulate fee transfer: first take fee from user
2332            let fee_amount = U256::from(100e18);
2333            let mut token = TIP20Token::new(token_id);
2334            token.transfer_fee_pre_tx(user, fee_amount)?;
2335
2336            // Get opted-in supply after pre_tx
2337            let opted_in_after_pre = token.get_opted_in_supply()?;
2338
2339            // Now refund part of it back
2340            let refund_amount = U256::from(40e18);
2341            let actual_used = U256::from(60e18);
2342            token.transfer_fee_post_tx(user, refund_amount, actual_used)?;
2343
2344            // After transfer_fee_post_tx, the opted-in supply should increase by refund amount
2345            let final_opted_in = token.get_opted_in_supply()?;
2346
2347            assert_eq!(
2348                final_opted_in,
2349                opted_in_after_pre + refund_amount.to::<u128>(),
2350                "opted-in supply should increase by refund amount"
2351            );
2352
2353            // User should have accumulated rewards
2354            let user_info = token.get_user_reward_info(user)?;
2355            assert!(
2356                user_info.reward_balance > U256::ZERO,
2357                "user should have accumulated rewards"
2358            );
2359
2360            Ok(())
2361        })
2362    }
2363
2364    #[test]
2365    fn test_transfer_fee_post_tx_no_rewards_pre_moderato() -> eyre::Result<()> {
2366        // Test with Adagio (pre-Moderato) - rewards should NOT be handled
2367        let (mut storage, admin) = setup_storage();
2368        storage.set_spec(TempoHardfork::Adagio);
2369        let user = Address::random();
2370
2371        StorageCtx::enter(&mut storage, || {
2372            let mint_amount = U256::from(1000e18);
2373            let reward_amount = U256::from(100e18);
2374
2375            // Setup token with rewards enabled
2376            let (token_id, initial_opted_in) =
2377                setup_token_with_rewards(admin, user, mint_amount, reward_amount)?;
2378
2379            // Simulate fee transfer: first take fee from user
2380            let fee_amount = U256::from(100e18);
2381            let mut token = TIP20Token::new(token_id);
2382            token.transfer_fee_pre_tx(user, fee_amount)?;
2383
2384            // Get opted-in supply after pre_tx (should be unchanged pre-Moderato)
2385            let opted_in_after_pre = token.get_opted_in_supply()?;
2386            assert_eq!(
2387                opted_in_after_pre, initial_opted_in,
2388                "opted-in supply should be unchanged in pre_tx pre-Moderato"
2389            );
2390
2391            // Now refund part of it back
2392            let refund_amount = U256::from(40e18);
2393            let actual_used = U256::from(60e18);
2394            token.transfer_fee_post_tx(user, refund_amount, actual_used)?;
2395
2396            // After transfer_fee_post_tx, the opted-in supply should still be unchanged (rewards not handled)
2397            let final_opted_in = token.get_opted_in_supply()?;
2398
2399            assert_eq!(
2400                final_opted_in, initial_opted_in,
2401                "opted-in supply should remain unchanged pre-Moderato"
2402            );
2403
2404            // User should NOT have accumulated rewards
2405            let user_info = token.get_user_reward_info(user)?;
2406            assert_eq!(
2407                user_info.reward_balance,
2408                U256::ZERO,
2409                "user should NOT have accumulated rewards pre-Moderato"
2410            );
2411
2412            Ok(())
2413        })
2414    }
2415
2416    #[test]
2417    fn test_initialize_supply_cap_post_moderato() -> eyre::Result<()> {
2418        let (mut storage, admin) = setup_storage();
2419
2420        storage.set_spec(TempoHardfork::Moderato);
2421
2422        StorageCtx::enter(&mut storage, || {
2423            let token_id = setup_factory_with_token(admin, "Test", "TST")?;
2424            let token = TIP20Token::new(token_id);
2425
2426            let supply_cap = token.supply_cap()?;
2427            assert_eq!(supply_cap, U256::from(u128::MAX));
2428
2429            Ok(())
2430        })
2431    }
2432
2433    #[test]
2434    fn test_initialize_supply_cap_pre_moderato() -> eyre::Result<()> {
2435        let (mut storage, admin) = setup_storage();
2436
2437        storage.set_spec(TempoHardfork::Adagio);
2438
2439        StorageCtx::enter(&mut storage, || {
2440            let token_id = setup_factory_with_token(admin, "Test", "TST")?;
2441            let token = TIP20Token::new(token_id);
2442
2443            let supply_cap = token.supply_cap()?;
2444            assert_eq!(supply_cap, U256::MAX);
2445
2446            Ok(())
2447        })
2448    }
2449
2450    #[test]
2451    fn test_unable_to_burn_blocked_from_protected_address() -> eyre::Result<()> {
2452        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
2453        let admin = Address::random();
2454        let burner = Address::random();
2455
2456        StorageCtx::enter(&mut storage, || {
2457            // Initialize token
2458            initialize_path_usd(admin)?;
2459            let token_id = 1;
2460            let mut token = TIP20Token::new(token_id);
2461            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
2462
2463            // Grant BURN_BLOCKED_ROLE to burner
2464            token.grant_role_internal(burner, *BURN_BLOCKED_ROLE)?;
2465
2466            // Simulate collected fees
2467            token.grant_role_internal(admin, *ISSUER_ROLE)?;
2468            token.mint(
2469                admin,
2470                ITIP20::mintCall {
2471                    to: TIP_FEE_MANAGER_ADDRESS,
2472                    amount: U256::from(1000),
2473                },
2474            )?;
2475
2476            // Attempt to burn from FeeManager
2477            let result = token.burn_blocked(
2478                burner,
2479                ITIP20::burnBlockedCall {
2480                    from: TIP_FEE_MANAGER_ADDRESS,
2481                    amount: U256::from(500),
2482                },
2483            );
2484
2485            assert!(matches!(
2486                result,
2487                Err(TempoPrecompileError::TIP20(TIP20Error::ProtectedAddress(_)))
2488            ));
2489
2490            // Verify FeeManager balance is unchanged
2491            let balance = token.balance_of(ITIP20::balanceOfCall {
2492                account: TIP_FEE_MANAGER_ADDRESS,
2493            })?;
2494            assert_eq!(balance, U256::from(1000));
2495
2496            // Mint tokens to StablecoinExchange
2497            token.mint(
2498                admin,
2499                ITIP20::mintCall {
2500                    to: STABLECOIN_EXCHANGE_ADDRESS,
2501                    amount: U256::from(1000),
2502                },
2503            )?;
2504
2505            // Attempt to burn from StablecoinExchange
2506            let result = token.burn_blocked(
2507                burner,
2508                ITIP20::burnBlockedCall {
2509                    from: STABLECOIN_EXCHANGE_ADDRESS,
2510                    amount: U256::from(500),
2511                },
2512            );
2513
2514            assert!(matches!(
2515                result,
2516                Err(TempoPrecompileError::TIP20(TIP20Error::ProtectedAddress(_)))
2517            ));
2518
2519            // Verify StablecoinExchange balance is unchanged
2520            let balance = token.balance_of(ITIP20::balanceOfCall {
2521                account: STABLECOIN_EXCHANGE_ADDRESS,
2522            })?;
2523            assert_eq!(balance, U256::from(1000));
2524
2525            Ok(())
2526        })
2527    }
2528
2529    #[test]
2530    fn test_set_fee_recipient() -> eyre::Result<()> {
2531        let (mut storage, admin) = setup_storage();
2532
2533        storage.set_spec(TempoHardfork::Adagio);
2534
2535        StorageCtx::enter(&mut storage, || {
2536            let token_id = setup_factory_with_token(admin, "Test", "TST")?;
2537            let mut token = TIP20Token::new(token_id);
2538
2539            let fee_recipient = token.get_fee_recipient(admin)?;
2540            assert_eq!(fee_recipient, Address::ZERO);
2541
2542            let expected_recipient = Address::random();
2543            token.set_fee_recipient(admin, expected_recipient)?;
2544
2545            let fee_recipient = token.get_fee_recipient(admin)?;
2546            assert_eq!(fee_recipient, expected_recipient);
2547
2548            let result = token.set_fee_recipient(Address::random(), expected_recipient);
2549            assert!(result.is_err());
2550
2551            Ok(())
2552        })
2553    }
2554
2555    #[test]
2556    fn test_initialize_usd_token_post_allegro_moderato() -> eyre::Result<()> {
2557        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::AllegroModerato);
2558        let admin = Address::random();
2559
2560        StorageCtx::enter(&mut storage, || {
2561            // USD token with zero quote token should succeed
2562            let mut token = TIP20Token::new(1);
2563            assert!(
2564                token
2565                    .initialize(
2566                        "TestToken",
2567                        "TEST",
2568                        "USD",
2569                        Address::ZERO,
2570                        admin,
2571                        Address::ZERO
2572                    )
2573                    .is_ok()
2574            );
2575
2576            // Non-USD token with zero quote token should succeed
2577            let mut eur_token = TIP20Token::new(2);
2578            assert!(
2579                eur_token
2580                    .initialize(
2581                        "EuroToken",
2582                        "EUR",
2583                        "EUR",
2584                        Address::ZERO,
2585                        admin,
2586                        Address::ZERO
2587                    )
2588                    .is_ok()
2589            );
2590
2591            // USD token with non-USD quote token should fail
2592            let mut usd_token = TIP20Token::new(3);
2593            let eur_token_address = token_id_to_address(2);
2594            assert!(
2595                usd_token
2596                    .initialize(
2597                        "USDToken",
2598                        "USD",
2599                        "USD",
2600                        eur_token_address,
2601                        admin,
2602                        Address::ZERO
2603                    )
2604                    .is_err()
2605            );
2606
2607            Ok(())
2608        })
2609    }
2610
2611    #[test]
2612    fn test_initialize_usd_token_pre_allegro_moderato() -> eyre::Result<()> {
2613        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
2614        let admin = Address::random();
2615
2616        StorageCtx::enter(&mut storage, || {
2617            // USD token with zero quote token should fail (no skip for zero quote token pre-AllegroModerato)
2618            let mut token = TIP20Token::new(1);
2619            assert!(
2620                token
2621                    .initialize(
2622                        "TestToken",
2623                        "TEST",
2624                        "USD",
2625                        Address::ZERO,
2626                        admin,
2627                        Address::ZERO
2628                    )
2629                    .is_err()
2630            );
2631
2632            // Non-USD token with zero quote token should succeed
2633            let mut eur_token = TIP20Token::new(1);
2634            assert!(
2635                eur_token
2636                    .initialize(
2637                        "EuroToken",
2638                        "EUR",
2639                        "EUR",
2640                        Address::ZERO,
2641                        admin,
2642                        Address::ZERO,
2643                    )
2644                    .is_ok()
2645            );
2646
2647            Ok(())
2648        })
2649    }
2650
2651    #[test]
2652    fn test_deploy_path_usd_via_factory_post_allegro_moderato() -> eyre::Result<()> {
2653        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::AllegroModerato);
2654        let admin = Address::random();
2655
2656        StorageCtx::enter(&mut storage, || {
2657            let mut factory = TIP20Factory::new();
2658            factory.initialize()?;
2659
2660            let path_usd_address = deploy_path_usd(&mut factory, admin)?;
2661            assert_eq!(path_usd_address, PATH_USD_ADDRESS);
2662
2663            let path_usd = TIP20Token::from_address(PATH_USD_ADDRESS)?;
2664            assert_eq!(path_usd.currency()?, "USD");
2665            assert_eq!(path_usd.quote_token()?, Address::ZERO);
2666            Ok(())
2667        })
2668    }
2669
2670    #[test]
2671    fn test_deploy_path_usd_fails_if_token_already_deployed_post_allegro_moderato()
2672    -> eyre::Result<()> {
2673        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::AllegroModerato);
2674        let admin = Address::random();
2675
2676        StorageCtx::enter(&mut storage, || {
2677            let mut factory = TIP20Factory::new();
2678            factory.initialize()?;
2679
2680            deploy_path_usd(&mut factory, admin)?;
2681
2682            let result = deploy_path_usd(&mut factory, admin);
2683            assert!(
2684                result.is_err(),
2685                "deploy_path_usd should fail if a token has already been deployed"
2686            );
2687            Ok(())
2688        })
2689    }
2690}