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
18const NAME_POST_ALLEGRETTO: &str = "pathUSD";
20const 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 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 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 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 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 pub fn pause_role() -> B256 {
229 TIP20Token::pause_role()
230 }
231
232 pub fn unpause_role() -> B256 {
237 TIP20Token::unpause_role()
238 }
239
240 pub fn issuer_role() -> B256 {
245 TIP20Token::issuer_role()
246 }
247
248 pub fn burn_blocked_role() -> B256 {
253 TIP20Token::burn_blocked_role()
254 }
255
256 pub fn transfer_role() -> B256 {
261 *TRANSFER_ROLE
262 }
263
264 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 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 path_usd.token.grant_role(
1059 admin,
1060 IRolesAuth::grantRoleCall {
1061 role: *ISSUER_ROLE,
1062 account: user,
1063 },
1064 )?;
1065
1066 assert!(path_usd.token.has_role(IRolesAuth::hasRoleCall {
1068 role: *ISSUER_ROLE,
1069 account: user,
1070 })?);
1071
1072 path_usd.token.revoke_role(
1074 admin,
1075 IRolesAuth::revokeRoleCall {
1076 role: *ISSUER_ROLE,
1077 account: user,
1078 },
1079 )?;
1080
1081 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 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 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 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 path_usd.token.set_supply_cap(
1159 admin,
1160 ITIP20::setSupplyCapCall {
1161 newSupplyCap: supply_cap,
1162 },
1163 )?;
1164
1165 path_usd.mint(
1167 admin,
1168 ITIP20::mintCall {
1169 to: recipient,
1170 amount: U256::from(1000),
1171 },
1172 )?;
1173
1174 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 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 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 path_usd.mint(admin, ITIP20::mintCall { to: sender, amount })?;
1242
1243 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 path_usd.mint(admin, ITIP20::mintCall { to: owner, amount })?;
1281 path_usd.approve(owner, ITIP20::approveCall { spender, amount })?;
1282
1283 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 path_usd.mint(admin, ITIP20::mintCall { to: sender, amount })?;
1322
1323 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 path_usd.mint(admin, ITIP20::mintCall { to: owner, amount })?;
1361 path_usd.approve(owner, ITIP20::approveCall { spender, amount })?;
1362
1363 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}