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
30pub const U128_MAX: U256 = uint!(0xffffffffffffffffffffffffffffffff_U256);
32
33const TIP20_DECIMALS: u8 = 6;
35
36pub const USD_CURRENCY: &str = "USD";
38
39const TIP20_TOKEN_PREFIX: [u8; 12] = hex!("20C000000000000000000000");
41
42pub fn is_tip20_prefix(token: Address) -> bool {
47 token.as_slice().starts_with(&TIP20_TOKEN_PREFIX)
48}
49
50pub 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 roles: Mapping<Address, Mapping<B256, bool>>,
67 role_admins: Mapping<B256, B256>,
68
69 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 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 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: 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
106pub 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 pub fn pause_role() -> B256 {
166 *PAUSE_ROLE
167 }
168
169 pub fn unpause_role() -> B256 {
174 *UNPAUSE_ROLE
175 }
176
177 pub fn issuer_role() -> B256 {
182 *ISSUER_ROLE
183 }
184
185 pub fn burn_blocked_role() -> B256 {
190 *BURN_BLOCKED_ROLE
191 }
192
193 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 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 if self.storage.spec().is_allegro_moderato() {
270 if !TIP20Factory::new().is_tip20(call.newQuoteToken)? {
272 return Err(TIP20Error::invalid_quote_token().into());
273 }
274 } else {
275 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 if new_token_id >= factory_token_id_counter {
285 return Err(TIP20Error::invalid_quote_token().into());
286 }
287 }
288
289 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 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 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 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 pub fn get_fee_recipient(&self, _msg_sender: Address) -> Result<Address> {
352 self.fee_recipient.read()
353 }
354
355 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 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 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 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 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 if !self.storage.spec().is_allegro_moderato() {
444 self.emit_event(TIP20Event::Mint(ITIP20::Mint { to, amount }))?;
445 }
446 Ok(())
447 }
448
449 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 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 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 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 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 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 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 pub fn approve(&mut self, msg_sender: Address, call: ITIP20::approveCall) -> Result<bool> {
560 if self.storage.spec().is_allegretto() {
562 let old_allowance = self.get_allowance(msg_sender, call.spender)?;
564
565 let mut keychain = AccountKeychain::new();
567 keychain.authorize_approve(msg_sender, self.address, old_allowance, call.amount)?;
568 }
569
570 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 if self.storage.spec().is_allegretto() {
590 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 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 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 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 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
698impl 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 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 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 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 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 self.next_quote_token.write(quote_token)?;
749
750 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 if self.storage.spec().is_allegretto() {
760 self.fee_recipient.write(fee_recipient)?;
761 }
762
763 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 if is_tip20_prefix(to) {
798 return Err(TIP20Error::invalid_recipient().into());
799 }
800 Ok(())
801 }
802
803 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 let from_authorized = registry.is_authorized(ITIP403Registry::isAuthorizedCall {
810 policyId: transfer_policy_id,
811 user: from,
812 })?;
813
814 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 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 let timestamp = self.storage.timestamp();
842 self.accrue(timestamp)?;
843
844 self.handle_rewards_on_transfer(from, to, amount)?;
845
846 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 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 if self.storage.spec().is_moderato() {
877 let current_timestamp = self.storage.timestamp();
879 self.accrue(current_timestamp)?;
880
881 let from_reward_recipient = self.update_rewards(from)?;
883
884 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 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 if refund.is_zero() {
932 return Ok(());
933 }
934
935 if self.storage.spec().is_moderato() {
937 let to_reward_recipient = self.update_rewards(to)?;
940
941 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 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 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 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 token.mint(
1053 admin,
1054 ITIP20::mintCall {
1055 to: admin,
1056 amount: reward_amount,
1057 },
1058 )?;
1059
1060 token.mint(
1062 admin,
1063 ITIP20::mintCall {
1064 to: user,
1065 amount: mint_amount,
1066 },
1067 )?;
1068
1069 token.set_reward_recipient(user, ITIP20::setRewardRecipientCall { recipient: user })?;
1071
1072 let initial_opted_in = token.get_opted_in_supply()?;
1074 assert_eq!(initial_opted_in, mint_amount.to::<u128>());
1075
1076 token.start_reward(
1078 admin,
1079 ITIP20::startRewardCall {
1080 amount: reward_amount,
1081 secs: 100,
1082 },
1083 )?;
1084
1085 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 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 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 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 token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1158
1159 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); 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 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 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 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 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 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 token.set_next_quote_token(
1685 admin,
1686 ITIP20::setNextQuoteTokenCall {
1687 newQuoteToken: quote_token_address,
1688 },
1689 )?;
1690
1691 assert_eq!(token.next_quote_token()?, quote_token_address);
1693
1694 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 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 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 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 token.set_next_quote_token(
1813 admin,
1814 ITIP20::setNextQuoteTokenCall {
1815 newQuoteToken: quote_token_address,
1816 },
1817 )?;
1818
1819 token.complete_quote_token_update(admin, ITIP20::completeQuoteTokenUpdateCall {})?;
1821
1822 assert_eq!(token.quote_token()?, quote_token_address);
1824
1825 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 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 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 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 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 token.set_next_quote_token(
1896 admin,
1897 ITIP20::setNextQuoteTokenCall {
1898 newQuoteToken: quote_token_address,
1899 },
1900 )?;
1901
1902 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 token.initialize(
1948 "Test",
1949 "TST",
1950 ¤cy,
1951 PATH_USD_ADDRESS,
1952 admin,
1953 Address::ZERO,
1954 )?;
1955
1956 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 ¤cy,
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 let token_id = setup_factory_with_token(admin, "TEST", "TST")?;
2007 let token_address = token_id_to_address(token_id);
2008
2009 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 ¤cy,
2046 PATH_USD_ADDRESS,
2047 admin,
2048 Address::ZERO,
2049 )?;
2050
2051 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 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 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 ¤cy_1,
2116 PATH_USD_ADDRESS,
2117 admin,
2118 Address::ZERO,
2119 )?;
2120
2121 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 ¤cy_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 ¤cy,
2163 PATH_USD_ADDRESS,
2164 admin,
2165 Address::ZERO,
2166 )?;
2167
2168 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 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 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 let (token_id, initial_opted_in) =
2244 setup_token_with_rewards(admin, user, mint_amount, reward_amount)?;
2245
2246 StorageCtx.set_spec(TempoHardfork::Moderato);
2248
2249 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 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 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 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 let (token_id, initial_opted_in) =
2286 setup_token_with_rewards(admin, user, mint_amount, reward_amount)?;
2287
2288 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 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 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 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 let (token_id, _initial_opted_in) =
2327 setup_token_with_rewards(admin, user, mint_amount, reward_amount)?;
2328
2329 StorageCtx.set_spec(TempoHardfork::Moderato);
2331 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 let opted_in_after_pre = token.get_opted_in_supply()?;
2338
2339 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 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 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 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 let (token_id, initial_opted_in) =
2377 setup_token_with_rewards(admin, user, mint_amount, reward_amount)?;
2378
2379 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 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 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 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 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_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 token.grant_role_internal(burner, *BURN_BLOCKED_ROLE)?;
2465
2466 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 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 let balance = token.balance_of(ITIP20::balanceOfCall {
2492 account: TIP_FEE_MANAGER_ADDRESS,
2493 })?;
2494 assert_eq!(balance, U256::from(1000));
2495
2496 token.mint(
2498 admin,
2499 ITIP20::mintCall {
2500 to: STABLECOIN_EXCHANGE_ADDRESS,
2501 amount: U256::from(1000),
2502 },
2503 )?;
2504
2505 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 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 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 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 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 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 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}