tempo_precompiles/path_usd/
mod.rs

1pub mod dispatch;
2
3use crate::{
4    STABLECOIN_EXCHANGE_ADDRESS,
5    error::Result,
6    storage::StorageCtx,
7    tip20::{ITIP20, TIP20Token},
8};
9use alloy::primitives::{Address, B256, U256, keccak256};
10use std::sync::LazyLock;
11pub use tempo_contracts::precompiles::IPathUSD;
12use tempo_contracts::precompiles::TIP20Error;
13
14pub static TRANSFER_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"TRANSFER_ROLE"));
15pub static RECEIVE_WITH_MEMO_ROLE: LazyLock<B256> =
16    LazyLock::new(|| keccak256(b"RECEIVE_WITH_MEMO_ROLE"));
17
18/// Name of TIP20 post allegretto. Note that the name and symbol are the same value
19const NAME_POST_ALLEGRETTO: &str = "pathUSD";
20/// Name of TIP20 pre allegretto. Note that the name and symbol are the same value
21const NAME_PRE_ALLEGRETTO: &str = "linkingUSD";
22const CURRENCY: &str = "USD";
23
24pub struct PathUSD {
25    pub token: TIP20Token,
26    storage: StorageCtx,
27}
28
29impl Default for PathUSD {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl PathUSD {
36    pub fn new() -> Self {
37        Self {
38            token: TIP20Token::new(0),
39            storage: StorageCtx::default(),
40        }
41    }
42
43    pub fn initialize(&mut self, admin: Address) -> Result<()> {
44        let (name, symbol) = if self.storage.spec().is_allegretto() {
45            (NAME_POST_ALLEGRETTO, NAME_POST_ALLEGRETTO)
46        } else {
47            (NAME_PRE_ALLEGRETTO, NAME_PRE_ALLEGRETTO)
48        };
49
50        self.token
51            .initialize(name, symbol, CURRENCY, Address::ZERO, admin, Address::ZERO)
52    }
53
54    fn is_transfer_authorized(&self, sender: Address) -> Result<bool> {
55        let authorized = sender == STABLECOIN_EXCHANGE_ADDRESS
56            || self.token.has_role_internal(sender, *TRANSFER_ROLE)?;
57
58        Ok(authorized)
59    }
60
61    fn is_transfer_from_authorized(&self, sender: Address, from: Address) -> Result<bool> {
62        let authorized = sender == STABLECOIN_EXCHANGE_ADDRESS
63            || self.token.has_role_internal(from, *TRANSFER_ROLE)?;
64        Ok(authorized)
65    }
66
67    fn is_transfer_with_memo_authorized(
68        &self,
69        sender: Address,
70        recipient: Address,
71    ) -> Result<bool> {
72        let authorized = sender == STABLECOIN_EXCHANGE_ADDRESS
73            || self.token.has_role_internal(sender, *TRANSFER_ROLE)?
74            || self
75                .token
76                .has_role_internal(recipient, *RECEIVE_WITH_MEMO_ROLE)?;
77
78        Ok(authorized)
79    }
80
81    fn is_transfer_from_with_memo_authorized(
82        &self,
83        sender: Address,
84        from: Address,
85        recipient: Address,
86    ) -> Result<bool> {
87        let authorized = sender == STABLECOIN_EXCHANGE_ADDRESS
88            || self.token.has_role_internal(from, *TRANSFER_ROLE)?
89            || self
90                .token
91                .has_role_internal(recipient, *RECEIVE_WITH_MEMO_ROLE)?;
92
93        Ok(authorized)
94    }
95
96    pub fn transfer(&mut self, msg_sender: Address, call: ITIP20::transferCall) -> Result<bool> {
97        // Post allegretto, use default tip20 logic
98        if self.storage.spec().is_allegretto() {
99            return self.token.transfer(msg_sender, call);
100        }
101
102        if self.is_transfer_authorized(msg_sender)? {
103            self.token.transfer(msg_sender, call)
104        } else {
105            Err(TIP20Error::transfers_disabled().into())
106        }
107    }
108
109    pub fn transfer_from(
110        &mut self,
111        msg_sender: Address,
112        call: ITIP20::transferFromCall,
113    ) -> Result<bool> {
114        // Post allegretto, use default tip20 logic
115        if self.storage.spec().is_allegretto() {
116            return self.token.transfer_from(msg_sender, call);
117        }
118
119        if self.is_transfer_from_authorized(msg_sender, call.from)?
120            || msg_sender == STABLECOIN_EXCHANGE_ADDRESS
121        {
122            self.token.transfer_from(msg_sender, call)
123        } else {
124            Err(TIP20Error::transfers_disabled().into())
125        }
126    }
127
128    pub fn transfer_with_memo(
129        &mut self,
130        msg_sender: Address,
131        call: ITIP20::transferWithMemoCall,
132    ) -> Result<()> {
133        // Post allegretto, use default tip20 logic
134        if self.storage.spec().is_allegretto() {
135            return self.token.transfer_with_memo(msg_sender, call);
136        }
137
138        if self.is_transfer_with_memo_authorized(msg_sender, call.to)? {
139            self.token.transfer_with_memo(msg_sender, call)
140        } else {
141            Err(TIP20Error::transfers_disabled().into())
142        }
143    }
144
145    pub fn transfer_from_with_memo(
146        &mut self,
147        msg_sender: Address,
148        call: ITIP20::transferFromWithMemoCall,
149    ) -> Result<bool> {
150        // Post allegretto, use default tip20 logic
151        if self.storage.spec().is_allegretto() {
152            return self.token.transfer_from_with_memo(msg_sender, call);
153        }
154
155        if self.is_transfer_from_with_memo_authorized(msg_sender, call.from, call.to)?
156            || msg_sender == STABLECOIN_EXCHANGE_ADDRESS
157        {
158            self.token.transfer_from_with_memo(msg_sender, call)
159        } else {
160            Err(TIP20Error::transfers_disabled().into())
161        }
162    }
163
164    pub fn name(&self) -> Result<String> {
165        if self.storage.spec().is_allegretto() {
166            Ok(NAME_POST_ALLEGRETTO.to_string())
167        } else {
168            self.token.name()
169        }
170    }
171
172    pub fn symbol(&self) -> Result<String> {
173        if self.storage.spec().is_allegretto() {
174            Ok(NAME_POST_ALLEGRETTO.to_string())
175        } else {
176            self.token.symbol()
177        }
178    }
179
180    pub fn currency(&self) -> Result<String> {
181        self.token.currency()
182    }
183
184    pub fn decimals(&self) -> Result<u8> {
185        self.token.decimals()
186    }
187
188    pub fn total_supply(&self) -> Result<U256> {
189        self.token.total_supply()
190    }
191
192    pub fn balance_of(&self, call: ITIP20::balanceOfCall) -> Result<U256> {
193        self.token.balance_of(call)
194    }
195
196    pub fn allowance(&self, call: ITIP20::allowanceCall) -> Result<U256> {
197        self.token.allowance(call)
198    }
199
200    pub fn approve(&mut self, sender: Address, call: ITIP20::approveCall) -> Result<bool> {
201        self.token.approve(sender, call)
202    }
203
204    pub fn mint(&mut self, sender: Address, call: ITIP20::mintCall) -> Result<()> {
205        self.token.mint(sender, call)
206    }
207
208    pub fn burn(&mut self, sender: Address, call: ITIP20::burnCall) -> Result<()> {
209        self.token.burn(sender, call)
210    }
211
212    pub fn pause(&mut self, sender: Address, call: ITIP20::pauseCall) -> Result<()> {
213        self.token.pause(sender, call)
214    }
215
216    pub fn unpause(&mut self, sender: Address, call: ITIP20::unpauseCall) -> Result<()> {
217        self.token.unpause(sender, call)
218    }
219
220    pub fn paused(&self) -> Result<bool> {
221        self.token.paused()
222    }
223
224    /// Returns the PAUSE_ROLE constant
225    ///
226    /// This role identifier grants permission to pause the token contract.
227    /// The role is computed as `keccak256("PAUSE_ROLE")`.
228    pub fn pause_role() -> B256 {
229        TIP20Token::pause_role()
230    }
231
232    /// Returns the UNPAUSE_ROLE constant
233    ///
234    /// This role identifier grants permission to unpause the token contract.
235    /// The role is computed as `keccak256("UNPAUSE_ROLE")`.
236    pub fn unpause_role() -> B256 {
237        TIP20Token::unpause_role()
238    }
239
240    /// Returns the ISSUER_ROLE constant
241    ///
242    /// This role identifier grants permission to mint and burn tokens.
243    /// The role is computed as `keccak256("ISSUER_ROLE")`.
244    pub fn issuer_role() -> B256 {
245        TIP20Token::issuer_role()
246    }
247
248    /// Returns the BURN_BLOCKED_ROLE constant
249    ///
250    /// This role identifier grants permission to burn tokens from blocked accounts.
251    /// The role is computed as `keccak256("BURN_BLOCKED_ROLE")`.
252    pub fn burn_blocked_role() -> B256 {
253        TIP20Token::burn_blocked_role()
254    }
255
256    /// Returns the TRANSFER_ROLE constant
257    ///
258    /// This role identifier grants permission to transfer pathUSD tokens.
259    /// The role is computed as `keccak256("TRANSFER_ROLE")`.
260    pub fn transfer_role() -> B256 {
261        *TRANSFER_ROLE
262    }
263
264    /// Returns the RECEIVE_WITH_MEMO_ROLE constant
265    ///
266    /// This role identifier grants permission to receive pathUSD tokens.
267    /// The role is computed as `keccak256("RECEIVE_WITH_MEMO_ROLE")`.
268    pub fn receive_with_memo_role() -> B256 {
269        *RECEIVE_WITH_MEMO_ROLE
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use alloy_primitives::uint;
276    use tempo_chainspec::hardfork::TempoHardfork;
277    use tempo_contracts::precompiles::RolesAuthError;
278
279    use super::*;
280    use crate::{
281        error::TempoPrecompileError,
282        storage::hashmap::HashMapStorageProvider,
283        test_util::setup_storage,
284        tip20::{IRolesAuth, ISSUER_ROLE, PAUSE_ROLE, UNPAUSE_ROLE},
285    };
286
287    fn transfer_test_setup(admin: Address) -> Result<PathUSD> {
288        let mut path_usd = PathUSD::new();
289        path_usd.initialize(admin)?;
290        path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
291
292        Ok(path_usd)
293    }
294
295    #[test]
296    fn test_metadata_pre_allegretto() -> eyre::Result<()> {
297        let (mut storage, admin) = setup_storage();
298        storage.set_spec(TempoHardfork::Moderato);
299
300        StorageCtx::enter(&mut storage, || {
301            let path_usd = transfer_test_setup(admin)?;
302
303            assert_eq!(path_usd.name()?, NAME_PRE_ALLEGRETTO);
304            assert_eq!(path_usd.symbol()?, NAME_PRE_ALLEGRETTO);
305            assert_eq!(path_usd.currency()?, "USD");
306            Ok(())
307        })
308    }
309
310    #[test]
311    fn test_metadata_post_allegretto() -> eyre::Result<()> {
312        let (mut storage, admin) = setup_storage();
313        storage.set_spec(TempoHardfork::Allegretto);
314
315        StorageCtx::enter(&mut storage, || {
316            let path_usd = transfer_test_setup(admin)?;
317
318            assert_eq!(path_usd.name()?, NAME_POST_ALLEGRETTO);
319            assert_eq!(path_usd.symbol()?, NAME_POST_ALLEGRETTO);
320            assert_eq!(path_usd.currency()?, "USD");
321            Ok(())
322        })
323    }
324
325    #[test]
326    fn test_transfer_reverts_pre_allegretto() -> eyre::Result<()> {
327        let (mut storage, admin) = setup_storage();
328        storage.set_spec(TempoHardfork::Moderato);
329
330        StorageCtx::enter(&mut storage, || {
331            let mut path_usd = transfer_test_setup(admin)?;
332
333            let result = path_usd.transfer(
334                Address::random(),
335                ITIP20::transferCall {
336                    to: Address::random(),
337                    amount: U256::random(),
338                },
339            );
340
341            assert_eq!(
342                result.unwrap_err(),
343                TempoPrecompileError::TIP20(TIP20Error::transfers_disabled())
344            );
345
346            Ok(())
347        })
348    }
349
350    #[test]
351    fn test_transfer_from_reverts_pre_allegretto() -> eyre::Result<()> {
352        let (mut storage, admin) = setup_storage();
353        storage.set_spec(TempoHardfork::Moderato);
354
355        StorageCtx::enter(&mut storage, || {
356            let mut path_usd = transfer_test_setup(admin)?;
357
358            let result = path_usd.transfer_from(
359                Address::random(),
360                ITIP20::transferFromCall {
361                    from: Address::random(),
362                    to: Address::random(),
363                    amount: U256::random(),
364                },
365            );
366            assert_eq!(
367                result.unwrap_err(),
368                TempoPrecompileError::TIP20(TIP20Error::transfers_disabled())
369            );
370            Ok(())
371        })
372    }
373
374    #[test]
375    fn test_transfer_with_memo_reverts_pre_allegretto() -> eyre::Result<()> {
376        let (mut storage, admin) = setup_storage();
377        storage.set_spec(TempoHardfork::Moderato);
378
379        StorageCtx::enter(&mut storage, || {
380            let mut path_usd = transfer_test_setup(admin)?;
381
382            let result = path_usd.transfer_with_memo(
383                Address::random(),
384                ITIP20::transferWithMemoCall {
385                    to: Address::random(),
386                    amount: U256::from(100),
387                    memo: [0u8; 32].into(),
388                },
389            );
390            assert_eq!(
391                result.unwrap_err(),
392                TempoPrecompileError::TIP20(TIP20Error::transfers_disabled())
393            );
394
395            Ok(())
396        })
397    }
398
399    #[test]
400    fn test_transfer_from_with_memo_reverts_pre_allegretto() -> eyre::Result<()> {
401        let (mut storage, admin) = setup_storage();
402        storage.set_spec(TempoHardfork::Moderato);
403
404        StorageCtx::enter(&mut storage, || {
405            let mut path_usd = transfer_test_setup(admin)?;
406
407            let result = path_usd.transfer_from_with_memo(
408                Address::random(),
409                ITIP20::transferFromWithMemoCall {
410                    from: Address::random(),
411                    to: Address::random(),
412                    amount: U256::from(100),
413                    memo: [0u8; 32].into(),
414                },
415            );
416            assert_eq!(
417                result.unwrap_err(),
418                TempoPrecompileError::TIP20(TIP20Error::transfers_disabled())
419            );
420
421            Ok(())
422        })
423    }
424
425    #[test]
426    fn test_mint() -> eyre::Result<()> {
427        let (mut storage, admin) = setup_storage();
428
429        StorageCtx::enter(&mut storage, || {
430            let mut path_usd = transfer_test_setup(admin)?;
431            let recipient = Address::random();
432            let amount = U256::from(1000);
433
434            let balance_before =
435                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
436
437            path_usd.mint(
438                admin,
439                ITIP20::mintCall {
440                    to: recipient,
441                    amount,
442                },
443            )?;
444
445            let balance_after =
446                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
447
448            assert_eq!(balance_after, balance_before + amount);
449            Ok(())
450        })
451    }
452
453    #[test]
454    fn test_burn() -> eyre::Result<()> {
455        let (mut storage, admin) = setup_storage();
456
457        StorageCtx::enter(&mut storage, || {
458            let mut path_usd = PathUSD::new();
459            let amount = U256::from(1000);
460
461            path_usd.initialize(admin)?;
462            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
463
464            path_usd.mint(admin, ITIP20::mintCall { to: admin, amount })?;
465
466            let balance_before = path_usd.balance_of(ITIP20::balanceOfCall { account: admin })?;
467
468            path_usd.burn(admin, ITIP20::burnCall { amount })?;
469
470            let balance_after = path_usd.balance_of(ITIP20::balanceOfCall { account: admin })?;
471            assert_eq!(balance_after, balance_before - amount);
472            Ok(())
473        })
474    }
475
476    #[test]
477    fn test_approve() -> eyre::Result<()> {
478        let (mut storage, admin) = setup_storage();
479
480        StorageCtx::enter(&mut storage, || {
481            let mut path_usd = PathUSD::new();
482            let owner = Address::random();
483            let spender = Address::random();
484            let amount = U256::from(1000);
485
486            path_usd.initialize(admin)?;
487
488            let result = path_usd.approve(owner, ITIP20::approveCall { spender, amount })?;
489
490            assert!(result);
491
492            let allowance = path_usd.allowance(ITIP20::allowanceCall { owner, spender })?;
493            assert_eq!(allowance, amount);
494            Ok(())
495        })
496    }
497
498    #[test]
499    fn test_transfer_with_stablecoin_exchange() -> eyre::Result<()> {
500        let (mut storage, admin) = setup_storage();
501
502        StorageCtx::enter(&mut storage, || {
503            let mut path_usd = PathUSD::new();
504            let recipient = Address::random();
505            let amount = U256::from(1000);
506
507            path_usd.initialize(admin)?;
508            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
509
510            path_usd.mint(
511                admin,
512                ITIP20::mintCall {
513                    to: STABLECOIN_EXCHANGE_ADDRESS,
514                    amount,
515                },
516            )?;
517
518            let dex_balance_before = path_usd.balance_of(ITIP20::balanceOfCall {
519                account: STABLECOIN_EXCHANGE_ADDRESS,
520            })?;
521
522            let recipient_balance_before =
523                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
524
525            let result = path_usd.transfer(
526                STABLECOIN_EXCHANGE_ADDRESS,
527                ITIP20::transferCall {
528                    to: recipient,
529                    amount,
530                },
531            )?;
532            assert!(result);
533
534            let dex_balance_after = path_usd.balance_of(ITIP20::balanceOfCall {
535                account: STABLECOIN_EXCHANGE_ADDRESS,
536            })?;
537
538            let recipient_balance_after =
539                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
540
541            assert_eq!(dex_balance_after, dex_balance_before - amount);
542            assert_eq!(recipient_balance_after, recipient_balance_before + amount);
543            Ok(())
544        })
545    }
546
547    #[test]
548    fn test_transfer_from_with_stablecoin_exchange() -> eyre::Result<()> {
549        let (mut storage, admin) = setup_storage();
550
551        StorageCtx::enter(&mut storage, || {
552            let mut path_usd = PathUSD::new();
553            let from = Address::random();
554            let to = Address::random();
555            let amount = U256::from(1000);
556
557            path_usd.initialize(admin)?;
558            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
559
560            path_usd.mint(admin, ITIP20::mintCall { to: from, amount })?;
561
562            path_usd.approve(
563                from,
564                ITIP20::approveCall {
565                    spender: STABLECOIN_EXCHANGE_ADDRESS,
566                    amount,
567                },
568            )?;
569
570            let from_balance_before =
571                path_usd.balance_of(ITIP20::balanceOfCall { account: from })?;
572
573            let to_balance_before = path_usd.balance_of(ITIP20::balanceOfCall { account: to })?;
574
575            let allowance_before = path_usd.allowance(ITIP20::allowanceCall {
576                owner: from,
577                spender: STABLECOIN_EXCHANGE_ADDRESS,
578            })?;
579
580            let result = path_usd.transfer_from(
581                STABLECOIN_EXCHANGE_ADDRESS,
582                ITIP20::transferFromCall { from, to, amount },
583            )?;
584
585            assert!(result);
586
587            let from_balance_after =
588                path_usd.balance_of(ITIP20::balanceOfCall { account: from })?;
589
590            let to_balance_after = path_usd.balance_of(ITIP20::balanceOfCall { account: to })?;
591
592            let allowance_after = path_usd.allowance(ITIP20::allowanceCall {
593                owner: from,
594                spender: STABLECOIN_EXCHANGE_ADDRESS,
595            })?;
596
597            assert_eq!(from_balance_after, from_balance_before - amount);
598            assert_eq!(to_balance_after, to_balance_before + amount);
599            assert_eq!(allowance_after, allowance_before - amount);
600            Ok(())
601        })
602    }
603
604    #[test]
605    fn test_transfer_with_transfer_role() -> eyre::Result<()> {
606        let (mut storage, admin) = setup_storage();
607
608        StorageCtx::enter(&mut storage, || {
609            let mut path_usd = PathUSD::new();
610            let sender = Address::random();
611            let recipient = Address::random();
612            let amount = U256::from(1000);
613
614            path_usd.initialize(admin)?;
615            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
616            path_usd.token.grant_role_internal(sender, *TRANSFER_ROLE)?;
617
618            path_usd.mint(admin, ITIP20::mintCall { to: sender, amount })?;
619
620            let sender_balance_before =
621                path_usd.balance_of(ITIP20::balanceOfCall { account: sender })?;
622
623            let recipient_balance_before =
624                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
625
626            let result = path_usd.transfer(
627                sender,
628                ITIP20::transferCall {
629                    to: recipient,
630                    amount,
631                },
632            )?;
633            assert!(result);
634
635            let sender_balance_after =
636                path_usd.balance_of(ITIP20::balanceOfCall { account: sender })?;
637            let recipient_balance_after =
638                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
639
640            assert_eq!(sender_balance_after, sender_balance_before - amount);
641            assert_eq!(recipient_balance_after, recipient_balance_before + amount);
642            Ok(())
643        })
644    }
645
646    #[test]
647    fn test_transfer_with_receive_role_reverts_pre_allegretto() -> eyre::Result<()> {
648        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
649        let admin = Address::random();
650        let sender = Address::random();
651        let recipient = Address::random();
652        let amount = U256::from(1000);
653
654        StorageCtx::enter(&mut storage, || {
655            let mut path_usd = PathUSD::new();
656            path_usd.initialize(admin)?;
657            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
658            path_usd
659                .token
660                .grant_role_internal(recipient, *RECEIVE_WITH_MEMO_ROLE)?;
661
662            path_usd.mint(admin, ITIP20::mintCall { to: sender, amount })?;
663
664            let result = path_usd.transfer(
665                sender,
666                ITIP20::transferCall {
667                    to: recipient,
668                    amount,
669                },
670            );
671
672            assert_eq!(
673                result.unwrap_err(),
674                TempoPrecompileError::TIP20(TIP20Error::transfers_disabled())
675            );
676
677            Ok(())
678        })
679    }
680
681    #[test]
682    fn test_transfer_from_with_transfer_role() -> eyre::Result<()> {
683        let (mut storage, admin) = setup_storage();
684        let from = Address::random();
685        let to = Address::random();
686        let spender = Address::random();
687        let amount = U256::from(1000);
688
689        StorageCtx::enter(&mut storage, || {
690            let mut path_usd = PathUSD::new();
691            path_usd.initialize(admin)?;
692            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
693            path_usd.token.grant_role_internal(from, *TRANSFER_ROLE)?;
694
695            path_usd.mint(admin, ITIP20::mintCall { to: from, amount })?;
696
697            path_usd.approve(from, ITIP20::approveCall { spender, amount })?;
698
699            let from_balance_before =
700                path_usd.balance_of(ITIP20::balanceOfCall { account: from })?;
701
702            let to_balance_before = path_usd.balance_of(ITIP20::balanceOfCall { account: to })?;
703
704            let allowance_before = path_usd.allowance(ITIP20::allowanceCall {
705                owner: from,
706                spender,
707            })?;
708
709            let result =
710                path_usd.transfer_from(spender, ITIP20::transferFromCall { from, to, amount })?;
711
712            assert!(result);
713
714            let from_balance_after =
715                path_usd.balance_of(ITIP20::balanceOfCall { account: from })?;
716            let to_balance_after = path_usd.balance_of(ITIP20::balanceOfCall { account: to })?;
717            let allowance_after = path_usd.allowance(ITIP20::allowanceCall {
718                owner: from,
719                spender,
720            })?;
721
722            assert_eq!(from_balance_after, from_balance_before - amount);
723            assert_eq!(to_balance_after, to_balance_before + amount);
724            assert_eq!(allowance_after, allowance_before - amount);
725            Ok(())
726        })
727    }
728
729    #[test]
730    fn test_transfer_from_with_receive_role_reverts_pre_allegretto() -> eyre::Result<()> {
731        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
732        let admin = Address::random();
733        let from = Address::random();
734        let to = Address::random();
735        let spender = Address::random();
736        let amount = U256::from(1000);
737
738        StorageCtx::enter(&mut storage, || {
739            let mut path_usd = PathUSD::new();
740            path_usd.initialize(admin)?;
741            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
742            path_usd
743                .token
744                .grant_role_internal(to, *RECEIVE_WITH_MEMO_ROLE)?;
745
746            path_usd.mint(admin, ITIP20::mintCall { to: from, amount })?;
747
748            path_usd.approve(from, ITIP20::approveCall { spender, amount })?;
749
750            let result =
751                path_usd.transfer_from(spender, ITIP20::transferFromCall { from, to, amount });
752
753            assert_eq!(
754                result.unwrap_err(),
755                TempoPrecompileError::TIP20(TIP20Error::transfers_disabled())
756            );
757
758            Ok(())
759        })
760    }
761
762    #[test]
763    fn test_transfer_with_memo_with_transfer_role() -> eyre::Result<()> {
764        let (mut storage, admin) = setup_storage();
765
766        StorageCtx::enter(&mut storage, || {
767            let mut path_usd = PathUSD::new();
768            let sender = Address::random();
769            let recipient = Address::random();
770            let amount = U256::from(1000);
771            let memo = [1u8; 32];
772
773            path_usd.initialize(admin)?;
774            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
775            path_usd.token.grant_role_internal(sender, *TRANSFER_ROLE)?;
776
777            path_usd.mint(admin, ITIP20::mintCall { to: sender, amount })?;
778
779            let sender_balance_before =
780                path_usd.balance_of(ITIP20::balanceOfCall { account: sender })?;
781            let recipient_balance_before =
782                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
783
784            path_usd.transfer_with_memo(
785                sender,
786                ITIP20::transferWithMemoCall {
787                    to: recipient,
788                    amount,
789                    memo: memo.into(),
790                },
791            )?;
792
793            let sender_balance_after =
794                path_usd.balance_of(ITIP20::balanceOfCall { account: sender })?;
795            let recipient_balance_after =
796                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
797
798            assert_eq!(sender_balance_after, sender_balance_before - amount);
799            assert_eq!(recipient_balance_after, recipient_balance_before + amount);
800            Ok(())
801        })
802    }
803
804    #[test]
805    fn test_transfer_with_memo_with_receive_role() -> eyre::Result<()> {
806        let (mut storage, admin) = setup_storage();
807
808        StorageCtx::enter(&mut storage, || {
809            let mut path_usd = PathUSD::new();
810            let sender = Address::random();
811            let recipient = Address::random();
812            let amount = U256::from(1000);
813            let memo = [1u8; 32];
814
815            path_usd.initialize(admin)?;
816            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
817            path_usd
818                .token
819                .grant_role_internal(recipient, *RECEIVE_WITH_MEMO_ROLE)?;
820
821            path_usd.mint(admin, ITIP20::mintCall { to: sender, amount })?;
822
823            let sender_balance_before =
824                path_usd.balance_of(ITIP20::balanceOfCall { account: sender })?;
825            let recipient_balance_before =
826                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
827
828            path_usd.transfer_with_memo(
829                sender,
830                ITIP20::transferWithMemoCall {
831                    to: recipient,
832                    amount,
833                    memo: memo.into(),
834                },
835            )?;
836
837            let sender_balance_after =
838                path_usd.balance_of(ITIP20::balanceOfCall { account: sender })?;
839            let recipient_balance_after =
840                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
841
842            assert_eq!(sender_balance_after, sender_balance_before - amount);
843            assert_eq!(recipient_balance_after, recipient_balance_before + amount);
844            Ok(())
845        })
846    }
847
848    #[test]
849    fn test_transfer_from_with_memo_with_stablecoin_exchange() -> eyre::Result<()> {
850        let (mut storage, admin) = setup_storage();
851
852        StorageCtx::enter(&mut storage, || {
853            let mut path_usd = PathUSD::new();
854            let from = Address::random();
855            let to = Address::random();
856            let amount = U256::from(1000);
857            let memo = [1u8; 32];
858
859            path_usd.initialize(admin)?;
860            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
861
862            path_usd.mint(admin, ITIP20::mintCall { to: from, amount })?;
863
864            path_usd.approve(
865                from,
866                ITIP20::approveCall {
867                    spender: STABLECOIN_EXCHANGE_ADDRESS,
868                    amount,
869                },
870            )?;
871
872            let from_balance_before =
873                path_usd.balance_of(ITIP20::balanceOfCall { account: from })?;
874            let to_balance_before = path_usd.balance_of(ITIP20::balanceOfCall { account: to })?;
875            let allowance_before = path_usd.allowance(ITIP20::allowanceCall {
876                owner: from,
877                spender: STABLECOIN_EXCHANGE_ADDRESS,
878            })?;
879
880            let result = path_usd.transfer_from_with_memo(
881                STABLECOIN_EXCHANGE_ADDRESS,
882                ITIP20::transferFromWithMemoCall {
883                    from,
884                    to,
885                    amount,
886                    memo: memo.into(),
887                },
888            )?;
889
890            assert!(result);
891
892            let from_balance_after =
893                path_usd.balance_of(ITIP20::balanceOfCall { account: from })?;
894            let to_balance_after = path_usd.balance_of(ITIP20::balanceOfCall { account: to })?;
895            let allowance_after = path_usd.allowance(ITIP20::allowanceCall {
896                owner: from,
897                spender: STABLECOIN_EXCHANGE_ADDRESS,
898            })?;
899
900            assert_eq!(from_balance_after, from_balance_before - amount);
901            assert_eq!(to_balance_after, to_balance_before + amount);
902            assert_eq!(allowance_after, allowance_before - amount);
903            Ok(())
904        })
905    }
906
907    #[test]
908    fn test_transfer_from_with_memo_with_transfer_role() -> eyre::Result<()> {
909        let (mut storage, admin) = setup_storage();
910
911        StorageCtx::enter(&mut storage, || {
912            let mut path_usd = PathUSD::new();
913            let from = Address::random();
914            let to = Address::random();
915            let spender = Address::random();
916            let amount = U256::from(1000);
917            let memo = [1u8; 32];
918
919            path_usd.initialize(admin)?;
920            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
921            path_usd.token.grant_role_internal(from, *TRANSFER_ROLE)?;
922
923            path_usd.mint(admin, ITIP20::mintCall { to: from, amount })?;
924
925            path_usd.approve(from, ITIP20::approveCall { spender, amount })?;
926
927            let from_balance_before =
928                path_usd.balance_of(ITIP20::balanceOfCall { account: from })?;
929            let to_balance_before = path_usd.balance_of(ITIP20::balanceOfCall { account: to })?;
930            let allowance_before = path_usd.allowance(ITIP20::allowanceCall {
931                owner: from,
932                spender,
933            })?;
934
935            let result = path_usd.transfer_from_with_memo(
936                spender,
937                ITIP20::transferFromWithMemoCall {
938                    from,
939                    to,
940                    amount,
941                    memo: memo.into(),
942                },
943            )?;
944
945            assert!(result);
946
947            let from_balance_after =
948                path_usd.balance_of(ITIP20::balanceOfCall { account: from })?;
949            let to_balance_after = path_usd.balance_of(ITIP20::balanceOfCall { account: to })?;
950            let allowance_after = path_usd.allowance(ITIP20::allowanceCall {
951                owner: from,
952                spender,
953            })?;
954
955            assert_eq!(from_balance_after, from_balance_before - amount);
956            assert_eq!(to_balance_after, to_balance_before + amount);
957            assert_eq!(allowance_after, allowance_before - amount);
958            Ok(())
959        })
960    }
961
962    #[test]
963    fn test_transfer_from_with_memo_with_receive_role() -> eyre::Result<()> {
964        let (mut storage, admin) = setup_storage();
965
966        StorageCtx::enter(&mut storage, || {
967            let mut path_usd = PathUSD::new();
968            let from = Address::random();
969            let to = Address::random();
970            let spender = Address::random();
971            let amount = U256::from(1000);
972            let memo = [1u8; 32];
973
974            path_usd.initialize(admin)?;
975            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
976            path_usd
977                .token
978                .grant_role_internal(to, *RECEIVE_WITH_MEMO_ROLE)?;
979
980            path_usd.mint(admin, ITIP20::mintCall { to: from, amount })?;
981
982            path_usd.approve(from, ITIP20::approveCall { spender, amount })?;
983
984            let from_balance_before =
985                path_usd.balance_of(ITIP20::balanceOfCall { account: from })?;
986            let to_balance_before = path_usd.balance_of(ITIP20::balanceOfCall { account: to })?;
987            let allowance_before = path_usd.allowance(ITIP20::allowanceCall {
988                owner: from,
989                spender,
990            })?;
991
992            let result = path_usd.transfer_from_with_memo(
993                spender,
994                ITIP20::transferFromWithMemoCall {
995                    from,
996                    to,
997                    amount,
998                    memo: memo.into(),
999                },
1000            )?;
1001
1002            assert!(result);
1003
1004            let from_balance_after =
1005                path_usd.balance_of(ITIP20::balanceOfCall { account: from })?;
1006            let to_balance_after = path_usd.balance_of(ITIP20::balanceOfCall { account: to })?;
1007            let allowance_after = path_usd.allowance(ITIP20::allowanceCall {
1008                owner: from,
1009                spender,
1010            })?;
1011
1012            assert_eq!(from_balance_after, from_balance_before - amount);
1013            assert_eq!(to_balance_after, to_balance_before + amount);
1014            assert_eq!(allowance_after, allowance_before - amount);
1015            Ok(())
1016        })
1017    }
1018
1019    #[test]
1020    fn test_pause_and_unpause() -> eyre::Result<()> {
1021        let (mut storage, admin) = setup_storage();
1022
1023        StorageCtx::enter(&mut storage, || {
1024            let mut path_usd = PathUSD::new();
1025            let pauser = Address::random();
1026            let unpauser = Address::random();
1027
1028            path_usd.initialize(admin)?;
1029
1030            // Grant PAUSE_ROLE and UNPAUSE_ROLE
1031            path_usd.token.grant_role_internal(pauser, *PAUSE_ROLE)?;
1032            path_usd
1033                .token
1034                .grant_role_internal(unpauser, *UNPAUSE_ROLE)?;
1035
1036            assert!(!path_usd.paused()?);
1037
1038            path_usd.pause(pauser, ITIP20::pauseCall {})?;
1039            assert!(path_usd.paused()?);
1040
1041            path_usd.unpause(unpauser, ITIP20::unpauseCall {})?;
1042            assert!(!path_usd.paused()?);
1043            Ok(())
1044        })
1045    }
1046
1047    #[test]
1048    fn test_role_management() -> eyre::Result<()> {
1049        let (mut storage, admin) = setup_storage();
1050
1051        StorageCtx::enter(&mut storage, || {
1052            let mut path_usd = PathUSD::new();
1053            let user = Address::random();
1054
1055            path_usd.initialize(admin)?;
1056
1057            // Grant ISSUER_ROLE to user
1058            path_usd.token.grant_role(
1059                admin,
1060                IRolesAuth::grantRoleCall {
1061                    role: *ISSUER_ROLE,
1062                    account: user,
1063                },
1064            )?;
1065
1066            // Check that user has the role
1067            assert!(path_usd.token.has_role(IRolesAuth::hasRoleCall {
1068                role: *ISSUER_ROLE,
1069                account: user,
1070            })?);
1071
1072            // Revoke the role
1073            path_usd.token.revoke_role(
1074                admin,
1075                IRolesAuth::revokeRoleCall {
1076                    role: *ISSUER_ROLE,
1077                    account: user,
1078                },
1079            )?;
1080
1081            // Check that user no longer has the role
1082            assert!(!path_usd.token.has_role(IRolesAuth::hasRoleCall {
1083                role: *ISSUER_ROLE,
1084                account: user,
1085            })?);
1086            Ok(())
1087        })
1088    }
1089
1090    #[test]
1091    fn test_supply_cap() -> eyre::Result<()> {
1092        let (mut storage, admin) = setup_storage();
1093
1094        StorageCtx::enter(&mut storage, || {
1095            let mut path_usd = PathUSD::new();
1096            let recipient = Address::random();
1097            let supply_cap = U256::from(1000);
1098
1099            path_usd.initialize(admin)?;
1100
1101            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
1102
1103            // Set supply cap
1104            path_usd.token.set_supply_cap(
1105                admin,
1106                ITIP20::setSupplyCapCall {
1107                    newSupplyCap: supply_cap,
1108                },
1109            )?;
1110
1111            assert_eq!(path_usd.token.supply_cap()?, supply_cap);
1112
1113            // Try to mint more than supply cap
1114            let result = path_usd.mint(
1115                admin,
1116                ITIP20::mintCall {
1117                    to: recipient,
1118                    amount: U256::from(1001),
1119                },
1120            );
1121
1122            assert_eq!(
1123                result.unwrap_err(),
1124                TempoPrecompileError::TIP20(TIP20Error::supply_cap_exceeded())
1125            );
1126            Ok(())
1127        })
1128    }
1129
1130    #[test]
1131    fn test_invalid_supply_caps() -> eyre::Result<()> {
1132        let (mut storage, admin) = setup_storage();
1133
1134        StorageCtx::enter(&mut storage, || {
1135            let mut path_usd = PathUSD::new();
1136            let recipient = Address::random();
1137            let supply_cap = U256::from(1000);
1138            let bad_supply_cap = uint!(0x100000000000000000000000000000000_U256);
1139
1140            path_usd.initialize(admin)?;
1141
1142            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
1143
1144            // Set supply cap to u128 max plus one
1145            let result = path_usd.token.set_supply_cap(
1146                admin,
1147                ITIP20::setSupplyCapCall {
1148                    newSupplyCap: bad_supply_cap,
1149                },
1150            );
1151
1152            assert_eq!(
1153                result.unwrap_err(),
1154                TempoPrecompileError::TIP20(TIP20Error::supply_cap_exceeded())
1155            );
1156
1157            // Set supply cap
1158            path_usd.token.set_supply_cap(
1159                admin,
1160                ITIP20::setSupplyCapCall {
1161                    newSupplyCap: supply_cap,
1162                },
1163            )?;
1164
1165            // Try to mint the exact supply cap
1166            path_usd.mint(
1167                admin,
1168                ITIP20::mintCall {
1169                    to: recipient,
1170                    amount: U256::from(1000),
1171                },
1172            )?;
1173
1174            // Try to set the supply cap to something lower than the total supply
1175            let smaller_supply_cap = U256::from(999);
1176            let result = path_usd.token.set_supply_cap(
1177                admin,
1178                ITIP20::setSupplyCapCall {
1179                    newSupplyCap: smaller_supply_cap,
1180                },
1181            );
1182
1183            assert_eq!(
1184                result.unwrap_err(),
1185                TempoPrecompileError::TIP20(TIP20Error::invalid_supply_cap())
1186            );
1187            Ok(())
1188        })
1189    }
1190
1191    #[test]
1192    fn test_change_transfer_policy_id() -> eyre::Result<()> {
1193        let (mut storage, admin) = setup_storage();
1194
1195        StorageCtx::enter(&mut storage, || {
1196            let mut path_usd = PathUSD::new();
1197            let new_policy_id = 42u64;
1198
1199            path_usd.initialize(admin)?;
1200
1201            // Admin can change transfer policy ID
1202            path_usd.token.change_transfer_policy_id(
1203                admin,
1204                ITIP20::changeTransferPolicyIdCall {
1205                    newPolicyId: new_policy_id,
1206                },
1207            )?;
1208
1209            assert_eq!(path_usd.token.transfer_policy_id()?, new_policy_id);
1210
1211            // Non-admin cannot change transfer policy ID
1212            let non_admin = Address::random();
1213            let result = path_usd.token.change_transfer_policy_id(
1214                non_admin,
1215                ITIP20::changeTransferPolicyIdCall { newPolicyId: 100 },
1216            );
1217
1218            assert_eq!(
1219                result.unwrap_err(),
1220                TempoPrecompileError::RolesAuthError(RolesAuthError::unauthorized())
1221            );
1222            Ok(())
1223        })
1224    }
1225
1226    #[test]
1227    fn test_transfer_post_allegretto() -> eyre::Result<()> {
1228        let (mut storage, admin) = setup_storage();
1229        storage.set_spec(TempoHardfork::Allegretto);
1230
1231        StorageCtx::enter(&mut storage, || {
1232            let mut path_usd = PathUSD::new();
1233            let sender = Address::random();
1234            let recipient = Address::random();
1235            let amount = U256::from(1000);
1236
1237            path_usd.initialize(admin)?;
1238            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
1239
1240            // Mint to sender without any special roles
1241            path_usd.mint(admin, ITIP20::mintCall { to: sender, amount })?;
1242
1243            // Post-Allegretto: transfer should work without TRANSFER_ROLE
1244            let result = path_usd.transfer(
1245                sender,
1246                ITIP20::transferCall {
1247                    to: recipient,
1248                    amount,
1249                },
1250            )?;
1251
1252            assert!(result);
1253
1254            let sender_balance = path_usd.balance_of(ITIP20::balanceOfCall { account: sender })?;
1255            let recipient_balance =
1256                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
1257
1258            assert_eq!(sender_balance, U256::ZERO);
1259            assert_eq!(recipient_balance, amount);
1260            Ok(())
1261        })
1262    }
1263
1264    #[test]
1265    fn test_transfer_from_post_allegretto() -> eyre::Result<()> {
1266        let (mut storage, admin) = setup_storage();
1267        storage.set_spec(TempoHardfork::Allegretto);
1268
1269        StorageCtx::enter(&mut storage, || {
1270            let mut path_usd = PathUSD::new();
1271            let owner = Address::random();
1272            let spender = Address::random();
1273            let recipient = Address::random();
1274            let amount = U256::from(1000);
1275
1276            path_usd.initialize(admin)?;
1277            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
1278
1279            // Mint to owner and approve spender
1280            path_usd.mint(admin, ITIP20::mintCall { to: owner, amount })?;
1281            path_usd.approve(owner, ITIP20::approveCall { spender, amount })?;
1282
1283            // Post-Allegretto: transfer_from should work without TRANSFER_ROLE
1284            let result = path_usd.transfer_from(
1285                spender,
1286                ITIP20::transferFromCall {
1287                    from: owner,
1288                    to: recipient,
1289                    amount,
1290                },
1291            )?;
1292
1293            assert!(result);
1294
1295            let owner_balance = path_usd.balance_of(ITIP20::balanceOfCall { account: owner })?;
1296            let recipient_balance =
1297                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
1298
1299            assert_eq!(owner_balance, U256::ZERO);
1300            assert_eq!(recipient_balance, amount);
1301            Ok(())
1302        })
1303    }
1304
1305    #[test]
1306    fn test_transfer_with_memo_post_allegretto() -> eyre::Result<()> {
1307        let (mut storage, admin) = setup_storage();
1308        storage.set_spec(TempoHardfork::Allegretto);
1309
1310        StorageCtx::enter(&mut storage, || {
1311            let mut path_usd = PathUSD::new();
1312            let sender = Address::random();
1313            let recipient = Address::random();
1314            let amount = U256::from(1000);
1315            let memo = [1u8; 32];
1316
1317            path_usd.initialize(admin)?;
1318            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
1319
1320            // Mint to sender without any special roles
1321            path_usd.mint(admin, ITIP20::mintCall { to: sender, amount })?;
1322
1323            // Post-Allegretto: transfer_with_memo should work without TRANSFER_ROLE or RECEIVE_WITH_MEMO_ROLE
1324            path_usd.transfer_with_memo(
1325                sender,
1326                ITIP20::transferWithMemoCall {
1327                    to: recipient,
1328                    amount,
1329                    memo: memo.into(),
1330                },
1331            )?;
1332
1333            let sender_balance = path_usd.balance_of(ITIP20::balanceOfCall { account: sender })?;
1334            let recipient_balance =
1335                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
1336
1337            assert_eq!(sender_balance, U256::ZERO);
1338            assert_eq!(recipient_balance, amount);
1339            Ok(())
1340        })
1341    }
1342
1343    #[test]
1344    fn test_transfer_from_with_memo_post_allegretto() -> eyre::Result<()> {
1345        let (mut storage, admin) = setup_storage();
1346        storage.set_spec(TempoHardfork::Allegretto);
1347
1348        StorageCtx::enter(&mut storage, || {
1349            let mut path_usd = PathUSD::new();
1350            let owner = Address::random();
1351            let spender = Address::random();
1352            let recipient = Address::random();
1353            let amount = U256::from(1000);
1354            let memo = [1u8; 32];
1355
1356            path_usd.initialize(admin)?;
1357            path_usd.token.grant_role_internal(admin, *ISSUER_ROLE)?;
1358
1359            // Mint to owner and approve spender
1360            path_usd.mint(admin, ITIP20::mintCall { to: owner, amount })?;
1361            path_usd.approve(owner, ITIP20::approveCall { spender, amount })?;
1362
1363            // Post-Allegretto: transfer_from_with_memo should work without any special roles
1364            let result = path_usd.transfer_from_with_memo(
1365                spender,
1366                ITIP20::transferFromWithMemoCall {
1367                    from: owner,
1368                    to: recipient,
1369                    amount,
1370                    memo: memo.into(),
1371                },
1372            )?;
1373
1374            assert!(result);
1375
1376            let owner_balance = path_usd.balance_of(ITIP20::balanceOfCall { account: owner })?;
1377            let recipient_balance =
1378                path_usd.balance_of(ITIP20::balanceOfCall { account: recipient })?;
1379
1380            assert_eq!(owner_balance, U256::ZERO);
1381            assert_eq!(recipient_balance, amount);
1382            Ok(())
1383        })
1384    }
1385}