1pub mod dispatch;
12pub mod rewards;
13pub mod roles;
14
15use tempo_contracts::precompiles::STABLECOIN_DEX_ADDRESS;
16pub use tempo_contracts::precompiles::{
17 IRolesAuth, ITIP20, RolesAuthError, RolesAuthEvent, TIP20Error, TIP20Event, USD_CURRENCY,
18};
19
20pub use slots as tip20_slots;
22
23use crate::{
24 PATH_USD_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
25 account_keychain::AccountKeychain,
26 address_registry::AddressRegistry,
27 error::{Result, TempoPrecompileError},
28 storage::{Handler, Mapping},
29 tip20::{rewards::UserRewardInfo, roles::DEFAULT_ADMIN_ROLE},
30 tip20_factory::TIP20Factory,
31 tip403_registry::{AuthRole, ITIP403Registry, TIP403Registry},
32};
33use alloy::{
34 primitives::{Address, B256, U256, keccak256, uint},
35 sol_types::SolValue,
36};
37use std::sync::LazyLock;
38use tempo_precompiles_macros::contract;
39use tempo_primitives::TempoAddressExt;
40pub use tempo_primitives::is_tip20_prefix;
41use tracing::trace;
42
43pub const U128_MAX: U256 = uint!(0xffffffffffffffffffffffffffffffff_U256);
45
46use tempo_contracts::precompiles::DECIMALS as TIP20_DECIMALS;
47
48pub fn validate_usd_currency(token: Address) -> Result<()> {
54 if TIP20Token::from_address(token)?.currency()? != USD_CURRENCY {
55 return Err(TIP20Error::invalid_currency().into());
56 }
57 Ok(())
58}
59
60#[contract]
74pub struct TIP20Token {
75 roles: Mapping<Address, Mapping<B256, bool>>,
77 role_admins: Mapping<B256, B256>,
78
79 name: String,
81 symbol: String,
82 currency: String,
83 _domain_separator: B256,
85 quote_token: Address,
86 next_quote_token: Address,
87 transfer_policy_id: u64,
88
89 total_supply: U256,
91 balances: Mapping<Address, U256>,
92 allowances: Mapping<Address, Mapping<Address, U256>>,
93 permit_nonces: Mapping<Address, U256>,
94 paused: bool,
95 supply_cap: U256,
96 _salts: Mapping<B256, bool>,
98
99 global_reward_per_token: U256,
101 opted_in_supply: u128,
102 user_reward_info: Mapping<Address, UserRewardInfo>,
103}
104
105pub static PERMIT_TYPEHASH: LazyLock<B256> = LazyLock::new(|| {
107 keccak256(b"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
108});
109
110pub static EIP712_DOMAIN_TYPEHASH: LazyLock<B256> = LazyLock::new(|| {
112 keccak256(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
113});
114
115pub static VERSION_HASH: LazyLock<B256> = LazyLock::new(|| keccak256(b"1"));
117
118pub static PAUSE_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"PAUSE_ROLE"));
120pub static UNPAUSE_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"UNPAUSE_ROLE"));
122pub static ISSUER_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"ISSUER_ROLE"));
124pub static BURN_BLOCKED_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"BURN_BLOCKED_ROLE"));
126
127impl TIP20Token {
128 pub fn name(&self) -> Result<String> {
130 self.name.read()
131 }
132
133 pub fn symbol(&self) -> Result<String> {
135 self.symbol.read()
136 }
137
138 pub fn decimals(&self) -> Result<u8> {
140 Ok(TIP20_DECIMALS)
141 }
142
143 pub fn currency(&self) -> Result<String> {
145 self.currency.read()
146 }
147
148 pub fn total_supply(&self) -> Result<U256> {
150 self.total_supply.read()
151 }
152
153 pub fn quote_token(&self) -> Result<Address> {
155 self.quote_token.read()
156 }
157
158 pub fn next_quote_token(&self) -> Result<Address> {
160 self.next_quote_token.read()
161 }
162
163 pub fn supply_cap(&self) -> Result<U256> {
165 self.supply_cap.read()
166 }
167
168 pub fn paused(&self) -> Result<bool> {
170 self.paused.read()
171 }
172
173 pub fn transfer_policy_id(&self) -> Result<u64> {
175 self.transfer_policy_id.read()
176 }
177
178 pub fn pause_role() -> B256 {
183 *PAUSE_ROLE
184 }
185
186 pub fn unpause_role() -> B256 {
191 *UNPAUSE_ROLE
192 }
193
194 pub fn issuer_role() -> B256 {
199 *ISSUER_ROLE
200 }
201
202 pub fn burn_blocked_role() -> B256 {
207 *BURN_BLOCKED_ROLE
208 }
209
210 pub fn balance_of(&self, call: ITIP20::balanceOfCall) -> Result<U256> {
212 self.balances[call.account].read()
213 }
214
215 pub fn allowance(&self, call: ITIP20::allowanceCall) -> Result<U256> {
217 self.allowances[call.owner][call.spender].read()
218 }
219
220 pub fn change_transfer_policy_id(
226 &mut self,
227 msg_sender: Address,
228 call: ITIP20::changeTransferPolicyIdCall,
229 ) -> Result<()> {
230 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
231
232 if !TIP403Registry::new().policy_exists(ITIP403Registry::policyExistsCall {
234 policyId: call.newPolicyId,
235 })? {
236 return Err(TIP20Error::invalid_transfer_policy_id().into());
237 }
238
239 self.transfer_policy_id.write(call.newPolicyId)?;
240
241 self.emit_event(TIP20Event::TransferPolicyUpdate(
242 ITIP20::TransferPolicyUpdate {
243 updater: msg_sender,
244 newPolicyId: call.newPolicyId,
245 },
246 ))
247 }
248
249 pub fn set_supply_cap(
256 &mut self,
257 msg_sender: Address,
258 call: ITIP20::setSupplyCapCall,
259 ) -> Result<()> {
260 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
261 if call.newSupplyCap < self.total_supply()? {
262 return Err(TIP20Error::invalid_supply_cap().into());
263 }
264
265 if call.newSupplyCap > U128_MAX {
266 return Err(TIP20Error::supply_cap_exceeded().into());
267 }
268
269 self.supply_cap.write(call.newSupplyCap)?;
270
271 self.emit_event(TIP20Event::SupplyCapUpdate(ITIP20::SupplyCapUpdate {
272 updater: msg_sender,
273 newSupplyCap: call.newSupplyCap,
274 }))
275 }
276
277 pub fn pause(&mut self, msg_sender: Address, _call: ITIP20::pauseCall) -> Result<()> {
282 self.check_role(msg_sender, *PAUSE_ROLE)?;
283 self.paused.write(true)?;
284
285 self.emit_event(TIP20Event::PauseStateUpdate(ITIP20::PauseStateUpdate {
286 updater: msg_sender,
287 isPaused: true,
288 }))
289 }
290
291 pub fn unpause(&mut self, msg_sender: Address, _call: ITIP20::unpauseCall) -> Result<()> {
296 self.check_role(msg_sender, *UNPAUSE_ROLE)?;
297 self.paused.write(false)?;
298
299 self.emit_event(TIP20Event::PauseStateUpdate(ITIP20::PauseStateUpdate {
300 updater: msg_sender,
301 isPaused: false,
302 }))
303 }
304
305 pub fn set_next_quote_token(
314 &mut self,
315 msg_sender: Address,
316 call: ITIP20::setNextQuoteTokenCall,
317 ) -> Result<()> {
318 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
319
320 if self.address == PATH_USD_ADDRESS {
321 return Err(TIP20Error::invalid_quote_token().into());
322 }
323
324 if !TIP20Factory::new().is_tip20(call.newQuoteToken)? {
327 return Err(TIP20Error::invalid_quote_token().into());
328 }
329
330 let currency = self.currency()?;
332 if currency == USD_CURRENCY {
333 let quote_token_currency = Self::from_address(call.newQuoteToken)?.currency()?;
334 if quote_token_currency != USD_CURRENCY {
335 return Err(TIP20Error::invalid_quote_token().into());
336 }
337 }
338
339 self.next_quote_token.write(call.newQuoteToken)?;
340
341 self.emit_event(TIP20Event::NextQuoteTokenSet(ITIP20::NextQuoteTokenSet {
342 updater: msg_sender,
343 nextQuoteToken: call.newQuoteToken,
344 }))
345 }
346
347 pub fn complete_quote_token_update(
354 &mut self,
355 msg_sender: Address,
356 _call: ITIP20::completeQuoteTokenUpdateCall,
357 ) -> Result<()> {
358 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
359
360 let next_quote_token = self.next_quote_token()?;
361
362 let mut current = next_quote_token;
365 while current != PATH_USD_ADDRESS {
366 if current == self.address {
367 return Err(TIP20Error::invalid_quote_token().into());
368 }
369
370 current = Self::from_address(current)?.quote_token()?;
371 }
372
373 self.quote_token.write(next_quote_token)?;
375
376 self.emit_event(TIP20Event::QuoteTokenUpdate(ITIP20::QuoteTokenUpdate {
377 updater: msg_sender,
378 newQuoteToken: next_quote_token,
379 }))
380 }
381
382 pub fn mint(&mut self, msg_sender: Address, call: ITIP20::mintCall) -> Result<()> {
396 let to = Recipient::resolve(call.to)?;
397 self._mint(msg_sender, &to, call.amount)?;
398
399 self.emit_event(TIP20Event::Mint(ITIP20::Mint {
400 to: call.to,
401 amount: call.amount,
402 }))?;
403 if let Some(hop) = to.build_virtual_transfer_event(call.amount) {
404 self.emit_event(hop)?;
405 }
406
407 Ok(())
408 }
409
410 pub fn mint_with_memo(
412 &mut self,
413 msg_sender: Address,
414 call: ITIP20::mintWithMemoCall,
415 ) -> Result<()> {
416 let to = Recipient::resolve(call.to)?;
417 self._mint(msg_sender, &to, call.amount)?;
418
419 self.emit_event(TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
420 from: Address::ZERO,
421 to: call.to,
422 amount: call.amount,
423 memo: call.memo,
424 }))?;
425 self.emit_event(TIP20Event::Mint(ITIP20::Mint {
426 to: call.to,
427 amount: call.amount,
428 }))?;
429 if let Some(hop) = to.build_virtual_transfer_event(call.amount) {
430 self.emit_event(hop)?;
431 }
432 Ok(())
433 }
434
435 fn _mint(&mut self, msg_sender: Address, to: &Recipient, amount: U256) -> Result<()> {
437 self.check_role(msg_sender, *ISSUER_ROLE)?;
438 let total_supply = self.total_supply()?;
439
440 self.validate_mint(to)?;
442
443 let new_supply = total_supply
444 .checked_add(amount)
445 .ok_or(TempoPrecompileError::under_overflow())?;
446
447 let supply_cap = self.supply_cap()?;
448 if new_supply > supply_cap {
449 return Err(TIP20Error::supply_cap_exceeded().into());
450 }
451
452 self.handle_rewards_on_mint(to.target, amount)?;
453
454 self.set_total_supply(new_supply)?;
455 let to_balance = self.get_balance(to.target)?;
456 let new_to_balance: alloy::primitives::Uint<256, 4> = to_balance
457 .checked_add(amount)
458 .ok_or(TempoPrecompileError::under_overflow())?;
459 self.set_balance(to.target, new_to_balance)?;
460
461 self.emit_event(to.build_transfer_event(Address::ZERO, amount))
462 }
463
464 pub fn burn(&mut self, msg_sender: Address, call: ITIP20::burnCall) -> Result<()> {
471 self._burn(msg_sender, call.amount)?;
472 self.emit_event(TIP20Event::Burn(ITIP20::Burn {
473 from: msg_sender,
474 amount: call.amount,
475 }))
476 }
477
478 pub fn burn_with_memo(
480 &mut self,
481 msg_sender: Address,
482 call: ITIP20::burnWithMemoCall,
483 ) -> Result<()> {
484 self._burn(msg_sender, call.amount)?;
485
486 self.emit_event(TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
487 from: msg_sender,
488 to: Address::ZERO,
489 amount: call.amount,
490 memo: call.memo,
491 }))?;
492 self.emit_event(TIP20Event::Burn(ITIP20::Burn {
493 from: msg_sender,
494 amount: call.amount,
495 }))
496 }
497
498 pub fn burn_blocked(
506 &mut self,
507 msg_sender: Address,
508 call: ITIP20::burnBlockedCall,
509 ) -> Result<()> {
510 if self.storage.spec().is_t3() {
512 self.check_not_paused()?;
513 }
514 self.check_role(msg_sender, *BURN_BLOCKED_ROLE)?;
515
516 if matches!(call.from, TIP_FEE_MANAGER_ADDRESS | STABLECOIN_DEX_ADDRESS) {
518 return Err(TIP20Error::protected_address().into());
519 }
520
521 let policy_id = self.transfer_policy_id()?;
523 if TIP403Registry::new().is_authorized_as(policy_id, call.from, AuthRole::sender())? {
524 return Err(TIP20Error::policy_forbids().into());
526 }
527
528 self._transfer(call.from, &Recipient::direct(Address::ZERO), call.amount)?;
529
530 let total_supply = self.total_supply()?;
531 let new_supply =
532 total_supply
533 .checked_sub(call.amount)
534 .ok_or(TIP20Error::insufficient_balance(
535 total_supply,
536 call.amount,
537 self.address,
538 ))?;
539 self.set_total_supply(new_supply)?;
540
541 self.emit_event(TIP20Event::BurnBlocked(ITIP20::BurnBlocked {
542 from: call.from,
543 amount: call.amount,
544 }))
545 }
546
547 fn _burn(&mut self, msg_sender: Address, amount: U256) -> Result<()> {
548 if self.storage.spec().is_t3() {
550 self.check_not_paused()?;
551 }
552 self.check_role(msg_sender, *ISSUER_ROLE)?;
553
554 self._transfer(msg_sender, &Recipient::direct(Address::ZERO), amount)?;
555
556 let total_supply = self.total_supply()?;
557 let new_supply =
558 total_supply
559 .checked_sub(amount)
560 .ok_or(TIP20Error::insufficient_balance(
561 total_supply,
562 amount,
563 self.address,
564 ))?;
565 self.set_total_supply(new_supply)
566 }
567
568 pub fn approve(&mut self, msg_sender: Address, call: ITIP20::approveCall) -> Result<bool> {
575 AccountKeychain::new().authorize_approve(
577 msg_sender,
578 self.address,
579 self.get_allowance(msg_sender, call.spender)?,
580 call.amount,
581 )?;
582
583 self.set_allowance(msg_sender, call.spender, call.amount)?;
585
586 self.emit_event(TIP20Event::Approval(ITIP20::Approval {
587 owner: msg_sender,
588 spender: call.spender,
589 amount: call.amount,
590 }))?;
591
592 Ok(true)
593 }
594
595 pub fn nonces(&self, call: ITIP20::noncesCall) -> Result<U256> {
599 self.permit_nonces[call.owner].read()
600 }
601
602 pub fn domain_separator(&self) -> Result<B256> {
604 let name = self.name()?;
605 let name_hash = self.storage.keccak256(name.as_bytes())?;
606 let chain_id = U256::from(self.storage.chain_id());
607
608 let encoded = (
609 *EIP712_DOMAIN_TYPEHASH,
610 name_hash,
611 *VERSION_HASH,
612 chain_id,
613 self.address,
614 )
615 .abi_encode();
616
617 self.storage.keccak256(&encoded)
618 }
619
620 pub fn permit(&mut self, call: ITIP20::permitCall) -> Result<()> {
629 if self.storage.timestamp() > call.deadline {
631 return Err(TIP20Error::permit_expired().into());
632 }
633
634 let nonce = self.permit_nonces[call.owner].read()?;
636 let struct_hash = self.storage.keccak256(
637 &(
638 *PERMIT_TYPEHASH,
639 call.owner,
640 call.spender,
641 call.value,
642 nonce,
643 call.deadline,
644 )
645 .abi_encode(),
646 )?;
647
648 let domain_separator = self.domain_separator()?;
650 let digest = self.storage.keccak256(
651 &[
652 &[0x19, 0x01],
653 domain_separator.as_slice(),
654 struct_hash.as_slice(),
655 ]
656 .concat(),
657 )?;
658
659 let recovered = self
662 .storage
663 .recover_signer(digest, call.v, call.r, call.s)?
664 .ok_or(TIP20Error::invalid_signature())?;
665 if recovered != call.owner {
666 return Err(TIP20Error::invalid_signature().into());
667 }
668
669 self.permit_nonces[call.owner].write(
671 nonce
672 .checked_add(U256::from(1))
673 .ok_or(TempoPrecompileError::under_overflow())?,
674 )?;
675
676 self.set_allowance(call.owner, call.spender, call.value)?;
678
679 self.emit_event(TIP20Event::Approval(ITIP20::Approval {
681 owner: call.owner,
682 spender: call.spender,
683 amount: call.value,
684 }))
685 }
686
687 pub fn transfer(&mut self, msg_sender: Address, call: ITIP20::transferCall) -> Result<bool> {
697 trace!(%msg_sender, ?call, "transferring TIP20");
698 let to = Recipient::resolve(call.to)?;
699 self.validate_transfer(msg_sender, &to)?;
700 self.check_and_update_spending_limit(msg_sender, call.amount)?;
701
702 self._transfer(msg_sender, &to, call.amount)?;
703 if let Some(hop) = to.build_virtual_transfer_event(call.amount) {
704 self.emit_event(hop)?;
705 }
706 Ok(true)
707 }
708
709 pub fn transfer_from(
719 &mut self,
720 msg_sender: Address,
721 call: ITIP20::transferFromCall,
722 ) -> Result<bool> {
723 let to = Recipient::resolve(call.to)?;
724 self._transfer_from(msg_sender, call.from, &to, call.amount)?;
725 if let Some(hop) = to.build_virtual_transfer_event(call.amount) {
726 self.emit_event(hop)?;
727 }
728 Ok(true)
729 }
730
731 pub fn transfer_from_with_memo(
733 &mut self,
734 msg_sender: Address,
735 call: ITIP20::transferFromWithMemoCall,
736 ) -> Result<bool> {
737 let to = Recipient::resolve(call.to)?;
738 self._transfer_from(msg_sender, call.from, &to, call.amount)?;
739
740 self.emit_event(TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
741 from: call.from,
742 to: call.to,
743 amount: call.amount,
744 memo: call.memo,
745 }))?;
746 if let Some(hop) = to.build_virtual_transfer_event(call.amount) {
747 self.emit_event(hop)?;
748 }
749 Ok(true)
750 }
751
752 pub fn system_transfer_from(
763 &mut self,
764 from: Address,
765 to: Address,
766 amount: U256,
767 ) -> Result<bool> {
768 let to = Recipient::resolve(to)?;
769 self.validate_transfer(from, &to)?;
770 self.check_and_update_spending_limit(from, amount)?;
771
772 self._transfer(from, &to, amount)?;
773 if let Some(hop) = to.build_virtual_transfer_event(amount) {
774 self.emit_event(hop)?;
775 }
776
777 Ok(true)
778 }
779
780 fn _transfer_from(
781 &mut self,
782 msg_sender: Address,
783 from: Address,
784 to: &Recipient,
785 amount: U256,
786 ) -> Result<bool> {
787 self.validate_transfer(from, to)?;
788
789 let allowed = self.get_allowance(from, msg_sender)?;
790 if amount > allowed {
791 return Err(TIP20Error::insufficient_allowance().into());
792 }
793
794 if allowed != U256::MAX {
795 let new_allowance = allowed
796 .checked_sub(amount)
797 .ok_or(TIP20Error::insufficient_allowance())?;
798 self.set_allowance(from, msg_sender, new_allowance)?;
799 }
800
801 self._transfer(from, to, amount)?;
802
803 Ok(true)
804 }
805
806 pub fn transfer_with_memo(
808 &mut self,
809 msg_sender: Address,
810 call: ITIP20::transferWithMemoCall,
811 ) -> Result<()> {
812 let to = Recipient::resolve(call.to)?;
813 self.validate_transfer(msg_sender, &to)?;
814 self.check_and_update_spending_limit(msg_sender, call.amount)?;
815
816 self._transfer(msg_sender, &to, call.amount)?;
817
818 self.emit_event(TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
819 from: msg_sender,
820 to: call.to,
821 amount: call.amount,
822 memo: call.memo,
823 }))?;
824 if let Some(hop) = to.build_virtual_transfer_event(call.amount) {
825 self.emit_event(hop)?;
826 }
827 Ok(())
828 }
829}
830
831impl TIP20Token {
833 pub fn from_address(address: Address) -> Result<Self> {
838 if !address.is_tip20() {
839 return Err(TIP20Error::invalid_token().into());
840 }
841 Ok(Self::__new(address))
842 }
843
844 #[inline]
849 pub fn from_address_unchecked(address: Address) -> Self {
850 debug_assert!(address.is_tip20(), "address must have TIP20 prefix");
851 Self::__new(address)
852 }
853
854 pub fn initialize(
857 &mut self,
858 msg_sender: Address,
859 name: &str,
860 symbol: &str,
861 currency: &str,
862 quote_token: Address,
863 admin: Address,
864 ) -> Result<()> {
865 trace!(%name, address=%self.address, "Initializing token");
866
867 self.__initialize()?;
869
870 self.name.write(name.to_string())?;
871 self.symbol.write(symbol.to_string())?;
872 self.currency.write(currency.to_string())?;
873
874 self.quote_token.write(quote_token)?;
875 self.next_quote_token.write(quote_token)?;
877
878 self.supply_cap.write(U128_MAX)?;
880 self.transfer_policy_id.write(1)?;
881
882 self.initialize_roles()?;
884 self.grant_default_admin(msg_sender, admin)
885 }
886
887 fn get_balance(&self, account: Address) -> Result<U256> {
888 self.balances[account].read()
889 }
890
891 fn set_balance(&mut self, account: Address, amount: U256) -> Result<()> {
892 self.balances[account].write(amount)
893 }
894
895 fn get_allowance(&self, owner: Address, spender: Address) -> Result<U256> {
896 self.allowances[owner][spender].read()
897 }
898
899 fn set_allowance(&mut self, owner: Address, spender: Address, amount: U256) -> Result<()> {
900 self.allowances[owner][spender].write(amount)
901 }
902
903 fn set_total_supply(&mut self, amount: U256) -> Result<()> {
904 self.total_supply.write(amount)
905 }
906
907 pub fn check_not_paused(&self) -> Result<()> {
908 if self.paused()? {
909 return Err(TIP20Error::contract_paused().into());
910 }
911 Ok(())
912 }
913
914 fn validate_transfer(&self, from: Address, to: &Recipient) -> Result<()> {
917 self.check_not_paused()?;
918 to.validate()?;
919 self.ensure_transfer_authorized(from, to.target)
920 }
921
922 fn validate_mint(&self, to: &Recipient) -> Result<()> {
925 if self.storage.spec().is_t3() {
926 self.check_not_paused()?;
927 to.validate()?;
928 }
929
930 if !TIP403Registry::new().is_authorized_as(
932 self.transfer_policy_id()?,
933 to.target,
934 AuthRole::mint_recipient(),
935 )? {
936 return Err(TIP20Error::policy_forbids().into());
937 }
938
939 Ok(())
940 }
941
942 pub fn is_transfer_authorized(&self, from: Address, to: Address) -> Result<bool> {
947 let policy_id = self.transfer_policy_id()?;
948 let registry = TIP403Registry::new();
949
950 let sender_auth = registry.is_authorized_as(policy_id, from, AuthRole::sender())?;
952 if self.storage.spec().is_t2() && !sender_auth {
953 return Ok(false);
954 }
955 let recipient_auth = registry.is_authorized_as(policy_id, to, AuthRole::recipient())?;
956 Ok(sender_auth && recipient_auth)
957 }
958
959 pub fn ensure_transfer_authorized(&self, from: Address, to: Address) -> Result<()> {
964 if !self.is_transfer_authorized(from, to)? {
965 return Err(TIP20Error::policy_forbids().into());
966 }
967
968 Ok(())
969 }
970
971 pub fn check_and_update_spending_limit(&mut self, from: Address, amount: U256) -> Result<()> {
976 AccountKeychain::new().authorize_transfer(from, self.address, amount)
977 }
978
979 fn _transfer(&mut self, from: Address, to: &Recipient, amount: U256) -> Result<()> {
984 let from_balance = self.get_balance(from)?;
985 if amount > from_balance {
986 return Err(
987 TIP20Error::insufficient_balance(from_balance, amount, self.address).into(),
988 );
989 }
990
991 self.handle_rewards_on_transfer(from, to.target, amount)?;
992
993 let new_from_balance = from_balance
995 .checked_sub(amount)
996 .ok_or(TempoPrecompileError::under_overflow())?;
997
998 self.set_balance(from, new_from_balance)?;
999
1000 if to.target != Address::ZERO {
1001 let to_balance = self.get_balance(to.target)?;
1002 let new_to_balance = to_balance
1003 .checked_add(amount)
1004 .ok_or(TempoPrecompileError::under_overflow())?;
1005
1006 self.set_balance(to.target, new_to_balance)?;
1007 }
1008
1009 self.emit_event(to.build_transfer_event(from, amount))
1010 }
1011
1012 pub fn transfer_fee_pre_tx(&mut self, from: Address, amount: U256) -> Result<()> {
1020 self.check_not_paused()?;
1025 let from_balance = self.get_balance(from)?;
1026 if amount > from_balance {
1027 return Err(
1028 TIP20Error::insufficient_balance(from_balance, amount, self.address).into(),
1029 );
1030 }
1031
1032 self.check_and_update_spending_limit(from, amount)?;
1033
1034 let from_reward_recipient = self.update_rewards(from)?;
1036
1037 if from_reward_recipient != Address::ZERO {
1039 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
1040 .checked_sub(amount)
1041 .ok_or(TempoPrecompileError::under_overflow())?;
1042 self.set_opted_in_supply(
1043 opted_in_supply
1044 .try_into()
1045 .map_err(|_| TempoPrecompileError::under_overflow())?,
1046 )?;
1047 }
1048
1049 let new_from_balance =
1050 from_balance
1051 .checked_sub(amount)
1052 .ok_or(TIP20Error::insufficient_balance(
1053 from_balance,
1054 amount,
1055 self.address,
1056 ))?;
1057
1058 self.set_balance(from, new_from_balance)?;
1059
1060 let to_balance = self.get_balance(TIP_FEE_MANAGER_ADDRESS)?;
1061 let new_to_balance = to_balance
1062 .checked_add(amount)
1063 .ok_or(TIP20Error::supply_cap_exceeded())?;
1064 self.set_balance(TIP_FEE_MANAGER_ADDRESS, new_to_balance)
1065 }
1066
1067 pub fn transfer_fee_post_tx(
1072 &mut self,
1073 to: Address,
1074 refund: U256,
1075 actual_spending: U256,
1076 ) -> Result<()> {
1077 self.emit_event(TIP20Event::Transfer(ITIP20::Transfer {
1078 from: to,
1079 to: TIP_FEE_MANAGER_ADDRESS,
1080 amount: actual_spending,
1081 }))?;
1082
1083 if refund.is_zero() {
1085 return Ok(());
1086 }
1087
1088 if self.storage.spec().is_t1c() {
1089 AccountKeychain::new().refund_spending_limit(to, self.address, refund)?;
1090 }
1091
1092 let to_reward_recipient = self.update_rewards(to)?;
1094
1095 if to_reward_recipient != Address::ZERO {
1097 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
1098 .checked_add(refund)
1099 .ok_or(TempoPrecompileError::under_overflow())?;
1100 self.set_opted_in_supply(
1101 opted_in_supply
1102 .try_into()
1103 .map_err(|_| TempoPrecompileError::under_overflow())?,
1104 )?;
1105 }
1106
1107 let from_balance = self.get_balance(TIP_FEE_MANAGER_ADDRESS)?;
1108 let new_from_balance =
1109 from_balance
1110 .checked_sub(refund)
1111 .ok_or(TIP20Error::insufficient_balance(
1112 from_balance,
1113 refund,
1114 self.address,
1115 ))?;
1116
1117 self.set_balance(TIP_FEE_MANAGER_ADDRESS, new_from_balance)?;
1118
1119 let to_balance = self.get_balance(to)?;
1120 let new_to_balance = to_balance
1121 .checked_add(refund)
1122 .ok_or(TIP20Error::supply_cap_exceeded())?;
1123 self.set_balance(to, new_to_balance)
1124 }
1125}
1126
1127#[derive(Debug, PartialEq)]
1134pub(crate) struct Recipient {
1135 pub(crate) target: Address,
1137 pub(crate) virtual_addr: Option<Address>,
1139}
1140
1141impl Recipient {
1142 #[inline]
1144 pub(crate) fn direct(addr: Address) -> Self {
1145 Self {
1146 target: addr,
1147 virtual_addr: None,
1148 }
1149 }
1150
1151 pub(crate) fn resolve(addr: Address) -> Result<Self> {
1156 let effective = AddressRegistry::new().resolve_recipient(addr)?;
1157 Ok(if effective == addr {
1158 Self::direct(addr)
1159 } else {
1160 Self {
1161 target: effective,
1162 virtual_addr: Some(addr),
1163 }
1164 })
1165 }
1166
1167 pub(crate) fn validate(&self) -> Result<()> {
1171 if self.target.is_zero() || self.target.is_tip20() {
1172 return Err(TIP20Error::invalid_recipient().into());
1173 }
1174 Ok(())
1175 }
1176
1177 pub(crate) fn build_transfer_event(&self, from: Address, amount: U256) -> TIP20Event {
1182 TIP20Event::Transfer(ITIP20::Transfer {
1183 from,
1184 to: self.virtual_addr.unwrap_or(self.target),
1185 amount,
1186 })
1187 }
1188
1189 pub(crate) fn build_virtual_transfer_event(&self, amount: U256) -> Option<TIP20Event> {
1192 self.virtual_addr.map(|virtual_addr| {
1193 TIP20Event::Transfer(ITIP20::Transfer {
1194 from: virtual_addr,
1195 to: self.target,
1196 amount,
1197 })
1198 })
1199 }
1200}
1201
1202#[cfg(test)]
1203mod recipient_tests {
1204 use super::*;
1205 use crate::{
1206 address_registry::{AddressRegistry, MasterId, UserTag},
1207 error::TempoPrecompileError,
1208 storage::{StorageCtx, hashmap::HashMapStorageProvider},
1209 test_util::{VIRTUAL_MASTER, register_virtual_master},
1210 };
1211 use alloy::primitives::{Address, U256};
1212 use tempo_chainspec::hardfork::TempoHardfork;
1213
1214 #[test]
1215 fn test_resolve() -> eyre::Result<()> {
1216 let addr = Address::repeat_byte(0x11);
1218 assert_eq!(
1219 Recipient::direct(addr),
1220 Recipient {
1221 target: addr,
1222 virtual_addr: None
1223 }
1224 );
1225
1226 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
1228 StorageCtx::enter(&mut storage, || {
1229 let r = Recipient::resolve(addr)?;
1230 assert_eq!(
1231 r,
1232 Recipient {
1233 target: addr,
1234 virtual_addr: None
1235 }
1236 );
1237
1238 let mut registry = AddressRegistry::new();
1240 let (_, virtual_addr) = register_virtual_master(&mut registry)?;
1241 let r = Recipient::resolve(virtual_addr)?;
1242 assert_eq!(
1243 r,
1244 Recipient {
1245 target: VIRTUAL_MASTER,
1246 virtual_addr: Some(virtual_addr)
1247 }
1248 );
1249
1250 let unregistered = Address::new_virtual(MasterId::ZERO, UserTag::ZERO);
1252 assert!(Recipient::resolve(unregistered).is_err());
1253
1254 Ok::<_, TempoPrecompileError>(())
1255 })?;
1256
1257 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
1259 StorageCtx::enter(&mut storage, || {
1260 let virtual_addr = Address::new_virtual(MasterId::ZERO, UserTag::ZERO);
1261 let r = Recipient::resolve(virtual_addr)?;
1262 assert_eq!(
1263 r,
1264 Recipient {
1265 target: virtual_addr,
1266 virtual_addr: None
1267 }
1268 );
1269 Ok::<_, TempoPrecompileError>(())
1270 })?;
1271 Ok(())
1272 }
1273
1274 #[test]
1275 fn test_validate() {
1276 assert!(Recipient::direct(Address::ZERO).validate().is_err());
1277 assert!(
1278 Recipient::direct(crate::PATH_USD_ADDRESS)
1279 .validate()
1280 .is_err()
1281 );
1282 assert!(
1283 Recipient::direct(Address::repeat_byte(0x11))
1284 .validate()
1285 .is_ok()
1286 );
1287 }
1288
1289 #[test]
1290 fn test_build_events() {
1291 let from = Address::repeat_byte(0x01);
1292 let target = Address::repeat_byte(0x02);
1293 let vaddr = Address::repeat_byte(0x03);
1294 let amount = U256::from(42);
1295
1296 let direct = Recipient::direct(target);
1297 let virt = Recipient {
1298 target,
1299 virtual_addr: Some(vaddr),
1300 };
1301
1302 assert!(matches!(direct.build_transfer_event(from, amount),
1304 TIP20Event::Transfer(ITIP20::Transfer { to, .. }) if to == target));
1305 assert!(matches!(virt.build_transfer_event(from, amount),
1306 TIP20Event::Transfer(ITIP20::Transfer { to, .. }) if to == vaddr));
1307
1308 assert!(direct.build_virtual_transfer_event(amount).is_none());
1310 let hop = virt.build_virtual_transfer_event(amount).unwrap();
1311 assert!(matches!(hop,
1312 TIP20Event::Transfer(ITIP20::Transfer { from, to, .. })
1313 if from == vaddr && to == target));
1314 }
1315}
1316
1317#[cfg(test)]
1318pub(crate) mod tests {
1319 use alloy::primitives::{Address, FixedBytes, IntoLogData, U256, hex};
1320 use tempo_contracts::precompiles::ITIP20Factory;
1321
1322 use super::*;
1323 use crate::{
1324 PATH_USD_ADDRESS,
1325 account_keychain::{
1326 AccountKeychain, KeyRestrictions, SignatureType, TokenLimit, authorizeKeyCall,
1327 getRemainingLimitCall,
1328 },
1329 address_registry::{AddressRegistry, MasterId, UserTag},
1330 error::TempoPrecompileError,
1331 storage::{StorageCtx, hashmap::HashMapStorageProvider},
1332 test_util::{TIP20Setup, VIRTUAL_MASTER, register_virtual_master, setup_storage},
1333 };
1334 use rand_08::{Rng, distributions::Alphanumeric, thread_rng};
1335 use tempo_chainspec::hardfork::TempoHardfork;
1336
1337 #[test]
1338 fn test_mint_increases_balance_and_supply() -> eyre::Result<()> {
1339 let (mut storage, admin) = setup_storage();
1340 let addr = Address::random();
1341 let amount = U256::random() % U256::from(u128::MAX);
1342
1343 StorageCtx::enter(&mut storage, || {
1344 let mut token = TIP20Setup::create("Test", "TST", admin)
1345 .with_issuer(admin)
1346 .clear_events()
1347 .apply()?;
1348
1349 token.mint(admin, ITIP20::mintCall { to: addr, amount })?;
1350
1351 assert_eq!(token.get_balance(addr)?, amount);
1352 assert_eq!(token.total_supply()?, amount);
1353
1354 token.assert_emitted_events(vec![
1355 TIP20Event::Transfer(ITIP20::Transfer {
1356 from: Address::ZERO,
1357 to: addr,
1358 amount,
1359 }),
1360 TIP20Event::Mint(ITIP20::Mint { to: addr, amount }),
1361 ]);
1362
1363 Ok(())
1364 })
1365 }
1366
1367 #[test]
1368 fn test_transfer_moves_balance() -> eyre::Result<()> {
1369 let (mut storage, admin) = setup_storage();
1370 let from = Address::random();
1371 let to = Address::random();
1372 let amount = U256::random() % U256::from(u128::MAX);
1373
1374 StorageCtx::enter(&mut storage, || {
1375 let mut token = TIP20Setup::create("Test", "TST", admin)
1376 .with_issuer(admin)
1377 .with_mint(from, amount)
1378 .clear_events()
1379 .apply()?;
1380
1381 token.transfer(from, ITIP20::transferCall { to, amount })?;
1382
1383 assert_eq!(token.get_balance(from)?, U256::ZERO);
1384 assert_eq!(token.get_balance(to)?, amount);
1385 assert_eq!(token.total_supply()?, amount); token.assert_emitted_events(vec![TIP20Event::Transfer(ITIP20::Transfer {
1388 from,
1389 to,
1390 amount,
1391 })]);
1392
1393 Ok(())
1394 })
1395 }
1396
1397 #[test]
1398 fn test_transfer_insufficient_balance_fails() -> eyre::Result<()> {
1399 let (mut storage, admin) = setup_storage();
1400 let from = Address::random();
1401 let to = Address::random();
1402 let amount = U256::random() % U256::from(u128::MAX);
1403
1404 StorageCtx::enter(&mut storage, || {
1405 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1406
1407 let result = token.transfer(from, ITIP20::transferCall { to, amount });
1408 assert!(matches!(
1409 result,
1410 Err(TempoPrecompileError::TIP20(
1411 TIP20Error::InsufficientBalance(_)
1412 ))
1413 ));
1414
1415 Ok(())
1416 })
1417 }
1418
1419 #[test]
1420 fn test_mint_with_memo() -> eyre::Result<()> {
1421 let mut storage = HashMapStorageProvider::new(1);
1422 let admin = Address::random();
1423 let amount = U256::random() % U256::from(u128::MAX);
1424 let to = Address::random();
1425 let memo = FixedBytes::random();
1426
1427 StorageCtx::enter(&mut storage, || {
1428 let mut token = TIP20Setup::create("Test", "TST", admin)
1429 .with_issuer(admin)
1430 .clear_events()
1431 .apply()?;
1432
1433 token.mint_with_memo(admin, ITIP20::mintWithMemoCall { to, amount, memo })?;
1434
1435 token.assert_emitted_events(vec![
1437 TIP20Event::Transfer(ITIP20::Transfer {
1438 from: Address::ZERO,
1439 to,
1440 amount,
1441 }),
1442 TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
1443 from: Address::ZERO,
1444 to,
1445 amount,
1446 memo,
1447 }),
1448 TIP20Event::Mint(ITIP20::Mint { to, amount }),
1449 ]);
1450
1451 Ok(())
1452 })
1453 }
1454
1455 #[test]
1456 fn test_burn_with_memo() -> eyre::Result<()> {
1457 let mut storage = HashMapStorageProvider::new(1);
1458 let admin = Address::random();
1459 let amount = U256::random() % U256::from(u128::MAX);
1460 let memo = FixedBytes::random();
1461
1462 StorageCtx::enter(&mut storage, || {
1463 let mut token = TIP20Setup::create("Test", "TST", admin)
1464 .with_issuer(admin)
1465 .with_mint(admin, amount)
1466 .clear_events()
1467 .apply()?;
1468
1469 token.burn_with_memo(admin, ITIP20::burnWithMemoCall { amount, memo })?;
1470 token.assert_emitted_events(vec![
1471 TIP20Event::Transfer(ITIP20::Transfer {
1472 from: admin,
1473 to: Address::ZERO,
1474 amount,
1475 }),
1476 TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
1477 from: admin,
1478 to: Address::ZERO,
1479 amount,
1480 memo,
1481 }),
1482 TIP20Event::Burn(ITIP20::Burn {
1483 from: admin,
1484 amount,
1485 }),
1486 ]);
1487
1488 Ok(())
1489 })
1490 }
1491
1492 #[test]
1493 fn test_transfer_from_with_memo_from_address() -> eyre::Result<()> {
1494 let mut storage = HashMapStorageProvider::new(1);
1495 let admin = Address::random();
1496 let owner = Address::random();
1497 let spender = Address::random();
1498 let to = Address::random();
1499 let memo = FixedBytes::random();
1500 let amount = U256::random() % U256::from(u128::MAX);
1501
1502 StorageCtx::enter(&mut storage, || {
1503 let mut token = TIP20Setup::create("Test", "TST", admin)
1504 .with_issuer(admin)
1505 .with_mint(owner, amount)
1506 .with_approval(owner, spender, amount)
1507 .clear_events()
1508 .apply()?;
1509
1510 token.transfer_from_with_memo(
1511 spender,
1512 ITIP20::transferFromWithMemoCall {
1513 from: owner,
1514 to,
1515 amount,
1516 memo,
1517 },
1518 )?;
1519
1520 token.assert_emitted_events(vec![
1522 TIP20Event::Transfer(ITIP20::Transfer {
1523 from: owner,
1524 to,
1525 amount,
1526 }),
1527 TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
1528 from: owner,
1529 to,
1530 amount,
1531 memo,
1532 }),
1533 ]);
1534
1535 Ok(())
1536 })
1537 }
1538
1539 #[test]
1540 fn test_transfer_fee_pre_tx() -> eyre::Result<()> {
1541 let mut storage = HashMapStorageProvider::new(1);
1542 let admin = Address::random();
1543 let user = Address::random();
1544 let amount = U256::from(100);
1545 let fee_amount = amount / U256::from(2);
1546
1547 StorageCtx::enter(&mut storage, || {
1548 let mut token = TIP20Setup::create("Test", "TST", admin)
1549 .with_issuer(admin)
1550 .with_mint(user, amount)
1551 .apply()?;
1552
1553 token.transfer_fee_pre_tx(user, fee_amount)?;
1554
1555 assert_eq!(token.get_balance(user)?, fee_amount);
1556 assert_eq!(token.get_balance(TIP_FEE_MANAGER_ADDRESS)?, fee_amount);
1557
1558 Ok(())
1559 })
1560 }
1561
1562 #[test]
1563 fn test_transfer_fee_pre_tx_insufficient_balance() -> eyre::Result<()> {
1564 let mut storage = HashMapStorageProvider::new(1);
1565 let admin = Address::random();
1566 let user = Address::random();
1567 let amount = U256::from(100);
1568 let fee_amount = amount / U256::from(2);
1569
1570 StorageCtx::enter(&mut storage, || {
1571 let mut token = TIP20Setup::create("Test", "TST", admin)
1572 .with_issuer(admin)
1573 .apply()?;
1574
1575 assert_eq!(
1576 token.transfer_fee_pre_tx(user, fee_amount),
1577 Err(TempoPrecompileError::TIP20(
1578 TIP20Error::insufficient_balance(U256::ZERO, fee_amount, token.address)
1579 ))
1580 );
1581 Ok(())
1582 })
1583 }
1584
1585 #[test]
1586 fn test_transfer_fee_pre_tx_paused() -> eyre::Result<()> {
1587 let mut storage = HashMapStorageProvider::new(1);
1588 let admin = Address::random();
1589 let user = Address::random();
1590 let amount = U256::from(100);
1591 let fee_amount = amount / U256::from(2);
1592
1593 StorageCtx::enter(&mut storage, || {
1594 let mut token = TIP20Setup::create("Test", "TST", admin)
1595 .with_issuer(admin)
1596 .with_role(admin, *PAUSE_ROLE)
1597 .with_mint(user, amount)
1598 .apply()?;
1599
1600 token.pause(admin, ITIP20::pauseCall {})?;
1602
1603 assert_eq!(
1605 token.transfer_fee_pre_tx(user, fee_amount),
1606 Err(TempoPrecompileError::TIP20(TIP20Error::contract_paused()))
1607 );
1608 Ok(())
1609 })
1610 }
1611
1612 #[test]
1613 fn test_transfer_fee_post_tx() -> eyre::Result<()> {
1614 let mut storage = HashMapStorageProvider::new(1);
1615 let admin = Address::random();
1616 let user = Address::random();
1617 let initial_fee = U256::from(100);
1618 let refund_amount = U256::from(30);
1619 let gas_used = U256::from(10);
1620
1621 StorageCtx::enter(&mut storage, || {
1622 let mut token = TIP20Setup::create("Test", "TST", admin)
1623 .with_issuer(admin)
1624 .with_mint(TIP_FEE_MANAGER_ADDRESS, initial_fee)
1625 .apply()?;
1626
1627 token.transfer_fee_post_tx(user, refund_amount, gas_used)?;
1628
1629 assert_eq!(token.get_balance(user)?, refund_amount);
1630 assert_eq!(
1631 token.get_balance(TIP_FEE_MANAGER_ADDRESS)?,
1632 initial_fee - refund_amount
1633 );
1634 assert_eq!(
1635 token.emitted_events().last().unwrap(),
1636 &TIP20Event::Transfer(ITIP20::Transfer {
1637 from: user,
1638 to: TIP_FEE_MANAGER_ADDRESS,
1639 amount: gas_used
1640 })
1641 .into_log_data()
1642 );
1643
1644 Ok(())
1645 })
1646 }
1647
1648 #[test]
1649 fn test_transfer_fee_post_tx_refunds_spending_limit() -> eyre::Result<()> {
1650 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
1651 let admin = Address::random();
1652 let user = Address::random();
1653 let access_key = Address::random();
1654 let max_fee = U256::from(1000);
1655 let refund_amount = U256::from(300);
1656 let gas_used = U256::from(100);
1657
1658 StorageCtx::enter(&mut storage, || {
1659 let mut token = TIP20Setup::create("Test", "TST", admin)
1660 .with_issuer(admin)
1661 .with_mint(TIP_FEE_MANAGER_ADDRESS, max_fee)
1662 .apply()?;
1663
1664 let token_address = token.address;
1665 let spending_limit = U256::from(2000);
1666
1667 let mut keychain = AccountKeychain::new();
1669 keychain.initialize()?;
1670 keychain.set_transaction_key(Address::ZERO)?;
1671
1672 keychain.authorize_key(
1673 user,
1674 authorizeKeyCall {
1675 keyId: access_key,
1676 signatureType: SignatureType::Secp256k1,
1677 config: KeyRestrictions {
1678 expiry: u64::MAX,
1679 enforceLimits: true,
1680 limits: vec![TokenLimit {
1681 token: token_address,
1682 amount: spending_limit,
1683 period: 0,
1684 }],
1685 allowAnyCalls: true,
1686 allowedCalls: vec![],
1687 },
1688 },
1689 )?;
1690
1691 keychain.set_transaction_key(access_key)?;
1693 keychain.set_tx_origin(user)?;
1694 keychain.authorize_transfer(user, token_address, max_fee)?;
1695
1696 let remaining_after_deduction =
1697 keychain.get_remaining_limit(getRemainingLimitCall {
1698 account: user,
1699 keyId: access_key,
1700 token: token_address,
1701 })?;
1702 assert_eq!(remaining_after_deduction, spending_limit - max_fee);
1703
1704 token.transfer_fee_post_tx(user, refund_amount, gas_used)?;
1706
1707 let remaining_after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
1708 account: user,
1709 keyId: access_key,
1710 token: token_address,
1711 })?;
1712 assert_eq!(
1713 remaining_after_refund,
1714 spending_limit - max_fee + refund_amount,
1715 "spending limit should be restored by refund amount"
1716 );
1717
1718 Ok(())
1719 })
1720 }
1721
1722 #[test]
1723 fn test_transfer_fee_post_tx_pre_t1c() -> eyre::Result<()> {
1724 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1B);
1725 let admin = Address::random();
1726 let user = Address::random();
1727 let access_key = Address::random();
1728 let max_fee = U256::from(1000);
1729 let refund_amount = U256::from(300);
1730 let gas_used = U256::from(100);
1731
1732 StorageCtx::enter(&mut storage, || {
1733 let mut token = TIP20Setup::create("Test", "TST", admin)
1734 .with_issuer(admin)
1735 .with_mint(TIP_FEE_MANAGER_ADDRESS, max_fee)
1736 .apply()?;
1737
1738 let token_address = token.address;
1739 let spending_limit = U256::from(2000);
1740
1741 let mut keychain = AccountKeychain::new();
1742 keychain.initialize()?;
1743 keychain.set_transaction_key(Address::ZERO)?;
1744
1745 keychain.authorize_key(
1746 user,
1747 authorizeKeyCall {
1748 keyId: access_key,
1749 signatureType: SignatureType::Secp256k1,
1750 config: KeyRestrictions {
1751 expiry: u64::MAX,
1752 enforceLimits: true,
1753 limits: vec![TokenLimit {
1754 token: token_address,
1755 amount: spending_limit,
1756 period: 0,
1757 }],
1758 allowAnyCalls: true,
1759 allowedCalls: vec![],
1760 },
1761 },
1762 )?;
1763
1764 keychain.set_transaction_key(access_key)?;
1765 keychain.set_tx_origin(user)?;
1766 keychain.authorize_transfer(user, token_address, max_fee)?;
1767
1768 let remaining_after_deduction =
1769 keychain.get_remaining_limit(getRemainingLimitCall {
1770 account: user,
1771 keyId: access_key,
1772 token: token_address,
1773 })?;
1774 assert_eq!(remaining_after_deduction, spending_limit - max_fee);
1775
1776 token.transfer_fee_post_tx(user, refund_amount, gas_used)?;
1777
1778 let remaining_after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
1780 account: user,
1781 keyId: access_key,
1782 token: token_address,
1783 })?;
1784 assert_eq!(remaining_after_refund, spending_limit - max_fee);
1785
1786 Ok(())
1787 })
1788 }
1789
1790 #[test]
1791 fn test_transfer_from_insufficient_allowance() -> eyre::Result<()> {
1792 let mut storage = HashMapStorageProvider::new(1);
1793 let admin = Address::random();
1794 let from = Address::random();
1795 let spender = Address::random();
1796 let to = Address::random();
1797 let amount = U256::random() % U256::from(u128::MAX);
1798
1799 StorageCtx::enter(&mut storage, || {
1800 let mut token = TIP20Setup::create("Test", "TST", admin)
1801 .with_issuer(admin)
1802 .with_mint(from, amount)
1803 .apply()?;
1804
1805 assert!(matches!(
1806 token.transfer_from(spender, ITIP20::transferFromCall { from, to, amount }),
1807 Err(TempoPrecompileError::TIP20(
1808 TIP20Error::InsufficientAllowance(_)
1809 ))
1810 ));
1811
1812 Ok(())
1813 })
1814 }
1815
1816 #[test]
1817 fn test_system_transfer_from() -> eyre::Result<()> {
1818 let mut storage = HashMapStorageProvider::new(1);
1819 let admin = Address::random();
1820 let from = Address::random();
1821 let to = Address::random();
1822 let amount = U256::random() % U256::from(u128::MAX);
1823
1824 StorageCtx::enter(&mut storage, || {
1825 let mut token = TIP20Setup::create("Test", "TST", admin)
1826 .with_issuer(admin)
1827 .with_mint(from, amount)
1828 .apply()?;
1829
1830 assert!(token.system_transfer_from(from, to, amount).is_ok());
1831 assert_eq!(
1832 token.emitted_events().last().unwrap(),
1833 &TIP20Event::Transfer(ITIP20::Transfer { from, to, amount }).into_log_data()
1834 );
1835
1836 Ok(())
1837 })
1838 }
1839
1840 #[test]
1841 fn test_initialize_sets_next_quote_token() -> eyre::Result<()> {
1842 let mut storage = HashMapStorageProvider::new(1);
1843 let admin = Address::random();
1844
1845 StorageCtx::enter(&mut storage, || {
1846 let token = TIP20Setup::create("Test", "TST", admin).apply()?;
1847
1848 assert_eq!(token.quote_token()?, PATH_USD_ADDRESS);
1850 assert_eq!(token.next_quote_token()?, PATH_USD_ADDRESS);
1851
1852 Ok(())
1853 })
1854 }
1855
1856 #[test]
1857 fn test_update_quote_token() -> eyre::Result<()> {
1858 let mut storage = HashMapStorageProvider::new(1);
1859 let admin = Address::random();
1860
1861 StorageCtx::enter(&mut storage, || {
1862 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1863
1864 let new_quote_token = TIP20Setup::create("New Quote", "NQ", admin).apply()?;
1866 let new_quote_token_address = new_quote_token.address;
1867
1868 assert_eq!(token.quote_token()?, PATH_USD_ADDRESS);
1870
1871 token.set_next_quote_token(
1873 admin,
1874 ITIP20::setNextQuoteTokenCall {
1875 newQuoteToken: new_quote_token_address,
1876 },
1877 )?;
1878
1879 assert_eq!(token.next_quote_token()?, new_quote_token_address);
1881
1882 assert_eq!(
1884 token.emitted_events().last().unwrap(),
1885 &TIP20Event::NextQuoteTokenSet(ITIP20::NextQuoteTokenSet {
1886 updater: admin,
1887 nextQuoteToken: new_quote_token_address,
1888 })
1889 .into_log_data()
1890 );
1891
1892 Ok(())
1893 })
1894 }
1895
1896 #[test]
1897 fn test_update_quote_token_requires_admin() -> eyre::Result<()> {
1898 let mut storage = HashMapStorageProvider::new(1);
1899 let admin = Address::random();
1900 let non_admin = Address::random();
1901
1902 StorageCtx::enter(&mut storage, || {
1903 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1904
1905 let quote_token_address = token.quote_token()?;
1907
1908 let result = token.set_next_quote_token(
1910 non_admin,
1911 ITIP20::setNextQuoteTokenCall {
1912 newQuoteToken: quote_token_address,
1913 },
1914 );
1915
1916 assert!(matches!(
1917 result,
1918 Err(TempoPrecompileError::RolesAuthError(
1919 RolesAuthError::Unauthorized(_)
1920 ))
1921 ));
1922
1923 Ok(())
1924 })
1925 }
1926
1927 #[test]
1928 fn test_update_quote_token_rejects_non_tip20() -> eyre::Result<()> {
1929 let mut storage = HashMapStorageProvider::new(1);
1930 let admin = Address::random();
1931
1932 StorageCtx::enter(&mut storage, || {
1933 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1934
1935 let non_tip20_address = Address::random();
1937 let result = token.set_next_quote_token(
1938 admin,
1939 ITIP20::setNextQuoteTokenCall {
1940 newQuoteToken: non_tip20_address,
1941 },
1942 );
1943
1944 assert!(matches!(
1945 result,
1946 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
1947 _
1948 )))
1949 ));
1950
1951 Ok(())
1952 })
1953 }
1954
1955 #[test]
1956 fn test_update_quote_token_rejects_undeployed_token() -> eyre::Result<()> {
1957 let mut storage = HashMapStorageProvider::new(1);
1958 let admin = Address::random();
1959
1960 StorageCtx::enter(&mut storage, || {
1961 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1962
1963 let undeployed_token_address =
1966 Address::from(hex!("20C0000000000000000000000000000000000999"));
1967 let result = token.set_next_quote_token(
1968 admin,
1969 ITIP20::setNextQuoteTokenCall {
1970 newQuoteToken: undeployed_token_address,
1971 },
1972 );
1973
1974 assert!(matches!(
1975 result,
1976 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
1977 _
1978 )))
1979 ));
1980
1981 Ok(())
1982 })
1983 }
1984
1985 #[test]
1986 fn test_finalize_quote_token_update() -> eyre::Result<()> {
1987 let mut storage = HashMapStorageProvider::new(1);
1988 let admin = Address::random();
1989
1990 StorageCtx::enter(&mut storage, || {
1991 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1992 let quote_token_address = token.quote_token()?;
1993
1994 token.set_next_quote_token(
1996 admin,
1997 ITIP20::setNextQuoteTokenCall {
1998 newQuoteToken: quote_token_address,
1999 },
2000 )?;
2001
2002 token.complete_quote_token_update(admin, ITIP20::completeQuoteTokenUpdateCall {})?;
2004
2005 assert_eq!(token.quote_token()?, quote_token_address);
2007
2008 assert_eq!(
2010 token.emitted_events().last().unwrap(),
2011 &TIP20Event::QuoteTokenUpdate(ITIP20::QuoteTokenUpdate {
2012 updater: admin,
2013 newQuoteToken: quote_token_address,
2014 })
2015 .into_log_data()
2016 );
2017
2018 Ok(())
2019 })
2020 }
2021
2022 #[test]
2023 fn test_finalize_quote_token_update_detects_loop() -> eyre::Result<()> {
2024 let mut storage = HashMapStorageProvider::new(1);
2025 let admin = Address::random();
2026
2027 StorageCtx::enter(&mut storage, || {
2028 let mut token_b = TIP20Setup::create("Token B", "TKB", admin).apply()?;
2030 let token_a = TIP20Setup::create("Token A", "TKA", admin)
2032 .quote_token(token_b.address)
2033 .apply()?;
2034
2035 token_b.set_next_quote_token(
2037 admin,
2038 ITIP20::setNextQuoteTokenCall {
2039 newQuoteToken: token_a.address,
2040 },
2041 )?;
2042
2043 let result =
2045 token_b.complete_quote_token_update(admin, ITIP20::completeQuoteTokenUpdateCall {});
2046
2047 assert!(matches!(
2048 result,
2049 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
2050 _
2051 )))
2052 ));
2053
2054 Ok(())
2055 })
2056 }
2057
2058 #[test]
2059 fn test_finalize_quote_token_update_requires_admin() -> eyre::Result<()> {
2060 let mut storage = HashMapStorageProvider::new(1);
2061 let admin = Address::random();
2062 let non_admin = Address::random();
2063
2064 StorageCtx::enter(&mut storage, || {
2065 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2066 let quote_token_address = token.quote_token()?;
2067
2068 token.set_next_quote_token(
2070 admin,
2071 ITIP20::setNextQuoteTokenCall {
2072 newQuoteToken: quote_token_address,
2073 },
2074 )?;
2075
2076 let result = token
2078 .complete_quote_token_update(non_admin, ITIP20::completeQuoteTokenUpdateCall {});
2079
2080 assert!(matches!(
2081 result,
2082 Err(TempoPrecompileError::RolesAuthError(
2083 RolesAuthError::Unauthorized(_)
2084 ))
2085 ));
2086
2087 Ok(())
2088 })
2089 }
2090
2091 #[test]
2092 fn test_arbitrary_currency() -> eyre::Result<()> {
2093 let mut storage = HashMapStorageProvider::new(1);
2094 let admin = Address::random();
2095
2096 StorageCtx::enter(&mut storage, || {
2097 for _ in 0..50 {
2098 let currency: String = thread_rng()
2099 .sample_iter(&Alphanumeric)
2100 .take(31)
2101 .map(char::from)
2102 .collect();
2103
2104 let token = TIP20Setup::create("Test", "TST", admin)
2106 .currency(¤cy)
2107 .apply()?;
2108
2109 let stored_currency = token.currency()?;
2111 assert_eq!(stored_currency, currency,);
2112 }
2113
2114 Ok(())
2115 })
2116 }
2117
2118 #[test]
2119 fn test_from_address() -> eyre::Result<()> {
2120 let mut storage = HashMapStorageProvider::new(1);
2121 let admin = Address::random();
2122
2123 StorageCtx::enter(&mut storage, || {
2124 let token = TIP20Setup::create("Test", "TST", admin).apply()?;
2126 let via_from_address = TIP20Token::from_address(token.address)?.address;
2127
2128 assert_eq!(
2129 via_from_address, token.address,
2130 "from_address should use the provided address directly"
2131 );
2132
2133 let _path_usd = TIP20Setup::path_usd(admin).apply()?;
2135 let via_from_address_reserved = TIP20Token::from_address(PATH_USD_ADDRESS)?.address;
2136
2137 assert_eq!(
2138 via_from_address_reserved, PATH_USD_ADDRESS,
2139 "from_address should work for reserved addresses too"
2140 );
2141
2142 Ok(())
2143 })
2144 }
2145
2146 #[test]
2147 fn test_new_invalid_quote_token() -> eyre::Result<()> {
2148 let mut storage = HashMapStorageProvider::new(1);
2149 let admin = Address::random();
2150
2151 StorageCtx::enter(&mut storage, || {
2152 let currency: String = thread_rng()
2153 .sample_iter(&Alphanumeric)
2154 .take(31)
2155 .map(char::from)
2156 .collect();
2157
2158 let token = TIP20Setup::create("Token", "T", admin)
2159 .currency(¤cy)
2160 .apply()?;
2161
2162 TIP20Setup::create("USD Token", "USDT", admin)
2164 .currency(USD_CURRENCY)
2165 .quote_token(token.address)
2166 .expect_tip20_err(TIP20Error::invalid_quote_token());
2167
2168 Ok(())
2169 })
2170 }
2171
2172 #[test]
2173 fn test_new_valid_quote_token() -> eyre::Result<()> {
2174 let mut storage = HashMapStorageProvider::new(1);
2175 let admin = Address::random();
2176
2177 StorageCtx::enter(&mut storage, || {
2178 let usd_token1 = TIP20Setup::create("USD Token", "USDT", admin).apply()?;
2179
2180 let _usd_token2 = TIP20Setup::create("USD Token", "USDT", admin)
2182 .quote_token(usd_token1.address)
2183 .apply()?;
2184
2185 let currency_1: String = thread_rng()
2187 .sample_iter(&Alphanumeric)
2188 .take(31)
2189 .map(char::from)
2190 .collect();
2191
2192 let token_1 = TIP20Setup::create("USD Token", "USDT", admin)
2193 .currency(currency_1)
2194 .apply()?;
2195
2196 let currency_2: String = thread_rng()
2198 .sample_iter(&Alphanumeric)
2199 .take(31)
2200 .map(char::from)
2201 .collect();
2202
2203 let _token_2 = TIP20Setup::create("USD Token", "USDT", admin)
2204 .currency(currency_2)
2205 .quote_token(token_1.address)
2206 .apply()?;
2207
2208 Ok(())
2209 })
2210 }
2211
2212 #[test]
2213 fn test_update_quote_token_invalid_token() -> eyre::Result<()> {
2214 let mut storage = HashMapStorageProvider::new(1);
2215 let admin = Address::random();
2216
2217 StorageCtx::enter(&mut storage, || {
2218 let _path_usd = TIP20Setup::path_usd(admin).apply()?;
2219
2220 let currency: String = thread_rng()
2221 .sample_iter(&Alphanumeric)
2222 .take(31)
2223 .map(char::from)
2224 .collect();
2225
2226 let token_1 = TIP20Setup::create("Token 1", "TK1", admin)
2227 .currency(¤cy)
2228 .apply()?;
2229
2230 let mut usd_token = TIP20Setup::create("USD Token", "USDT", admin).apply()?;
2232
2233 let result = usd_token.set_next_quote_token(
2235 admin,
2236 ITIP20::setNextQuoteTokenCall {
2237 newQuoteToken: token_1.address,
2238 },
2239 );
2240
2241 assert!(result.is_err_and(
2242 |err| err == TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
2243 ));
2244
2245 Ok(())
2246 })
2247 }
2248
2249 #[test]
2250 fn test_is_tip20_prefix() -> eyre::Result<()> {
2251 let mut storage = HashMapStorageProvider::new(1);
2252 let sender = Address::random();
2253
2254 StorageCtx::enter(&mut storage, || {
2255 let _path_usd = TIP20Setup::path_usd(sender).apply()?;
2256
2257 let created_tip20 = TIP20Factory::new().create_token(
2258 sender,
2259 ITIP20Factory::createTokenCall {
2260 name: "Test Token".to_string(),
2261 symbol: "TEST".to_string(),
2262 currency: "USD".to_string(),
2263 quoteToken: crate::PATH_USD_ADDRESS,
2264 admin: sender,
2265 salt: B256::random(),
2266 },
2267 )?;
2268 let non_tip20 = Address::random();
2269
2270 assert!(PATH_USD_ADDRESS.is_tip20());
2271 assert!(created_tip20.is_tip20());
2272 assert!(!non_tip20.is_tip20());
2273 Ok(())
2274 })
2275 }
2276
2277 #[test]
2278 fn test_initialize_supply_cap() -> eyre::Result<()> {
2279 let mut storage = HashMapStorageProvider::new(1);
2280 let admin = Address::random();
2281
2282 StorageCtx::enter(&mut storage, || {
2283 let token = TIP20Setup::create("Token", "TKN", admin).apply()?;
2284
2285 let supply_cap = token.supply_cap()?;
2286 assert_eq!(supply_cap, U256::from(u128::MAX));
2287
2288 Ok(())
2289 })
2290 }
2291
2292 #[test]
2293 fn test_unable_to_burn_blocked_from_protected_address() -> eyre::Result<()> {
2294 let mut storage = HashMapStorageProvider::new(1);
2295 let admin = Address::random();
2296 let burner = Address::random();
2297 let amount = (U256::random() % U256::from(u128::MAX)) / U256::from(2);
2298
2299 StorageCtx::enter(&mut storage, || {
2300 let mut token = TIP20Setup::create("Token", "TKN", admin)
2301 .with_issuer(admin)
2302 .with_role(burner, *BURN_BLOCKED_ROLE)
2304 .with_mint(TIP_FEE_MANAGER_ADDRESS, amount)
2306 .with_mint(STABLECOIN_DEX_ADDRESS, amount)
2308 .apply()?;
2309
2310 let result = token.burn_blocked(
2312 burner,
2313 ITIP20::burnBlockedCall {
2314 from: TIP_FEE_MANAGER_ADDRESS,
2315 amount: amount / U256::from(2),
2316 },
2317 );
2318
2319 assert!(matches!(
2320 result,
2321 Err(TempoPrecompileError::TIP20(TIP20Error::ProtectedAddress(_)))
2322 ));
2323
2324 let balance = token.balance_of(ITIP20::balanceOfCall {
2326 account: TIP_FEE_MANAGER_ADDRESS,
2327 })?;
2328 assert_eq!(balance, amount);
2329
2330 let result = token.burn_blocked(
2332 burner,
2333 ITIP20::burnBlockedCall {
2334 from: STABLECOIN_DEX_ADDRESS,
2335 amount: amount / U256::from(2),
2336 },
2337 );
2338
2339 assert!(matches!(
2340 result,
2341 Err(TempoPrecompileError::TIP20(TIP20Error::ProtectedAddress(_)))
2342 ));
2343
2344 let balance = token.balance_of(ITIP20::balanceOfCall {
2346 account: STABLECOIN_DEX_ADDRESS,
2347 })?;
2348 assert_eq!(balance, amount);
2349
2350 Ok(())
2351 })
2352 }
2353
2354 #[test]
2355 fn test_initialize_usd_token() -> eyre::Result<()> {
2356 let mut storage = HashMapStorageProvider::new(1);
2357 let admin = Address::random();
2358
2359 StorageCtx::enter(&mut storage, || {
2360 let _token = TIP20Setup::create("TestToken", "TEST", admin).apply()?;
2362
2363 let eur_token = TIP20Setup::create("EuroToken", "EUR", admin)
2365 .currency("EUR")
2366 .apply()?;
2367
2368 TIP20Setup::create("USDToken", "USD", admin)
2370 .quote_token(eur_token.address)
2371 .expect_tip20_err(TIP20Error::invalid_quote_token());
2372
2373 Ok(())
2374 })
2375 }
2376
2377 #[test]
2378 fn test_change_transfer_policy_id_invalid_policy() -> eyre::Result<()> {
2379 let mut storage = HashMapStorageProvider::new(1);
2380 let admin = Address::random();
2381
2382 StorageCtx::enter(&mut storage, || {
2383 let mut token = TIP20Setup::path_usd(admin).apply()?;
2384
2385 let mut registry = TIP403Registry::new();
2387 registry.initialize()?;
2388
2389 let invalid_policy_id = 999u64;
2391 let result = token.change_transfer_policy_id(
2392 admin,
2393 ITIP20::changeTransferPolicyIdCall {
2394 newPolicyId: invalid_policy_id,
2395 },
2396 );
2397
2398 assert!(matches!(
2399 result.unwrap_err(),
2400 TempoPrecompileError::TIP20(TIP20Error::InvalidTransferPolicyId(_))
2401 ));
2402
2403 Ok(())
2404 })
2405 }
2406
2407 #[test]
2408 fn test_transfer_invalid_recipient() -> eyre::Result<()> {
2409 let mut storage = HashMapStorageProvider::new(1);
2410 let admin = Address::random();
2411 let bob = Address::random();
2412 let amount = U256::random() % U256::from(u128::MAX);
2413
2414 StorageCtx::enter(&mut storage, || {
2415 let mut token = TIP20Setup::create("Token", "TKN", admin)
2416 .with_issuer(admin)
2417 .with_mint(admin, amount)
2418 .with_approval(admin, bob, amount)
2419 .apply()?;
2420
2421 let result = token.transfer(
2422 admin,
2423 ITIP20::transferCall {
2424 to: Address::ZERO,
2425 amount,
2426 },
2427 );
2428 assert!(result.is_err_and(|err| err.to_string().contains("InvalidRecipient")));
2429
2430 let result = token.transfer_from(
2431 bob,
2432 ITIP20::transferFromCall {
2433 from: admin,
2434 to: Address::ZERO,
2435 amount,
2436 },
2437 );
2438 assert!(result.is_err_and(|err| err.to_string().contains("InvalidRecipient")));
2439
2440 Ok(())
2441 })
2442 }
2443
2444 #[test]
2445 fn test_change_transfer_policy_id() -> eyre::Result<()> {
2446 let mut storage = HashMapStorageProvider::new(1);
2447 let admin = Address::random();
2448
2449 StorageCtx::enter(&mut storage, || {
2450 let mut token = TIP20Setup::path_usd(admin).apply()?;
2451
2452 let mut registry = TIP403Registry::new();
2454 registry.initialize()?;
2455
2456 token.change_transfer_policy_id(
2458 admin,
2459 ITIP20::changeTransferPolicyIdCall { newPolicyId: 0 },
2460 )?;
2461 assert_eq!(token.transfer_policy_id()?, 0);
2462
2463 token.change_transfer_policy_id(
2464 admin,
2465 ITIP20::changeTransferPolicyIdCall { newPolicyId: 1 },
2466 )?;
2467 assert_eq!(token.transfer_policy_id()?, 1);
2468
2469 let mut rng = rand_08::thread_rng();
2471 for _ in 0..20 {
2472 let invalid_policy_id = rng.gen_range(2..u64::MAX);
2473 let result = token.change_transfer_policy_id(
2474 admin,
2475 ITIP20::changeTransferPolicyIdCall {
2476 newPolicyId: invalid_policy_id,
2477 },
2478 );
2479 assert!(matches!(
2480 result.unwrap_err(),
2481 TempoPrecompileError::TIP20(TIP20Error::InvalidTransferPolicyId(_))
2482 ));
2483 }
2484
2485 let mut valid_policy_ids = Vec::new();
2487 for i in 0..10 {
2488 let policy_id = registry.create_policy(
2489 admin,
2490 ITIP403Registry::createPolicyCall {
2491 admin,
2492 policyType: if i % 2 == 0 {
2493 ITIP403Registry::PolicyType::WHITELIST
2494 } else {
2495 ITIP403Registry::PolicyType::BLACKLIST
2496 },
2497 },
2498 )?;
2499 valid_policy_ids.push(policy_id);
2500 }
2501
2502 for policy_id in valid_policy_ids {
2504 let result = token.change_transfer_policy_id(
2505 admin,
2506 ITIP20::changeTransferPolicyIdCall {
2507 newPolicyId: policy_id,
2508 },
2509 );
2510 assert!(result.is_ok());
2511 assert_eq!(token.transfer_policy_id()?, policy_id);
2512 }
2513
2514 Ok(())
2515 })
2516 }
2517
2518 #[test]
2519 fn test_is_transfer_authorized() -> eyre::Result<()> {
2520 use tempo_chainspec::hardfork::TempoHardfork;
2521
2522 let admin = Address::random();
2523 let sender = Address::random();
2524 let recipient = Address::random();
2525
2526 for hardfork in [TempoHardfork::T0, TempoHardfork::T1] {
2527 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
2528
2529 StorageCtx::enter(&mut storage, || {
2530 let token = TIP20Setup::path_usd(admin).apply()?;
2531
2532 let mut registry = TIP403Registry::new();
2534 registry.initialize()?;
2535
2536 let policy_id = registry.create_policy(
2537 admin,
2538 ITIP403Registry::createPolicyCall {
2539 admin,
2540 policyType: ITIP403Registry::PolicyType::WHITELIST,
2541 },
2542 )?;
2543
2544 let mut token = token;
2546 token.change_transfer_policy_id(
2547 admin,
2548 ITIP20::changeTransferPolicyIdCall {
2549 newPolicyId: policy_id,
2550 },
2551 )?;
2552
2553 registry.modify_policy_whitelist(
2555 admin,
2556 ITIP403Registry::modifyPolicyWhitelistCall {
2557 policyId: policy_id,
2558 account: recipient,
2559 allowed: true,
2560 },
2561 )?;
2562 assert!(!token.is_transfer_authorized(sender, recipient)?);
2563
2564 registry.modify_policy_whitelist(
2566 admin,
2567 ITIP403Registry::modifyPolicyWhitelistCall {
2568 policyId: policy_id,
2569 account: sender,
2570 allowed: true,
2571 },
2572 )?;
2573 registry.modify_policy_whitelist(
2574 admin,
2575 ITIP403Registry::modifyPolicyWhitelistCall {
2576 policyId: policy_id,
2577 account: recipient,
2578 allowed: false,
2579 },
2580 )?;
2581 assert!(!token.is_transfer_authorized(sender, recipient)?);
2582
2583 registry.modify_policy_whitelist(
2585 admin,
2586 ITIP403Registry::modifyPolicyWhitelistCall {
2587 policyId: policy_id,
2588 account: recipient,
2589 allowed: true,
2590 },
2591 )?;
2592 assert!(token.is_transfer_authorized(sender, recipient)?);
2593
2594 Ok::<_, TempoPrecompileError>(())
2595 })?;
2596 }
2597
2598 Ok(())
2599 }
2600
2601 #[test]
2602 fn test_set_next_quote_token_rejects_path_usd() -> eyre::Result<()> {
2603 let mut storage = HashMapStorageProvider::new(1);
2604 let admin = Address::random();
2605
2606 StorageCtx::enter(&mut storage, || {
2607 let mut path_usd = TIP20Setup::path_usd(admin).apply()?;
2608 let other_token = TIP20Setup::create("Test", "T", admin).apply()?;
2609
2610 let result = path_usd.set_next_quote_token(
2612 admin,
2613 ITIP20::setNextQuoteTokenCall {
2614 newQuoteToken: other_token.address,
2615 },
2616 );
2617 assert!(matches!(
2618 result,
2619 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
2620 _
2621 )))
2622 ));
2623
2624 Ok(())
2625 })
2626 }
2627
2628 #[test]
2629 fn test_non_path_usd_cycle_detection() -> eyre::Result<()> {
2630 let mut storage = HashMapStorageProvider::new(1);
2631 let admin = Address::random();
2632
2633 StorageCtx::enter(&mut storage, || {
2634 TIP20Setup::path_usd(admin).apply()?;
2635
2636 let mut token_b = TIP20Setup::create("TokenB", "TKNB", admin).apply()?;
2637 let token_a = TIP20Setup::create("TokenA", "TKNA", admin)
2638 .quote_token(token_b.address)
2639 .apply()?;
2640
2641 assert_eq!(token_a.quote_token()?, token_b.address);
2643 assert_eq!(token_b.quote_token()?, PATH_USD_ADDRESS);
2644
2645 token_b.set_next_quote_token(
2647 admin,
2648 ITIP20::setNextQuoteTokenCall {
2649 newQuoteToken: token_a.address,
2650 },
2651 )?;
2652
2653 let result =
2654 token_b.complete_quote_token_update(admin, ITIP20::completeQuoteTokenUpdateCall {});
2655
2656 assert!(matches!(
2657 result,
2658 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
2659 _
2660 )))
2661 ));
2662
2663 assert_eq!(token_a.quote_token()?, token_b.address);
2665 assert_eq!(token_b.quote_token()?, PATH_USD_ADDRESS);
2666
2667 Ok(())
2668 })
2669 }
2670
2671 #[test]
2676 fn test_mint_to_virtual_address_credits_master() -> eyre::Result<()> {
2677 let amount = U256::from(1000);
2678
2679 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
2680 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
2681 let admin = Address::random();
2682
2683 StorageCtx::enter(&mut storage, || {
2684 let mut registry = AddressRegistry::new();
2685 let (_, virtual_addr) = register_virtual_master(&mut registry)?;
2686 let credited = if hardfork.is_t3() {
2687 VIRTUAL_MASTER
2688 } else {
2689 virtual_addr
2690 };
2691
2692 let mut token = TIP20Setup::create("Test", "TST", admin)
2693 .with_issuer(admin)
2694 .clear_events()
2695 .apply()?;
2696
2697 token.mint(
2699 admin,
2700 ITIP20::mintCall {
2701 to: virtual_addr,
2702 amount,
2703 },
2704 )?;
2705
2706 if hardfork.is_t3() {
2707 assert_eq!(token.get_balance(VIRTUAL_MASTER)?, amount);
2709 assert_eq!(token.get_balance(virtual_addr)?, U256::ZERO);
2710 assert_eq!(token.total_supply()?, amount);
2711
2712 token.assert_emitted_events(vec![
2714 TIP20Event::Transfer(ITIP20::Transfer {
2715 from: Address::ZERO,
2716 to: virtual_addr,
2717 amount,
2718 }),
2719 TIP20Event::Mint(ITIP20::Mint {
2720 to: virtual_addr,
2721 amount,
2722 }),
2723 TIP20Event::Transfer(ITIP20::Transfer {
2724 from: virtual_addr,
2725 to: VIRTUAL_MASTER,
2726 amount,
2727 }),
2728 ]);
2729 } else {
2730 assert_eq!(token.get_balance(virtual_addr)?, amount);
2732 assert_eq!(token.get_balance(VIRTUAL_MASTER)?, U256::ZERO);
2733 }
2734
2735 let pre = token.get_balance(credited)?;
2737 token.mint_with_memo(
2738 admin,
2739 ITIP20::mintWithMemoCall {
2740 to: virtual_addr,
2741 amount,
2742 memo: FixedBytes::ZERO,
2743 },
2744 )?;
2745 assert_eq!(token.get_balance(credited)? - pre, amount);
2746
2747 Ok::<_, TempoPrecompileError>(())
2748 })?;
2749 }
2750 Ok(())
2751 }
2752
2753 #[test]
2754 fn test_transfer_to_virtual_address_credits_master() -> eyre::Result<()> {
2755 let amount = U256::from(500);
2756
2757 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
2758 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
2759 let admin = Address::random();
2760 let sender = Address::random();
2761
2762 StorageCtx::enter(&mut storage, || {
2763 let mut registry = AddressRegistry::new();
2764 let (_, virtual_addr) = register_virtual_master(&mut registry)?;
2765 let credited = if hardfork.is_t3() {
2766 VIRTUAL_MASTER
2767 } else {
2768 virtual_addr
2769 };
2770
2771 let mut token = TIP20Setup::create("Test", "TST", admin)
2772 .with_issuer(admin)
2773 .with_mint(sender, amount * U256::from(2))
2774 .clear_events()
2775 .apply()?;
2776
2777 token.transfer(
2779 sender,
2780 ITIP20::transferCall {
2781 to: virtual_addr,
2782 amount,
2783 },
2784 )?;
2785
2786 if hardfork.is_t3() {
2787 assert_eq!(token.get_balance(VIRTUAL_MASTER)?, amount);
2788 assert_eq!(token.get_balance(virtual_addr)?, U256::ZERO);
2789
2790 token.assert_emitted_events(vec![
2792 TIP20Event::Transfer(ITIP20::Transfer {
2793 from: sender,
2794 to: virtual_addr,
2795 amount,
2796 }),
2797 TIP20Event::Transfer(ITIP20::Transfer {
2798 from: virtual_addr,
2799 to: VIRTUAL_MASTER,
2800 amount,
2801 }),
2802 ]);
2803 } else {
2804 assert_eq!(token.get_balance(virtual_addr)?, amount);
2805 assert_eq!(token.get_balance(VIRTUAL_MASTER)?, U256::ZERO);
2806 }
2807
2808 let pre = token.get_balance(credited)?;
2810 token.transfer_with_memo(
2811 sender,
2812 ITIP20::transferWithMemoCall {
2813 to: virtual_addr,
2814 amount,
2815 memo: FixedBytes::ZERO,
2816 },
2817 )?;
2818 assert_eq!(token.get_balance(credited)? - pre, amount);
2819
2820 Ok::<_, TempoPrecompileError>(())
2821 })?;
2822 }
2823 Ok(())
2824 }
2825
2826 #[test]
2827 fn test_transfer_from_to_virtual_address_credits_master() -> eyre::Result<()> {
2828 let amount = U256::from(300);
2829
2830 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
2831 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
2832 let admin = Address::random();
2833 let owner = Address::random();
2834 let spender = Address::random();
2835
2836 StorageCtx::enter(&mut storage, || {
2837 let mut registry = AddressRegistry::new();
2838 let (_, virtual_addr) = register_virtual_master(&mut registry)?;
2839 let credited = if hardfork.is_t3() {
2840 VIRTUAL_MASTER
2841 } else {
2842 virtual_addr
2843 };
2844
2845 let total = amount * U256::from(2);
2846 let mut token = TIP20Setup::create("Test", "TST", admin)
2847 .with_issuer(admin)
2848 .with_mint(owner, total)
2849 .with_approval(owner, spender, total)
2850 .clear_events()
2851 .apply()?;
2852
2853 token.transfer_from(
2855 spender,
2856 ITIP20::transferFromCall {
2857 from: owner,
2858 to: virtual_addr,
2859 amount,
2860 },
2861 )?;
2862
2863 if hardfork.is_t3() {
2864 assert_eq!(token.get_balance(VIRTUAL_MASTER)?, amount);
2865 assert_eq!(token.get_balance(virtual_addr)?, U256::ZERO);
2866 } else {
2867 assert_eq!(token.get_balance(virtual_addr)?, amount);
2868 assert_eq!(token.get_balance(VIRTUAL_MASTER)?, U256::ZERO);
2869 }
2870
2871 let pre = token.get_balance(credited)?;
2873 token.transfer_from_with_memo(
2874 spender,
2875 ITIP20::transferFromWithMemoCall {
2876 from: owner,
2877 to: virtual_addr,
2878 amount,
2879 memo: FixedBytes::ZERO,
2880 },
2881 )?;
2882 assert_eq!(token.get_balance(credited)? - pre, amount);
2883
2884 Ok::<_, TempoPrecompileError>(())
2885 })?;
2886 }
2887 Ok(())
2888 }
2889
2890 #[test]
2891 #[rustfmt::skip]
2892 fn test_unregistered_virtual_reverts_on_t3() -> eyre::Result<()> {
2893 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
2894 let admin = Address::random();
2895 let sender = Address::random();
2896 let spender = Address::random();
2897 let to = Address::new_virtual(MasterId::ZERO, UserTag::ZERO);
2898 let amount = U256::from(100);
2899 let memo = FixedBytes::ZERO;
2900
2901 StorageCtx::enter(&mut storage, || {
2902 let mut token = TIP20Setup::create("Test", "TST", admin)
2903 .with_issuer(admin)
2904 .with_mint(sender, amount)
2905 .with_approval(sender, spender, amount)
2906 .apply()?;
2907
2908 assert!(token.mint(admin, ITIP20::mintCall { to, amount }).is_err());
2910 assert!(token.mint_with_memo(admin, ITIP20::mintWithMemoCall { to, amount, memo }).is_err());
2911 assert!(token.transfer(sender, ITIP20::transferCall { to, amount }).is_err());
2912 assert!(token.transfer_with_memo(sender, ITIP20::transferWithMemoCall { to, amount, memo }).is_err());
2913 assert!(token.transfer_from(spender, ITIP20::transferFromCall { from: sender, to, amount }).is_err());
2914 assert!(token.transfer_from_with_memo(spender, ITIP20::transferFromWithMemoCall { from: sender, to, amount, memo }).is_err());
2915
2916 Ok(())
2917 })
2918 }
2919
2920 mod permit_tests {
2925 use super::*;
2926 use alloy::sol_types::SolValue;
2927 use alloy_signer::SignerSync;
2928 use alloy_signer_local::PrivateKeySigner;
2929 use tempo_chainspec::hardfork::TempoHardfork;
2930
2931 const CHAIN_ID: u64 = 42;
2932
2933 fn setup_t2_storage() -> HashMapStorageProvider {
2935 HashMapStorageProvider::new_with_spec(CHAIN_ID, TempoHardfork::T2)
2936 }
2937
2938 fn sign_permit(
2940 signer: &PrivateKeySigner,
2941 token_name: &str,
2942 token_address: Address,
2943 spender: Address,
2944 value: U256,
2945 nonce: U256,
2946 deadline: U256,
2947 ) -> (u8, B256, B256) {
2948 let domain_separator = compute_domain_separator(token_name, token_address);
2949 let struct_hash = keccak256(
2950 (
2951 *PERMIT_TYPEHASH,
2952 signer.address(),
2953 spender,
2954 value,
2955 nonce,
2956 deadline,
2957 )
2958 .abi_encode(),
2959 );
2960 let digest = keccak256(
2961 [
2962 &[0x19, 0x01],
2963 domain_separator.as_slice(),
2964 struct_hash.as_slice(),
2965 ]
2966 .concat(),
2967 );
2968
2969 let sig = signer.sign_hash_sync(&digest).unwrap();
2970 let v = sig.v() as u8 + 27;
2971 let r: B256 = sig.r().into();
2972 let s: B256 = sig.s().into();
2973 (v, r, s)
2974 }
2975
2976 fn compute_domain_separator(token_name: &str, token_address: Address) -> B256 {
2977 keccak256(
2978 (
2979 *EIP712_DOMAIN_TYPEHASH,
2980 keccak256(token_name.as_bytes()),
2981 *VERSION_HASH,
2982 U256::from(CHAIN_ID),
2983 token_address,
2984 )
2985 .abi_encode(),
2986 )
2987 }
2988
2989 struct PermitFixture {
2990 storage: HashMapStorageProvider,
2991 admin: Address,
2992 signer: PrivateKeySigner,
2993 spender: Address,
2994 }
2995
2996 impl PermitFixture {
2997 fn new() -> Self {
2998 Self {
2999 storage: setup_t2_storage(),
3000 admin: Address::random(),
3001 signer: PrivateKeySigner::random(),
3002 spender: Address::random(),
3003 }
3004 }
3005 }
3006
3007 fn make_permit_call(
3008 signer: &PrivateKeySigner,
3009 spender: Address,
3010 token_address: Address,
3011 value: U256,
3012 nonce: U256,
3013 deadline: U256,
3014 ) -> ITIP20::permitCall {
3015 let (v, r, s) = sign_permit(
3016 signer,
3017 "Test",
3018 token_address,
3019 spender,
3020 value,
3021 nonce,
3022 deadline,
3023 );
3024 ITIP20::permitCall {
3025 owner: signer.address(),
3026 spender,
3027 value,
3028 deadline,
3029 v,
3030 r,
3031 s,
3032 }
3033 }
3034
3035 #[test]
3036 fn test_permit_happy_path() -> eyre::Result<()> {
3037 let PermitFixture {
3038 mut storage,
3039 admin,
3040 ref signer,
3041 spender,
3042 } = PermitFixture::new();
3043 let owner = signer.address();
3044 let value = U256::from(1000);
3045
3046 StorageCtx::enter(&mut storage, || {
3047 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3048 let call =
3049 make_permit_call(signer, spender, token.address, value, U256::ZERO, U256::MAX);
3050 token.permit(call)?;
3051
3052 let allowance = token.allowance(ITIP20::allowanceCall { owner, spender })?;
3054 assert_eq!(allowance, value);
3055
3056 let nonce = token.nonces(ITIP20::noncesCall { owner })?;
3058 assert_eq!(nonce, U256::from(1));
3059
3060 Ok(())
3061 })
3062 }
3063
3064 #[test]
3065 fn test_permit_expired() -> eyre::Result<()> {
3066 let PermitFixture {
3067 mut storage,
3068 admin,
3069 ref signer,
3070 spender,
3071 } = PermitFixture::new();
3072 let value = U256::from(1000);
3073 let deadline = U256::ZERO;
3075
3076 StorageCtx::enter(&mut storage, || {
3077 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3078 let call =
3079 make_permit_call(signer, spender, token.address, value, U256::ZERO, deadline);
3080
3081 let result = token.permit(call);
3082
3083 assert!(matches!(
3084 result,
3085 Err(TempoPrecompileError::TIP20(TIP20Error::PermitExpired(_)))
3086 ));
3087
3088 Ok(())
3089 })
3090 }
3091
3092 #[test]
3093 fn test_permit_invalid_signature() -> eyre::Result<()> {
3094 let mut storage = setup_t2_storage();
3095 let admin = Address::random();
3096 let owner = Address::random();
3097 let spender = Address::random();
3098 let value = U256::from(1000);
3099 let deadline = U256::MAX;
3100
3101 StorageCtx::enter(&mut storage, || {
3102 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3103
3104 let result = token.permit(ITIP20::permitCall {
3106 owner,
3107 spender,
3108 value,
3109 deadline,
3110 v: 27,
3111 r: B256::ZERO,
3112 s: B256::ZERO,
3113 });
3114
3115 assert!(matches!(
3116 result,
3117 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
3118 ));
3119
3120 Ok(())
3121 })
3122 }
3123
3124 #[test]
3125 fn test_permit_wrong_signer() -> eyre::Result<()> {
3126 let PermitFixture {
3127 mut storage,
3128 admin,
3129 ref signer,
3130 spender,
3131 } = PermitFixture::new();
3132 let wrong_owner = Address::random(); let value = U256::from(1000);
3134 let deadline = U256::MAX;
3135
3136 StorageCtx::enter(&mut storage, || {
3137 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3138
3139 let (v, r, s) = sign_permit(
3141 signer,
3142 "Test",
3143 token.address,
3144 spender,
3145 value,
3146 U256::ZERO,
3147 deadline,
3148 );
3149
3150 let result = token.permit(ITIP20::permitCall {
3151 owner: wrong_owner, spender,
3153 value,
3154 deadline,
3155 v,
3156 r,
3157 s,
3158 });
3159
3160 assert!(matches!(
3161 result,
3162 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
3163 ));
3164
3165 Ok(())
3166 })
3167 }
3168
3169 #[test]
3170 fn test_permit_replay_protection() -> eyre::Result<()> {
3171 let PermitFixture {
3172 mut storage,
3173 admin,
3174 ref signer,
3175 spender,
3176 } = PermitFixture::new();
3177 let value = U256::from(1000);
3178
3179 StorageCtx::enter(&mut storage, || {
3180 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3181 let call =
3182 make_permit_call(signer, spender, token.address, value, U256::ZERO, U256::MAX);
3183
3184 token.permit(call.clone())?;
3186
3187 let result = token.permit(call);
3189
3190 assert!(matches!(
3191 result,
3192 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
3193 ));
3194
3195 Ok(())
3196 })
3197 }
3198
3199 #[test]
3200 fn test_permit_nonce_tracking() -> eyre::Result<()> {
3201 let PermitFixture {
3202 mut storage,
3203 admin,
3204 ref signer,
3205 spender,
3206 } = PermitFixture::new();
3207 let owner = signer.address();
3208
3209 StorageCtx::enter(&mut storage, || {
3210 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3211
3212 assert_eq!(token.nonces(ITIP20::noncesCall { owner })?, U256::ZERO);
3214
3215 for i in 0u64..3 {
3217 let nonce = U256::from(i);
3218 let value = U256::from(100 * (i + 1));
3219 let call =
3220 make_permit_call(signer, spender, token.address, value, nonce, U256::MAX);
3221 token.permit(call)?;
3222
3223 assert_eq!(
3224 token.nonces(ITIP20::noncesCall { owner })?,
3225 U256::from(i + 1)
3226 );
3227 }
3228
3229 Ok(())
3230 })
3231 }
3232
3233 #[test]
3234 fn test_permit_works_when_paused() -> eyre::Result<()> {
3235 let PermitFixture {
3236 mut storage,
3237 admin,
3238 ref signer,
3239 spender,
3240 } = PermitFixture::new();
3241 let owner = signer.address();
3242 let value = U256::from(1000);
3243
3244 StorageCtx::enter(&mut storage, || {
3245 let mut token = TIP20Setup::create("Test", "TST", admin)
3246 .with_role(admin, *PAUSE_ROLE)
3247 .apply()?;
3248
3249 token.pause(admin, ITIP20::pauseCall {})?;
3251 assert!(token.paused()?);
3252
3253 let call =
3254 make_permit_call(signer, spender, token.address, value, U256::ZERO, U256::MAX);
3255
3256 token.permit(call)?;
3258
3259 assert_eq!(
3260 token.allowance(ITIP20::allowanceCall { owner, spender })?,
3261 value
3262 );
3263
3264 Ok(())
3265 })
3266 }
3267
3268 #[test]
3269 fn test_permit_domain_separator() -> eyre::Result<()> {
3270 let PermitFixture {
3271 mut storage, admin, ..
3272 } = PermitFixture::new();
3273
3274 StorageCtx::enter(&mut storage, || {
3275 let token = TIP20Setup::create("Test", "TST", admin).apply()?;
3276
3277 let ds = token.domain_separator()?;
3278 let expected = compute_domain_separator("Test", token.address);
3279 assert_eq!(ds, expected);
3280
3281 Ok(())
3282 })
3283 }
3284
3285 #[test]
3286 fn test_permit_max_allowance() -> eyre::Result<()> {
3287 let PermitFixture {
3288 mut storage,
3289 admin,
3290 ref signer,
3291 spender,
3292 } = PermitFixture::new();
3293 let owner = signer.address();
3294
3295 StorageCtx::enter(&mut storage, || {
3296 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3297 let call = make_permit_call(
3298 signer,
3299 spender,
3300 token.address,
3301 U256::MAX,
3302 U256::ZERO,
3303 U256::MAX,
3304 );
3305 token.permit(call)?;
3306
3307 assert_eq!(
3308 token.allowance(ITIP20::allowanceCall { owner, spender })?,
3309 U256::MAX
3310 );
3311
3312 Ok(())
3313 })
3314 }
3315
3316 #[test]
3317 fn test_permit_allowance_override() -> eyre::Result<()> {
3318 let PermitFixture {
3319 mut storage,
3320 admin,
3321 ref signer,
3322 spender,
3323 } = PermitFixture::new();
3324 let owner = signer.address();
3325
3326 StorageCtx::enter(&mut storage, || {
3327 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3328
3329 let call = make_permit_call(
3331 signer,
3332 spender,
3333 token.address,
3334 U256::from(1000),
3335 U256::ZERO,
3336 U256::MAX,
3337 );
3338 token.permit(call)?;
3339 assert_eq!(
3340 token.allowance(ITIP20::allowanceCall { owner, spender })?,
3341 U256::from(1000)
3342 );
3343
3344 let call = make_permit_call(
3346 signer,
3347 spender,
3348 token.address,
3349 U256::ZERO,
3350 U256::from(1),
3351 U256::MAX,
3352 );
3353 token.permit(call)?;
3354 assert_eq!(
3355 token.allowance(ITIP20::allowanceCall { owner, spender })?,
3356 U256::ZERO
3357 );
3358
3359 Ok(())
3360 })
3361 }
3362
3363 #[test]
3364 fn test_permit_invalid_v_values() -> eyre::Result<()> {
3365 let PermitFixture {
3366 mut storage,
3367 admin,
3368 spender,
3369 ..
3370 } = PermitFixture::new();
3371
3372 StorageCtx::enter(&mut storage, || {
3373 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3374
3375 for v in [0u8, 1] {
3376 let result = token.permit(ITIP20::permitCall {
3377 owner: admin,
3378 spender,
3379 value: U256::from(1000),
3380 deadline: U256::MAX,
3381 v,
3382 r: B256::ZERO,
3383 s: B256::ZERO,
3384 });
3385
3386 assert!(
3387 matches!(
3388 result,
3389 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
3390 ),
3391 "v={v} should revert with InvalidSignature"
3392 );
3393 }
3394
3395 Ok(())
3396 })
3397 }
3398
3399 #[test]
3400 fn test_permit_zero_address_recovery_reverts() -> eyre::Result<()> {
3401 let PermitFixture {
3402 mut storage,
3403 admin,
3404 spender,
3405 ..
3406 } = PermitFixture::new();
3407
3408 StorageCtx::enter(&mut storage, || {
3409 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3410
3411 let result = token.permit(ITIP20::permitCall {
3412 owner: Address::ZERO,
3413 spender,
3414 value: U256::from(1000),
3415 deadline: U256::MAX,
3416 v: 27,
3417 r: B256::ZERO,
3418 s: B256::ZERO,
3419 });
3420
3421 assert!(matches!(
3422 result,
3423 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
3424 ));
3425
3426 Ok(())
3427 })
3428 }
3429
3430 #[test]
3431 fn test_permit_domain_separator_changes_with_chain_id() -> eyre::Result<()> {
3432 let PermitFixture { admin, .. } = PermitFixture::new();
3433
3434 let mut storage_a = setup_t2_storage();
3435 let mut storage_b =
3436 HashMapStorageProvider::new_with_spec(CHAIN_ID + 1, TempoHardfork::T2);
3437
3438 let ds_a = StorageCtx::enter(&mut storage_a, || {
3439 TIP20Setup::create("Test", "TST", admin)
3440 .apply()?
3441 .domain_separator()
3442 })?;
3443
3444 let ds_b = StorageCtx::enter(&mut storage_b, || {
3445 TIP20Setup::create("Test", "TST", admin)
3446 .apply()?
3447 .domain_separator()
3448 })?;
3449
3450 assert_ne!(
3451 ds_a, ds_b,
3452 "domain separator must change when chainId changes"
3453 );
3454
3455 Ok(())
3456 }
3457 }
3458
3459 #[test]
3460 fn test_mint_rejects_when_paused_on_t3() -> eyre::Result<()> {
3461 let to = Address::random();
3462 let amount = U256::from(1000);
3463 let memo = FixedBytes::random();
3464
3465 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
3466 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
3467 let admin = Address::random();
3468
3469 StorageCtx::enter(&mut storage, || {
3470 let mut token = TIP20Setup::create("Test", "TST", admin)
3471 .with_issuer(admin)
3472 .with_role(admin, *PAUSE_ROLE)
3473 .apply()?;
3474
3475 token.pause(admin, ITIP20::pauseCall {})?;
3476
3477 let mint_result = token.mint(admin, ITIP20::mintCall { to, amount });
3478 let mint_memo_result =
3479 token.mint_with_memo(admin, ITIP20::mintWithMemoCall { to, amount, memo });
3480
3481 if hardfork.is_t3() {
3482 let expected = TempoPrecompileError::TIP20(TIP20Error::contract_paused());
3483 assert_eq!(mint_result, Err(expected.clone()));
3484 assert_eq!(mint_memo_result, Err(expected));
3485 } else {
3486 assert!(mint_result.is_ok());
3487 assert!(mint_memo_result.is_ok());
3488 }
3489
3490 Ok::<_, TempoPrecompileError>(())
3491 })?;
3492 }
3493 Ok(())
3494 }
3495
3496 #[test]
3497 fn test_burn_rejects_when_paused_on_t3() -> eyre::Result<()> {
3498 let amount = U256::from(500);
3499 let memo = FixedBytes::random();
3500
3501 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
3502 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
3503 let admin = Address::random();
3504
3505 StorageCtx::enter(&mut storage, || {
3506 let mut token = TIP20Setup::create("Test", "TST", admin)
3507 .with_issuer(admin)
3508 .with_role(admin, *PAUSE_ROLE)
3509 .with_mint(admin, amount * U256::from(2))
3510 .apply()?;
3511
3512 token.pause(admin, ITIP20::pauseCall {})?;
3513
3514 let burn_result = token.burn(admin, ITIP20::burnCall { amount });
3515 let burn_memo_result =
3516 token.burn_with_memo(admin, ITIP20::burnWithMemoCall { amount, memo });
3517
3518 if hardfork.is_t3() {
3519 let expected = TempoPrecompileError::TIP20(TIP20Error::contract_paused());
3520 assert_eq!(burn_result, Err(expected.clone()));
3521 assert_eq!(burn_memo_result, Err(expected));
3522 } else {
3523 assert!(burn_result.is_ok());
3524 assert!(burn_memo_result.is_ok());
3525 }
3526
3527 Ok::<_, TempoPrecompileError>(())
3528 })?;
3529 }
3530 Ok(())
3531 }
3532
3533 #[test]
3534 fn test_burn_blocked_rejects_when_paused_on_t3() -> eyre::Result<()> {
3535 let amount = U256::from(500);
3536 let blocked = Address::random();
3537
3538 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
3539 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
3540 let admin = Address::random();
3541
3542 StorageCtx::enter(&mut storage, || {
3543 let mut registry = TIP403Registry::new();
3545 registry.initialize()?;
3546 let policy_id = registry.create_policy(
3547 admin,
3548 ITIP403Registry::createPolicyCall {
3549 admin,
3550 policyType: ITIP403Registry::PolicyType::BLACKLIST,
3551 },
3552 )?;
3553 registry.modify_policy_blacklist(
3554 admin,
3555 ITIP403Registry::modifyPolicyBlacklistCall {
3556 policyId: policy_id,
3557 account: blocked,
3558 restricted: true,
3559 },
3560 )?;
3561
3562 let mut token = TIP20Setup::create("Test", "TST", admin)
3563 .with_issuer(admin)
3564 .with_role(admin, *PAUSE_ROLE)
3565 .with_role(admin, *BURN_BLOCKED_ROLE)
3566 .with_mint(blocked, amount)
3567 .apply()?;
3568
3569 token.change_transfer_policy_id(
3571 admin,
3572 ITIP20::changeTransferPolicyIdCall {
3573 newPolicyId: policy_id,
3574 },
3575 )?;
3576
3577 token.pause(admin, ITIP20::pauseCall {})?;
3579
3580 let result = token.burn_blocked(
3581 admin,
3582 ITIP20::burnBlockedCall {
3583 from: blocked,
3584 amount,
3585 },
3586 );
3587
3588 if hardfork.is_t3() {
3589 assert_eq!(
3590 result,
3591 Err(TempoPrecompileError::TIP20(TIP20Error::contract_paused()))
3592 );
3593 } else {
3594 assert!(result.is_ok());
3596 assert_eq!(token.get_balance(blocked)?, U256::ZERO);
3597 }
3598
3599 Ok::<_, TempoPrecompileError>(())
3600 })?;
3601 }
3602 Ok(())
3603 }
3604}