1pub mod dispatch;
12pub mod rewards;
13pub mod roles;
14
15pub use tempo_contracts::precompiles::{
16 IRolesAuth, ITIP20, RolesAuthError, RolesAuthEvent, TIP20Error, TIP20Event, USD_CURRENCY,
17};
18pub use tempo_primitives::is_tip20_prefix;
19
20pub use slots as tip20_slots;
22
23use crate::{
24 PATH_USD_ADDRESS, RECEIVE_POLICY_GUARD_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
25 account_keychain::AccountKeychain,
26 address_registry::AddressRegistry,
27 error::{Result, TempoPrecompileError},
28 receive_policy_guard::{InboundKind, ReceivePolicyGuard, RecoveryMode},
29 storage::{Handler, Mapping},
30 tip20::{rewards::UserRewardInfo, roles::DEFAULT_ADMIN_ROLE},
31 tip20_factory::TIP20Factory,
32 tip403_registry::{AuthRole, ITIP403Registry, TIP403Registry},
33};
34use alloy::{
35 primitives::{Address, B256, U256, keccak256, uint},
36 sol_types::SolValue,
37};
38use std::sync::LazyLock;
39use tempo_chainspec::hardfork::TempoHardfork;
40use tempo_contracts::precompiles::{
41 DECIMALS as TIP20_DECIMALS, ReceivePolicyGuardError, STABLECOIN_DEX_ADDRESS,
42 TIP20_CHANNEL_RESERVE_ADDRESS,
43};
44use tempo_precompiles_macros::contract;
45use tempo_primitives::TempoAddressExt;
46use tracing::trace;
47
48pub const U128_MAX: U256 = uint!(0xffffffffffffffffffffffffffffffff_U256);
50
51pub fn validate_usd_currency(token: Address) -> Result<()> {
57 if TIP20Token::from_address(token)?.currency()? != USD_CURRENCY {
58 return Err(TIP20Error::invalid_currency().into());
59 }
60 Ok(())
61}
62
63#[contract]
77pub struct TIP20Token {
78 roles: Mapping<Address, Mapping<B256, bool>>,
80 role_admins: Mapping<B256, B256>,
81
82 name: String,
84 symbol: String,
85 currency: String,
86 logo_uri: String,
92 quote_token: Address,
93 next_quote_token: Address,
94 transfer_policy_id: u64,
95
96 total_supply: U256,
98 balances: Mapping<Address, U256>,
99 allowances: Mapping<Address, Mapping<Address, U256>>,
100 permit_nonces: Mapping<Address, U256>,
101 paused: bool,
102 supply_cap: U256,
103 _salts: Mapping<B256, bool>,
105
106 global_reward_per_token: U256,
108 opted_in_supply: u128,
109 user_reward_info: Mapping<Address, UserRewardInfo>,
110}
111
112pub static PERMIT_TYPEHASH: LazyLock<B256> = LazyLock::new(|| {
114 keccak256(b"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
115});
116
117pub static EIP712_DOMAIN_TYPEHASH: LazyLock<B256> = LazyLock::new(|| {
119 keccak256(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
120});
121
122pub static VERSION_HASH: LazyLock<B256> = LazyLock::new(|| keccak256(b"1"));
124
125pub static PAUSE_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"PAUSE_ROLE"));
127pub static UNPAUSE_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"UNPAUSE_ROLE"));
129pub static ISSUER_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"ISSUER_ROLE"));
131pub static BURN_BLOCKED_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"BURN_BLOCKED_ROLE"));
133
134#[rustfmt::skip]
135pub const PROTECTED: &[(TempoHardfork, &[Address])] = &[
137 (TempoHardfork::Genesis, &[TIP_FEE_MANAGER_ADDRESS, STABLECOIN_DEX_ADDRESS]),
138 (TempoHardfork::T5, &[TIP20_CHANNEL_RESERVE_ADDRESS]),
139 (TempoHardfork::T6, &[RECEIVE_POLICY_GUARD_ADDRESS]),
140];
141
142impl TIP20Token {
143 pub fn name(&self) -> Result<String> {
145 self.name.read()
146 }
147
148 pub fn symbol(&self) -> Result<String> {
150 self.symbol.read()
151 }
152
153 pub fn decimals(&self) -> Result<u8> {
155 Ok(TIP20_DECIMALS)
156 }
157
158 pub fn currency(&self) -> Result<String> {
160 self.currency.read()
161 }
162
163 pub fn logo_uri(&self) -> Result<String> {
167 self.logo_uri.read()
168 }
169
170 pub fn total_supply(&self) -> Result<U256> {
172 self.total_supply.read()
173 }
174
175 pub fn quote_token(&self) -> Result<Address> {
177 self.quote_token.read()
178 }
179
180 pub fn next_quote_token(&self) -> Result<Address> {
182 self.next_quote_token.read()
183 }
184
185 pub fn supply_cap(&self) -> Result<U256> {
187 self.supply_cap.read()
188 }
189
190 pub fn paused(&self) -> Result<bool> {
192 self.paused.read()
193 }
194
195 pub fn transfer_policy_id(&self) -> Result<u64> {
197 self.transfer_policy_id.read()
198 }
199
200 pub fn pause_role() -> B256 {
205 *PAUSE_ROLE
206 }
207
208 pub fn unpause_role() -> B256 {
213 *UNPAUSE_ROLE
214 }
215
216 pub fn issuer_role() -> B256 {
221 *ISSUER_ROLE
222 }
223
224 pub fn burn_blocked_role() -> B256 {
229 *BURN_BLOCKED_ROLE
230 }
231
232 pub fn balance_of(&self, call: ITIP20::balanceOfCall) -> Result<U256> {
234 self.balances[call.account].read()
235 }
236
237 pub fn allowance(&self, call: ITIP20::allowanceCall) -> Result<U256> {
239 self.allowances[call.owner][call.spender].read()
240 }
241
242 pub fn change_transfer_policy_id(
248 &mut self,
249 msg_sender: Address,
250 call: ITIP20::changeTransferPolicyIdCall,
251 ) -> Result<()> {
252 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
253
254 if !TIP403Registry::new().policy_exists(ITIP403Registry::policyExistsCall {
256 policyId: call.newPolicyId,
257 })? {
258 return Err(TIP20Error::invalid_transfer_policy_id().into());
259 }
260
261 self.transfer_policy_id.write(call.newPolicyId)?;
262
263 self.emit_event(TIP20Event::transfer_policy_update(
264 msg_sender,
265 call.newPolicyId,
266 ))
267 }
268
269 pub fn set_supply_cap(
276 &mut self,
277 msg_sender: Address,
278 call: ITIP20::setSupplyCapCall,
279 ) -> Result<()> {
280 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
281 if call.newSupplyCap < self.total_supply()? {
282 return Err(TIP20Error::invalid_supply_cap().into());
283 }
284
285 if call.newSupplyCap > U128_MAX {
286 return Err(TIP20Error::supply_cap_exceeded().into());
287 }
288
289 self.supply_cap.write(call.newSupplyCap)?;
290
291 self.emit_event(TIP20Event::supply_cap_update(msg_sender, call.newSupplyCap))
292 }
293
294 pub const MAX_LOGO_URI_BYTES: usize = 256;
298
299 pub const ALLOWED_LOGO_URI_SCHEMES: &'static [&'static str] =
305 &["https", "http", "ipfs", "data"];
306
307 pub(crate) fn validate_logo_uri(uri: &str) -> Result<()> {
313 if uri.len() > Self::MAX_LOGO_URI_BYTES {
314 return Err(TIP20Error::logo_uri_too_long().into());
315 }
316 if !uri.is_empty() && !Self::is_allowed_logo_uri(uri) {
317 return Err(TIP20Error::invalid_logo_uri().into());
318 }
319 Ok(())
320 }
321
322 fn is_allowed_logo_uri(uri: &str) -> bool {
323 let Some((scheme, _rest)) = uri.split_once(':') else {
324 return false;
325 };
326
327 let mut bytes = scheme.bytes();
328 let Some(first) = bytes.next() else {
329 return false;
330 };
331 if !first.is_ascii_alphabetic() {
332 return false;
333 }
334 if !bytes.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b'.')) {
335 return false;
336 }
337
338 Self::ALLOWED_LOGO_URI_SCHEMES
339 .iter()
340 .any(|allowed| scheme.eq_ignore_ascii_case(allowed))
341 }
342
343 pub fn set_logo_uri(
353 &mut self,
354 msg_sender: Address,
355 call: ITIP20::setLogoURICall,
356 ) -> Result<()> {
357 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
358 self.write_logo_uri(msg_sender, call.newLogoURI)
359 }
360
361 pub(crate) fn write_logo_uri(&mut self, updater: Address, new_logo_uri: String) -> Result<()> {
366 Self::validate_logo_uri(&new_logo_uri)?;
367
368 self.logo_uri.write(new_logo_uri.clone())?;
369
370 self.emit_event(TIP20Event::LogoURIUpdated(ITIP20::LogoURIUpdated {
371 updater,
372 newLogoURI: new_logo_uri,
373 }))
374 }
375
376 pub fn pause(&mut self, msg_sender: Address, _call: ITIP20::pauseCall) -> Result<()> {
383 self.check_role(msg_sender, *PAUSE_ROLE)?;
384 self.paused.write(true)?;
385
386 self.emit_event(TIP20Event::pause_state_update(msg_sender, true))
387 }
388
389 pub fn unpause(&mut self, msg_sender: Address, _call: ITIP20::unpauseCall) -> Result<()> {
394 self.check_role(msg_sender, *UNPAUSE_ROLE)?;
395 self.paused.write(false)?;
396
397 self.emit_event(TIP20Event::pause_state_update(msg_sender, false))
398 }
399
400 pub fn set_next_quote_token(
409 &mut self,
410 msg_sender: Address,
411 call: ITIP20::setNextQuoteTokenCall,
412 ) -> Result<()> {
413 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
414
415 if self.address == PATH_USD_ADDRESS {
416 return Err(TIP20Error::invalid_quote_token().into());
417 }
418
419 if !TIP20Factory::new().is_tip20(call.newQuoteToken)? {
422 return Err(TIP20Error::invalid_quote_token().into());
423 }
424
425 let currency = self.currency()?;
427 if currency == USD_CURRENCY {
428 let quote_token_currency = Self::from_address(call.newQuoteToken)?.currency()?;
429 if quote_token_currency != USD_CURRENCY {
430 return Err(TIP20Error::invalid_quote_token().into());
431 }
432 }
433
434 self.next_quote_token.write(call.newQuoteToken)?;
435
436 self.emit_event(TIP20Event::next_quote_token_set(
437 msg_sender,
438 call.newQuoteToken,
439 ))
440 }
441
442 pub fn complete_quote_token_update(
449 &mut self,
450 msg_sender: Address,
451 _call: ITIP20::completeQuoteTokenUpdateCall,
452 ) -> Result<()> {
453 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
454
455 let next_quote_token = self.next_quote_token()?;
456
457 let mut current = next_quote_token;
460 while current != PATH_USD_ADDRESS {
461 if current == self.address {
462 return Err(TIP20Error::invalid_quote_token().into());
463 }
464
465 current = Self::from_address(current)?.quote_token()?;
466 }
467
468 self.quote_token.write(next_quote_token)?;
470
471 self.emit_event(TIP20Event::quote_token_update(msg_sender, next_quote_token))
472 }
473
474 pub fn mint(&mut self, msg_sender: Address, call: ITIP20::mintCall) -> Result<()> {
488 let Some((total_supply, to)) =
489 self.validate_mint(msg_sender, call.to, call.amount, B256::ZERO)?
490 else {
491 return Ok(());
492 };
493
494 self._mint(&to, total_supply, call.amount)?;
495 self.emit_event(TIP20Event::mint(call.to, call.amount))?;
496 if let Some(hop) = to.build_virtual_transfer_event(call.amount) {
497 self.emit_event(hop)?;
498 }
499
500 Ok(())
501 }
502
503 pub fn mint_with_memo(
505 &mut self,
506 msg_sender: Address,
507 call: ITIP20::mintWithMemoCall,
508 ) -> Result<()> {
509 let Some((total_supply, to)) =
510 self.validate_mint(msg_sender, call.to, call.amount, call.memo)?
511 else {
512 return Ok(());
513 };
514
515 self._mint(&to, total_supply, call.amount)?;
516 self.emit_event(TIP20Event::transfer_with_memo(
517 Address::ZERO,
518 call.to,
519 call.amount,
520 call.memo,
521 ))?;
522 self.emit_event(TIP20Event::mint(call.to, call.amount))?;
523 if let Some(hop) = to.build_virtual_transfer_event(call.amount) {
524 self.emit_event(hop)?;
525 }
526 Ok(())
527 }
528
529 pub(crate) fn _mint(&mut self, to: &Recipient, total_supply: U256, amount: U256) -> Result<()> {
531 let new_supply = total_supply
532 .checked_add(amount)
533 .ok_or(TempoPrecompileError::under_overflow())?;
534
535 let supply_cap = self.supply_cap()?;
536 if new_supply > supply_cap {
537 return Err(TIP20Error::supply_cap_exceeded().into());
538 }
539
540 self.handle_rewards_on_mint(to.target, amount)?;
541
542 self.set_total_supply(new_supply)?;
543 self.increment_balance(to.target, amount)?;
544
545 self.emit_event(to.build_transfer_event(Address::ZERO, amount))
546 }
547
548 pub fn burn(&mut self, msg_sender: Address, call: ITIP20::burnCall) -> Result<()> {
555 self._burn(msg_sender, call.amount)?;
556 self.emit_event(TIP20Event::burn(msg_sender, call.amount))
557 }
558
559 pub fn burn_with_memo(
561 &mut self,
562 msg_sender: Address,
563 call: ITIP20::burnWithMemoCall,
564 ) -> Result<()> {
565 self._burn(msg_sender, call.amount)?;
566
567 self.emit_event(TIP20Event::transfer_with_memo(
568 msg_sender,
569 Address::ZERO,
570 call.amount,
571 call.memo,
572 ))?;
573 self.emit_event(TIP20Event::burn(msg_sender, call.amount))
574 }
575
576 pub fn burn_blocked(
585 &mut self,
586 msg_sender: Address,
587 owner: Address,
588 amount: U256,
589 check_protected: bool,
590 ) -> Result<()> {
591 let hardfork = self.storage.spec();
592
593 if hardfork.is_t3() {
595 self.check_not_paused()?;
596 }
597 self.check_role(msg_sender, *BURN_BLOCKED_ROLE)?;
598
599 if check_protected {
600 if PROTECTED
602 .iter()
603 .any(|(hf, addr)| hardfork >= *hf && addr.contains(&owner))
604 || (hardfork.is_t5() && owner == self.address)
605 {
606 return Err(TIP20Error::protected_address().into());
607 }
608 }
609
610 let policy_id = self.transfer_policy_id()?;
612 if TIP403Registry::new().is_authorized_as(policy_id, owner, AuthRole::sender())? {
613 return Err(TIP20Error::policy_forbids().into());
615 }
616
617 let burn_from = if check_protected {
618 owner
619 } else {
620 RECEIVE_POLICY_GUARD_ADDRESS
621 };
622 self._transfer(burn_from, &Recipient::direct(Address::ZERO), amount)?;
623
624 let total_supply = self.total_supply()?;
625 let new_supply =
626 total_supply
627 .checked_sub(amount)
628 .ok_or(TIP20Error::insufficient_balance(
629 total_supply,
630 amount,
631 self.address,
632 ))?;
633 self.set_total_supply(new_supply)?;
634
635 self.emit_event(TIP20Event::burn_blocked(owner, amount))
636 }
637
638 fn _burn(&mut self, msg_sender: Address, amount: U256) -> Result<()> {
639 if self.storage.spec().is_t3() {
641 self.check_not_paused()?;
642 }
643 self.check_role(msg_sender, *ISSUER_ROLE)?;
644
645 self._transfer(msg_sender, &Recipient::direct(Address::ZERO), amount)?;
646
647 let total_supply = self.total_supply()?;
648 let new_supply =
649 total_supply
650 .checked_sub(amount)
651 .ok_or(TIP20Error::insufficient_balance(
652 total_supply,
653 amount,
654 self.address,
655 ))?;
656 self.set_total_supply(new_supply)
657 }
658
659 pub fn approve(&mut self, msg_sender: Address, call: ITIP20::approveCall) -> Result<bool> {
666 AccountKeychain::new().authorize_approve(
668 msg_sender,
669 self.address,
670 self.get_allowance(msg_sender, call.spender)?,
671 call.amount,
672 )?;
673
674 self.set_allowance(msg_sender, call.spender, call.amount)?;
676
677 self.emit_event(TIP20Event::approval(msg_sender, call.spender, call.amount))?;
678
679 Ok(true)
680 }
681
682 pub fn nonces(&self, call: ITIP20::noncesCall) -> Result<U256> {
686 self.permit_nonces[call.owner].read()
687 }
688
689 pub fn domain_separator(&self) -> Result<B256> {
691 let name = self.name()?;
692 let name_hash = self.storage.keccak256(name.as_bytes())?;
693 let chain_id = U256::from(self.storage.chain_id());
694
695 let encoded = (
696 *EIP712_DOMAIN_TYPEHASH,
697 name_hash,
698 *VERSION_HASH,
699 chain_id,
700 self.address,
701 )
702 .abi_encode();
703
704 self.storage.keccak256(&encoded)
705 }
706
707 pub fn permit(&mut self, call: ITIP20::permitCall) -> Result<()> {
716 if self.storage.timestamp() > call.deadline {
718 return Err(TIP20Error::permit_expired().into());
719 }
720
721 let nonce = self.permit_nonces[call.owner].read()?;
723 let struct_hash = self.storage.keccak256(
724 &(
725 *PERMIT_TYPEHASH,
726 call.owner,
727 call.spender,
728 call.value,
729 nonce,
730 call.deadline,
731 )
732 .abi_encode(),
733 )?;
734
735 let domain_separator = self.domain_separator()?;
737 let digest = self.storage.keccak256(
738 &[
739 &[0x19, 0x01],
740 domain_separator.as_slice(),
741 struct_hash.as_slice(),
742 ]
743 .concat(),
744 )?;
745
746 let recovered = self
749 .storage
750 .recover_signer(digest, call.v, call.r, call.s)?
751 .ok_or(TIP20Error::invalid_signature())?;
752 if recovered != call.owner {
753 return Err(TIP20Error::invalid_signature().into());
754 }
755
756 self.permit_nonces[call.owner].write(
758 nonce
759 .checked_add(U256::from(1))
760 .ok_or(TempoPrecompileError::under_overflow())?,
761 )?;
762
763 self.set_allowance(call.owner, call.spender, call.value)?;
765
766 self.emit_event(TIP20Event::approval(call.owner, call.spender, call.value))
768 }
769
770 pub fn transfer(&mut self, msg_sender: Address, call: ITIP20::transferCall) -> Result<bool> {
780 trace!(%msg_sender, ?call, "transferring TIP20");
781 let Some(to) =
782 self.validate_transfer(None, msg_sender, call.to, call.amount, B256::ZERO)?
783 else {
784 return Ok(true);
785 };
786
787 self._transfer(msg_sender, &to, call.amount)?;
788 if let Some(hop) = to.build_virtual_transfer_event(call.amount) {
789 self.emit_event(hop)?;
790 }
791
792 Ok(true)
793 }
794
795 pub fn transfer_from(
805 &mut self,
806 msg_sender: Address,
807 call: ITIP20::transferFromCall,
808 ) -> Result<bool> {
809 let Some(to) = self.validate_transfer(
810 Some(msg_sender),
811 call.from,
812 call.to,
813 call.amount,
814 B256::ZERO,
815 )?
816 else {
817 return Ok(true);
818 };
819
820 self._transfer(call.from, &to, call.amount)?;
821 if let Some(hop) = to.build_virtual_transfer_event(call.amount) {
822 self.emit_event(hop)?;
823 }
824
825 Ok(true)
826 }
827
828 pub fn transfer_from_with_memo(
830 &mut self,
831 msg_sender: Address,
832 call: ITIP20::transferFromWithMemoCall,
833 ) -> Result<bool> {
834 let Some(to) =
835 self.validate_transfer(Some(msg_sender), call.from, call.to, call.amount, call.memo)?
836 else {
837 return Ok(true);
838 };
839
840 self._transfer(call.from, &to, call.amount)?;
841 self.emit_event(TIP20Event::transfer_with_memo(
842 call.from,
843 call.to,
844 call.amount,
845 call.memo,
846 ))?;
847 if let Some(hop) = to.build_virtual_transfer_event(call.amount) {
848 self.emit_event(hop)?;
849 }
850 Ok(true)
851 }
852
853 pub fn system_transfer_from(
873 &mut self,
874 caller: Address,
875 from: Address,
876 amount: U256,
877 ) -> Result<bool> {
878 let spec = self.storage.spec();
880 if spec.is_t5() && !crate::address_registry::is_implicitly_approved(caller, spec) {
881 return Err(TIP20Error::unauthorized().into());
882 }
883
884 let Some(to) = self.validate_transfer(None, from, caller, amount, B256::ZERO)? else {
885 return Ok(true);
886 };
887
888 self._transfer(from, &to, amount)?;
889 if let Some(hop) = to.build_virtual_transfer_event(amount) {
890 self.emit_event(hop)?;
891 }
892
893 Ok(true)
894 }
895
896 fn consume_allowance(&mut self, owner: Address, spender: Address, amount: U256) -> Result<()> {
898 let allowed = self.get_allowance(owner, spender)?;
899 if amount > allowed {
900 return Err(TIP20Error::insufficient_allowance().into());
901 }
902
903 if allowed != U256::MAX {
904 let new_allowance = allowed
905 .checked_sub(amount)
906 .ok_or(TIP20Error::insufficient_allowance())?;
907 self.set_allowance(owner, spender, new_allowance)?;
908 }
909 Ok(())
910 }
911
912 pub fn transfer_with_memo(
914 &mut self,
915 msg_sender: Address,
916 call: ITIP20::transferWithMemoCall,
917 ) -> Result<()> {
918 let Some(to) = self.validate_transfer(None, msg_sender, call.to, call.amount, call.memo)?
919 else {
920 return Ok(());
921 };
922
923 self._transfer(msg_sender, &to, call.amount)?;
924 self.emit_event(TIP20Event::transfer_with_memo(
925 msg_sender,
926 call.to,
927 call.amount,
928 call.memo,
929 ))?;
930 if let Some(hop) = to.build_virtual_transfer_event(call.amount) {
931 self.emit_event(hop)?;
932 }
933 Ok(())
934 }
935}
936
937impl TIP20Token {
939 pub fn from_address(address: Address) -> Result<Self> {
944 if !address.is_tip20() {
945 return Err(TIP20Error::invalid_token().into());
946 }
947 Ok(Self::__new(address))
948 }
949
950 #[inline]
955 pub fn from_address_unchecked(address: Address) -> Self {
956 debug_assert!(address.is_tip20(), "address must have TIP20 prefix");
957 Self::__new(address)
958 }
959
960 pub fn initialize(
963 &mut self,
964 msg_sender: Address,
965 name: &str,
966 symbol: &str,
967 currency: &str,
968 quote_token: Address,
969 admin: Address,
970 ) -> Result<()> {
971 trace!(%name, address=%self.address, "Initializing token");
972
973 self.__initialize()?;
975
976 self.name.write(name.to_string())?;
977 self.symbol.write(symbol.to_string())?;
978 self.currency.write(currency.to_string())?;
979
980 self.quote_token.write(quote_token)?;
981 self.next_quote_token.write(quote_token)?;
983
984 self.supply_cap.write(U128_MAX)?;
986 self.transfer_policy_id.write(1)?;
987
988 self.initialize_roles()?;
990 self.grant_default_admin(msg_sender, admin)
991 }
992
993 fn get_balance(&self, account: Address) -> Result<U256> {
994 self.balances[account].read()
995 }
996
997 fn set_balance(&mut self, account: Address, amount: U256) -> Result<()> {
998 self.balances[account].write(amount)
999 }
1000
1001 fn increment_balance(&mut self, account: Address, amount: U256) -> Result<()> {
1002 self.balances[account].sinc(amount).map_err(|err| {
1003 if err == TempoPrecompileError::under_overflow() {
1004 TIP20Error::supply_cap_exceeded().into()
1005 } else {
1006 err
1007 }
1008 })
1009 }
1010
1011 fn decrement_balance(&mut self, account: Address, amount: U256) -> Result<()> {
1012 self.balances[account]
1013 .sdec(amount)
1014 .map_err(|err| match err {
1015 TempoPrecompileError::StorageDeltaUnderflow(current) => {
1016 TIP20Error::insufficient_balance(current, amount, self.address).into()
1017 }
1018 err => err,
1019 })
1020 }
1021
1022 fn get_allowance(&self, owner: Address, spender: Address) -> Result<U256> {
1023 self.allowances[owner][spender].read()
1024 }
1025
1026 fn set_allowance(&mut self, owner: Address, spender: Address, amount: U256) -> Result<()> {
1027 self.allowances[owner][spender].write(amount)
1028 }
1029
1030 fn set_total_supply(&mut self, amount: U256) -> Result<()> {
1031 self.total_supply.write(amount)
1032 }
1033
1034 pub fn check_not_paused(&self) -> Result<()> {
1035 if self.paused()? {
1036 return Err(TIP20Error::contract_paused().into());
1037 }
1038 Ok(())
1039 }
1040
1041 fn validate_transfer(
1051 &mut self,
1052 spender: Option<Address>,
1053 from: Address,
1054 to: Address,
1055 amount: U256,
1056 memo: B256,
1057 ) -> Result<Option<Recipient>> {
1058 let to = Recipient::resolve(to)?;
1059 self.check_not_paused()?;
1060 to.validate()?;
1061 self.ensure_transfer_authorized(from, to.target)?;
1062
1063 if let Some(spender) = spender {
1064 self.consume_allowance(from, spender, amount)?;
1065 } else {
1066 self.check_and_update_spending_limit(from, amount)?;
1067 }
1068
1069 if self.validate_inbound_or_block(from, &to, amount, None, memo)? {
1070 return Ok(None);
1071 }
1072
1073 Ok(Some(to))
1074 }
1075
1076 fn validate_mint(
1083 &mut self,
1084 msg_sender: Address,
1085 to: Address,
1086 amount: U256,
1087 memo: B256,
1088 ) -> Result<Option<(U256, Recipient)>> {
1089 let to = Recipient::resolve(to)?;
1090 self.check_role(msg_sender, *ISSUER_ROLE)?;
1091 let total_supply = self.total_supply()?;
1092
1093 if self.storage.spec().is_t3() {
1094 self.check_not_paused()?;
1095 to.validate()?;
1096 }
1097
1098 if !TIP403Registry::new().is_authorized_as(
1100 self.transfer_policy_id()?,
1101 to.target,
1102 AuthRole::mint_recipient(),
1103 )? {
1104 return Err(TIP20Error::policy_forbids().into());
1105 }
1106
1107 if self.validate_inbound_or_block(msg_sender, &to, amount, Some(total_supply), memo)? {
1108 return Ok(None);
1109 }
1110
1111 Ok(Some((total_supply, to)))
1112 }
1113
1114 pub fn is_transfer_authorized(&self, from: Address, to: Address) -> Result<bool> {
1119 let policy_id = self.transfer_policy_id()?;
1120 let registry = TIP403Registry::new();
1121
1122 let sender_auth = registry.is_authorized_as(policy_id, from, AuthRole::sender())?;
1124 if self.storage.spec().is_t2() && !sender_auth {
1125 return Ok(false);
1126 }
1127 let recipient_auth = registry.is_authorized_as(policy_id, to, AuthRole::recipient())?;
1128 Ok(sender_auth && recipient_auth)
1129 }
1130
1131 pub fn ensure_transfer_authorized(&self, from: Address, to: Address) -> Result<()> {
1136 if !self.is_transfer_authorized(from, to)? {
1137 return Err(TIP20Error::policy_forbids().into());
1138 }
1139
1140 Ok(())
1141 }
1142
1143 pub fn ensure_authorized_as(&self, user: Address, role: AuthRole) -> Result<()> {
1148 let policy_id = self.transfer_policy_id()?;
1149 if !TIP403Registry::new().is_authorized_as(policy_id, user, role)? {
1150 return Err(TIP20Error::policy_forbids().into());
1151 }
1152 Ok(())
1153 }
1154
1155 pub fn check_and_update_spending_limit(&mut self, from: Address, amount: U256) -> Result<()> {
1160 AccountKeychain::new().authorize_transfer(from, self.address, amount)
1161 }
1162
1163 pub(crate) fn _transfer(&mut self, from: Address, to: &Recipient, amount: U256) -> Result<()> {
1168 let from_balance = if !self.storage.spec().is_t8() {
1169 let from_balance = self.get_balance(from)?;
1170 if amount > from_balance {
1171 return Err(
1172 TIP20Error::insufficient_balance(from_balance, amount, self.address).into(),
1173 );
1174 }
1175 Some(from_balance)
1176 } else {
1177 None
1178 };
1179
1180 self.handle_rewards_on_transfer(from, to.target, amount)?;
1181
1182 if let Some(from_balance) = from_balance {
1187 let new_from_balance = from_balance
1189 .checked_sub(amount)
1190 .ok_or(TempoPrecompileError::under_overflow())?;
1191
1192 self.set_balance(from, new_from_balance)?;
1193 } else {
1194 self.decrement_balance(from, amount)?;
1196 }
1197
1198 if to.target != Address::ZERO {
1199 self.increment_balance(to.target, amount)?;
1200 }
1201
1202 self.emit_event(to.build_transfer_event(from, amount))
1203 }
1204
1205 pub(crate) fn validate_inbound_or_block(
1209 &mut self,
1210 originator: Address,
1211 to: &Recipient,
1212 amount: U256,
1213 mint_total_supply: Option<U256>,
1214 memo: B256,
1215 ) -> Result<bool> {
1216 if !self.storage.spec().is_t6() {
1217 return Ok(false);
1218 }
1219 if to.target == RECEIVE_POLICY_GUARD_ADDRESS {
1220 return Err(ReceivePolicyGuardError::address_reserved().into());
1221 }
1222
1223 let token = self.address;
1224 let Some((reason, recovery)) =
1225 TIP403Registry::new().check_receive_policy(token, originator, to.target)?
1226 else {
1227 return Ok(false);
1228 };
1229
1230 let guard = Recipient::direct(RECEIVE_POLICY_GUARD_ADDRESS);
1231 let kind = if let Some(total_supply) = mint_total_supply {
1232 self._mint(&guard, total_supply, amount)?;
1233 self.emit_event(TIP20Event::mint(guard.target, amount))?;
1234 InboundKind::MINT
1235 } else {
1236 self._transfer(originator, &guard, amount)?;
1237 InboundKind::TRANSFER
1238 };
1239 ReceivePolicyGuard::new()
1240 .store_blocked(token, originator, to, recovery, amount, reason, kind, memo)?;
1241
1242 Ok(true)
1243 }
1244
1245 pub(crate) fn release_blocked_funds(
1248 &mut self,
1249 originator: Address,
1250 receiver: Address,
1251 to: Address,
1252 amount: U256,
1253 recovery_mode: RecoveryMode,
1254 recovery_auth: Address,
1255 ) -> Result<()> {
1256 debug_assert!(
1257 to != RECEIVE_POLICY_GUARD_ADDRESS,
1258 "checked in ReceivePolicyGuard::claim"
1259 );
1260
1261 self.check_not_paused()?;
1262 let destination = Recipient::resolve(to)?;
1263 destination.validate()?;
1264 if recovery_mode.is_reroute(to, receiver) {
1265 let policy_subject = recovery_mode.policy_subject(originator, receiver);
1266 self.ensure_transfer_authorized(policy_subject, destination.target)?;
1267 if TIP403Registry::new()
1268 .validate_receive_policy(self.address, policy_subject, destination.target)?
1269 .is_some()
1270 {
1271 return Err(TIP20Error::policy_forbids().into());
1272 }
1273 if let Some(addr) = recovery_mode.spending_account(recovery_auth) {
1274 self.check_and_update_spending_limit(addr, amount)?;
1275 }
1276 } else {
1277 self.ensure_authorized_as(destination.target, AuthRole::recipient())?;
1278 }
1279
1280 self._transfer(RECEIVE_POLICY_GUARD_ADDRESS, &destination, amount)?;
1281 if let Some(hop) = destination.build_virtual_transfer_event(amount) {
1282 self.emit_event(hop)?;
1283 }
1284 Ok(())
1285 }
1286
1287 pub fn transfer_fee_pre_tx(&mut self, from: Address, amount: U256) -> Result<()> {
1295 self.check_not_paused()?;
1300 self.check_and_update_spending_limit(from, amount)?;
1301
1302 let from_reward_recipient = self.update_rewards(from)?;
1304
1305 if from_reward_recipient != Address::ZERO {
1307 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
1308 .checked_sub(amount)
1309 .ok_or(TempoPrecompileError::under_overflow())?;
1310 self.set_opted_in_supply(
1311 opted_in_supply
1312 .try_into()
1313 .map_err(|_| TempoPrecompileError::under_overflow())?,
1314 )?;
1315 }
1316
1317 self.decrement_balance(from, amount)?;
1318 self.increment_balance(TIP_FEE_MANAGER_ADDRESS, amount)?;
1319
1320 Ok(())
1321 }
1322
1323 pub fn transfer_fee_post_tx(
1328 &mut self,
1329 to: Address,
1330 refund: U256,
1331 actual_spending: U256,
1332 ) -> Result<()> {
1333 self.emit_event(TIP20Event::transfer(
1334 to,
1335 TIP_FEE_MANAGER_ADDRESS,
1336 actual_spending,
1337 ))?;
1338
1339 if refund.is_zero() {
1341 return Ok(());
1342 }
1343
1344 if self.storage.spec().is_t1c() {
1345 AccountKeychain::new().refund_spending_limit(to, self.address, refund)?;
1346 }
1347
1348 let to_reward_recipient = self.update_rewards(to)?;
1350
1351 if to_reward_recipient != Address::ZERO {
1353 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
1354 .checked_add(refund)
1355 .ok_or(TempoPrecompileError::under_overflow())?;
1356 self.set_opted_in_supply(
1357 opted_in_supply
1358 .try_into()
1359 .map_err(|_| TempoPrecompileError::under_overflow())?,
1360 )?;
1361 }
1362
1363 self.decrement_balance(TIP_FEE_MANAGER_ADDRESS, refund)?;
1364 self.increment_balance(to, refund)?;
1365
1366 Ok(())
1367 }
1368}
1369
1370#[derive(Debug, PartialEq)]
1377pub(crate) struct Recipient {
1378 pub(crate) target: Address,
1380 pub(crate) virtual_addr: Option<Address>,
1382}
1383
1384impl Recipient {
1385 #[inline]
1387 pub(crate) fn direct(addr: Address) -> Self {
1388 Self {
1389 target: addr,
1390 virtual_addr: None,
1391 }
1392 }
1393
1394 pub(crate) fn resolve(addr: Address) -> Result<Self> {
1399 let effective = AddressRegistry::new().resolve_recipient(addr)?;
1400 Ok(if effective == addr {
1401 Self::direct(addr)
1402 } else {
1403 Self {
1404 target: effective,
1405 virtual_addr: Some(addr),
1406 }
1407 })
1408 }
1409
1410 pub(crate) fn validate(&self) -> Result<()> {
1414 if self.target.is_zero() || self.target.is_tip20() {
1415 return Err(TIP20Error::invalid_recipient().into());
1416 }
1417 Ok(())
1418 }
1419
1420 pub(crate) fn build_transfer_event(&self, from: Address, amount: U256) -> TIP20Event {
1425 TIP20Event::transfer(from, self.virtual_addr.unwrap_or(self.target), amount)
1426 }
1427
1428 pub(crate) fn build_virtual_transfer_event(&self, amount: U256) -> Option<TIP20Event> {
1431 self.virtual_addr
1432 .map(|virtual_addr| TIP20Event::transfer(virtual_addr, self.target, amount))
1433 }
1434}
1435
1436#[cfg(test)]
1437mod recipient_tests {
1438 use super::*;
1439 use crate::{
1440 address_registry::{AddressRegistry, MasterId, UserTag},
1441 error::TempoPrecompileError,
1442 storage::{StorageCtx, hashmap::HashMapStorageProvider},
1443 test_util::{VIRTUAL_MASTER, register_virtual_master},
1444 };
1445 use alloy::primitives::{Address, U256};
1446 use tempo_chainspec::hardfork::TempoHardfork;
1447
1448 #[test]
1449 fn test_resolve() -> eyre::Result<()> {
1450 let addr = Address::repeat_byte(0x11);
1452 assert_eq!(
1453 Recipient::direct(addr),
1454 Recipient {
1455 target: addr,
1456 virtual_addr: None
1457 }
1458 );
1459
1460 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
1462 StorageCtx::enter(&mut storage, || {
1463 let r = Recipient::resolve(addr)?;
1464 assert_eq!(
1465 r,
1466 Recipient {
1467 target: addr,
1468 virtual_addr: None
1469 }
1470 );
1471
1472 let mut registry = AddressRegistry::new();
1474 let (_, virtual_addr) = register_virtual_master(&mut registry)?;
1475 let r = Recipient::resolve(virtual_addr)?;
1476 assert_eq!(
1477 r,
1478 Recipient {
1479 target: VIRTUAL_MASTER,
1480 virtual_addr: Some(virtual_addr)
1481 }
1482 );
1483
1484 let unregistered = Address::new_virtual(MasterId::ZERO, UserTag::ZERO);
1486 assert!(Recipient::resolve(unregistered).is_err());
1487
1488 Ok::<_, TempoPrecompileError>(())
1489 })?;
1490
1491 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
1493 StorageCtx::enter(&mut storage, || {
1494 let virtual_addr = Address::new_virtual(MasterId::ZERO, UserTag::ZERO);
1495 let r = Recipient::resolve(virtual_addr)?;
1496 assert_eq!(
1497 r,
1498 Recipient {
1499 target: virtual_addr,
1500 virtual_addr: None
1501 }
1502 );
1503 Ok::<_, TempoPrecompileError>(())
1504 })?;
1505 Ok(())
1506 }
1507
1508 #[test]
1509 fn test_validate() {
1510 assert!(Recipient::direct(Address::ZERO).validate().is_err());
1511 assert!(
1512 Recipient::direct(crate::PATH_USD_ADDRESS)
1513 .validate()
1514 .is_err()
1515 );
1516 assert!(
1517 Recipient::direct(Address::repeat_byte(0x11))
1518 .validate()
1519 .is_ok()
1520 );
1521 }
1522
1523 #[test]
1524 fn test_build_events() {
1525 let from = Address::repeat_byte(0x01);
1526 let target = Address::repeat_byte(0x02);
1527 let vaddr = Address::repeat_byte(0x03);
1528 let amount = U256::from(42);
1529
1530 let direct = Recipient::direct(target);
1531 let virt = Recipient {
1532 target,
1533 virtual_addr: Some(vaddr),
1534 };
1535
1536 assert!(matches!(direct.build_transfer_event(from, amount),
1538 TIP20Event::Transfer(ITIP20::Transfer { to, .. }) if to == target));
1539 assert!(matches!(virt.build_transfer_event(from, amount),
1540 TIP20Event::Transfer(ITIP20::Transfer { to, .. }) if to == vaddr));
1541
1542 assert!(direct.build_virtual_transfer_event(amount).is_none());
1544 let hop = virt.build_virtual_transfer_event(amount).unwrap();
1545 assert!(matches!(hop,
1546 TIP20Event::Transfer(ITIP20::Transfer { from, to, .. })
1547 if from == vaddr && to == target));
1548 }
1549}
1550
1551#[cfg(test)]
1552pub(crate) mod tests {
1553 use super::*;
1554 use crate::{
1555 PATH_USD_ADDRESS,
1556 account_keychain::{
1557 AccountKeychain, KeyRestrictions, SignatureType, TokenLimit, getRemainingLimitCall,
1558 },
1559 address_registry::{AddressRegistry, MasterId, UserTag},
1560 error::TempoPrecompileError,
1561 receive_policy_guard::ReceivePolicyGuard,
1562 storage::{StorageCtx, hashmap::HashMapStorageProvider},
1563 test_util::{TIP20Setup, VIRTUAL_MASTER, register_virtual_master, setup_storage},
1564 tip403_registry::REJECT_ALL_POLICY_ID,
1565 };
1566 use alloy::primitives::{Address, FixedBytes, IntoLogData, U256, hex};
1567 use rand_08::{Rng, distributions::Alphanumeric, thread_rng};
1568 use tempo_chainspec::hardfork::TempoHardfork;
1569 use tempo_contracts::precompiles::{
1570 IReceivePolicyGuard, ReceivePolicyGuardEvent, createTokenCall,
1571 };
1572
1573 #[test]
1574 fn test_mint_increases_balance_and_supply() -> eyre::Result<()> {
1575 let (mut storage, admin) = setup_storage();
1576 let addr = Address::random();
1577 let amount = U256::random() % U256::from(u128::MAX);
1578
1579 StorageCtx::enter(&mut storage, || {
1580 let mut token = TIP20Setup::create("Test", "TST", admin)
1581 .with_issuer(admin)
1582 .clear_events()
1583 .apply()?;
1584
1585 token.mint(admin, ITIP20::mintCall { to: addr, amount })?;
1586
1587 assert_eq!(token.get_balance(addr)?, amount);
1588 assert_eq!(token.total_supply()?, amount);
1589
1590 token.assert_emitted_events(vec![
1591 TIP20Event::transfer(Address::ZERO, addr, amount),
1592 TIP20Event::mint(addr, amount),
1593 ]);
1594
1595 Ok(())
1596 })
1597 }
1598
1599 #[test]
1600 fn test_transfer_moves_balance() -> eyre::Result<()> {
1601 let (mut storage, admin) = setup_storage();
1602 let from = Address::random();
1603 let to = Address::random();
1604 let amount = U256::random() % U256::from(u128::MAX);
1605
1606 StorageCtx::enter(&mut storage, || {
1607 let mut token = TIP20Setup::create("Test", "TST", admin)
1608 .with_issuer(admin)
1609 .with_mint(from, amount)
1610 .clear_events()
1611 .apply()?;
1612
1613 token.transfer(from, ITIP20::transferCall { to, amount })?;
1614
1615 assert_eq!(token.get_balance(from)?, U256::ZERO);
1616 assert_eq!(token.get_balance(to)?, amount);
1617 assert_eq!(token.total_supply()?, amount); token.assert_emitted_events(vec![TIP20Event::transfer(from, to, amount)]);
1620
1621 Ok(())
1622 })
1623 }
1624
1625 mod tip1028_tests {
1626 use super::*;
1627 use crate::{
1628 receive_policy_guard::BLOCKED_RECEIPT_VERSION, tip403_registry::ALLOW_ALL_POLICY_ID,
1629 };
1630
1631 const BLOCKED_AT: u64 = 1_728_100;
1632
1633 fn set_receive_policy(
1634 receiver: Address,
1635 sender_policy_id: u64,
1636 token_filter_id: u64,
1637 recovery_address: Address,
1638 ) -> Result<()> {
1639 TIP403Registry::new().set_receive_policy(
1640 receiver,
1641 ITIP403Registry::setReceivePolicyCall {
1642 senderPolicyId: sender_policy_id,
1643 tokenFilterId: token_filter_id,
1644 recoveryAuthority: recovery_address,
1645 },
1646 )
1647 }
1648
1649 #[test]
1650 fn test_transfer_blocked_by_receive_policy_guards_funds() -> eyre::Result<()> {
1651 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1652 storage.set_timestamp(U256::from(BLOCKED_AT));
1653 let admin = Address::random();
1654 let sender = Address::random();
1655 let receiver = Address::random();
1656 let amount = U256::from(100u64);
1657
1658 StorageCtx::enter(&mut storage, || {
1659 let mut token = TIP20Setup::create("Test", "TST", admin)
1660 .with_issuer(admin)
1661 .with_mint(sender, amount)
1662 .clear_events()
1663 .apply()?;
1664 set_receive_policy(
1665 receiver,
1666 REJECT_ALL_POLICY_ID,
1667 ALLOW_ALL_POLICY_ID,
1668 Address::ZERO,
1669 )?;
1670
1671 token.transfer(
1672 sender,
1673 ITIP20::transferCall {
1674 to: receiver,
1675 amount,
1676 },
1677 )?;
1678
1679 assert_eq!(token.get_balance(sender)?, U256::ZERO);
1680 assert_eq!(token.get_balance(receiver)?, U256::ZERO);
1681 assert_eq!(token.get_balance(RECEIVE_POLICY_GUARD_ADDRESS)?, amount);
1682 token.assert_emitted_events(vec![TIP20Event::Transfer(ITIP20::Transfer {
1683 from: sender,
1684 to: RECEIVE_POLICY_GUARD_ADDRESS,
1685 amount,
1686 })]);
1687
1688 let receipt = IReceivePolicyGuard::ClaimReceiptV1::new(
1689 token.address,
1690 Address::ZERO,
1691 sender,
1692 receiver,
1693 BLOCKED_AT,
1694 1,
1695 ITIP403Registry::BlockedReason::RECEIVE_POLICY as u8,
1696 InboundKind::TRANSFER,
1697 B256::ZERO,
1698 );
1699 let guard = ReceivePolicyGuard::new();
1700 assert_eq!(guard.balance_of(receipt.abi_encode().into())?, amount);
1701 guard.assert_emitted_events(vec![ReceivePolicyGuardEvent::TransferBlocked(
1702 IReceivePolicyGuard::TransferBlocked {
1703 token: token.address,
1704 receiver,
1705 blockedNonce: 1,
1706 receiptVersion: BLOCKED_RECEIPT_VERSION,
1707 amount,
1708 receipt: receipt.abi_encode().into(),
1709 },
1710 )]);
1711
1712 Ok(())
1713 })
1714 }
1715
1716 #[test]
1717 #[rustfmt::skip]
1718 fn test_release_blocked_funds_receive_policy_paths() -> eyre::Result<()> {
1719 let admin = Address::random();
1720 let originator = Address::random();
1721 let receiver = Address::random();
1722 let third_party = Address::random();
1723 let open_destination = Address::random();
1724 let blocked_destination = Address::random();
1725 let amount = U256::from(10u64);
1726
1727 for (mode, recovery_auth, destination, destination_policy_blocks, should_succeed) in [
1728 (RecoveryMode::Receiver, receiver, receiver, true, true),
1731 (RecoveryMode::Receiver, receiver, blocked_destination, true, false),
1733 (RecoveryMode::Originator, originator, blocked_destination, true, false),
1734 (RecoveryMode::Originator, originator, originator, false, true),
1735 (RecoveryMode::ThirdParty, third_party, receiver, true, true),
1738 (RecoveryMode::ThirdParty, third_party, open_destination, false, true),
1739 ] {
1740 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1741 StorageCtx::enter(&mut storage, || {
1742 let mut token = TIP20Setup::create("Test", "TST", admin)
1743 .with_issuer(admin)
1744 .apply()?;
1745 token.set_balance(RECEIVE_POLICY_GUARD_ADDRESS, amount)?;
1746
1747 set_receive_policy(
1748 receiver,
1749 REJECT_ALL_POLICY_ID,
1750 ALLOW_ALL_POLICY_ID,
1751 Address::ZERO,
1752 )?;
1753 if destination_policy_blocks && destination != receiver {
1754 set_receive_policy(
1755 destination,
1756 REJECT_ALL_POLICY_ID,
1757 ALLOW_ALL_POLICY_ID,
1758 Address::ZERO,
1759 )?;
1760 }
1761
1762 let result = token.release_blocked_funds(
1763 originator,
1764 receiver,
1765 destination,
1766 amount,
1767 mode,
1768 recovery_auth,
1769 );
1770
1771 if should_succeed {
1772 result?;
1773 assert_eq!(token.get_balance(RECEIVE_POLICY_GUARD_ADDRESS)?, U256::ZERO);
1774 assert_eq!(token.get_balance(destination)?, amount);
1775 } else {
1776 assert_eq!(result.unwrap_err(), TIP20Error::policy_forbids().into());
1777 assert_eq!(token.get_balance(RECEIVE_POLICY_GUARD_ADDRESS)?, amount);
1778 assert_eq!(token.get_balance(destination)?, U256::ZERO);
1779 }
1780
1781 Ok::<(), TempoPrecompileError>(())
1782 })?;
1783 }
1784
1785 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1786 StorageCtx::enter(&mut storage, || {
1787 let mut registry = TIP403Registry::new();
1788 let recipient_policy = registry.create_policy_with_accounts(
1789 admin,
1790 ITIP403Registry::createPolicyWithAccountsCall {
1791 admin,
1792 policyType: ITIP403Registry::PolicyType::WHITELIST,
1793 accounts: vec![receiver],
1794 },
1795 )?;
1796 let transfer_policy = registry.create_compound_policy(
1797 admin,
1798 ITIP403Registry::createCompoundPolicyCall {
1799 senderPolicyId: REJECT_ALL_POLICY_ID,
1800 recipientPolicyId: recipient_policy,
1801 mintRecipientPolicyId: ALLOW_ALL_POLICY_ID,
1802 },
1803 )?;
1804
1805 let mut token = TIP20Setup::create("Test", "TST", admin)
1806 .with_issuer(admin)
1807 .apply()?;
1808 token.change_transfer_policy_id(
1809 admin,
1810 ITIP20::changeTransferPolicyIdCall {
1811 newPolicyId: transfer_policy,
1812 },
1813 )?;
1814 token.set_balance(RECEIVE_POLICY_GUARD_ADDRESS, amount)?;
1815
1816 token.release_blocked_funds(
1820 originator,
1821 receiver,
1822 receiver,
1823 amount,
1824 RecoveryMode::ThirdParty,
1825 third_party,
1826 )?;
1827
1828 assert_eq!(token.get_balance(RECEIVE_POLICY_GUARD_ADDRESS)?, U256::ZERO);
1829 assert_eq!(token.get_balance(receiver)?, amount);
1830
1831 Ok::<(), TempoPrecompileError>(())
1832 })?;
1833
1834 Ok(())
1835 }
1836
1837 #[test]
1838 fn test_transfer_blocked_by_token_filter_records_reason() -> eyre::Result<()> {
1839 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1840 storage.set_timestamp(U256::from(BLOCKED_AT));
1841 let admin = Address::random();
1842 let sender = Address::random();
1843 let receiver = Address::random();
1844 let amount = U256::from(40u64);
1845
1846 StorageCtx::enter(&mut storage, || {
1847 let mut token = TIP20Setup::create("Test", "TST", admin)
1848 .with_issuer(admin)
1849 .with_mint(sender, amount)
1850 .apply()?;
1851 set_receive_policy(
1852 receiver,
1853 ALLOW_ALL_POLICY_ID,
1854 REJECT_ALL_POLICY_ID,
1855 Address::ZERO,
1856 )?;
1857
1858 token.transfer(
1859 sender,
1860 ITIP20::transferCall {
1861 to: receiver,
1862 amount,
1863 },
1864 )?;
1865
1866 let receipt = IReceivePolicyGuard::ClaimReceiptV1::new(
1867 token.address,
1868 Address::ZERO,
1869 sender,
1870 receiver,
1871 BLOCKED_AT,
1872 1,
1873 ITIP403Registry::BlockedReason::TOKEN_FILTER as u8,
1874 InboundKind::TRANSFER,
1875 B256::ZERO,
1876 );
1877 let guard = ReceivePolicyGuard::new();
1878 assert_eq!(guard.balance_of(receipt.abi_encode().into())?, amount);
1879 guard.assert_emitted_events(vec![ReceivePolicyGuardEvent::TransferBlocked(
1880 IReceivePolicyGuard::TransferBlocked {
1881 token: token.address,
1882 receiver,
1883 blockedNonce: 1,
1884 receiptVersion: BLOCKED_RECEIPT_VERSION,
1885 amount,
1886 receipt: receipt.abi_encode().into(),
1887 },
1888 )]);
1889
1890 Ok(())
1891 })
1892 }
1893
1894 #[test]
1895 fn test_transfer_to_guard_address_rejects() -> eyre::Result<()> {
1896 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1897 let admin = Address::random();
1898 let sender = Address::random();
1899 let amount = U256::from(10u64);
1900
1901 StorageCtx::enter(&mut storage, || {
1902 let mut token = TIP20Setup::create("Test", "TST", admin)
1903 .with_issuer(admin)
1904 .with_mint(sender, amount)
1905 .apply()?;
1906
1907 let result = token.transfer(
1908 sender,
1909 ITIP20::transferCall {
1910 to: RECEIVE_POLICY_GUARD_ADDRESS,
1911 amount,
1912 },
1913 );
1914 assert!(matches!(
1915 result,
1916 Err(e) if e == ReceivePolicyGuardError::address_reserved().into()
1917 ));
1918 assert_eq!(token.get_balance(sender)?, amount);
1919 assert_eq!(token.get_balance(RECEIVE_POLICY_GUARD_ADDRESS)?, U256::ZERO);
1920
1921 Ok(())
1922 })
1923 }
1924
1925 #[test]
1926 fn test_pre_t6_receive_policy_does_not_guard() -> eyre::Result<()> {
1927 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1928 storage.set_timestamp(U256::from(BLOCKED_AT));
1929 let admin = Address::random();
1930 let sender = Address::random();
1931 let receiver = Address::random();
1932 let amount = U256::from(25u64);
1933
1934 StorageCtx::enter(&mut storage, || {
1935 let mut token = TIP20Setup::create("Test", "TST", admin)
1936 .with_issuer(admin)
1937 .with_mint(sender, amount)
1938 .clear_events()
1939 .apply()?;
1940 set_receive_policy(
1941 receiver,
1942 REJECT_ALL_POLICY_ID,
1943 ALLOW_ALL_POLICY_ID,
1944 Address::ZERO,
1945 )?;
1946
1947 token.transfer(
1948 sender,
1949 ITIP20::transferCall {
1950 to: receiver,
1951 amount,
1952 },
1953 )?;
1954
1955 assert_eq!(token.get_balance(sender)?, U256::ZERO);
1956 assert_eq!(token.get_balance(receiver)?, amount);
1957 assert_eq!(token.get_balance(RECEIVE_POLICY_GUARD_ADDRESS)?, U256::ZERO);
1958 token.assert_emitted_events(vec![TIP20Event::Transfer(ITIP20::Transfer {
1959 from: sender,
1960 to: receiver,
1961 amount,
1962 })]);
1963 let receipt = IReceivePolicyGuard::ClaimReceiptV1::new(
1964 token.address,
1965 sender,
1966 sender,
1967 receiver,
1968 BLOCKED_AT,
1969 1,
1970 ITIP403Registry::BlockedReason::RECEIVE_POLICY as u8,
1971 InboundKind::TRANSFER,
1972 B256::ZERO,
1973 );
1974 assert_eq!(
1975 ReceivePolicyGuard::new().balance_of(receipt.abi_encode().into())?,
1976 U256::ZERO
1977 );
1978
1979 Ok(())
1980 })
1981 }
1982
1983 #[test]
1984 fn test_transfer_from_blocked_consumes_allowance() -> eyre::Result<()> {
1985 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1986 storage.set_timestamp(U256::from(BLOCKED_AT));
1987 let admin = Address::random();
1988 let owner = Address::random();
1989 let spender = Address::random();
1990 let receiver = Address::random();
1991 let amount = U256::from(30u64);
1992 let allowance = amount + U256::from(5u64);
1993
1994 StorageCtx::enter(&mut storage, || {
1995 let mut token = TIP20Setup::create("Test", "TST", admin)
1996 .with_issuer(admin)
1997 .with_mint(owner, amount)
1998 .with_approval(owner, spender, allowance)
1999 .apply()?;
2000 set_receive_policy(
2001 receiver,
2002 REJECT_ALL_POLICY_ID,
2003 ALLOW_ALL_POLICY_ID,
2004 Address::ZERO,
2005 )?;
2006
2007 token.transfer_from(
2008 spender,
2009 ITIP20::transferFromCall {
2010 from: owner,
2011 to: receiver,
2012 amount,
2013 },
2014 )?;
2015
2016 assert_eq!(
2017 token.allowance(ITIP20::allowanceCall { owner, spender })?,
2018 allowance - amount
2019 );
2020 assert_eq!(token.get_balance(owner)?, U256::ZERO);
2021 assert_eq!(token.get_balance(receiver)?, U256::ZERO);
2022 assert_eq!(token.get_balance(RECEIVE_POLICY_GUARD_ADDRESS)?, amount);
2023
2024 Ok(())
2025 })
2026 }
2027
2028 #[test]
2029 fn test_transfer_with_memo_blocked_preserves_memo() -> eyre::Result<()> {
2030 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
2031 storage.set_timestamp(U256::from(BLOCKED_AT));
2032 let admin = Address::random();
2033 let sender = Address::random();
2034 let receiver = Address::random();
2035 let amount = U256::from(55u64);
2036 let memo = B256::repeat_byte(0xab);
2037
2038 StorageCtx::enter(&mut storage, || {
2039 let mut token = TIP20Setup::create("Test", "TST", admin)
2040 .with_issuer(admin)
2041 .with_mint(sender, amount)
2042 .clear_events()
2043 .apply()?;
2044 set_receive_policy(
2045 receiver,
2046 REJECT_ALL_POLICY_ID,
2047 ALLOW_ALL_POLICY_ID,
2048 Address::ZERO,
2049 )?;
2050
2051 token.transfer_with_memo(
2052 sender,
2053 ITIP20::transferWithMemoCall {
2054 to: receiver,
2055 amount,
2056 memo,
2057 },
2058 )?;
2059
2060 token.assert_emitted_events(vec![TIP20Event::Transfer(ITIP20::Transfer {
2061 from: sender,
2062 to: RECEIVE_POLICY_GUARD_ADDRESS,
2063 amount,
2064 })]);
2065 let receipt = IReceivePolicyGuard::ClaimReceiptV1::new(
2066 token.address,
2067 Address::ZERO,
2068 sender,
2069 receiver,
2070 BLOCKED_AT,
2071 1,
2072 ITIP403Registry::BlockedReason::RECEIVE_POLICY as u8,
2073 InboundKind::TRANSFER,
2074 memo,
2075 );
2076 assert_eq!(
2077 ReceivePolicyGuard::new().balance_of(receipt.abi_encode().into())?,
2078 amount
2079 );
2080
2081 Ok(())
2082 })
2083 }
2084
2085 #[test]
2086 fn test_mint_blocked_credits_guard() -> eyre::Result<()> {
2087 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
2088 storage.set_timestamp(U256::from(BLOCKED_AT));
2089 let admin = Address::random();
2090 let receiver = Address::random();
2091 let amount = U256::from(70u64);
2092
2093 StorageCtx::enter(&mut storage, || {
2094 let mut token = TIP20Setup::create("Test", "TST", admin)
2095 .with_issuer(admin)
2096 .clear_events()
2097 .apply()?;
2098 set_receive_policy(
2099 receiver,
2100 REJECT_ALL_POLICY_ID,
2101 ALLOW_ALL_POLICY_ID,
2102 Address::ZERO,
2103 )?;
2104
2105 let mut guard = ReceivePolicyGuard::new();
2106 guard.clear_emitted_events();
2107 token.mint(
2108 admin,
2109 ITIP20::mintCall {
2110 to: receiver,
2111 amount,
2112 },
2113 )?;
2114
2115 assert_eq!(token.total_supply()?, amount);
2116 assert_eq!(token.get_balance(receiver)?, U256::ZERO);
2117 assert_eq!(token.get_balance(RECEIVE_POLICY_GUARD_ADDRESS)?, amount);
2118 token.assert_emitted_events(vec![
2119 TIP20Event::transfer(Address::ZERO, RECEIVE_POLICY_GUARD_ADDRESS, amount),
2120 TIP20Event::mint(RECEIVE_POLICY_GUARD_ADDRESS, amount),
2121 ]);
2122 let receipt = IReceivePolicyGuard::ClaimReceiptV1::new(
2123 token.address,
2124 Address::ZERO,
2125 admin,
2126 receiver,
2127 BLOCKED_AT,
2128 1,
2129 ITIP403Registry::BlockedReason::RECEIVE_POLICY as u8,
2130 InboundKind::MINT,
2131 B256::ZERO,
2132 );
2133 guard.assert_emitted_events(vec![ReceivePolicyGuardEvent::TransferBlocked(
2134 IReceivePolicyGuard::TransferBlocked {
2135 token: token.address,
2136 receiver,
2137 blockedNonce: 1,
2138 receiptVersion: BLOCKED_RECEIPT_VERSION,
2139 amount,
2140 receipt: receipt.abi_encode().into(),
2141 },
2142 )]);
2143 assert_eq!(guard.balance_of(receipt.abi_encode().into())?, amount);
2144
2145 Ok(())
2146 })
2147 }
2148 }
2149
2150 #[test]
2151 fn test_transfer_insufficient_balance_fails() -> eyre::Result<()> {
2152 let (mut storage, admin) = setup_storage();
2153 let from = Address::random();
2154 let to = Address::random();
2155 let amount = U256::random() % U256::from(u128::MAX);
2156
2157 StorageCtx::enter(&mut storage, || {
2158 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2159
2160 let result = token.transfer(from, ITIP20::transferCall { to, amount });
2161 assert!(matches!(
2162 result,
2163 Err(TempoPrecompileError::TIP20(
2164 TIP20Error::InsufficientBalance(_)
2165 ))
2166 ));
2167
2168 Ok(())
2169 })
2170 }
2171
2172 #[test]
2173 fn test_mint_with_memo() -> eyre::Result<()> {
2174 let mut storage = HashMapStorageProvider::new(1);
2175 let admin = Address::random();
2176 let amount = U256::random() % U256::from(u128::MAX);
2177 let to = Address::random();
2178 let memo = FixedBytes::random();
2179
2180 StorageCtx::enter(&mut storage, || {
2181 let mut token = TIP20Setup::create("Test", "TST", admin)
2182 .with_issuer(admin)
2183 .clear_events()
2184 .apply()?;
2185
2186 token.mint_with_memo(admin, ITIP20::mintWithMemoCall { to, amount, memo })?;
2187
2188 token.assert_emitted_events(vec![
2190 TIP20Event::transfer(Address::ZERO, to, amount),
2191 TIP20Event::transfer_with_memo(Address::ZERO, to, amount, memo),
2192 TIP20Event::mint(to, amount),
2193 ]);
2194
2195 Ok(())
2196 })
2197 }
2198
2199 #[test]
2200 fn test_burn_with_memo() -> eyre::Result<()> {
2201 let mut storage = HashMapStorageProvider::new(1);
2202 let admin = Address::random();
2203 let amount = U256::random() % U256::from(u128::MAX);
2204 let memo = FixedBytes::random();
2205
2206 StorageCtx::enter(&mut storage, || {
2207 let mut token = TIP20Setup::create("Test", "TST", admin)
2208 .with_issuer(admin)
2209 .with_mint(admin, amount)
2210 .clear_events()
2211 .apply()?;
2212
2213 token.burn_with_memo(admin, ITIP20::burnWithMemoCall { amount, memo })?;
2214 token.assert_emitted_events(vec![
2215 TIP20Event::transfer(admin, Address::ZERO, amount),
2216 TIP20Event::transfer_with_memo(admin, Address::ZERO, amount, memo),
2217 TIP20Event::burn(admin, amount),
2218 ]);
2219
2220 Ok(())
2221 })
2222 }
2223
2224 #[test]
2225 fn test_transfer_from_with_memo_from_address() -> eyre::Result<()> {
2226 let mut storage = HashMapStorageProvider::new(1);
2227 let admin = Address::random();
2228 let owner = Address::random();
2229 let spender = Address::random();
2230 let to = Address::random();
2231 let memo = FixedBytes::random();
2232 let amount = U256::random() % U256::from(u128::MAX);
2233
2234 StorageCtx::enter(&mut storage, || {
2235 let mut token = TIP20Setup::create("Test", "TST", admin)
2236 .with_issuer(admin)
2237 .with_mint(owner, amount)
2238 .with_approval(owner, spender, amount)
2239 .clear_events()
2240 .apply()?;
2241
2242 token.transfer_from_with_memo(
2243 spender,
2244 ITIP20::transferFromWithMemoCall {
2245 from: owner,
2246 to,
2247 amount,
2248 memo,
2249 },
2250 )?;
2251
2252 token.assert_emitted_events(vec![
2254 TIP20Event::transfer(owner, to, amount),
2255 TIP20Event::transfer_with_memo(owner, to, amount, memo),
2256 ]);
2257
2258 Ok(())
2259 })
2260 }
2261
2262 #[test]
2263 fn test_transfer_fee_pre_tx() -> eyre::Result<()> {
2264 let mut storage = HashMapStorageProvider::new(1);
2265 let admin = Address::random();
2266 let user = Address::random();
2267 let amount = U256::from(100);
2268 let fee_amount = amount / U256::from(2);
2269
2270 StorageCtx::enter(&mut storage, || {
2271 let mut token = TIP20Setup::create("Test", "TST", admin)
2272 .with_issuer(admin)
2273 .with_mint(user, amount)
2274 .apply()?;
2275
2276 token.transfer_fee_pre_tx(user, fee_amount)?;
2277
2278 assert_eq!(token.get_balance(user)?, fee_amount);
2279 assert_eq!(token.get_balance(TIP_FEE_MANAGER_ADDRESS)?, fee_amount);
2280
2281 Ok(())
2282 })
2283 }
2284
2285 #[test]
2286 fn test_transfer_fee_pre_tx_insufficient_balance() -> eyre::Result<()> {
2287 let mut storage = HashMapStorageProvider::new(1);
2288 let admin = Address::random();
2289 let user = Address::random();
2290 let amount = U256::from(100);
2291 let fee_amount = amount / U256::from(2);
2292
2293 StorageCtx::enter(&mut storage, || {
2294 let mut token = TIP20Setup::create("Test", "TST", admin)
2295 .with_issuer(admin)
2296 .apply()?;
2297
2298 assert_eq!(
2299 token.transfer_fee_pre_tx(user, fee_amount),
2300 Err(TempoPrecompileError::TIP20(
2301 TIP20Error::insufficient_balance(U256::ZERO, fee_amount, token.address)
2302 ))
2303 );
2304 Ok(())
2305 })
2306 }
2307
2308 #[test]
2309 fn test_transfer_fee_pre_tx_fee_manager_overflow() -> eyre::Result<()> {
2310 let mut storage = HashMapStorageProvider::new(1);
2311 let admin = Address::random();
2312 let user = Address::random();
2313 let fee_amount = U256::ONE;
2314
2315 StorageCtx::enter(&mut storage, || {
2316 let mut token = TIP20Setup::create("Test", "TST", admin)
2317 .with_issuer(admin)
2318 .with_mint(user, fee_amount)
2319 .apply()?;
2320 token.set_balance(TIP_FEE_MANAGER_ADDRESS, U256::MAX)?;
2321
2322 assert_eq!(
2323 token.transfer_fee_pre_tx(user, fee_amount),
2324 Err(TempoPrecompileError::TIP20(
2325 TIP20Error::supply_cap_exceeded()
2326 ))
2327 );
2328 Ok(())
2329 })
2330 }
2331
2332 #[test]
2333 fn test_transfer_fee_pre_tx_paused() -> eyre::Result<()> {
2334 let mut storage = HashMapStorageProvider::new(1);
2335 let admin = Address::random();
2336 let user = Address::random();
2337 let amount = U256::from(100);
2338 let fee_amount = amount / U256::from(2);
2339
2340 StorageCtx::enter(&mut storage, || {
2341 let mut token = TIP20Setup::create("Test", "TST", admin)
2342 .with_issuer(admin)
2343 .with_role(admin, *PAUSE_ROLE)
2344 .with_mint(user, amount)
2345 .apply()?;
2346
2347 token.pause(admin, ITIP20::pauseCall {})?;
2349
2350 assert_eq!(
2352 token.transfer_fee_pre_tx(user, fee_amount),
2353 Err(TempoPrecompileError::TIP20(TIP20Error::contract_paused()))
2354 );
2355 Ok(())
2356 })
2357 }
2358
2359 #[test]
2360 fn test_transfer_fee_post_tx() -> eyre::Result<()> {
2361 let mut storage = HashMapStorageProvider::new(1);
2362 let admin = Address::random();
2363 let user = Address::random();
2364 let initial_fee = U256::from(100);
2365 let refund_amount = U256::from(30);
2366 let gas_used = U256::from(10);
2367
2368 StorageCtx::enter(&mut storage, || {
2369 let mut token = TIP20Setup::create("Test", "TST", admin)
2370 .with_issuer(admin)
2371 .with_mint(TIP_FEE_MANAGER_ADDRESS, initial_fee)
2372 .apply()?;
2373
2374 token.transfer_fee_post_tx(user, refund_amount, gas_used)?;
2375
2376 assert_eq!(token.get_balance(user)?, refund_amount);
2377 assert_eq!(
2378 token.get_balance(TIP_FEE_MANAGER_ADDRESS)?,
2379 initial_fee - refund_amount
2380 );
2381 assert_eq!(
2382 token.emitted_events().last().unwrap(),
2383 &TIP20Event::transfer(user, TIP_FEE_MANAGER_ADDRESS, gas_used).into_log_data()
2384 );
2385
2386 Ok(())
2387 })
2388 }
2389
2390 #[test]
2391 fn test_transfer_fee_post_tx_insufficient_fee_manager_balance() -> eyre::Result<()> {
2392 let mut storage = HashMapStorageProvider::new(1);
2393 let admin = Address::random();
2394 let user = Address::random();
2395 let initial_fee = U256::from(10);
2396 let refund_amount = U256::from(30);
2397
2398 StorageCtx::enter(&mut storage, || {
2399 let mut token = TIP20Setup::create("Test", "TST", admin)
2400 .with_issuer(admin)
2401 .with_mint(TIP_FEE_MANAGER_ADDRESS, initial_fee)
2402 .apply()?;
2403
2404 assert_eq!(
2405 token.transfer_fee_post_tx(user, refund_amount, U256::ZERO),
2406 Err(TempoPrecompileError::TIP20(
2407 TIP20Error::insufficient_balance(initial_fee, refund_amount, token.address)
2408 ))
2409 );
2410
2411 Ok(())
2412 })
2413 }
2414
2415 #[test]
2416 fn test_transfer_fee_post_tx_recipient_overflow() -> eyre::Result<()> {
2417 let mut storage = HashMapStorageProvider::new(1);
2418 let admin = Address::random();
2419 let user = Address::random();
2420 let refund_amount = U256::ONE;
2421
2422 StorageCtx::enter(&mut storage, || {
2423 let mut token = TIP20Setup::create("Test", "TST", admin)
2424 .with_issuer(admin)
2425 .apply()?;
2426 token.set_balance(TIP_FEE_MANAGER_ADDRESS, refund_amount)?;
2427 token.set_balance(user, U256::MAX)?;
2428
2429 assert_eq!(
2430 token.transfer_fee_post_tx(user, refund_amount, U256::ZERO),
2431 Err(TempoPrecompileError::TIP20(
2432 TIP20Error::supply_cap_exceeded()
2433 ))
2434 );
2435 Ok(())
2436 })
2437 }
2438
2439 #[test]
2440 fn test_transfer_fee_post_tx_refunds_spending_limit() -> eyre::Result<()> {
2441 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
2442 let admin = Address::random();
2443 let user = Address::random();
2444 let access_key = Address::random();
2445 let max_fee = U256::from(1000);
2446 let refund_amount = U256::from(300);
2447 let gas_used = U256::from(100);
2448
2449 StorageCtx::enter(&mut storage, || {
2450 let mut token = TIP20Setup::create("Test", "TST", admin)
2451 .with_issuer(admin)
2452 .with_mint(TIP_FEE_MANAGER_ADDRESS, max_fee)
2453 .apply()?;
2454
2455 let token_address = token.address;
2456 let spending_limit = U256::from(2000);
2457
2458 let mut keychain = AccountKeychain::new();
2460 keychain.initialize()?;
2461 keychain.set_transaction_key(Address::ZERO)?;
2462
2463 keychain.authorize_key(
2464 user,
2465 access_key,
2466 SignatureType::Secp256k1,
2467 KeyRestrictions {
2468 expiry: u64::MAX,
2469 enforceLimits: true,
2470 limits: vec![TokenLimit {
2471 token: token_address,
2472 amount: spending_limit,
2473 period: 0,
2474 }],
2475 allowAnyCalls: true,
2476 allowedCalls: vec![],
2477 },
2478 None,
2479 )?;
2480
2481 keychain.set_transaction_key(access_key)?;
2483 keychain.set_tx_origin(user)?;
2484 keychain.authorize_transfer(user, token_address, max_fee)?;
2485
2486 let remaining_after_deduction =
2487 keychain.get_remaining_limit(getRemainingLimitCall {
2488 account: user,
2489 keyId: access_key,
2490 token: token_address,
2491 })?;
2492 assert_eq!(remaining_after_deduction, spending_limit - max_fee);
2493
2494 token.transfer_fee_post_tx(user, refund_amount, gas_used)?;
2496
2497 let remaining_after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
2498 account: user,
2499 keyId: access_key,
2500 token: token_address,
2501 })?;
2502 assert_eq!(
2503 remaining_after_refund,
2504 spending_limit - max_fee + refund_amount,
2505 "spending limit should be restored by refund amount"
2506 );
2507
2508 Ok(())
2509 })
2510 }
2511
2512 #[test]
2513 fn test_transfer_fee_post_tx_pre_t1c() -> eyre::Result<()> {
2514 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1B);
2515 let admin = Address::random();
2516 let user = Address::random();
2517 let access_key = Address::random();
2518 let max_fee = U256::from(1000);
2519 let refund_amount = U256::from(300);
2520 let gas_used = U256::from(100);
2521
2522 StorageCtx::enter(&mut storage, || {
2523 let mut token = TIP20Setup::create("Test", "TST", admin)
2524 .with_issuer(admin)
2525 .with_mint(TIP_FEE_MANAGER_ADDRESS, max_fee)
2526 .apply()?;
2527
2528 let token_address = token.address;
2529 let spending_limit = U256::from(2000);
2530
2531 let mut keychain = AccountKeychain::new();
2532 keychain.initialize()?;
2533 keychain.set_transaction_key(Address::ZERO)?;
2534
2535 keychain.authorize_key(
2536 user,
2537 access_key,
2538 SignatureType::Secp256k1,
2539 KeyRestrictions {
2540 expiry: u64::MAX,
2541 enforceLimits: true,
2542 limits: vec![TokenLimit {
2543 token: token_address,
2544 amount: spending_limit,
2545 period: 0,
2546 }],
2547 allowAnyCalls: true,
2548 allowedCalls: vec![],
2549 },
2550 None,
2551 )?;
2552
2553 keychain.set_transaction_key(access_key)?;
2554 keychain.set_tx_origin(user)?;
2555 keychain.authorize_transfer(user, token_address, max_fee)?;
2556
2557 let remaining_after_deduction =
2558 keychain.get_remaining_limit(getRemainingLimitCall {
2559 account: user,
2560 keyId: access_key,
2561 token: token_address,
2562 })?;
2563 assert_eq!(remaining_after_deduction, spending_limit - max_fee);
2564
2565 token.transfer_fee_post_tx(user, refund_amount, gas_used)?;
2566
2567 let remaining_after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
2569 account: user,
2570 keyId: access_key,
2571 token: token_address,
2572 })?;
2573 assert_eq!(remaining_after_refund, spending_limit - max_fee);
2574
2575 Ok(())
2576 })
2577 }
2578
2579 #[test]
2580 fn test_transfer_from_insufficient_allowance() -> eyre::Result<()> {
2581 let mut storage = HashMapStorageProvider::new(1);
2582 let admin = Address::random();
2583 let from = Address::random();
2584 let spender = Address::random();
2585 let to = Address::random();
2586 let amount = U256::random() % U256::from(u128::MAX);
2587
2588 StorageCtx::enter(&mut storage, || {
2589 let mut token = TIP20Setup::create("Test", "TST", admin)
2590 .with_issuer(admin)
2591 .with_mint(from, amount)
2592 .apply()?;
2593
2594 assert!(matches!(
2595 token.transfer_from(spender, ITIP20::transferFromCall { from, to, amount }),
2596 Err(TempoPrecompileError::TIP20(
2597 TIP20Error::InsufficientAllowance(_)
2598 ))
2599 ));
2600
2601 Ok(())
2602 })
2603 }
2604
2605 #[test]
2606 fn test_system_transfer_from() -> eyre::Result<()> {
2607 let mut storage = HashMapStorageProvider::new(1);
2608 let admin = Address::random();
2609 let from = Address::random();
2610 let to = Address::random();
2611 let amount = U256::random() % U256::from(u128::MAX);
2612
2613 StorageCtx::enter(&mut storage, || {
2614 let mut token = TIP20Setup::create("Test", "TST", admin)
2615 .with_issuer(admin)
2616 .with_mint(from, amount)
2617 .apply()?;
2618
2619 assert!(token.system_transfer_from(to, from, amount).is_ok());
2621 assert_eq!(
2622 token.emitted_events().last().unwrap(),
2623 &TIP20Event::transfer(from, to, amount).into_log_data()
2624 );
2625
2626 Ok(())
2627 })
2628 }
2629
2630 #[test]
2631 fn test_system_transfer_from_t5_authorized() -> eyre::Result<()> {
2632 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
2633 let admin = Address::random();
2634 let from = Address::random();
2635 let amount = U256::random() % U256::from(u128::MAX);
2636
2637 StorageCtx::enter(&mut storage, || {
2638 let mut token = TIP20Setup::create("Test", "TST", admin)
2639 .with_issuer(admin)
2640 .with_mint(from, amount)
2641 .apply()?;
2642
2643 assert!(
2645 token
2646 .system_transfer_from(TIP_FEE_MANAGER_ADDRESS, from, amount)
2647 .is_ok()
2648 );
2649
2650 Ok(())
2651 })
2652 }
2653
2654 #[test]
2655 fn test_system_transfer_from_t5_unauthorized_reverts() -> eyre::Result<()> {
2656 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
2657 let admin = Address::random();
2658 let unlisted = Address::random();
2659 let from = Address::random();
2660 let amount = U256::random() % U256::from(u128::MAX);
2661
2662 StorageCtx::enter(&mut storage, || {
2663 let mut token = TIP20Setup::create("Test", "TST", admin)
2664 .with_issuer(admin)
2665 .with_mint(from, amount)
2666 .apply()?;
2667
2668 assert!(matches!(
2670 token.system_transfer_from(unlisted, from, amount),
2671 Err(TempoPrecompileError::TIP20(TIP20Error::Unauthorized(_)))
2672 ));
2673
2674 Ok(())
2675 })
2676 }
2677
2678 #[test]
2679 fn test_initialize_sets_next_quote_token() -> eyre::Result<()> {
2680 let mut storage = HashMapStorageProvider::new(1);
2681 let admin = Address::random();
2682
2683 StorageCtx::enter(&mut storage, || {
2684 let token = TIP20Setup::create("Test", "TST", admin).apply()?;
2685
2686 assert_eq!(token.quote_token()?, PATH_USD_ADDRESS);
2688 assert_eq!(token.next_quote_token()?, PATH_USD_ADDRESS);
2689
2690 Ok(())
2691 })
2692 }
2693
2694 #[test]
2695 fn test_update_quote_token() -> eyre::Result<()> {
2696 let mut storage = HashMapStorageProvider::new(1);
2697 let admin = Address::random();
2698
2699 StorageCtx::enter(&mut storage, || {
2700 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2701
2702 let new_quote_token = TIP20Setup::create("New Quote", "NQ", admin).apply()?;
2704 let new_quote_token_address = new_quote_token.address;
2705
2706 assert_eq!(token.quote_token()?, PATH_USD_ADDRESS);
2708
2709 token.set_next_quote_token(
2711 admin,
2712 ITIP20::setNextQuoteTokenCall {
2713 newQuoteToken: new_quote_token_address,
2714 },
2715 )?;
2716
2717 assert_eq!(token.next_quote_token()?, new_quote_token_address);
2719
2720 assert_eq!(
2722 token.emitted_events().last().unwrap(),
2723 &TIP20Event::next_quote_token_set(admin, new_quote_token_address).into_log_data()
2724 );
2725
2726 Ok(())
2727 })
2728 }
2729
2730 #[test]
2731 fn test_update_quote_token_requires_admin() -> eyre::Result<()> {
2732 let mut storage = HashMapStorageProvider::new(1);
2733 let admin = Address::random();
2734 let non_admin = Address::random();
2735
2736 StorageCtx::enter(&mut storage, || {
2737 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2738
2739 let quote_token_address = token.quote_token()?;
2741
2742 let result = token.set_next_quote_token(
2744 non_admin,
2745 ITIP20::setNextQuoteTokenCall {
2746 newQuoteToken: quote_token_address,
2747 },
2748 );
2749
2750 assert!(matches!(
2751 result,
2752 Err(TempoPrecompileError::RolesAuthError(
2753 RolesAuthError::Unauthorized(_)
2754 ))
2755 ));
2756
2757 Ok(())
2758 })
2759 }
2760
2761 #[test]
2762 fn test_update_quote_token_rejects_non_tip20() -> eyre::Result<()> {
2763 let mut storage = HashMapStorageProvider::new(1);
2764 let admin = Address::random();
2765
2766 StorageCtx::enter(&mut storage, || {
2767 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2768
2769 let non_tip20_address = Address::random();
2771 let result = token.set_next_quote_token(
2772 admin,
2773 ITIP20::setNextQuoteTokenCall {
2774 newQuoteToken: non_tip20_address,
2775 },
2776 );
2777
2778 assert!(matches!(
2779 result,
2780 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
2781 _
2782 )))
2783 ));
2784
2785 Ok(())
2786 })
2787 }
2788
2789 #[test]
2790 fn test_update_quote_token_rejects_undeployed_token() -> eyre::Result<()> {
2791 let mut storage = HashMapStorageProvider::new(1);
2792 let admin = Address::random();
2793
2794 StorageCtx::enter(&mut storage, || {
2795 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2796
2797 let undeployed_token_address =
2800 Address::from(hex!("20C0000000000000000000000000000000000999"));
2801 let result = token.set_next_quote_token(
2802 admin,
2803 ITIP20::setNextQuoteTokenCall {
2804 newQuoteToken: undeployed_token_address,
2805 },
2806 );
2807
2808 assert!(matches!(
2809 result,
2810 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
2811 _
2812 )))
2813 ));
2814
2815 Ok(())
2816 })
2817 }
2818
2819 #[test]
2820 fn test_finalize_quote_token_update() -> eyre::Result<()> {
2821 let mut storage = HashMapStorageProvider::new(1);
2822 let admin = Address::random();
2823
2824 StorageCtx::enter(&mut storage, || {
2825 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2826 let quote_token_address = token.quote_token()?;
2827
2828 token.set_next_quote_token(
2830 admin,
2831 ITIP20::setNextQuoteTokenCall {
2832 newQuoteToken: quote_token_address,
2833 },
2834 )?;
2835
2836 token.complete_quote_token_update(admin, ITIP20::completeQuoteTokenUpdateCall {})?;
2838
2839 assert_eq!(token.quote_token()?, quote_token_address);
2841
2842 assert_eq!(
2844 token.emitted_events().last().unwrap(),
2845 &TIP20Event::quote_token_update(admin, quote_token_address).into_log_data()
2846 );
2847
2848 Ok(())
2849 })
2850 }
2851
2852 #[test]
2853 fn test_finalize_quote_token_update_detects_loop() -> eyre::Result<()> {
2854 let mut storage = HashMapStorageProvider::new(1);
2855 let admin = Address::random();
2856
2857 StorageCtx::enter(&mut storage, || {
2858 let mut token_b = TIP20Setup::create("Token B", "TKB", admin).apply()?;
2860 let token_a = TIP20Setup::create("Token A", "TKA", admin)
2862 .quote_token(token_b.address)
2863 .apply()?;
2864
2865 token_b.set_next_quote_token(
2867 admin,
2868 ITIP20::setNextQuoteTokenCall {
2869 newQuoteToken: token_a.address,
2870 },
2871 )?;
2872
2873 let result =
2875 token_b.complete_quote_token_update(admin, ITIP20::completeQuoteTokenUpdateCall {});
2876
2877 assert!(matches!(
2878 result,
2879 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
2880 _
2881 )))
2882 ));
2883
2884 Ok(())
2885 })
2886 }
2887
2888 #[test]
2889 fn test_finalize_quote_token_update_requires_admin() -> eyre::Result<()> {
2890 let mut storage = HashMapStorageProvider::new(1);
2891 let admin = Address::random();
2892 let non_admin = Address::random();
2893
2894 StorageCtx::enter(&mut storage, || {
2895 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2896 let quote_token_address = token.quote_token()?;
2897
2898 token.set_next_quote_token(
2900 admin,
2901 ITIP20::setNextQuoteTokenCall {
2902 newQuoteToken: quote_token_address,
2903 },
2904 )?;
2905
2906 let result = token
2908 .complete_quote_token_update(non_admin, ITIP20::completeQuoteTokenUpdateCall {});
2909
2910 assert!(matches!(
2911 result,
2912 Err(TempoPrecompileError::RolesAuthError(
2913 RolesAuthError::Unauthorized(_)
2914 ))
2915 ));
2916
2917 Ok(())
2918 })
2919 }
2920
2921 #[test]
2922 fn test_arbitrary_currency() -> eyre::Result<()> {
2923 let mut storage = HashMapStorageProvider::new(1);
2924 let admin = Address::random();
2925
2926 StorageCtx::enter(&mut storage, || {
2927 for _ in 0..50 {
2928 let currency: String = thread_rng()
2929 .sample_iter(&Alphanumeric)
2930 .take(31)
2931 .map(char::from)
2932 .collect();
2933
2934 let token = TIP20Setup::create("Test", "TST", admin)
2936 .currency(¤cy)
2937 .apply()?;
2938
2939 let stored_currency = token.currency()?;
2941 assert_eq!(stored_currency, currency,);
2942 }
2943
2944 Ok(())
2945 })
2946 }
2947
2948 #[test]
2949 fn test_validate_logo_uri() {
2950 const MAX: usize = TIP20Token::MAX_LOGO_URI_BYTES;
2951
2952 let prefix = "https://example.com/";
2954 let at_cap = format!("{prefix}{}", "a".repeat(MAX - prefix.len()));
2955 assert_eq!(at_cap.len(), MAX);
2956 for ok in [
2957 "",
2958 "https://example.com/icon.svg",
2959 "http://example.com/icon.png",
2960 "ipfs://QmXfzKRvjZz3u5JRgC4v5mGVbm9ahrUiB4DgzHBsnWbTMM",
2961 "data:image/svg+xml;base64,PHN2Zy8+",
2962 "HTTPS://example.com/ICON.svg",
2963 "IPFS://Qm123",
2964 &at_cap,
2965 ] {
2966 assert!(
2967 TIP20Token::validate_logo_uri(ok).is_ok(),
2968 "expected Ok for {ok:?}"
2969 );
2970 }
2971
2972 let too_long = format!("{prefix}{}", "a".repeat(MAX + 1 - prefix.len()));
2975 assert_eq!(too_long.len(), MAX + 1);
2976 assert!(matches!(
2977 TIP20Token::validate_logo_uri(&too_long),
2978 Err(TempoPrecompileError::TIP20(TIP20Error::LogoURITooLong(_))),
2979 ));
2980
2981 for bad in [
2983 "javascript:alert(1)",
2984 "file:///etc/passwd",
2985 "ftp://x.test/icon.png",
2986 "no-scheme-here",
2987 "://missing-scheme.test",
2988 "1https://digit-leading.test",
2989 ":empty-scheme",
2990 " https://leading-space.test",
2991 ] {
2992 assert!(
2993 matches!(
2994 TIP20Token::validate_logo_uri(bad),
2995 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidLogoURI(_))),
2996 ),
2997 "expected InvalidLogoURI for {bad:?}"
2998 );
2999 }
3000 }
3001
3002 #[test]
3003 fn test_set_logo_uri_non_admin_reverts() -> eyre::Result<()> {
3004 let mut storage = HashMapStorageProvider::new(1);
3005 let admin = Address::random();
3006 let non_admin = Address::random();
3007
3008 StorageCtx::enter(&mut storage, || {
3009 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3010
3011 let result = token.set_logo_uri(
3012 non_admin,
3013 ITIP20::setLogoURICall {
3014 newLogoURI: "https://example.com/icon.svg".to_string(),
3015 },
3016 );
3017
3018 assert!(matches!(
3019 result,
3020 Err(TempoPrecompileError::RolesAuthError(
3021 RolesAuthError::Unauthorized(_)
3022 ))
3023 ));
3024
3025 assert_eq!(token.logo_uri()?, "");
3027
3028 Ok(())
3029 })
3030 }
3031
3032 #[test]
3033 fn test_set_logo_uri_too_long_reverts() -> eyre::Result<()> {
3034 let mut storage = HashMapStorageProvider::new(1);
3035 let admin = Address::random();
3036
3037 StorageCtx::enter(&mut storage, || {
3038 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3039
3040 let prefix = "https://example.com/";
3043 let too_long = format!("{prefix}{}", "a".repeat(257 - prefix.len()));
3044 assert_eq!(too_long.len(), 257);
3045 let result = token.set_logo_uri(
3046 admin,
3047 ITIP20::setLogoURICall {
3048 newLogoURI: too_long,
3049 },
3050 );
3051
3052 assert!(matches!(
3053 result,
3054 Err(TempoPrecompileError::TIP20(TIP20Error::LogoURITooLong(_)))
3055 ));
3056
3057 let at_limit = format!("{prefix}{}", "a".repeat(256 - prefix.len()));
3059 assert_eq!(at_limit.len(), 256);
3060 token.set_logo_uri(
3061 admin,
3062 ITIP20::setLogoURICall {
3063 newLogoURI: at_limit.clone(),
3064 },
3065 )?;
3066 assert_eq!(token.logo_uri()?, at_limit);
3067
3068 Ok(())
3069 })
3070 }
3071
3072 #[test]
3073 fn test_set_logo_uri_writes_and_emits() -> eyre::Result<()> {
3074 let mut storage = HashMapStorageProvider::new(1);
3075 let admin = Address::random();
3076
3077 StorageCtx::enter(&mut storage, || {
3078 let mut token = TIP20Setup::create("Test", "TST", admin)
3079 .clear_events()
3080 .apply()?;
3081
3082 assert_eq!(token.logo_uri()?, "");
3084
3085 let uri = "https://example.com/icon.svg".to_string();
3086 token.set_logo_uri(
3087 admin,
3088 ITIP20::setLogoURICall {
3089 newLogoURI: uri.clone(),
3090 },
3091 )?;
3092
3093 assert_eq!(token.logo_uri()?, uri);
3094
3095 token.assert_emitted_events(vec![TIP20Event::LogoURIUpdated(ITIP20::LogoURIUpdated {
3096 updater: admin,
3097 newLogoURI: uri,
3098 })]);
3099
3100 Ok(())
3101 })
3102 }
3103
3104 #[test]
3105 fn test_set_logo_uri_empty_clears() -> eyre::Result<()> {
3106 let mut storage = HashMapStorageProvider::new(1);
3107 let admin = Address::random();
3108
3109 StorageCtx::enter(&mut storage, || {
3110 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
3111
3112 token.set_logo_uri(
3113 admin,
3114 ITIP20::setLogoURICall {
3115 newLogoURI: "https://example.com/icon.svg".to_string(),
3116 },
3117 )?;
3118 assert_eq!(token.logo_uri()?, "https://example.com/icon.svg");
3119
3120 token.set_logo_uri(
3122 admin,
3123 ITIP20::setLogoURICall {
3124 newLogoURI: String::new(),
3125 },
3126 )?;
3127 assert_eq!(token.logo_uri()?, "");
3128
3129 Ok(())
3130 })
3131 }
3132
3133 #[test]
3134 fn test_from_address() -> eyre::Result<()> {
3135 let mut storage = HashMapStorageProvider::new(1);
3136 let admin = Address::random();
3137
3138 StorageCtx::enter(&mut storage, || {
3139 let token = TIP20Setup::create("Test", "TST", admin).apply()?;
3141 let via_from_address = TIP20Token::from_address(token.address)?.address;
3142
3143 assert_eq!(
3144 via_from_address, token.address,
3145 "from_address should use the provided address directly"
3146 );
3147
3148 let _path_usd = TIP20Setup::path_usd(admin).apply()?;
3150 let via_from_address_reserved = TIP20Token::from_address(PATH_USD_ADDRESS)?.address;
3151
3152 assert_eq!(
3153 via_from_address_reserved, PATH_USD_ADDRESS,
3154 "from_address should work for reserved addresses too"
3155 );
3156
3157 Ok(())
3158 })
3159 }
3160
3161 #[test]
3162 fn test_new_invalid_quote_token() -> eyre::Result<()> {
3163 let mut storage = HashMapStorageProvider::new(1);
3164 let admin = Address::random();
3165
3166 StorageCtx::enter(&mut storage, || {
3167 let currency: String = thread_rng()
3168 .sample_iter(&Alphanumeric)
3169 .take(31)
3170 .map(char::from)
3171 .collect();
3172
3173 let token = TIP20Setup::create("Token", "T", admin)
3174 .currency(¤cy)
3175 .apply()?;
3176
3177 TIP20Setup::create("USD Token", "USDT", admin)
3179 .currency(USD_CURRENCY)
3180 .quote_token(token.address)
3181 .expect_tip20_err(TIP20Error::invalid_quote_token());
3182
3183 Ok(())
3184 })
3185 }
3186
3187 #[test]
3188 fn test_new_valid_quote_token() -> eyre::Result<()> {
3189 let mut storage = HashMapStorageProvider::new(1);
3190 let admin = Address::random();
3191
3192 StorageCtx::enter(&mut storage, || {
3193 let usd_token1 = TIP20Setup::create("USD Token", "USDT", admin).apply()?;
3194
3195 let _usd_token2 = TIP20Setup::create("USD Token", "USDT", admin)
3197 .quote_token(usd_token1.address)
3198 .apply()?;
3199
3200 let currency_1: String = thread_rng()
3202 .sample_iter(&Alphanumeric)
3203 .take(31)
3204 .map(char::from)
3205 .collect();
3206
3207 let token_1 = TIP20Setup::create("USD Token", "USDT", admin)
3208 .currency(currency_1)
3209 .apply()?;
3210
3211 let currency_2: String = thread_rng()
3213 .sample_iter(&Alphanumeric)
3214 .take(31)
3215 .map(char::from)
3216 .collect();
3217
3218 let _token_2 = TIP20Setup::create("USD Token", "USDT", admin)
3219 .currency(currency_2)
3220 .quote_token(token_1.address)
3221 .apply()?;
3222
3223 Ok(())
3224 })
3225 }
3226
3227 #[test]
3228 fn test_update_quote_token_invalid_token() -> eyre::Result<()> {
3229 let mut storage = HashMapStorageProvider::new(1);
3230 let admin = Address::random();
3231
3232 StorageCtx::enter(&mut storage, || {
3233 let _path_usd = TIP20Setup::path_usd(admin).apply()?;
3234
3235 let currency: String = thread_rng()
3236 .sample_iter(&Alphanumeric)
3237 .take(31)
3238 .map(char::from)
3239 .collect();
3240
3241 let token_1 = TIP20Setup::create("Token 1", "TK1", admin)
3242 .currency(¤cy)
3243 .apply()?;
3244
3245 let mut usd_token = TIP20Setup::create("USD Token", "USDT", admin).apply()?;
3247
3248 let result = usd_token.set_next_quote_token(
3250 admin,
3251 ITIP20::setNextQuoteTokenCall {
3252 newQuoteToken: token_1.address,
3253 },
3254 );
3255
3256 assert!(result.is_err_and(
3257 |err| err == TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
3258 ));
3259
3260 Ok(())
3261 })
3262 }
3263
3264 #[test]
3265 fn test_is_tip20_prefix() -> eyre::Result<()> {
3266 let mut storage = HashMapStorageProvider::new(1);
3267 let sender = Address::random();
3268
3269 StorageCtx::enter(&mut storage, || {
3270 let _path_usd = TIP20Setup::path_usd(sender).apply()?;
3271
3272 let created_tip20 = TIP20Factory::new().create_token(
3273 sender,
3274 createTokenCall {
3275 name: "Test Token".to_string(),
3276 symbol: "TEST".to_string(),
3277 currency: "USD".to_string(),
3278 quoteToken: crate::PATH_USD_ADDRESS,
3279 admin: sender,
3280 salt: B256::random(),
3281 },
3282 )?;
3283 let non_tip20 = Address::random();
3284
3285 assert!(PATH_USD_ADDRESS.is_tip20());
3286 assert!(created_tip20.is_tip20());
3287 assert!(!non_tip20.is_tip20());
3288 Ok(())
3289 })
3290 }
3291
3292 #[test]
3293 fn test_initialize_supply_cap() -> eyre::Result<()> {
3294 let mut storage = HashMapStorageProvider::new(1);
3295 let admin = Address::random();
3296
3297 StorageCtx::enter(&mut storage, || {
3298 let token = TIP20Setup::create("Token", "TKN", admin).apply()?;
3299
3300 let supply_cap = token.supply_cap()?;
3301 assert_eq!(supply_cap, U256::from(u128::MAX));
3302
3303 Ok(())
3304 })
3305 }
3306
3307 #[test]
3308 fn test_unable_to_burn_blocked_from_protected_address() -> eyre::Result<()> {
3309 let admin = Address::random();
3310 let burner = Address::random();
3311 let amount = (U256::random() % U256::from(u128::MAX)) / U256::from(8);
3312 let burn_amount = amount / U256::from(2);
3313
3314 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
3315 StorageCtx::enter(&mut storage, || {
3316 let mut token = TIP20Setup::create("Token", "TKN", admin)
3317 .with_issuer(admin)
3318 .with_role(burner, *BURN_BLOCKED_ROLE)
3319 .with_mint(TIP_FEE_MANAGER_ADDRESS, amount)
3320 .with_mint(STABLECOIN_DEX_ADDRESS, amount)
3321 .with_mint(TIP20_CHANNEL_RESERVE_ADDRESS, amount)
3322 .apply()?;
3323
3324 for protected in [
3325 token.address,
3326 TIP_FEE_MANAGER_ADDRESS,
3327 STABLECOIN_DEX_ADDRESS,
3328 RECEIVE_POLICY_GUARD_ADDRESS,
3329 TIP20_CHANNEL_RESERVE_ADDRESS,
3330 ] {
3331 let result = token.burn_blocked(burner, protected, burn_amount, true);
3332 assert_eq!(result.unwrap_err(), TIP20Error::protected_address().into());
3333 }
3334
3335 for minted in [TIP_FEE_MANAGER_ADDRESS, STABLECOIN_DEX_ADDRESS] {
3336 let balance = token.balance_of(ITIP20::balanceOfCall { account: minted })?;
3337 assert_eq!(balance, amount);
3338 }
3339
3340 Ok::<_, TempoPrecompileError>(())
3341 })?;
3342
3343 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
3346 StorageCtx::enter(&mut storage, || {
3347 let mut token = TIP20Setup::create("Token", "TKN", admin)
3348 .with_issuer(admin)
3349 .with_role(burner, *BURN_BLOCKED_ROLE)
3350 .apply()?;
3351
3352 for protected in [
3353 token.address,
3354 TIP_FEE_MANAGER_ADDRESS,
3355 STABLECOIN_DEX_ADDRESS,
3356 TIP20_CHANNEL_RESERVE_ADDRESS,
3357 ] {
3358 let result = token.burn_blocked(burner, protected, burn_amount, true);
3359 assert_eq!(result.unwrap_err(), TIP20Error::protected_address().into());
3360 }
3361
3362 token.change_transfer_policy_id(
3363 admin,
3364 ITIP20::changeTransferPolicyIdCall {
3365 newPolicyId: REJECT_ALL_POLICY_ID,
3366 },
3367 )?;
3368 token.set_balance(RECEIVE_POLICY_GUARD_ADDRESS, amount)?;
3369 token.set_total_supply(token.total_supply()? + amount)?;
3370
3371 token.burn_blocked(burner, RECEIVE_POLICY_GUARD_ADDRESS, burn_amount, true)?;
3372
3373 let balance = token.balance_of(ITIP20::balanceOfCall {
3374 account: RECEIVE_POLICY_GUARD_ADDRESS,
3375 })?;
3376 assert_eq!(balance, amount - burn_amount);
3377
3378 Ok::<_, TempoPrecompileError>(())
3379 })?;
3380
3381 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T4);
3384 StorageCtx::enter(&mut storage, || {
3385 let mut token = TIP20Setup::create("Token", "TKN", admin)
3386 .with_issuer(admin)
3387 .with_role(burner, *BURN_BLOCKED_ROLE)
3388 .with_mint(TIP20_CHANNEL_RESERVE_ADDRESS, amount)
3389 .apply()?;
3390
3391 token.change_transfer_policy_id(
3392 admin,
3393 ITIP20::changeTransferPolicyIdCall {
3394 newPolicyId: REJECT_ALL_POLICY_ID,
3395 },
3396 )?;
3397
3398 token.set_balance(token.address, amount)?;
3400 token.set_total_supply(token.total_supply()? + amount)?;
3401
3402 for unprotected in [TIP20_CHANNEL_RESERVE_ADDRESS, token.address] {
3403 token.burn_blocked(burner, unprotected, burn_amount, true)?;
3404
3405 let balance = token.balance_of(ITIP20::balanceOfCall {
3406 account: unprotected,
3407 })?;
3408 assert_eq!(balance, amount - burn_amount);
3409 }
3410
3411 Ok::<_, TempoPrecompileError>(())
3412 })?;
3413
3414 Ok(())
3415 }
3416
3417 #[test]
3418 fn test_initialize_usd_token() -> eyre::Result<()> {
3419 let mut storage = HashMapStorageProvider::new(1);
3420 let admin = Address::random();
3421
3422 StorageCtx::enter(&mut storage, || {
3423 let _token = TIP20Setup::create("TestToken", "TEST", admin).apply()?;
3425
3426 let eur_token = TIP20Setup::create("EuroToken", "EUR", admin)
3428 .currency("EUR")
3429 .apply()?;
3430
3431 TIP20Setup::create("USDToken", "USD", admin)
3433 .quote_token(eur_token.address)
3434 .expect_tip20_err(TIP20Error::invalid_quote_token());
3435
3436 Ok(())
3437 })
3438 }
3439
3440 #[test]
3441 fn test_change_transfer_policy_id_invalid_policy() -> eyre::Result<()> {
3442 let mut storage = HashMapStorageProvider::new(1);
3443 let admin = Address::random();
3444
3445 StorageCtx::enter(&mut storage, || {
3446 let mut token = TIP20Setup::path_usd(admin).apply()?;
3447
3448 let mut registry = TIP403Registry::new();
3450 registry.initialize()?;
3451
3452 let invalid_policy_id = 999u64;
3454 let result = token.change_transfer_policy_id(
3455 admin,
3456 ITIP20::changeTransferPolicyIdCall {
3457 newPolicyId: invalid_policy_id,
3458 },
3459 );
3460
3461 assert!(matches!(
3462 result.unwrap_err(),
3463 TempoPrecompileError::TIP20(TIP20Error::InvalidTransferPolicyId(_))
3464 ));
3465
3466 Ok(())
3467 })
3468 }
3469
3470 #[test]
3471 fn test_transfer_invalid_recipient() -> eyre::Result<()> {
3472 let mut storage = HashMapStorageProvider::new(1);
3473 let admin = Address::random();
3474 let bob = Address::random();
3475 let amount = U256::random() % U256::from(u128::MAX);
3476
3477 StorageCtx::enter(&mut storage, || {
3478 let mut token = TIP20Setup::create("Token", "TKN", admin)
3479 .with_issuer(admin)
3480 .with_mint(admin, amount)
3481 .with_approval(admin, bob, amount)
3482 .apply()?;
3483
3484 let result = token.transfer(
3485 admin,
3486 ITIP20::transferCall {
3487 to: Address::ZERO,
3488 amount,
3489 },
3490 );
3491 assert!(result.is_err_and(|err| err.to_string().contains("InvalidRecipient")));
3492
3493 let result = token.transfer_from(
3494 bob,
3495 ITIP20::transferFromCall {
3496 from: admin,
3497 to: Address::ZERO,
3498 amount,
3499 },
3500 );
3501 assert!(result.is_err_and(|err| err.to_string().contains("InvalidRecipient")));
3502
3503 Ok(())
3504 })
3505 }
3506
3507 #[test]
3508 fn test_change_transfer_policy_id() -> eyre::Result<()> {
3509 let mut storage = HashMapStorageProvider::new(1);
3510 let admin = Address::random();
3511
3512 StorageCtx::enter(&mut storage, || {
3513 let mut token = TIP20Setup::path_usd(admin).apply()?;
3514
3515 let mut registry = TIP403Registry::new();
3517 registry.initialize()?;
3518
3519 token.change_transfer_policy_id(
3521 admin,
3522 ITIP20::changeTransferPolicyIdCall { newPolicyId: 0 },
3523 )?;
3524 assert_eq!(token.transfer_policy_id()?, 0);
3525
3526 token.change_transfer_policy_id(
3527 admin,
3528 ITIP20::changeTransferPolicyIdCall { newPolicyId: 1 },
3529 )?;
3530 assert_eq!(token.transfer_policy_id()?, 1);
3531
3532 let mut rng = rand_08::thread_rng();
3534 for _ in 0..20 {
3535 let invalid_policy_id = rng.gen_range(2..u64::MAX);
3536 let result = token.change_transfer_policy_id(
3537 admin,
3538 ITIP20::changeTransferPolicyIdCall {
3539 newPolicyId: invalid_policy_id,
3540 },
3541 );
3542 assert!(matches!(
3543 result.unwrap_err(),
3544 TempoPrecompileError::TIP20(TIP20Error::InvalidTransferPolicyId(_))
3545 ));
3546 }
3547
3548 let mut valid_policy_ids = Vec::new();
3550 for i in 0..10 {
3551 let policy_id = registry.create_policy(
3552 admin,
3553 ITIP403Registry::createPolicyCall {
3554 admin,
3555 policyType: if i % 2 == 0 {
3556 ITIP403Registry::PolicyType::WHITELIST
3557 } else {
3558 ITIP403Registry::PolicyType::BLACKLIST
3559 },
3560 },
3561 )?;
3562 valid_policy_ids.push(policy_id);
3563 }
3564
3565 for policy_id in valid_policy_ids {
3567 let result = token.change_transfer_policy_id(
3568 admin,
3569 ITIP20::changeTransferPolicyIdCall {
3570 newPolicyId: policy_id,
3571 },
3572 );
3573 assert!(result.is_ok());
3574 assert_eq!(token.transfer_policy_id()?, policy_id);
3575 }
3576
3577 Ok(())
3578 })
3579 }
3580
3581 #[test]
3582 fn test_is_transfer_authorized() -> eyre::Result<()> {
3583 use tempo_chainspec::hardfork::TempoHardfork;
3584
3585 let admin = Address::random();
3586 let sender = Address::random();
3587 let recipient = Address::random();
3588
3589 for hardfork in [TempoHardfork::T0, TempoHardfork::T1] {
3590 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
3591
3592 StorageCtx::enter(&mut storage, || {
3593 let token = TIP20Setup::path_usd(admin).apply()?;
3594
3595 let mut registry = TIP403Registry::new();
3597 registry.initialize()?;
3598
3599 let policy_id = registry.create_policy(
3600 admin,
3601 ITIP403Registry::createPolicyCall {
3602 admin,
3603 policyType: ITIP403Registry::PolicyType::WHITELIST,
3604 },
3605 )?;
3606
3607 let mut token = token;
3609 token.change_transfer_policy_id(
3610 admin,
3611 ITIP20::changeTransferPolicyIdCall {
3612 newPolicyId: policy_id,
3613 },
3614 )?;
3615
3616 registry.modify_policy_whitelist(
3618 admin,
3619 ITIP403Registry::modifyPolicyWhitelistCall {
3620 policyId: policy_id,
3621 account: recipient,
3622 allowed: true,
3623 },
3624 )?;
3625 assert!(!token.is_transfer_authorized(sender, recipient)?);
3626
3627 registry.modify_policy_whitelist(
3629 admin,
3630 ITIP403Registry::modifyPolicyWhitelistCall {
3631 policyId: policy_id,
3632 account: sender,
3633 allowed: true,
3634 },
3635 )?;
3636 registry.modify_policy_whitelist(
3637 admin,
3638 ITIP403Registry::modifyPolicyWhitelistCall {
3639 policyId: policy_id,
3640 account: recipient,
3641 allowed: false,
3642 },
3643 )?;
3644 assert!(!token.is_transfer_authorized(sender, recipient)?);
3645
3646 registry.modify_policy_whitelist(
3648 admin,
3649 ITIP403Registry::modifyPolicyWhitelistCall {
3650 policyId: policy_id,
3651 account: recipient,
3652 allowed: true,
3653 },
3654 )?;
3655 assert!(token.is_transfer_authorized(sender, recipient)?);
3656
3657 Ok::<_, TempoPrecompileError>(())
3658 })?;
3659 }
3660
3661 Ok(())
3662 }
3663
3664 #[test]
3665 fn test_set_next_quote_token_rejects_path_usd() -> eyre::Result<()> {
3666 let mut storage = HashMapStorageProvider::new(1);
3667 let admin = Address::random();
3668
3669 StorageCtx::enter(&mut storage, || {
3670 let mut path_usd = TIP20Setup::path_usd(admin).apply()?;
3671 let other_token = TIP20Setup::create("Test", "T", admin).apply()?;
3672
3673 let result = path_usd.set_next_quote_token(
3675 admin,
3676 ITIP20::setNextQuoteTokenCall {
3677 newQuoteToken: other_token.address,
3678 },
3679 );
3680 assert!(matches!(
3681 result,
3682 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
3683 _
3684 )))
3685 ));
3686
3687 Ok(())
3688 })
3689 }
3690
3691 #[test]
3692 fn test_non_path_usd_cycle_detection() -> eyre::Result<()> {
3693 let mut storage = HashMapStorageProvider::new(1);
3694 let admin = Address::random();
3695
3696 StorageCtx::enter(&mut storage, || {
3697 TIP20Setup::path_usd(admin).apply()?;
3698
3699 let mut token_b = TIP20Setup::create("TokenB", "TKNB", admin).apply()?;
3700 let token_a = TIP20Setup::create("TokenA", "TKNA", admin)
3701 .quote_token(token_b.address)
3702 .apply()?;
3703
3704 assert_eq!(token_a.quote_token()?, token_b.address);
3706 assert_eq!(token_b.quote_token()?, PATH_USD_ADDRESS);
3707
3708 token_b.set_next_quote_token(
3710 admin,
3711 ITIP20::setNextQuoteTokenCall {
3712 newQuoteToken: token_a.address,
3713 },
3714 )?;
3715
3716 let result =
3717 token_b.complete_quote_token_update(admin, ITIP20::completeQuoteTokenUpdateCall {});
3718
3719 assert!(matches!(
3720 result,
3721 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
3722 _
3723 )))
3724 ));
3725
3726 assert_eq!(token_a.quote_token()?, token_b.address);
3728 assert_eq!(token_b.quote_token()?, PATH_USD_ADDRESS);
3729
3730 Ok(())
3731 })
3732 }
3733
3734 #[test]
3739 fn test_mint_to_virtual_address_credits_master() -> eyre::Result<()> {
3740 let amount = U256::from(1000);
3741
3742 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
3743 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
3744 let admin = Address::random();
3745
3746 StorageCtx::enter(&mut storage, || {
3747 let mut registry = AddressRegistry::new();
3748 let (_, virtual_addr) = register_virtual_master(&mut registry)?;
3749 let credited = if hardfork.is_t3() {
3750 VIRTUAL_MASTER
3751 } else {
3752 virtual_addr
3753 };
3754
3755 let mut token = TIP20Setup::create("Test", "TST", admin)
3756 .with_issuer(admin)
3757 .clear_events()
3758 .apply()?;
3759
3760 token.mint(
3762 admin,
3763 ITIP20::mintCall {
3764 to: virtual_addr,
3765 amount,
3766 },
3767 )?;
3768
3769 if hardfork.is_t3() {
3770 assert_eq!(token.get_balance(VIRTUAL_MASTER)?, amount);
3772 assert_eq!(token.get_balance(virtual_addr)?, U256::ZERO);
3773 assert_eq!(token.total_supply()?, amount);
3774
3775 token.assert_emitted_events(vec![
3777 TIP20Event::transfer(Address::ZERO, virtual_addr, amount),
3778 TIP20Event::mint(virtual_addr, amount),
3779 TIP20Event::transfer(virtual_addr, VIRTUAL_MASTER, amount),
3780 ]);
3781 } else {
3782 assert_eq!(token.get_balance(virtual_addr)?, amount);
3784 assert_eq!(token.get_balance(VIRTUAL_MASTER)?, U256::ZERO);
3785 }
3786
3787 let pre = token.get_balance(credited)?;
3789 token.mint_with_memo(
3790 admin,
3791 ITIP20::mintWithMemoCall {
3792 to: virtual_addr,
3793 amount,
3794 memo: FixedBytes::ZERO,
3795 },
3796 )?;
3797 assert_eq!(token.get_balance(credited)? - pre, amount);
3798
3799 Ok::<_, TempoPrecompileError>(())
3800 })?;
3801 }
3802 Ok(())
3803 }
3804
3805 #[test]
3806 fn test_transfer_to_virtual_address_credits_master() -> eyre::Result<()> {
3807 let amount = U256::from(500);
3808
3809 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
3810 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
3811 let admin = Address::random();
3812 let sender = Address::random();
3813
3814 StorageCtx::enter(&mut storage, || {
3815 let mut registry = AddressRegistry::new();
3816 let (_, virtual_addr) = register_virtual_master(&mut registry)?;
3817 let credited = if hardfork.is_t3() {
3818 VIRTUAL_MASTER
3819 } else {
3820 virtual_addr
3821 };
3822
3823 let mut token = TIP20Setup::create("Test", "TST", admin)
3824 .with_issuer(admin)
3825 .with_mint(sender, amount * U256::from(2))
3826 .clear_events()
3827 .apply()?;
3828
3829 token.transfer(
3831 sender,
3832 ITIP20::transferCall {
3833 to: virtual_addr,
3834 amount,
3835 },
3836 )?;
3837
3838 if hardfork.is_t3() {
3839 assert_eq!(token.get_balance(VIRTUAL_MASTER)?, amount);
3840 assert_eq!(token.get_balance(virtual_addr)?, U256::ZERO);
3841
3842 token.assert_emitted_events(vec![
3844 TIP20Event::transfer(sender, virtual_addr, amount),
3845 TIP20Event::transfer(virtual_addr, VIRTUAL_MASTER, amount),
3846 ]);
3847 } else {
3848 assert_eq!(token.get_balance(virtual_addr)?, amount);
3849 assert_eq!(token.get_balance(VIRTUAL_MASTER)?, U256::ZERO);
3850 }
3851
3852 let pre = token.get_balance(credited)?;
3854 token.transfer_with_memo(
3855 sender,
3856 ITIP20::transferWithMemoCall {
3857 to: virtual_addr,
3858 amount,
3859 memo: FixedBytes::ZERO,
3860 },
3861 )?;
3862 assert_eq!(token.get_balance(credited)? - pre, amount);
3863
3864 Ok::<_, TempoPrecompileError>(())
3865 })?;
3866 }
3867 Ok(())
3868 }
3869
3870 #[test]
3871 fn test_transfer_from_to_virtual_address_credits_master() -> eyre::Result<()> {
3872 let amount = U256::from(300);
3873
3874 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
3875 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
3876 let admin = Address::random();
3877 let owner = Address::random();
3878 let spender = Address::random();
3879
3880 StorageCtx::enter(&mut storage, || {
3881 let mut registry = AddressRegistry::new();
3882 let (_, virtual_addr) = register_virtual_master(&mut registry)?;
3883 let credited = if hardfork.is_t3() {
3884 VIRTUAL_MASTER
3885 } else {
3886 virtual_addr
3887 };
3888
3889 let total = amount * U256::from(2);
3890 let mut token = TIP20Setup::create("Test", "TST", admin)
3891 .with_issuer(admin)
3892 .with_mint(owner, total)
3893 .with_approval(owner, spender, total)
3894 .clear_events()
3895 .apply()?;
3896
3897 token.transfer_from(
3899 spender,
3900 ITIP20::transferFromCall {
3901 from: owner,
3902 to: virtual_addr,
3903 amount,
3904 },
3905 )?;
3906
3907 if hardfork.is_t3() {
3908 assert_eq!(token.get_balance(VIRTUAL_MASTER)?, amount);
3909 assert_eq!(token.get_balance(virtual_addr)?, U256::ZERO);
3910 } else {
3911 assert_eq!(token.get_balance(virtual_addr)?, amount);
3912 assert_eq!(token.get_balance(VIRTUAL_MASTER)?, U256::ZERO);
3913 }
3914
3915 let pre = token.get_balance(credited)?;
3917 token.transfer_from_with_memo(
3918 spender,
3919 ITIP20::transferFromWithMemoCall {
3920 from: owner,
3921 to: virtual_addr,
3922 amount,
3923 memo: FixedBytes::ZERO,
3924 },
3925 )?;
3926 assert_eq!(token.get_balance(credited)? - pre, amount);
3927
3928 Ok::<_, TempoPrecompileError>(())
3929 })?;
3930 }
3931 Ok(())
3932 }
3933
3934 #[test]
3935 #[rustfmt::skip]
3936 fn test_unregistered_virtual_reverts_on_t3() -> eyre::Result<()> {
3937 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3938 let admin = Address::random();
3939 let sender = Address::random();
3940 let spender = Address::random();
3941 let to = Address::new_virtual(MasterId::ZERO, UserTag::ZERO);
3942 let amount = U256::from(100);
3943 let memo = FixedBytes::ZERO;
3944
3945 StorageCtx::enter(&mut storage, || {
3946 let mut token = TIP20Setup::create("Test", "TST", admin)
3947 .with_issuer(admin)
3948 .with_mint(sender, amount)
3949 .with_approval(sender, spender, amount)
3950 .apply()?;
3951
3952 assert!(token.mint(admin, ITIP20::mintCall { to, amount }).is_err());
3954 assert!(token.mint_with_memo(admin, ITIP20::mintWithMemoCall { to, amount, memo }).is_err());
3955 assert!(token.transfer(sender, ITIP20::transferCall { to, amount }).is_err());
3956 assert!(token.transfer_with_memo(sender, ITIP20::transferWithMemoCall { to, amount, memo }).is_err());
3957 assert!(token.transfer_from(spender, ITIP20::transferFromCall { from: sender, to, amount }).is_err());
3958 assert!(token.transfer_from_with_memo(spender, ITIP20::transferFromWithMemoCall { from: sender, to, amount, memo }).is_err());
3959
3960 Ok(())
3961 })
3962 }
3963
3964 mod permit_tests {
3969 use super::*;
3970 use alloy::sol_types::SolValue;
3971 use alloy_signer::SignerSync;
3972 use alloy_signer_local::PrivateKeySigner;
3973 use tempo_chainspec::hardfork::TempoHardfork;
3974
3975 const CHAIN_ID: u64 = 42;
3976
3977 fn setup_t2_storage() -> HashMapStorageProvider {
3979 HashMapStorageProvider::new_with_spec(CHAIN_ID, TempoHardfork::T2)
3980 }
3981
3982 fn sign_permit(
3984 signer: &PrivateKeySigner,
3985 token_name: &str,
3986 token_address: Address,
3987 spender: Address,
3988 value: U256,
3989 nonce: U256,
3990 deadline: U256,
3991 ) -> (u8, B256, B256) {
3992 let domain_separator = compute_domain_separator(token_name, token_address);
3993 let struct_hash = keccak256(
3994 (
3995 *PERMIT_TYPEHASH,
3996 signer.address(),
3997 spender,
3998 value,
3999 nonce,
4000 deadline,
4001 )
4002 .abi_encode(),
4003 );
4004 let digest = keccak256(
4005 [
4006 &[0x19, 0x01],
4007 domain_separator.as_slice(),
4008 struct_hash.as_slice(),
4009 ]
4010 .concat(),
4011 );
4012
4013 let sig = signer.sign_hash_sync(&digest).unwrap();
4014 let v = u8::from(sig.v()) + 27;
4015 let r: B256 = sig.r().into();
4016 let s: B256 = sig.s().into();
4017 (v, r, s)
4018 }
4019
4020 fn compute_domain_separator(token_name: &str, token_address: Address) -> B256 {
4021 keccak256(
4022 (
4023 *EIP712_DOMAIN_TYPEHASH,
4024 keccak256(token_name.as_bytes()),
4025 *VERSION_HASH,
4026 U256::from(CHAIN_ID),
4027 token_address,
4028 )
4029 .abi_encode(),
4030 )
4031 }
4032
4033 struct PermitFixture {
4034 storage: HashMapStorageProvider,
4035 admin: Address,
4036 signer: PrivateKeySigner,
4037 spender: Address,
4038 }
4039
4040 impl PermitFixture {
4041 fn new() -> Self {
4042 Self {
4043 storage: setup_t2_storage(),
4044 admin: Address::random(),
4045 signer: PrivateKeySigner::random(),
4046 spender: Address::random(),
4047 }
4048 }
4049 }
4050
4051 fn make_permit_call(
4052 signer: &PrivateKeySigner,
4053 spender: Address,
4054 token_address: Address,
4055 value: U256,
4056 nonce: U256,
4057 deadline: U256,
4058 ) -> ITIP20::permitCall {
4059 let (v, r, s) = sign_permit(
4060 signer,
4061 "Test",
4062 token_address,
4063 spender,
4064 value,
4065 nonce,
4066 deadline,
4067 );
4068 ITIP20::permitCall {
4069 owner: signer.address(),
4070 spender,
4071 value,
4072 deadline,
4073 v,
4074 r,
4075 s,
4076 }
4077 }
4078
4079 #[test]
4080 fn test_permit_happy_path() -> eyre::Result<()> {
4081 let PermitFixture {
4082 mut storage,
4083 admin,
4084 ref signer,
4085 spender,
4086 } = PermitFixture::new();
4087 let owner = signer.address();
4088 let value = U256::from(1000);
4089
4090 StorageCtx::enter(&mut storage, || {
4091 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
4092 let call =
4093 make_permit_call(signer, spender, token.address, value, U256::ZERO, U256::MAX);
4094 token.permit(call)?;
4095
4096 let allowance = token.allowance(ITIP20::allowanceCall { owner, spender })?;
4098 assert_eq!(allowance, value);
4099
4100 let nonce = token.nonces(ITIP20::noncesCall { owner })?;
4102 assert_eq!(nonce, U256::from(1));
4103
4104 Ok(())
4105 })
4106 }
4107
4108 #[test]
4109 fn test_permit_expired() -> eyre::Result<()> {
4110 let PermitFixture {
4111 mut storage,
4112 admin,
4113 ref signer,
4114 spender,
4115 } = PermitFixture::new();
4116 let value = U256::from(1000);
4117 let deadline = U256::ZERO;
4119
4120 StorageCtx::enter(&mut storage, || {
4121 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
4122 let call =
4123 make_permit_call(signer, spender, token.address, value, U256::ZERO, deadline);
4124
4125 let result = token.permit(call);
4126
4127 assert!(matches!(
4128 result,
4129 Err(TempoPrecompileError::TIP20(TIP20Error::PermitExpired(_)))
4130 ));
4131
4132 Ok(())
4133 })
4134 }
4135
4136 #[test]
4137 fn test_permit_invalid_signature() -> eyre::Result<()> {
4138 let mut storage = setup_t2_storage();
4139 let admin = Address::random();
4140 let owner = Address::random();
4141 let spender = Address::random();
4142 let value = U256::from(1000);
4143 let deadline = U256::MAX;
4144
4145 StorageCtx::enter(&mut storage, || {
4146 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
4147
4148 let result = token.permit(ITIP20::permitCall {
4150 owner,
4151 spender,
4152 value,
4153 deadline,
4154 v: 27,
4155 r: B256::ZERO,
4156 s: B256::ZERO,
4157 });
4158
4159 assert!(matches!(
4160 result,
4161 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
4162 ));
4163
4164 Ok(())
4165 })
4166 }
4167
4168 #[test]
4169 fn test_permit_wrong_signer() -> eyre::Result<()> {
4170 let PermitFixture {
4171 mut storage,
4172 admin,
4173 ref signer,
4174 spender,
4175 } = PermitFixture::new();
4176 let wrong_owner = Address::random(); let value = U256::from(1000);
4178 let deadline = U256::MAX;
4179
4180 StorageCtx::enter(&mut storage, || {
4181 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
4182
4183 let (v, r, s) = sign_permit(
4185 signer,
4186 "Test",
4187 token.address,
4188 spender,
4189 value,
4190 U256::ZERO,
4191 deadline,
4192 );
4193
4194 let result = token.permit(ITIP20::permitCall {
4195 owner: wrong_owner, spender,
4197 value,
4198 deadline,
4199 v,
4200 r,
4201 s,
4202 });
4203
4204 assert!(matches!(
4205 result,
4206 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
4207 ));
4208
4209 Ok(())
4210 })
4211 }
4212
4213 #[test]
4214 fn test_permit_replay_protection() -> eyre::Result<()> {
4215 let PermitFixture {
4216 mut storage,
4217 admin,
4218 ref signer,
4219 spender,
4220 } = PermitFixture::new();
4221 let value = U256::from(1000);
4222
4223 StorageCtx::enter(&mut storage, || {
4224 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
4225 let call =
4226 make_permit_call(signer, spender, token.address, value, U256::ZERO, U256::MAX);
4227
4228 token.permit(call.clone())?;
4230
4231 let result = token.permit(call);
4233
4234 assert!(matches!(
4235 result,
4236 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
4237 ));
4238
4239 Ok(())
4240 })
4241 }
4242
4243 #[test]
4244 fn test_permit_nonce_tracking() -> eyre::Result<()> {
4245 let PermitFixture {
4246 mut storage,
4247 admin,
4248 ref signer,
4249 spender,
4250 } = PermitFixture::new();
4251 let owner = signer.address();
4252
4253 StorageCtx::enter(&mut storage, || {
4254 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
4255
4256 assert_eq!(token.nonces(ITIP20::noncesCall { owner })?, U256::ZERO);
4258
4259 for i in 0u64..3 {
4261 let nonce = U256::from(i);
4262 let value = U256::from(100 * (i + 1));
4263 let call =
4264 make_permit_call(signer, spender, token.address, value, nonce, U256::MAX);
4265 token.permit(call)?;
4266
4267 assert_eq!(
4268 token.nonces(ITIP20::noncesCall { owner })?,
4269 U256::from(i + 1)
4270 );
4271 }
4272
4273 Ok(())
4274 })
4275 }
4276
4277 #[test]
4278 fn test_permit_works_when_paused() -> eyre::Result<()> {
4279 let PermitFixture {
4280 mut storage,
4281 admin,
4282 ref signer,
4283 spender,
4284 } = PermitFixture::new();
4285 let owner = signer.address();
4286 let value = U256::from(1000);
4287
4288 StorageCtx::enter(&mut storage, || {
4289 let mut token = TIP20Setup::create("Test", "TST", admin)
4290 .with_role(admin, *PAUSE_ROLE)
4291 .apply()?;
4292
4293 token.pause(admin, ITIP20::pauseCall {})?;
4295 assert!(token.paused()?);
4296
4297 let call =
4298 make_permit_call(signer, spender, token.address, value, U256::ZERO, U256::MAX);
4299
4300 token.permit(call)?;
4302
4303 assert_eq!(
4304 token.allowance(ITIP20::allowanceCall { owner, spender })?,
4305 value
4306 );
4307
4308 Ok(())
4309 })
4310 }
4311
4312 #[test]
4313 fn test_permit_domain_separator() -> eyre::Result<()> {
4314 let PermitFixture {
4315 mut storage, admin, ..
4316 } = PermitFixture::new();
4317
4318 StorageCtx::enter(&mut storage, || {
4319 let token = TIP20Setup::create("Test", "TST", admin).apply()?;
4320
4321 let ds = token.domain_separator()?;
4322 let expected = compute_domain_separator("Test", token.address);
4323 assert_eq!(ds, expected);
4324
4325 Ok(())
4326 })
4327 }
4328
4329 #[test]
4330 fn test_permit_max_allowance() -> eyre::Result<()> {
4331 let PermitFixture {
4332 mut storage,
4333 admin,
4334 ref signer,
4335 spender,
4336 } = PermitFixture::new();
4337 let owner = signer.address();
4338
4339 StorageCtx::enter(&mut storage, || {
4340 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
4341 let call = make_permit_call(
4342 signer,
4343 spender,
4344 token.address,
4345 U256::MAX,
4346 U256::ZERO,
4347 U256::MAX,
4348 );
4349 token.permit(call)?;
4350
4351 assert_eq!(
4352 token.allowance(ITIP20::allowanceCall { owner, spender })?,
4353 U256::MAX
4354 );
4355
4356 Ok(())
4357 })
4358 }
4359
4360 #[test]
4361 fn test_permit_allowance_override() -> eyre::Result<()> {
4362 let PermitFixture {
4363 mut storage,
4364 admin,
4365 ref signer,
4366 spender,
4367 } = PermitFixture::new();
4368 let owner = signer.address();
4369
4370 StorageCtx::enter(&mut storage, || {
4371 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
4372
4373 let call = make_permit_call(
4375 signer,
4376 spender,
4377 token.address,
4378 U256::from(1000),
4379 U256::ZERO,
4380 U256::MAX,
4381 );
4382 token.permit(call)?;
4383 assert_eq!(
4384 token.allowance(ITIP20::allowanceCall { owner, spender })?,
4385 U256::from(1000)
4386 );
4387
4388 let call = make_permit_call(
4390 signer,
4391 spender,
4392 token.address,
4393 U256::ZERO,
4394 U256::from(1),
4395 U256::MAX,
4396 );
4397 token.permit(call)?;
4398 assert_eq!(
4399 token.allowance(ITIP20::allowanceCall { owner, spender })?,
4400 U256::ZERO
4401 );
4402
4403 Ok(())
4404 })
4405 }
4406
4407 #[test]
4408 fn test_permit_invalid_v_values() -> eyre::Result<()> {
4409 let PermitFixture {
4410 mut storage,
4411 admin,
4412 spender,
4413 ..
4414 } = PermitFixture::new();
4415
4416 StorageCtx::enter(&mut storage, || {
4417 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
4418
4419 for v in [0u8, 1] {
4420 let result = token.permit(ITIP20::permitCall {
4421 owner: admin,
4422 spender,
4423 value: U256::from(1000),
4424 deadline: U256::MAX,
4425 v,
4426 r: B256::ZERO,
4427 s: B256::ZERO,
4428 });
4429
4430 assert!(
4431 matches!(
4432 result,
4433 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
4434 ),
4435 "v={v} should revert with InvalidSignature"
4436 );
4437 }
4438
4439 Ok(())
4440 })
4441 }
4442
4443 #[test]
4444 fn test_permit_zero_address_recovery_reverts() -> eyre::Result<()> {
4445 let PermitFixture {
4446 mut storage,
4447 admin,
4448 spender,
4449 ..
4450 } = PermitFixture::new();
4451
4452 StorageCtx::enter(&mut storage, || {
4453 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
4454
4455 let result = token.permit(ITIP20::permitCall {
4456 owner: Address::ZERO,
4457 spender,
4458 value: U256::from(1000),
4459 deadline: U256::MAX,
4460 v: 27,
4461 r: B256::ZERO,
4462 s: B256::ZERO,
4463 });
4464
4465 assert!(matches!(
4466 result,
4467 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
4468 ));
4469
4470 Ok(())
4471 })
4472 }
4473
4474 #[test]
4475 fn test_permit_domain_separator_changes_with_chain_id() -> eyre::Result<()> {
4476 let PermitFixture { admin, .. } = PermitFixture::new();
4477
4478 let mut storage_a = setup_t2_storage();
4479 let mut storage_b =
4480 HashMapStorageProvider::new_with_spec(CHAIN_ID + 1, TempoHardfork::T2);
4481
4482 let ds_a = StorageCtx::enter(&mut storage_a, || {
4483 TIP20Setup::create("Test", "TST", admin)
4484 .apply()?
4485 .domain_separator()
4486 })?;
4487
4488 let ds_b = StorageCtx::enter(&mut storage_b, || {
4489 TIP20Setup::create("Test", "TST", admin)
4490 .apply()?
4491 .domain_separator()
4492 })?;
4493
4494 assert_ne!(
4495 ds_a, ds_b,
4496 "domain separator must change when chainId changes"
4497 );
4498
4499 Ok(())
4500 }
4501 }
4502
4503 #[test]
4504 fn test_mint_paused_pre_t6_short_circuits_before_policy_reads() -> eyre::Result<()> {
4505 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
4506 let (admin, amount) = (Address::random(), U256::random());
4507
4508 StorageCtx::enter(&mut storage, || {
4509 let mut token = TIP20Setup::create("Test", "TST", admin)
4510 .with_issuer(admin)
4511 .with_role(admin, *PAUSE_ROLE)
4512 .apply()?;
4513 token.pause(admin, ITIP20::pauseCall {})?;
4514
4515 token.storage.reset_counters();
4516 let result = token.mint(admin, ITIP20::mintCall { to: admin, amount });
4517
4518 assert_eq!(
4519 result,
4520 Err(TempoPrecompileError::TIP20(TIP20Error::contract_paused()))
4521 );
4522 assert_eq!(token.storage.counter_sload(), 3);
4524
4525 Ok::<_, TempoPrecompileError>(())
4526 })?;
4527
4528 Ok(())
4529 }
4530
4531 #[test]
4532 fn test_mint_rejects_when_paused_on_t3() -> eyre::Result<()> {
4533 let to = Address::random();
4534 let amount = U256::from(1000);
4535 let memo = FixedBytes::random();
4536
4537 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
4538 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
4539 let admin = Address::random();
4540
4541 StorageCtx::enter(&mut storage, || {
4542 let mut token = TIP20Setup::create("Test", "TST", admin)
4543 .with_issuer(admin)
4544 .with_role(admin, *PAUSE_ROLE)
4545 .apply()?;
4546
4547 token.pause(admin, ITIP20::pauseCall {})?;
4548
4549 let mint_result = token.mint(admin, ITIP20::mintCall { to, amount });
4550 let mint_memo_result =
4551 token.mint_with_memo(admin, ITIP20::mintWithMemoCall { to, amount, memo });
4552
4553 if hardfork.is_t3() {
4554 let expected = TempoPrecompileError::TIP20(TIP20Error::contract_paused());
4555 assert_eq!(mint_result, Err(expected.clone()));
4556 assert_eq!(mint_memo_result, Err(expected));
4557 } else {
4558 assert!(mint_result.is_ok());
4559 assert!(mint_memo_result.is_ok());
4560 }
4561
4562 Ok::<_, TempoPrecompileError>(())
4563 })?;
4564 }
4565 Ok(())
4566 }
4567
4568 #[test]
4569 fn test_burn_rejects_when_paused_on_t3() -> eyre::Result<()> {
4570 let amount = U256::from(500);
4571 let memo = FixedBytes::random();
4572
4573 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
4574 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
4575 let admin = Address::random();
4576
4577 StorageCtx::enter(&mut storage, || {
4578 let mut token = TIP20Setup::create("Test", "TST", admin)
4579 .with_issuer(admin)
4580 .with_role(admin, *PAUSE_ROLE)
4581 .with_mint(admin, amount * U256::from(2))
4582 .apply()?;
4583
4584 token.pause(admin, ITIP20::pauseCall {})?;
4585
4586 let burn_result = token.burn(admin, ITIP20::burnCall { amount });
4587 let burn_memo_result =
4588 token.burn_with_memo(admin, ITIP20::burnWithMemoCall { amount, memo });
4589
4590 if hardfork.is_t3() {
4591 let expected = TempoPrecompileError::TIP20(TIP20Error::contract_paused());
4592 assert_eq!(burn_result, Err(expected.clone()));
4593 assert_eq!(burn_memo_result, Err(expected));
4594 } else {
4595 assert!(burn_result.is_ok());
4596 assert!(burn_memo_result.is_ok());
4597 }
4598
4599 Ok::<_, TempoPrecompileError>(())
4600 })?;
4601 }
4602 Ok(())
4603 }
4604
4605 #[test]
4606 fn test_burn_blocked_rejects_when_paused_on_t3() -> eyre::Result<()> {
4607 let amount = U256::from(500);
4608 let blocked = Address::random();
4609
4610 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
4611 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
4612 let admin = Address::random();
4613
4614 StorageCtx::enter(&mut storage, || {
4615 let mut registry = TIP403Registry::new();
4617 registry.initialize()?;
4618 let policy_id = registry.create_policy(
4619 admin,
4620 ITIP403Registry::createPolicyCall {
4621 admin,
4622 policyType: ITIP403Registry::PolicyType::BLACKLIST,
4623 },
4624 )?;
4625 registry.modify_policy_blacklist(
4626 admin,
4627 ITIP403Registry::modifyPolicyBlacklistCall {
4628 policyId: policy_id,
4629 account: blocked,
4630 restricted: true,
4631 },
4632 )?;
4633
4634 let mut token = TIP20Setup::create("Test", "TST", admin)
4635 .with_issuer(admin)
4636 .with_role(admin, *PAUSE_ROLE)
4637 .with_role(admin, *BURN_BLOCKED_ROLE)
4638 .with_mint(blocked, amount)
4639 .apply()?;
4640
4641 token.change_transfer_policy_id(
4643 admin,
4644 ITIP20::changeTransferPolicyIdCall {
4645 newPolicyId: policy_id,
4646 },
4647 )?;
4648
4649 token.pause(admin, ITIP20::pauseCall {})?;
4651
4652 let result = token.burn_blocked(admin, blocked, amount, true);
4653
4654 if hardfork.is_t3() {
4655 assert_eq!(
4656 result,
4657 Err(TempoPrecompileError::TIP20(TIP20Error::contract_paused()))
4658 );
4659 } else {
4660 assert!(result.is_ok());
4662 assert_eq!(token.get_balance(blocked)?, U256::ZERO);
4663 }
4664
4665 Ok::<_, TempoPrecompileError>(())
4666 })?;
4667 }
4668 Ok(())
4669 }
4670}