1pub mod dispatch;
11pub mod rewards;
12pub mod roles;
13
14use tempo_contracts::precompiles::STABLECOIN_DEX_ADDRESS;
15pub use tempo_contracts::precompiles::{
16 IRolesAuth, ITIP20, RolesAuthError, RolesAuthEvent, TIP20Error, TIP20Event, USD_CURRENCY,
17};
18
19pub use slots as tip20_slots;
21
22use crate::{
23 PATH_USD_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
24 account_keychain::AccountKeychain,
25 error::{Result, TempoPrecompileError},
26 storage::{Handler, Mapping},
27 tip20::{rewards::UserRewardInfo, roles::DEFAULT_ADMIN_ROLE},
28 tip20_factory::TIP20Factory,
29 tip403_registry::{AuthRole, ITIP403Registry, TIP403Registry},
30};
31use alloy::{
32 hex,
33 primitives::{Address, B256, U256, keccak256, uint},
34 sol_types::SolValue,
35};
36use std::sync::LazyLock;
37use tempo_precompiles_macros::contract;
38use tracing::trace;
39
40pub const U128_MAX: U256 = uint!(0xffffffffffffffffffffffffffffffff_U256);
42
43const TIP20_DECIMALS: u8 = 6;
45
46const TIP20_TOKEN_PREFIX: [u8; 12] = hex!("20C000000000000000000000");
49
50pub fn is_tip20_prefix(token: Address) -> bool {
55 token.as_slice().starts_with(&TIP20_TOKEN_PREFIX)
56}
57
58pub fn validate_usd_currency(token: Address) -> Result<()> {
64 if TIP20Token::from_address(token)?.currency()? != USD_CURRENCY {
65 return Err(TIP20Error::invalid_currency().into());
66 }
67 Ok(())
68}
69
70#[contract]
83pub struct TIP20Token {
84 roles: Mapping<Address, Mapping<B256, bool>>,
86 role_admins: Mapping<B256, B256>,
87
88 name: String,
90 symbol: String,
91 currency: String,
92 _domain_separator: B256,
94 quote_token: Address,
95 next_quote_token: Address,
96 transfer_policy_id: u64,
97
98 total_supply: U256,
100 balances: Mapping<Address, U256>,
101 allowances: Mapping<Address, Mapping<Address, U256>>,
102 permit_nonces: Mapping<Address, U256>,
103 paused: bool,
104 supply_cap: U256,
105 _salts: Mapping<B256, bool>,
107
108 global_reward_per_token: U256,
110 opted_in_supply: u128,
111 user_reward_info: Mapping<Address, UserRewardInfo>,
112}
113
114pub static PERMIT_TYPEHASH: LazyLock<B256> = LazyLock::new(|| {
116 keccak256(b"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
117});
118
119pub static EIP712_DOMAIN_TYPEHASH: LazyLock<B256> = LazyLock::new(|| {
121 keccak256(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
122});
123
124pub static VERSION_HASH: LazyLock<B256> = LazyLock::new(|| keccak256(b"1"));
126
127pub static PAUSE_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"PAUSE_ROLE"));
129pub static UNPAUSE_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"UNPAUSE_ROLE"));
131pub static ISSUER_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"ISSUER_ROLE"));
133pub static BURN_BLOCKED_ROLE: LazyLock<B256> = LazyLock::new(|| keccak256(b"BURN_BLOCKED_ROLE"));
135
136impl TIP20Token {
137 pub fn name(&self) -> Result<String> {
139 self.name.read()
140 }
141
142 pub fn symbol(&self) -> Result<String> {
144 self.symbol.read()
145 }
146
147 pub fn decimals(&self) -> Result<u8> {
149 Ok(TIP20_DECIMALS)
150 }
151
152 pub fn currency(&self) -> Result<String> {
154 self.currency.read()
155 }
156
157 pub fn total_supply(&self) -> Result<U256> {
159 self.total_supply.read()
160 }
161
162 pub fn quote_token(&self) -> Result<Address> {
164 self.quote_token.read()
165 }
166
167 pub fn next_quote_token(&self) -> Result<Address> {
169 self.next_quote_token.read()
170 }
171
172 pub fn supply_cap(&self) -> Result<U256> {
174 self.supply_cap.read()
175 }
176
177 pub fn paused(&self) -> Result<bool> {
179 self.paused.read()
180 }
181
182 pub fn transfer_policy_id(&self) -> Result<u64> {
184 self.transfer_policy_id.read()
185 }
186
187 pub fn pause_role() -> B256 {
192 *PAUSE_ROLE
193 }
194
195 pub fn unpause_role() -> B256 {
200 *UNPAUSE_ROLE
201 }
202
203 pub fn issuer_role() -> B256 {
208 *ISSUER_ROLE
209 }
210
211 pub fn burn_blocked_role() -> B256 {
216 *BURN_BLOCKED_ROLE
217 }
218
219 pub fn balance_of(&self, call: ITIP20::balanceOfCall) -> Result<U256> {
221 self.balances[call.account].read()
222 }
223
224 pub fn allowance(&self, call: ITIP20::allowanceCall) -> Result<U256> {
226 self.allowances[call.owner][call.spender].read()
227 }
228
229 pub fn change_transfer_policy_id(
235 &mut self,
236 msg_sender: Address,
237 call: ITIP20::changeTransferPolicyIdCall,
238 ) -> Result<()> {
239 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
240
241 if !TIP403Registry::new().policy_exists(ITIP403Registry::policyExistsCall {
243 policyId: call.newPolicyId,
244 })? {
245 return Err(TIP20Error::invalid_transfer_policy_id().into());
246 }
247
248 self.transfer_policy_id.write(call.newPolicyId)?;
249
250 self.emit_event(TIP20Event::TransferPolicyUpdate(
251 ITIP20::TransferPolicyUpdate {
252 updater: msg_sender,
253 newPolicyId: call.newPolicyId,
254 },
255 ))
256 }
257
258 pub fn set_supply_cap(
265 &mut self,
266 msg_sender: Address,
267 call: ITIP20::setSupplyCapCall,
268 ) -> Result<()> {
269 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
270 if call.newSupplyCap < self.total_supply()? {
271 return Err(TIP20Error::invalid_supply_cap().into());
272 }
273
274 if call.newSupplyCap > U128_MAX {
275 return Err(TIP20Error::supply_cap_exceeded().into());
276 }
277
278 self.supply_cap.write(call.newSupplyCap)?;
279
280 self.emit_event(TIP20Event::SupplyCapUpdate(ITIP20::SupplyCapUpdate {
281 updater: msg_sender,
282 newSupplyCap: call.newSupplyCap,
283 }))
284 }
285
286 pub fn pause(&mut self, msg_sender: Address, _call: ITIP20::pauseCall) -> Result<()> {
291 self.check_role(msg_sender, *PAUSE_ROLE)?;
292 self.paused.write(true)?;
293
294 self.emit_event(TIP20Event::PauseStateUpdate(ITIP20::PauseStateUpdate {
295 updater: msg_sender,
296 isPaused: true,
297 }))
298 }
299
300 pub fn unpause(&mut self, msg_sender: Address, _call: ITIP20::unpauseCall) -> Result<()> {
305 self.check_role(msg_sender, *UNPAUSE_ROLE)?;
306 self.paused.write(false)?;
307
308 self.emit_event(TIP20Event::PauseStateUpdate(ITIP20::PauseStateUpdate {
309 updater: msg_sender,
310 isPaused: false,
311 }))
312 }
313
314 pub fn set_next_quote_token(
323 &mut self,
324 msg_sender: Address,
325 call: ITIP20::setNextQuoteTokenCall,
326 ) -> Result<()> {
327 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
328
329 if self.address == PATH_USD_ADDRESS {
330 return Err(TIP20Error::invalid_quote_token().into());
331 }
332
333 if !TIP20Factory::new().is_tip20(call.newQuoteToken)? {
336 return Err(TIP20Error::invalid_quote_token().into());
337 }
338
339 let currency = self.currency()?;
341 if currency == USD_CURRENCY {
342 let quote_token_currency = Self::from_address(call.newQuoteToken)?.currency()?;
343 if quote_token_currency != USD_CURRENCY {
344 return Err(TIP20Error::invalid_quote_token().into());
345 }
346 }
347
348 self.next_quote_token.write(call.newQuoteToken)?;
349
350 self.emit_event(TIP20Event::NextQuoteTokenSet(ITIP20::NextQuoteTokenSet {
351 updater: msg_sender,
352 nextQuoteToken: call.newQuoteToken,
353 }))
354 }
355
356 pub fn complete_quote_token_update(
363 &mut self,
364 msg_sender: Address,
365 _call: ITIP20::completeQuoteTokenUpdateCall,
366 ) -> Result<()> {
367 self.check_role(msg_sender, DEFAULT_ADMIN_ROLE)?;
368
369 let next_quote_token = self.next_quote_token()?;
370
371 let mut current = next_quote_token;
374 while current != PATH_USD_ADDRESS {
375 if current == self.address {
376 return Err(TIP20Error::invalid_quote_token().into());
377 }
378
379 current = Self::from_address(current)?.quote_token()?;
380 }
381
382 self.quote_token.write(next_quote_token)?;
384
385 self.emit_event(TIP20Event::QuoteTokenUpdate(ITIP20::QuoteTokenUpdate {
386 updater: msg_sender,
387 newQuoteToken: next_quote_token,
388 }))
389 }
390
391 pub fn mint(&mut self, msg_sender: Address, call: ITIP20::mintCall) -> Result<()> {
401 self._mint(msg_sender, call.to, call.amount)?;
402 self.emit_event(TIP20Event::Mint(ITIP20::Mint {
403 to: call.to,
404 amount: call.amount,
405 }))?;
406 Ok(())
407 }
408
409 pub fn mint_with_memo(
411 &mut self,
412 msg_sender: Address,
413 call: ITIP20::mintWithMemoCall,
414 ) -> Result<()> {
415 self._mint(msg_sender, call.to, call.amount)?;
416
417 self.emit_event(TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
418 from: Address::ZERO,
419 to: call.to,
420 amount: call.amount,
421 memo: call.memo,
422 }))?;
423 self.emit_event(TIP20Event::Mint(ITIP20::Mint {
424 to: call.to,
425 amount: call.amount,
426 }))
427 }
428
429 fn _mint(&mut self, msg_sender: Address, to: Address, amount: U256) -> Result<()> {
431 self.check_role(msg_sender, *ISSUER_ROLE)?;
432 let total_supply = self.total_supply()?;
433
434 let policy_id = self.transfer_policy_id()?;
436 if !TIP403Registry::new().is_authorized_as(policy_id, to, AuthRole::mint_recipient())? {
437 return Err(TIP20Error::policy_forbids().into());
438 }
439
440 let new_supply = total_supply
441 .checked_add(amount)
442 .ok_or(TempoPrecompileError::under_overflow())?;
443
444 let supply_cap = self.supply_cap()?;
445 if new_supply > supply_cap {
446 return Err(TIP20Error::supply_cap_exceeded().into());
447 }
448
449 self.handle_rewards_on_mint(to, amount)?;
450
451 self.set_total_supply(new_supply)?;
452 let to_balance = self.get_balance(to)?;
453 let new_to_balance: alloy::primitives::Uint<256, 4> = to_balance
454 .checked_add(amount)
455 .ok_or(TempoPrecompileError::under_overflow())?;
456 self.set_balance(to, new_to_balance)?;
457
458 self.emit_event(TIP20Event::Transfer(ITIP20::Transfer {
459 from: Address::ZERO,
460 to,
461 amount,
462 }))
463 }
464
465 pub fn burn(&mut self, msg_sender: Address, call: ITIP20::burnCall) -> Result<()> {
471 self._burn(msg_sender, call.amount)?;
472 self.emit_event(TIP20Event::Burn(ITIP20::Burn {
473 from: msg_sender,
474 amount: call.amount,
475 }))
476 }
477
478 pub fn burn_with_memo(
480 &mut self,
481 msg_sender: Address,
482 call: ITIP20::burnWithMemoCall,
483 ) -> Result<()> {
484 self._burn(msg_sender, call.amount)?;
485
486 self.emit_event(TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
487 from: msg_sender,
488 to: Address::ZERO,
489 amount: call.amount,
490 memo: call.memo,
491 }))?;
492 self.emit_event(TIP20Event::Burn(ITIP20::Burn {
493 from: msg_sender,
494 amount: call.amount,
495 }))
496 }
497
498 pub fn burn_blocked(
505 &mut self,
506 msg_sender: Address,
507 call: ITIP20::burnBlockedCall,
508 ) -> Result<()> {
509 self.check_role(msg_sender, *BURN_BLOCKED_ROLE)?;
510
511 if matches!(call.from, TIP_FEE_MANAGER_ADDRESS | STABLECOIN_DEX_ADDRESS) {
513 return Err(TIP20Error::protected_address().into());
514 }
515
516 let policy_id = self.transfer_policy_id()?;
518 if TIP403Registry::new().is_authorized_as(policy_id, call.from, AuthRole::sender())? {
519 return Err(TIP20Error::policy_forbids().into());
521 }
522
523 self._transfer(call.from, Address::ZERO, call.amount)?;
524
525 let total_supply = self.total_supply()?;
526 let new_supply =
527 total_supply
528 .checked_sub(call.amount)
529 .ok_or(TIP20Error::insufficient_balance(
530 total_supply,
531 call.amount,
532 self.address,
533 ))?;
534 self.set_total_supply(new_supply)?;
535
536 self.emit_event(TIP20Event::BurnBlocked(ITIP20::BurnBlocked {
537 from: call.from,
538 amount: call.amount,
539 }))
540 }
541
542 fn _burn(&mut self, msg_sender: Address, amount: U256) -> Result<()> {
543 self.check_role(msg_sender, *ISSUER_ROLE)?;
544
545 self._transfer(msg_sender, Address::ZERO, amount)?;
546
547 let total_supply = self.total_supply()?;
548 let new_supply =
549 total_supply
550 .checked_sub(amount)
551 .ok_or(TIP20Error::insufficient_balance(
552 total_supply,
553 amount,
554 self.address,
555 ))?;
556 self.set_total_supply(new_supply)
557 }
558
559 pub fn approve(&mut self, msg_sender: Address, call: ITIP20::approveCall) -> Result<bool> {
566 AccountKeychain::new().authorize_approve(
568 msg_sender,
569 self.address,
570 self.get_allowance(msg_sender, call.spender)?,
571 call.amount,
572 )?;
573
574 self.set_allowance(msg_sender, call.spender, call.amount)?;
576
577 self.emit_event(TIP20Event::Approval(ITIP20::Approval {
578 owner: msg_sender,
579 spender: call.spender,
580 amount: call.amount,
581 }))?;
582
583 Ok(true)
584 }
585
586 pub fn nonces(&self, call: ITIP20::noncesCall) -> Result<U256> {
590 self.permit_nonces[call.owner].read()
591 }
592
593 pub fn domain_separator(&self) -> Result<B256> {
595 let name = self.name()?;
596 let name_hash = self.storage.keccak256(name.as_bytes())?;
597 let chain_id = U256::from(self.storage.chain_id());
598
599 let encoded = (
600 *EIP712_DOMAIN_TYPEHASH,
601 name_hash,
602 *VERSION_HASH,
603 chain_id,
604 self.address,
605 )
606 .abi_encode();
607
608 self.storage.keccak256(&encoded)
609 }
610
611 pub fn permit(&mut self, call: ITIP20::permitCall) -> Result<()> {
620 if self.storage.timestamp() > call.deadline {
622 return Err(TIP20Error::permit_expired().into());
623 }
624
625 let nonce = self.permit_nonces[call.owner].read()?;
627 let struct_hash = self.storage.keccak256(
628 &(
629 *PERMIT_TYPEHASH,
630 call.owner,
631 call.spender,
632 call.value,
633 nonce,
634 call.deadline,
635 )
636 .abi_encode(),
637 )?;
638
639 let domain_separator = self.domain_separator()?;
641 let digest = self.storage.keccak256(
642 &[
643 &[0x19, 0x01],
644 domain_separator.as_slice(),
645 struct_hash.as_slice(),
646 ]
647 .concat(),
648 )?;
649
650 let recovered = self
653 .storage
654 .recover_signer(digest, call.v, call.r, call.s)?
655 .ok_or(TIP20Error::invalid_signature())?;
656 if recovered != call.owner {
657 return Err(TIP20Error::invalid_signature().into());
658 }
659
660 self.permit_nonces[call.owner].write(
662 nonce
663 .checked_add(U256::from(1))
664 .ok_or(TempoPrecompileError::under_overflow())?,
665 )?;
666
667 self.set_allowance(call.owner, call.spender, call.value)?;
669
670 self.emit_event(TIP20Event::Approval(ITIP20::Approval {
672 owner: call.owner,
673 spender: call.spender,
674 amount: call.value,
675 }))
676 }
677
678 pub fn transfer(&mut self, msg_sender: Address, call: ITIP20::transferCall) -> Result<bool> {
688 trace!(%msg_sender, ?call, "transferring TIP20");
689 self.check_not_paused()?;
690 self.check_recipient(call.to)?;
691 self.ensure_transfer_authorized(msg_sender, call.to)?;
692
693 AccountKeychain::new().authorize_transfer(msg_sender, self.address, call.amount)?;
695
696 self._transfer(msg_sender, call.to, call.amount)?;
697 Ok(true)
698 }
699
700 pub fn transfer_from(
710 &mut self,
711 msg_sender: Address,
712 call: ITIP20::transferFromCall,
713 ) -> Result<bool> {
714 self._transfer_from(msg_sender, call.from, call.to, call.amount)
715 }
716
717 pub fn transfer_from_with_memo(
719 &mut self,
720 msg_sender: Address,
721 call: ITIP20::transferFromWithMemoCall,
722 ) -> Result<bool> {
723 self._transfer_from(msg_sender, call.from, call.to, call.amount)?;
724
725 self.emit_event(TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
726 from: call.from,
727 to: call.to,
728 amount: call.amount,
729 memo: call.memo,
730 }))?;
731
732 Ok(true)
733 }
734
735 pub fn system_transfer_from(
746 &mut self,
747 from: Address,
748 to: Address,
749 amount: U256,
750 ) -> Result<bool> {
751 self.check_not_paused()?;
752 self.check_recipient(to)?;
753 self.ensure_transfer_authorized(from, to)?;
754 self.check_and_update_spending_limit(from, amount)?;
755
756 self._transfer(from, to, amount)?;
757
758 Ok(true)
759 }
760
761 fn _transfer_from(
762 &mut self,
763 msg_sender: Address,
764 from: Address,
765 to: Address,
766 amount: U256,
767 ) -> Result<bool> {
768 self.check_not_paused()?;
769 self.check_recipient(to)?;
770 self.ensure_transfer_authorized(from, to)?;
771
772 let allowed = self.get_allowance(from, msg_sender)?;
773 if amount > allowed {
774 return Err(TIP20Error::insufficient_allowance().into());
775 }
776
777 if allowed != U256::MAX {
778 let new_allowance = allowed
779 .checked_sub(amount)
780 .ok_or(TIP20Error::insufficient_allowance())?;
781 self.set_allowance(from, msg_sender, new_allowance)?;
782 }
783
784 self._transfer(from, to, amount)?;
785
786 Ok(true)
787 }
788
789 pub fn transfer_with_memo(
791 &mut self,
792 msg_sender: Address,
793 call: ITIP20::transferWithMemoCall,
794 ) -> Result<()> {
795 self.check_not_paused()?;
796 self.check_recipient(call.to)?;
797 self.ensure_transfer_authorized(msg_sender, call.to)?;
798 self.check_and_update_spending_limit(msg_sender, call.amount)?;
799
800 self._transfer(msg_sender, call.to, call.amount)?;
801
802 self.emit_event(TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
803 from: msg_sender,
804 to: call.to,
805 amount: call.amount,
806 memo: call.memo,
807 }))
808 }
809}
810
811impl TIP20Token {
813 pub fn from_address(address: Address) -> Result<Self> {
818 if !is_tip20_prefix(address) {
819 return Err(TIP20Error::invalid_token().into());
820 }
821 Ok(Self::__new(address))
822 }
823
824 #[inline]
829 pub fn from_address_unchecked(address: Address) -> Self {
830 debug_assert!(is_tip20_prefix(address), "address must have TIP20 prefix");
831 Self::__new(address)
832 }
833
834 pub fn initialize(
837 &mut self,
838 msg_sender: Address,
839 name: &str,
840 symbol: &str,
841 currency: &str,
842 quote_token: Address,
843 admin: Address,
844 ) -> Result<()> {
845 trace!(%name, address=%self.address, "Initializing token");
846
847 self.__initialize()?;
849
850 self.name.write(name.to_string())?;
851 self.symbol.write(symbol.to_string())?;
852 self.currency.write(currency.to_string())?;
853
854 self.quote_token.write(quote_token)?;
855 self.next_quote_token.write(quote_token)?;
857
858 self.supply_cap.write(U256::from(u128::MAX))?;
860 self.transfer_policy_id.write(1)?;
861
862 self.initialize_roles()?;
864 self.grant_default_admin(msg_sender, admin)
865 }
866
867 fn get_balance(&self, account: Address) -> Result<U256> {
868 self.balances[account].read()
869 }
870
871 fn set_balance(&mut self, account: Address, amount: U256) -> Result<()> {
872 self.balances[account].write(amount)
873 }
874
875 fn get_allowance(&self, owner: Address, spender: Address) -> Result<U256> {
876 self.allowances[owner][spender].read()
877 }
878
879 fn set_allowance(&mut self, owner: Address, spender: Address, amount: U256) -> Result<()> {
880 self.allowances[owner][spender].write(amount)
881 }
882
883 fn set_total_supply(&mut self, amount: U256) -> Result<()> {
884 self.total_supply.write(amount)
885 }
886
887 fn check_not_paused(&self) -> Result<()> {
888 if self.paused()? {
889 return Err(TIP20Error::contract_paused().into());
890 }
891 Ok(())
892 }
893
894 fn check_recipient(&self, to: Address) -> Result<()> {
898 if to.is_zero() || is_tip20_prefix(to) {
899 return Err(TIP20Error::invalid_recipient().into());
900 }
901 Ok(())
902 }
903
904 pub fn is_transfer_authorized(&self, from: Address, to: Address) -> Result<bool> {
909 let policy_id = self.transfer_policy_id()?;
910 let registry = TIP403Registry::new();
911
912 let sender_auth = registry.is_authorized_as(policy_id, from, AuthRole::sender())?;
914 if self.storage.spec().is_t2() && !sender_auth {
915 return Ok(false);
916 }
917 let recipient_auth = registry.is_authorized_as(policy_id, to, AuthRole::recipient())?;
918 Ok(sender_auth && recipient_auth)
919 }
920
921 pub fn ensure_transfer_authorized(&self, from: Address, to: Address) -> Result<()> {
926 if !self.is_transfer_authorized(from, to)? {
927 return Err(TIP20Error::policy_forbids().into());
928 }
929
930 Ok(())
931 }
932
933 pub fn check_and_update_spending_limit(&mut self, from: Address, amount: U256) -> Result<()> {
938 AccountKeychain::new().authorize_transfer(from, self.address, amount)
939 }
940
941 fn _transfer(&mut self, from: Address, to: Address, amount: U256) -> Result<()> {
942 let from_balance = self.get_balance(from)?;
943 if amount > from_balance {
944 return Err(
945 TIP20Error::insufficient_balance(from_balance, amount, self.address).into(),
946 );
947 }
948
949 self.handle_rewards_on_transfer(from, to, amount)?;
950
951 let new_from_balance = from_balance
953 .checked_sub(amount)
954 .ok_or(TempoPrecompileError::under_overflow())?;
955
956 self.set_balance(from, new_from_balance)?;
957
958 if to != Address::ZERO {
959 let to_balance = self.get_balance(to)?;
960 let new_to_balance = to_balance
961 .checked_add(amount)
962 .ok_or(TempoPrecompileError::under_overflow())?;
963
964 self.set_balance(to, new_to_balance)?;
965 }
966
967 self.emit_event(TIP20Event::Transfer(ITIP20::Transfer { from, to, amount }))
968 }
969
970 pub fn transfer_fee_pre_tx(&mut self, from: Address, amount: U256) -> Result<()> {
978 self.check_not_paused()?;
983 let from_balance = self.get_balance(from)?;
984 if amount > from_balance {
985 return Err(
986 TIP20Error::insufficient_balance(from_balance, amount, self.address).into(),
987 );
988 }
989
990 self.check_and_update_spending_limit(from, amount)?;
991
992 let from_reward_recipient = self.update_rewards(from)?;
994
995 if from_reward_recipient != Address::ZERO {
997 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
998 .checked_sub(amount)
999 .ok_or(TempoPrecompileError::under_overflow())?;
1000 self.set_opted_in_supply(
1001 opted_in_supply
1002 .try_into()
1003 .map_err(|_| TempoPrecompileError::under_overflow())?,
1004 )?;
1005 }
1006
1007 let new_from_balance =
1008 from_balance
1009 .checked_sub(amount)
1010 .ok_or(TIP20Error::insufficient_balance(
1011 from_balance,
1012 amount,
1013 self.address,
1014 ))?;
1015
1016 self.set_balance(from, new_from_balance)?;
1017
1018 let to_balance = self.get_balance(TIP_FEE_MANAGER_ADDRESS)?;
1019 let new_to_balance = to_balance
1020 .checked_add(amount)
1021 .ok_or(TIP20Error::supply_cap_exceeded())?;
1022 self.set_balance(TIP_FEE_MANAGER_ADDRESS, new_to_balance)
1023 }
1024
1025 pub fn transfer_fee_post_tx(
1030 &mut self,
1031 to: Address,
1032 refund: U256,
1033 actual_spending: U256,
1034 ) -> Result<()> {
1035 self.emit_event(TIP20Event::Transfer(ITIP20::Transfer {
1036 from: to,
1037 to: TIP_FEE_MANAGER_ADDRESS,
1038 amount: actual_spending,
1039 }))?;
1040
1041 if refund.is_zero() {
1043 return Ok(());
1044 }
1045
1046 if self.storage.spec().is_t1c() {
1047 AccountKeychain::new().refund_spending_limit(to, self.address, refund)?;
1048 }
1049
1050 let to_reward_recipient = self.update_rewards(to)?;
1052
1053 if to_reward_recipient != Address::ZERO {
1055 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
1056 .checked_add(refund)
1057 .ok_or(TempoPrecompileError::under_overflow())?;
1058 self.set_opted_in_supply(
1059 opted_in_supply
1060 .try_into()
1061 .map_err(|_| TempoPrecompileError::under_overflow())?,
1062 )?;
1063 }
1064
1065 let from_balance = self.get_balance(TIP_FEE_MANAGER_ADDRESS)?;
1066 let new_from_balance =
1067 from_balance
1068 .checked_sub(refund)
1069 .ok_or(TIP20Error::insufficient_balance(
1070 from_balance,
1071 refund,
1072 self.address,
1073 ))?;
1074
1075 self.set_balance(TIP_FEE_MANAGER_ADDRESS, new_from_balance)?;
1076
1077 let to_balance = self.get_balance(to)?;
1078 let new_to_balance = to_balance
1079 .checked_add(refund)
1080 .ok_or(TIP20Error::supply_cap_exceeded())?;
1081 self.set_balance(to, new_to_balance)
1082 }
1083}
1084
1085#[cfg(test)]
1086pub(crate) mod tests {
1087 use alloy::primitives::{Address, FixedBytes, IntoLogData, U256};
1088 use tempo_contracts::precompiles::{DEFAULT_FEE_TOKEN, ITIP20Factory};
1089
1090 use super::*;
1091 use crate::{
1092 PATH_USD_ADDRESS,
1093 account_keychain::{
1094 AccountKeychain, SignatureType, TokenLimit, authorizeKeyCall, getRemainingLimitCall,
1095 },
1096 error::TempoPrecompileError,
1097 storage::{StorageCtx, hashmap::HashMapStorageProvider},
1098 test_util::{TIP20Setup, setup_storage},
1099 };
1100 use rand_08::{Rng, distributions::Alphanumeric, thread_rng};
1101 use tempo_chainspec::hardfork::TempoHardfork;
1102
1103 #[test]
1104 fn test_mint_increases_balance_and_supply() -> eyre::Result<()> {
1105 let (mut storage, admin) = setup_storage();
1106 let addr = Address::random();
1107 let amount = U256::random() % U256::from(u128::MAX);
1108
1109 StorageCtx::enter(&mut storage, || {
1110 let mut token = TIP20Setup::create("Test", "TST", admin)
1111 .with_issuer(admin)
1112 .clear_events()
1113 .apply()?;
1114
1115 token.mint(admin, ITIP20::mintCall { to: addr, amount })?;
1116
1117 assert_eq!(token.get_balance(addr)?, amount);
1118 assert_eq!(token.total_supply()?, amount);
1119
1120 token.assert_emitted_events(vec![
1121 TIP20Event::Transfer(ITIP20::Transfer {
1122 from: Address::ZERO,
1123 to: addr,
1124 amount,
1125 }),
1126 TIP20Event::Mint(ITIP20::Mint { to: addr, amount }),
1127 ]);
1128
1129 Ok(())
1130 })
1131 }
1132
1133 #[test]
1134 fn test_transfer_moves_balance() -> eyre::Result<()> {
1135 let (mut storage, admin) = setup_storage();
1136 let from = Address::random();
1137 let to = Address::random();
1138 let amount = U256::random() % U256::from(u128::MAX);
1139
1140 StorageCtx::enter(&mut storage, || {
1141 let mut token = TIP20Setup::create("Test", "TST", admin)
1142 .with_issuer(admin)
1143 .with_mint(from, amount)
1144 .clear_events()
1145 .apply()?;
1146
1147 token.transfer(from, ITIP20::transferCall { to, amount })?;
1148
1149 assert_eq!(token.get_balance(from)?, U256::ZERO);
1150 assert_eq!(token.get_balance(to)?, amount);
1151 assert_eq!(token.total_supply()?, amount); token.assert_emitted_events(vec![TIP20Event::Transfer(ITIP20::Transfer {
1154 from,
1155 to,
1156 amount,
1157 })]);
1158
1159 Ok(())
1160 })
1161 }
1162
1163 #[test]
1164 fn test_transfer_insufficient_balance_fails() -> eyre::Result<()> {
1165 let (mut storage, admin) = setup_storage();
1166 let from = Address::random();
1167 let to = Address::random();
1168 let amount = U256::random() % U256::from(u128::MAX);
1169
1170 StorageCtx::enter(&mut storage, || {
1171 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1172
1173 let result = token.transfer(from, ITIP20::transferCall { to, amount });
1174 assert!(matches!(
1175 result,
1176 Err(TempoPrecompileError::TIP20(
1177 TIP20Error::InsufficientBalance(_)
1178 ))
1179 ));
1180
1181 Ok(())
1182 })
1183 }
1184
1185 #[test]
1186 fn test_mint_with_memo() -> eyre::Result<()> {
1187 let mut storage = HashMapStorageProvider::new(1);
1188 let admin = Address::random();
1189 let amount = U256::random() % U256::from(u128::MAX);
1190 let to = Address::random();
1191 let memo = FixedBytes::random();
1192
1193 StorageCtx::enter(&mut storage, || {
1194 let mut token = TIP20Setup::create("Test", "TST", admin)
1195 .with_issuer(admin)
1196 .clear_events()
1197 .apply()?;
1198
1199 token.mint_with_memo(admin, ITIP20::mintWithMemoCall { to, amount, memo })?;
1200
1201 token.assert_emitted_events(vec![
1203 TIP20Event::Transfer(ITIP20::Transfer {
1204 from: Address::ZERO,
1205 to,
1206 amount,
1207 }),
1208 TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
1209 from: Address::ZERO,
1210 to,
1211 amount,
1212 memo,
1213 }),
1214 TIP20Event::Mint(ITIP20::Mint { to, amount }),
1215 ]);
1216
1217 Ok(())
1218 })
1219 }
1220
1221 #[test]
1222 fn test_burn_with_memo() -> eyre::Result<()> {
1223 let mut storage = HashMapStorageProvider::new(1);
1224 let admin = Address::random();
1225 let amount = U256::random() % U256::from(u128::MAX);
1226 let memo = FixedBytes::random();
1227
1228 StorageCtx::enter(&mut storage, || {
1229 let mut token = TIP20Setup::create("Test", "TST", admin)
1230 .with_issuer(admin)
1231 .with_mint(admin, amount)
1232 .clear_events()
1233 .apply()?;
1234
1235 token.burn_with_memo(admin, ITIP20::burnWithMemoCall { amount, memo })?;
1236 token.assert_emitted_events(vec![
1237 TIP20Event::Transfer(ITIP20::Transfer {
1238 from: admin,
1239 to: Address::ZERO,
1240 amount,
1241 }),
1242 TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
1243 from: admin,
1244 to: Address::ZERO,
1245 amount,
1246 memo,
1247 }),
1248 TIP20Event::Burn(ITIP20::Burn {
1249 from: admin,
1250 amount,
1251 }),
1252 ]);
1253
1254 Ok(())
1255 })
1256 }
1257
1258 #[test]
1259 fn test_transfer_from_with_memo_from_address() -> eyre::Result<()> {
1260 let mut storage = HashMapStorageProvider::new(1);
1261 let admin = Address::random();
1262 let owner = Address::random();
1263 let spender = Address::random();
1264 let to = Address::random();
1265 let memo = FixedBytes::random();
1266 let amount = U256::random() % U256::from(u128::MAX);
1267
1268 StorageCtx::enter(&mut storage, || {
1269 let mut token = TIP20Setup::create("Test", "TST", admin)
1270 .with_issuer(admin)
1271 .with_mint(owner, amount)
1272 .with_approval(owner, spender, amount)
1273 .clear_events()
1274 .apply()?;
1275
1276 token.transfer_from_with_memo(
1277 spender,
1278 ITIP20::transferFromWithMemoCall {
1279 from: owner,
1280 to,
1281 amount,
1282 memo,
1283 },
1284 )?;
1285
1286 token.assert_emitted_events(vec![
1288 TIP20Event::Transfer(ITIP20::Transfer {
1289 from: owner,
1290 to,
1291 amount,
1292 }),
1293 TIP20Event::TransferWithMemo(ITIP20::TransferWithMemo {
1294 from: owner,
1295 to,
1296 amount,
1297 memo,
1298 }),
1299 ]);
1300
1301 Ok(())
1302 })
1303 }
1304
1305 #[test]
1306 fn test_transfer_fee_pre_tx() -> eyre::Result<()> {
1307 let mut storage = HashMapStorageProvider::new(1);
1308 let admin = Address::random();
1309 let user = Address::random();
1310 let amount = U256::from(100);
1311 let fee_amount = amount / U256::from(2);
1312
1313 StorageCtx::enter(&mut storage, || {
1314 let mut token = TIP20Setup::create("Test", "TST", admin)
1315 .with_issuer(admin)
1316 .with_mint(user, amount)
1317 .apply()?;
1318
1319 token.transfer_fee_pre_tx(user, fee_amount)?;
1320
1321 assert_eq!(token.get_balance(user)?, fee_amount);
1322 assert_eq!(token.get_balance(TIP_FEE_MANAGER_ADDRESS)?, fee_amount);
1323
1324 Ok(())
1325 })
1326 }
1327
1328 #[test]
1329 fn test_transfer_fee_pre_tx_insufficient_balance() -> eyre::Result<()> {
1330 let mut storage = HashMapStorageProvider::new(1);
1331 let admin = Address::random();
1332 let user = Address::random();
1333 let amount = U256::from(100);
1334 let fee_amount = amount / U256::from(2);
1335
1336 StorageCtx::enter(&mut storage, || {
1337 let mut token = TIP20Setup::create("Test", "TST", admin)
1338 .with_issuer(admin)
1339 .apply()?;
1340
1341 assert_eq!(
1342 token.transfer_fee_pre_tx(user, fee_amount),
1343 Err(TempoPrecompileError::TIP20(
1344 TIP20Error::insufficient_balance(U256::ZERO, fee_amount, token.address)
1345 ))
1346 );
1347 Ok(())
1348 })
1349 }
1350
1351 #[test]
1352 fn test_transfer_fee_pre_tx_paused() -> eyre::Result<()> {
1353 let mut storage = HashMapStorageProvider::new(1);
1354 let admin = Address::random();
1355 let user = Address::random();
1356 let amount = U256::from(100);
1357 let fee_amount = amount / U256::from(2);
1358
1359 StorageCtx::enter(&mut storage, || {
1360 let mut token = TIP20Setup::create("Test", "TST", admin)
1361 .with_issuer(admin)
1362 .with_role(admin, *PAUSE_ROLE)
1363 .with_mint(user, amount)
1364 .apply()?;
1365
1366 token.pause(admin, ITIP20::pauseCall {})?;
1368
1369 assert_eq!(
1371 token.transfer_fee_pre_tx(user, fee_amount),
1372 Err(TempoPrecompileError::TIP20(TIP20Error::contract_paused()))
1373 );
1374 Ok(())
1375 })
1376 }
1377
1378 #[test]
1379 fn test_transfer_fee_post_tx() -> eyre::Result<()> {
1380 let mut storage = HashMapStorageProvider::new(1);
1381 let admin = Address::random();
1382 let user = Address::random();
1383 let initial_fee = U256::from(100);
1384 let refund_amount = U256::from(30);
1385 let gas_used = U256::from(10);
1386
1387 StorageCtx::enter(&mut storage, || {
1388 let mut token = TIP20Setup::create("Test", "TST", admin)
1389 .with_issuer(admin)
1390 .with_mint(TIP_FEE_MANAGER_ADDRESS, initial_fee)
1391 .apply()?;
1392
1393 token.transfer_fee_post_tx(user, refund_amount, gas_used)?;
1394
1395 assert_eq!(token.get_balance(user)?, refund_amount);
1396 assert_eq!(
1397 token.get_balance(TIP_FEE_MANAGER_ADDRESS)?,
1398 initial_fee - refund_amount
1399 );
1400 assert_eq!(
1401 token.emitted_events().last().unwrap(),
1402 &TIP20Event::Transfer(ITIP20::Transfer {
1403 from: user,
1404 to: TIP_FEE_MANAGER_ADDRESS,
1405 amount: gas_used
1406 })
1407 .into_log_data()
1408 );
1409
1410 Ok(())
1411 })
1412 }
1413
1414 #[test]
1415 fn test_transfer_fee_post_tx_refunds_spending_limit() -> eyre::Result<()> {
1416 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
1417 let admin = Address::random();
1418 let user = Address::random();
1419 let access_key = Address::random();
1420 let max_fee = U256::from(1000);
1421 let refund_amount = U256::from(300);
1422 let gas_used = U256::from(100);
1423
1424 StorageCtx::enter(&mut storage, || {
1425 let mut token = TIP20Setup::create("Test", "TST", admin)
1426 .with_issuer(admin)
1427 .with_mint(TIP_FEE_MANAGER_ADDRESS, max_fee)
1428 .apply()?;
1429
1430 let token_address = token.address;
1431 let spending_limit = U256::from(2000);
1432
1433 let mut keychain = AccountKeychain::new();
1435 keychain.initialize()?;
1436 keychain.set_transaction_key(Address::ZERO)?;
1437
1438 keychain.authorize_key(
1439 user,
1440 authorizeKeyCall {
1441 keyId: access_key,
1442 signatureType: SignatureType::Secp256k1,
1443 expiry: u64::MAX,
1444 enforceLimits: true,
1445 limits: vec![TokenLimit {
1446 token: token_address,
1447 amount: spending_limit,
1448 }],
1449 },
1450 )?;
1451
1452 keychain.set_transaction_key(access_key)?;
1454 keychain.set_tx_origin(user)?;
1455 keychain.authorize_transfer(user, token_address, max_fee)?;
1456
1457 let remaining_after_deduction =
1458 keychain.get_remaining_limit(getRemainingLimitCall {
1459 account: user,
1460 keyId: access_key,
1461 token: token_address,
1462 })?;
1463 assert_eq!(remaining_after_deduction, spending_limit - max_fee);
1464
1465 token.transfer_fee_post_tx(user, refund_amount, gas_used)?;
1467
1468 let remaining_after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
1469 account: user,
1470 keyId: access_key,
1471 token: token_address,
1472 })?;
1473 assert_eq!(
1474 remaining_after_refund,
1475 spending_limit - max_fee + refund_amount,
1476 "spending limit should be restored by refund amount"
1477 );
1478
1479 Ok(())
1480 })
1481 }
1482
1483 #[test]
1484 fn test_transfer_fee_post_tx_pre_t1c() -> eyre::Result<()> {
1485 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1B);
1486 let admin = Address::random();
1487 let user = Address::random();
1488 let access_key = Address::random();
1489 let max_fee = U256::from(1000);
1490 let refund_amount = U256::from(300);
1491 let gas_used = U256::from(100);
1492
1493 StorageCtx::enter(&mut storage, || {
1494 let mut token = TIP20Setup::create("Test", "TST", admin)
1495 .with_issuer(admin)
1496 .with_mint(TIP_FEE_MANAGER_ADDRESS, max_fee)
1497 .apply()?;
1498
1499 let token_address = token.address;
1500 let spending_limit = U256::from(2000);
1501
1502 let mut keychain = AccountKeychain::new();
1503 keychain.initialize()?;
1504 keychain.set_transaction_key(Address::ZERO)?;
1505
1506 keychain.authorize_key(
1507 user,
1508 authorizeKeyCall {
1509 keyId: access_key,
1510 signatureType: SignatureType::Secp256k1,
1511 expiry: u64::MAX,
1512 enforceLimits: true,
1513 limits: vec![TokenLimit {
1514 token: token_address,
1515 amount: spending_limit,
1516 }],
1517 },
1518 )?;
1519
1520 keychain.set_transaction_key(access_key)?;
1521 keychain.set_tx_origin(user)?;
1522 keychain.authorize_transfer(user, token_address, max_fee)?;
1523
1524 let remaining_after_deduction =
1525 keychain.get_remaining_limit(getRemainingLimitCall {
1526 account: user,
1527 keyId: access_key,
1528 token: token_address,
1529 })?;
1530 assert_eq!(remaining_after_deduction, spending_limit - max_fee);
1531
1532 token.transfer_fee_post_tx(user, refund_amount, gas_used)?;
1533
1534 let remaining_after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
1536 account: user,
1537 keyId: access_key,
1538 token: token_address,
1539 })?;
1540 assert_eq!(remaining_after_refund, spending_limit - max_fee);
1541
1542 Ok(())
1543 })
1544 }
1545
1546 #[test]
1547 fn test_transfer_from_insufficient_allowance() -> eyre::Result<()> {
1548 let mut storage = HashMapStorageProvider::new(1);
1549 let admin = Address::random();
1550 let from = Address::random();
1551 let spender = Address::random();
1552 let to = Address::random();
1553 let amount = U256::random() % U256::from(u128::MAX);
1554
1555 StorageCtx::enter(&mut storage, || {
1556 let mut token = TIP20Setup::create("Test", "TST", admin)
1557 .with_issuer(admin)
1558 .with_mint(from, amount)
1559 .apply()?;
1560
1561 assert!(matches!(
1562 token.transfer_from(spender, ITIP20::transferFromCall { from, to, amount }),
1563 Err(TempoPrecompileError::TIP20(
1564 TIP20Error::InsufficientAllowance(_)
1565 ))
1566 ));
1567
1568 Ok(())
1569 })
1570 }
1571
1572 #[test]
1573 fn test_system_transfer_from() -> eyre::Result<()> {
1574 let mut storage = HashMapStorageProvider::new(1);
1575 let admin = Address::random();
1576 let from = Address::random();
1577 let to = Address::random();
1578 let amount = U256::random() % U256::from(u128::MAX);
1579
1580 StorageCtx::enter(&mut storage, || {
1581 let mut token = TIP20Setup::create("Test", "TST", admin)
1582 .with_issuer(admin)
1583 .with_mint(from, amount)
1584 .apply()?;
1585
1586 assert!(token.system_transfer_from(from, to, amount).is_ok());
1587 assert_eq!(
1588 token.emitted_events().last().unwrap(),
1589 &TIP20Event::Transfer(ITIP20::Transfer { from, to, amount }).into_log_data()
1590 );
1591
1592 Ok(())
1593 })
1594 }
1595
1596 #[test]
1597 fn test_initialize_sets_next_quote_token() -> eyre::Result<()> {
1598 let mut storage = HashMapStorageProvider::new(1);
1599 let admin = Address::random();
1600
1601 StorageCtx::enter(&mut storage, || {
1602 let token = TIP20Setup::create("Test", "TST", admin).apply()?;
1603
1604 assert_eq!(token.quote_token()?, PATH_USD_ADDRESS);
1606 assert_eq!(token.next_quote_token()?, PATH_USD_ADDRESS);
1607
1608 Ok(())
1609 })
1610 }
1611
1612 #[test]
1613 fn test_update_quote_token() -> eyre::Result<()> {
1614 let mut storage = HashMapStorageProvider::new(1);
1615 let admin = Address::random();
1616
1617 StorageCtx::enter(&mut storage, || {
1618 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1619
1620 let new_quote_token = TIP20Setup::create("New Quote", "NQ", admin).apply()?;
1622 let new_quote_token_address = new_quote_token.address;
1623
1624 assert_eq!(token.quote_token()?, PATH_USD_ADDRESS);
1626
1627 token.set_next_quote_token(
1629 admin,
1630 ITIP20::setNextQuoteTokenCall {
1631 newQuoteToken: new_quote_token_address,
1632 },
1633 )?;
1634
1635 assert_eq!(token.next_quote_token()?, new_quote_token_address);
1637
1638 assert_eq!(
1640 token.emitted_events().last().unwrap(),
1641 &TIP20Event::NextQuoteTokenSet(ITIP20::NextQuoteTokenSet {
1642 updater: admin,
1643 nextQuoteToken: new_quote_token_address,
1644 })
1645 .into_log_data()
1646 );
1647
1648 Ok(())
1649 })
1650 }
1651
1652 #[test]
1653 fn test_update_quote_token_requires_admin() -> eyre::Result<()> {
1654 let mut storage = HashMapStorageProvider::new(1);
1655 let admin = Address::random();
1656 let non_admin = Address::random();
1657
1658 StorageCtx::enter(&mut storage, || {
1659 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1660
1661 let quote_token_address = token.quote_token()?;
1663
1664 let result = token.set_next_quote_token(
1666 non_admin,
1667 ITIP20::setNextQuoteTokenCall {
1668 newQuoteToken: quote_token_address,
1669 },
1670 );
1671
1672 assert!(matches!(
1673 result,
1674 Err(TempoPrecompileError::RolesAuthError(
1675 RolesAuthError::Unauthorized(_)
1676 ))
1677 ));
1678
1679 Ok(())
1680 })
1681 }
1682
1683 #[test]
1684 fn test_update_quote_token_rejects_non_tip20() -> eyre::Result<()> {
1685 let mut storage = HashMapStorageProvider::new(1);
1686 let admin = Address::random();
1687
1688 StorageCtx::enter(&mut storage, || {
1689 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1690
1691 let non_tip20_address = Address::random();
1693 let result = token.set_next_quote_token(
1694 admin,
1695 ITIP20::setNextQuoteTokenCall {
1696 newQuoteToken: non_tip20_address,
1697 },
1698 );
1699
1700 assert!(matches!(
1701 result,
1702 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
1703 _
1704 )))
1705 ));
1706
1707 Ok(())
1708 })
1709 }
1710
1711 #[test]
1712 fn test_update_quote_token_rejects_undeployed_token() -> eyre::Result<()> {
1713 let mut storage = HashMapStorageProvider::new(1);
1714 let admin = Address::random();
1715
1716 StorageCtx::enter(&mut storage, || {
1717 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1718
1719 let undeployed_token_address =
1722 Address::from(hex!("20C0000000000000000000000000000000000999"));
1723 let result = token.set_next_quote_token(
1724 admin,
1725 ITIP20::setNextQuoteTokenCall {
1726 newQuoteToken: undeployed_token_address,
1727 },
1728 );
1729
1730 assert!(matches!(
1731 result,
1732 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
1733 _
1734 )))
1735 ));
1736
1737 Ok(())
1738 })
1739 }
1740
1741 #[test]
1742 fn test_finalize_quote_token_update() -> eyre::Result<()> {
1743 let mut storage = HashMapStorageProvider::new(1);
1744 let admin = Address::random();
1745
1746 StorageCtx::enter(&mut storage, || {
1747 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1748 let quote_token_address = token.quote_token()?;
1749
1750 token.set_next_quote_token(
1752 admin,
1753 ITIP20::setNextQuoteTokenCall {
1754 newQuoteToken: quote_token_address,
1755 },
1756 )?;
1757
1758 token.complete_quote_token_update(admin, ITIP20::completeQuoteTokenUpdateCall {})?;
1760
1761 assert_eq!(token.quote_token()?, quote_token_address);
1763
1764 assert_eq!(
1766 token.emitted_events().last().unwrap(),
1767 &TIP20Event::QuoteTokenUpdate(ITIP20::QuoteTokenUpdate {
1768 updater: admin,
1769 newQuoteToken: quote_token_address,
1770 })
1771 .into_log_data()
1772 );
1773
1774 Ok(())
1775 })
1776 }
1777
1778 #[test]
1779 fn test_finalize_quote_token_update_detects_loop() -> eyre::Result<()> {
1780 let mut storage = HashMapStorageProvider::new(1);
1781 let admin = Address::random();
1782
1783 StorageCtx::enter(&mut storage, || {
1784 let mut token_b = TIP20Setup::create("Token B", "TKB", admin).apply()?;
1786 let token_a = TIP20Setup::create("Token A", "TKA", admin)
1788 .quote_token(token_b.address)
1789 .apply()?;
1790
1791 token_b.set_next_quote_token(
1793 admin,
1794 ITIP20::setNextQuoteTokenCall {
1795 newQuoteToken: token_a.address,
1796 },
1797 )?;
1798
1799 let result =
1801 token_b.complete_quote_token_update(admin, ITIP20::completeQuoteTokenUpdateCall {});
1802
1803 assert!(matches!(
1804 result,
1805 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
1806 _
1807 )))
1808 ));
1809
1810 Ok(())
1811 })
1812 }
1813
1814 #[test]
1815 fn test_finalize_quote_token_update_requires_admin() -> eyre::Result<()> {
1816 let mut storage = HashMapStorageProvider::new(1);
1817 let admin = Address::random();
1818 let non_admin = Address::random();
1819
1820 StorageCtx::enter(&mut storage, || {
1821 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
1822 let quote_token_address = token.quote_token()?;
1823
1824 token.set_next_quote_token(
1826 admin,
1827 ITIP20::setNextQuoteTokenCall {
1828 newQuoteToken: quote_token_address,
1829 },
1830 )?;
1831
1832 let result = token
1834 .complete_quote_token_update(non_admin, ITIP20::completeQuoteTokenUpdateCall {});
1835
1836 assert!(matches!(
1837 result,
1838 Err(TempoPrecompileError::RolesAuthError(
1839 RolesAuthError::Unauthorized(_)
1840 ))
1841 ));
1842
1843 Ok(())
1844 })
1845 }
1846
1847 #[test]
1848 fn test_tip20_token_prefix() {
1849 assert_eq!(
1850 TIP20_TOKEN_PREFIX,
1851 [
1852 0x20, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
1853 ]
1854 );
1855 assert_eq!(&DEFAULT_FEE_TOKEN.as_slice()[..12], &TIP20_TOKEN_PREFIX);
1856 }
1857
1858 #[test]
1859 fn test_arbitrary_currency() -> eyre::Result<()> {
1860 let mut storage = HashMapStorageProvider::new(1);
1861 let admin = Address::random();
1862
1863 StorageCtx::enter(&mut storage, || {
1864 for _ in 0..50 {
1865 let currency: String = thread_rng()
1866 .sample_iter(&Alphanumeric)
1867 .take(31)
1868 .map(char::from)
1869 .collect();
1870
1871 let token = TIP20Setup::create("Test", "TST", admin)
1873 .currency(¤cy)
1874 .apply()?;
1875
1876 let stored_currency = token.currency()?;
1878 assert_eq!(stored_currency, currency,);
1879 }
1880
1881 Ok(())
1882 })
1883 }
1884
1885 #[test]
1886 fn test_from_address() -> eyre::Result<()> {
1887 let mut storage = HashMapStorageProvider::new(1);
1888 let admin = Address::random();
1889
1890 StorageCtx::enter(&mut storage, || {
1891 let token = TIP20Setup::create("Test", "TST", admin).apply()?;
1893 let via_from_address = TIP20Token::from_address(token.address)?.address;
1894
1895 assert_eq!(
1896 via_from_address, token.address,
1897 "from_address should use the provided address directly"
1898 );
1899
1900 let _path_usd = TIP20Setup::path_usd(admin).apply()?;
1902 let via_from_address_reserved = TIP20Token::from_address(PATH_USD_ADDRESS)?.address;
1903
1904 assert_eq!(
1905 via_from_address_reserved, PATH_USD_ADDRESS,
1906 "from_address should work for reserved addresses too"
1907 );
1908
1909 Ok(())
1910 })
1911 }
1912
1913 #[test]
1914 fn test_new_invalid_quote_token() -> eyre::Result<()> {
1915 let mut storage = HashMapStorageProvider::new(1);
1916 let admin = Address::random();
1917
1918 StorageCtx::enter(&mut storage, || {
1919 let currency: String = thread_rng()
1920 .sample_iter(&Alphanumeric)
1921 .take(31)
1922 .map(char::from)
1923 .collect();
1924
1925 let token = TIP20Setup::create("Token", "T", admin)
1926 .currency(¤cy)
1927 .apply()?;
1928
1929 TIP20Setup::create("USD Token", "USDT", admin)
1931 .currency(USD_CURRENCY)
1932 .quote_token(token.address)
1933 .expect_tip20_err(TIP20Error::invalid_quote_token());
1934
1935 Ok(())
1936 })
1937 }
1938
1939 #[test]
1940 fn test_new_valid_quote_token() -> eyre::Result<()> {
1941 let mut storage = HashMapStorageProvider::new(1);
1942 let admin = Address::random();
1943
1944 StorageCtx::enter(&mut storage, || {
1945 let usd_token1 = TIP20Setup::create("USD Token", "USDT", admin).apply()?;
1946
1947 let _usd_token2 = TIP20Setup::create("USD Token", "USDT", admin)
1949 .quote_token(usd_token1.address)
1950 .apply()?;
1951
1952 let currency_1: String = thread_rng()
1954 .sample_iter(&Alphanumeric)
1955 .take(31)
1956 .map(char::from)
1957 .collect();
1958
1959 let token_1 = TIP20Setup::create("USD Token", "USDT", admin)
1960 .currency(currency_1)
1961 .apply()?;
1962
1963 let currency_2: String = thread_rng()
1965 .sample_iter(&Alphanumeric)
1966 .take(31)
1967 .map(char::from)
1968 .collect();
1969
1970 let _token_2 = TIP20Setup::create("USD Token", "USDT", admin)
1971 .currency(currency_2)
1972 .quote_token(token_1.address)
1973 .apply()?;
1974
1975 Ok(())
1976 })
1977 }
1978
1979 #[test]
1980 fn test_update_quote_token_invalid_token() -> eyre::Result<()> {
1981 let mut storage = HashMapStorageProvider::new(1);
1982 let admin = Address::random();
1983
1984 StorageCtx::enter(&mut storage, || {
1985 let _path_usd = TIP20Setup::path_usd(admin).apply()?;
1986
1987 let currency: String = thread_rng()
1988 .sample_iter(&Alphanumeric)
1989 .take(31)
1990 .map(char::from)
1991 .collect();
1992
1993 let token_1 = TIP20Setup::create("Token 1", "TK1", admin)
1994 .currency(¤cy)
1995 .apply()?;
1996
1997 let mut usd_token = TIP20Setup::create("USD Token", "USDT", admin).apply()?;
1999
2000 let result = usd_token.set_next_quote_token(
2002 admin,
2003 ITIP20::setNextQuoteTokenCall {
2004 newQuoteToken: token_1.address,
2005 },
2006 );
2007
2008 assert!(result.is_err_and(
2009 |err| err == TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
2010 ));
2011
2012 Ok(())
2013 })
2014 }
2015
2016 #[test]
2017 fn test_is_tip20_prefix() -> eyre::Result<()> {
2018 let mut storage = HashMapStorageProvider::new(1);
2019 let sender = Address::random();
2020
2021 StorageCtx::enter(&mut storage, || {
2022 let _path_usd = TIP20Setup::path_usd(sender).apply()?;
2023
2024 let created_tip20 = TIP20Factory::new().create_token(
2025 sender,
2026 ITIP20Factory::createTokenCall {
2027 name: "Test Token".to_string(),
2028 symbol: "TEST".to_string(),
2029 currency: "USD".to_string(),
2030 quoteToken: crate::PATH_USD_ADDRESS,
2031 admin: sender,
2032 salt: B256::random(),
2033 },
2034 )?;
2035 let non_tip20 = Address::random();
2036
2037 assert!(is_tip20_prefix(PATH_USD_ADDRESS));
2038 assert!(is_tip20_prefix(created_tip20));
2039 assert!(!is_tip20_prefix(non_tip20));
2040 Ok(())
2041 })
2042 }
2043
2044 #[test]
2045 fn test_initialize_supply_cap() -> eyre::Result<()> {
2046 let mut storage = HashMapStorageProvider::new(1);
2047 let admin = Address::random();
2048
2049 StorageCtx::enter(&mut storage, || {
2050 let token = TIP20Setup::create("Token", "TKN", admin).apply()?;
2051
2052 let supply_cap = token.supply_cap()?;
2053 assert_eq!(supply_cap, U256::from(u128::MAX));
2054
2055 Ok(())
2056 })
2057 }
2058
2059 #[test]
2060 fn test_unable_to_burn_blocked_from_protected_address() -> eyre::Result<()> {
2061 let mut storage = HashMapStorageProvider::new(1);
2062 let admin = Address::random();
2063 let burner = Address::random();
2064 let amount = (U256::random() % U256::from(u128::MAX)) / U256::from(2);
2065
2066 StorageCtx::enter(&mut storage, || {
2067 let mut token = TIP20Setup::create("Token", "TKN", admin)
2068 .with_issuer(admin)
2069 .with_role(burner, *BURN_BLOCKED_ROLE)
2071 .with_mint(TIP_FEE_MANAGER_ADDRESS, amount)
2073 .with_mint(STABLECOIN_DEX_ADDRESS, amount)
2075 .apply()?;
2076
2077 let result = token.burn_blocked(
2079 burner,
2080 ITIP20::burnBlockedCall {
2081 from: TIP_FEE_MANAGER_ADDRESS,
2082 amount: amount / U256::from(2),
2083 },
2084 );
2085
2086 assert!(matches!(
2087 result,
2088 Err(TempoPrecompileError::TIP20(TIP20Error::ProtectedAddress(_)))
2089 ));
2090
2091 let balance = token.balance_of(ITIP20::balanceOfCall {
2093 account: TIP_FEE_MANAGER_ADDRESS,
2094 })?;
2095 assert_eq!(balance, amount);
2096
2097 let result = token.burn_blocked(
2099 burner,
2100 ITIP20::burnBlockedCall {
2101 from: STABLECOIN_DEX_ADDRESS,
2102 amount: amount / U256::from(2),
2103 },
2104 );
2105
2106 assert!(matches!(
2107 result,
2108 Err(TempoPrecompileError::TIP20(TIP20Error::ProtectedAddress(_)))
2109 ));
2110
2111 let balance = token.balance_of(ITIP20::balanceOfCall {
2113 account: STABLECOIN_DEX_ADDRESS,
2114 })?;
2115 assert_eq!(balance, amount);
2116
2117 Ok(())
2118 })
2119 }
2120
2121 #[test]
2122 fn test_initialize_usd_token() -> eyre::Result<()> {
2123 let mut storage = HashMapStorageProvider::new(1);
2124 let admin = Address::random();
2125
2126 StorageCtx::enter(&mut storage, || {
2127 let _token = TIP20Setup::create("TestToken", "TEST", admin).apply()?;
2129
2130 let eur_token = TIP20Setup::create("EuroToken", "EUR", admin)
2132 .currency("EUR")
2133 .apply()?;
2134
2135 TIP20Setup::create("USDToken", "USD", admin)
2137 .quote_token(eur_token.address)
2138 .expect_tip20_err(TIP20Error::invalid_quote_token());
2139
2140 Ok(())
2141 })
2142 }
2143
2144 #[test]
2145 fn test_change_transfer_policy_id_invalid_policy() -> eyre::Result<()> {
2146 let mut storage = HashMapStorageProvider::new(1);
2147 let admin = Address::random();
2148
2149 StorageCtx::enter(&mut storage, || {
2150 let mut token = TIP20Setup::path_usd(admin).apply()?;
2151
2152 let mut registry = TIP403Registry::new();
2154 registry.initialize()?;
2155
2156 let invalid_policy_id = 999u64;
2158 let result = token.change_transfer_policy_id(
2159 admin,
2160 ITIP20::changeTransferPolicyIdCall {
2161 newPolicyId: invalid_policy_id,
2162 },
2163 );
2164
2165 assert!(matches!(
2166 result.unwrap_err(),
2167 TempoPrecompileError::TIP20(TIP20Error::InvalidTransferPolicyId(_))
2168 ));
2169
2170 Ok(())
2171 })
2172 }
2173
2174 #[test]
2175 fn test_transfer_invalid_recipient() -> eyre::Result<()> {
2176 let mut storage = HashMapStorageProvider::new(1);
2177 let admin = Address::random();
2178 let bob = Address::random();
2179 let amount = U256::random() % U256::from(u128::MAX);
2180
2181 StorageCtx::enter(&mut storage, || {
2182 let mut token = TIP20Setup::create("Token", "TKN", admin)
2183 .with_issuer(admin)
2184 .with_mint(admin, amount)
2185 .with_approval(admin, bob, amount)
2186 .apply()?;
2187
2188 let result = token.transfer(
2189 admin,
2190 ITIP20::transferCall {
2191 to: Address::ZERO,
2192 amount,
2193 },
2194 );
2195 assert!(result.is_err_and(|err| err.to_string().contains("InvalidRecipient")));
2196
2197 let result = token.transfer_from(
2198 bob,
2199 ITIP20::transferFromCall {
2200 from: admin,
2201 to: Address::ZERO,
2202 amount,
2203 },
2204 );
2205 assert!(result.is_err_and(|err| err.to_string().contains("InvalidRecipient")));
2206
2207 Ok(())
2208 })
2209 }
2210
2211 #[test]
2212 fn test_change_transfer_policy_id() -> eyre::Result<()> {
2213 let mut storage = HashMapStorageProvider::new(1);
2214 let admin = Address::random();
2215
2216 StorageCtx::enter(&mut storage, || {
2217 let mut token = TIP20Setup::path_usd(admin).apply()?;
2218
2219 let mut registry = TIP403Registry::new();
2221 registry.initialize()?;
2222
2223 token.change_transfer_policy_id(
2225 admin,
2226 ITIP20::changeTransferPolicyIdCall { newPolicyId: 0 },
2227 )?;
2228 assert_eq!(token.transfer_policy_id()?, 0);
2229
2230 token.change_transfer_policy_id(
2231 admin,
2232 ITIP20::changeTransferPolicyIdCall { newPolicyId: 1 },
2233 )?;
2234 assert_eq!(token.transfer_policy_id()?, 1);
2235
2236 let mut rng = rand_08::thread_rng();
2238 for _ in 0..20 {
2239 let invalid_policy_id = rng.gen_range(2..u64::MAX);
2240 let result = token.change_transfer_policy_id(
2241 admin,
2242 ITIP20::changeTransferPolicyIdCall {
2243 newPolicyId: invalid_policy_id,
2244 },
2245 );
2246 assert!(matches!(
2247 result.unwrap_err(),
2248 TempoPrecompileError::TIP20(TIP20Error::InvalidTransferPolicyId(_))
2249 ));
2250 }
2251
2252 let mut valid_policy_ids = Vec::new();
2254 for i in 0..10 {
2255 let policy_id = registry.create_policy(
2256 admin,
2257 ITIP403Registry::createPolicyCall {
2258 admin,
2259 policyType: if i % 2 == 0 {
2260 ITIP403Registry::PolicyType::WHITELIST
2261 } else {
2262 ITIP403Registry::PolicyType::BLACKLIST
2263 },
2264 },
2265 )?;
2266 valid_policy_ids.push(policy_id);
2267 }
2268
2269 for policy_id in valid_policy_ids {
2271 let result = token.change_transfer_policy_id(
2272 admin,
2273 ITIP20::changeTransferPolicyIdCall {
2274 newPolicyId: policy_id,
2275 },
2276 );
2277 assert!(result.is_ok());
2278 assert_eq!(token.transfer_policy_id()?, policy_id);
2279 }
2280
2281 Ok(())
2282 })
2283 }
2284
2285 #[test]
2286 fn test_is_transfer_authorized() -> eyre::Result<()> {
2287 use tempo_chainspec::hardfork::TempoHardfork;
2288
2289 let admin = Address::random();
2290 let sender = Address::random();
2291 let recipient = Address::random();
2292
2293 for hardfork in [TempoHardfork::T0, TempoHardfork::T1] {
2294 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
2295
2296 StorageCtx::enter(&mut storage, || {
2297 let token = TIP20Setup::path_usd(admin).apply()?;
2298
2299 let mut registry = TIP403Registry::new();
2301 registry.initialize()?;
2302
2303 let policy_id = registry.create_policy(
2304 admin,
2305 ITIP403Registry::createPolicyCall {
2306 admin,
2307 policyType: ITIP403Registry::PolicyType::WHITELIST,
2308 },
2309 )?;
2310
2311 let mut token = token;
2313 token.change_transfer_policy_id(
2314 admin,
2315 ITIP20::changeTransferPolicyIdCall {
2316 newPolicyId: policy_id,
2317 },
2318 )?;
2319
2320 registry.modify_policy_whitelist(
2322 admin,
2323 ITIP403Registry::modifyPolicyWhitelistCall {
2324 policyId: policy_id,
2325 account: recipient,
2326 allowed: true,
2327 },
2328 )?;
2329 assert!(!token.is_transfer_authorized(sender, recipient)?);
2330
2331 registry.modify_policy_whitelist(
2333 admin,
2334 ITIP403Registry::modifyPolicyWhitelistCall {
2335 policyId: policy_id,
2336 account: sender,
2337 allowed: true,
2338 },
2339 )?;
2340 registry.modify_policy_whitelist(
2341 admin,
2342 ITIP403Registry::modifyPolicyWhitelistCall {
2343 policyId: policy_id,
2344 account: recipient,
2345 allowed: false,
2346 },
2347 )?;
2348 assert!(!token.is_transfer_authorized(sender, recipient)?);
2349
2350 registry.modify_policy_whitelist(
2352 admin,
2353 ITIP403Registry::modifyPolicyWhitelistCall {
2354 policyId: policy_id,
2355 account: recipient,
2356 allowed: true,
2357 },
2358 )?;
2359 assert!(token.is_transfer_authorized(sender, recipient)?);
2360
2361 Ok::<_, TempoPrecompileError>(())
2362 })?;
2363 }
2364
2365 Ok(())
2366 }
2367
2368 #[test]
2369 fn test_set_next_quote_token_rejects_path_usd() -> eyre::Result<()> {
2370 let mut storage = HashMapStorageProvider::new(1);
2371 let admin = Address::random();
2372
2373 StorageCtx::enter(&mut storage, || {
2374 let mut path_usd = TIP20Setup::path_usd(admin).apply()?;
2375 let other_token = TIP20Setup::create("Test", "T", admin).apply()?;
2376
2377 let result = path_usd.set_next_quote_token(
2379 admin,
2380 ITIP20::setNextQuoteTokenCall {
2381 newQuoteToken: other_token.address,
2382 },
2383 );
2384 assert!(matches!(
2385 result,
2386 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
2387 _
2388 )))
2389 ));
2390
2391 Ok(())
2392 })
2393 }
2394
2395 #[test]
2396 fn test_non_path_usd_cycle_detection() -> eyre::Result<()> {
2397 let mut storage = HashMapStorageProvider::new(1);
2398 let admin = Address::random();
2399
2400 StorageCtx::enter(&mut storage, || {
2401 TIP20Setup::path_usd(admin).apply()?;
2402
2403 let mut token_b = TIP20Setup::create("TokenB", "TKNB", admin).apply()?;
2404 let token_a = TIP20Setup::create("TokenA", "TKNA", admin)
2405 .quote_token(token_b.address)
2406 .apply()?;
2407
2408 assert_eq!(token_a.quote_token()?, token_b.address);
2410 assert_eq!(token_b.quote_token()?, PATH_USD_ADDRESS);
2411
2412 token_b.set_next_quote_token(
2414 admin,
2415 ITIP20::setNextQuoteTokenCall {
2416 newQuoteToken: token_a.address,
2417 },
2418 )?;
2419
2420 let result =
2421 token_b.complete_quote_token_update(admin, ITIP20::completeQuoteTokenUpdateCall {});
2422
2423 assert!(matches!(
2424 result,
2425 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
2426 _
2427 )))
2428 ));
2429
2430 assert_eq!(token_a.quote_token()?, token_b.address);
2432 assert_eq!(token_b.quote_token()?, PATH_USD_ADDRESS);
2433
2434 Ok(())
2435 })
2436 }
2437
2438 mod permit_tests {
2443 use super::*;
2444 use alloy::sol_types::SolValue;
2445 use alloy_signer::SignerSync;
2446 use alloy_signer_local::PrivateKeySigner;
2447 use tempo_chainspec::hardfork::TempoHardfork;
2448
2449 const CHAIN_ID: u64 = 42;
2450
2451 fn setup_t2_storage() -> HashMapStorageProvider {
2453 HashMapStorageProvider::new_with_spec(CHAIN_ID, TempoHardfork::T2)
2454 }
2455
2456 fn sign_permit(
2458 signer: &PrivateKeySigner,
2459 token_name: &str,
2460 token_address: Address,
2461 spender: Address,
2462 value: U256,
2463 nonce: U256,
2464 deadline: U256,
2465 ) -> (u8, B256, B256) {
2466 let domain_separator = compute_domain_separator(token_name, token_address);
2467 let struct_hash = keccak256(
2468 (
2469 *PERMIT_TYPEHASH,
2470 signer.address(),
2471 spender,
2472 value,
2473 nonce,
2474 deadline,
2475 )
2476 .abi_encode(),
2477 );
2478 let digest = keccak256(
2479 [
2480 &[0x19, 0x01],
2481 domain_separator.as_slice(),
2482 struct_hash.as_slice(),
2483 ]
2484 .concat(),
2485 );
2486
2487 let sig = signer.sign_hash_sync(&digest).unwrap();
2488 let v = sig.v() as u8 + 27;
2489 let r: B256 = sig.r().into();
2490 let s: B256 = sig.s().into();
2491 (v, r, s)
2492 }
2493
2494 fn compute_domain_separator(token_name: &str, token_address: Address) -> B256 {
2495 keccak256(
2496 (
2497 *EIP712_DOMAIN_TYPEHASH,
2498 keccak256(token_name.as_bytes()),
2499 *VERSION_HASH,
2500 U256::from(CHAIN_ID),
2501 token_address,
2502 )
2503 .abi_encode(),
2504 )
2505 }
2506
2507 struct PermitFixture {
2508 storage: HashMapStorageProvider,
2509 admin: Address,
2510 signer: PrivateKeySigner,
2511 spender: Address,
2512 }
2513
2514 impl PermitFixture {
2515 fn new() -> Self {
2516 Self {
2517 storage: setup_t2_storage(),
2518 admin: Address::random(),
2519 signer: PrivateKeySigner::random(),
2520 spender: Address::random(),
2521 }
2522 }
2523 }
2524
2525 fn make_permit_call(
2526 signer: &PrivateKeySigner,
2527 spender: Address,
2528 token_address: Address,
2529 value: U256,
2530 nonce: U256,
2531 deadline: U256,
2532 ) -> ITIP20::permitCall {
2533 let (v, r, s) = sign_permit(
2534 signer,
2535 "Test",
2536 token_address,
2537 spender,
2538 value,
2539 nonce,
2540 deadline,
2541 );
2542 ITIP20::permitCall {
2543 owner: signer.address(),
2544 spender,
2545 value,
2546 deadline,
2547 v,
2548 r,
2549 s,
2550 }
2551 }
2552
2553 #[test]
2554 fn test_permit_happy_path() -> eyre::Result<()> {
2555 let PermitFixture {
2556 mut storage,
2557 admin,
2558 ref signer,
2559 spender,
2560 } = PermitFixture::new();
2561 let owner = signer.address();
2562 let value = U256::from(1000);
2563
2564 StorageCtx::enter(&mut storage, || {
2565 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2566 let call =
2567 make_permit_call(signer, spender, token.address, value, U256::ZERO, U256::MAX);
2568 token.permit(call)?;
2569
2570 let allowance = token.allowance(ITIP20::allowanceCall { owner, spender })?;
2572 assert_eq!(allowance, value);
2573
2574 let nonce = token.nonces(ITIP20::noncesCall { owner })?;
2576 assert_eq!(nonce, U256::from(1));
2577
2578 Ok(())
2579 })
2580 }
2581
2582 #[test]
2583 fn test_permit_expired() -> eyre::Result<()> {
2584 let PermitFixture {
2585 mut storage,
2586 admin,
2587 ref signer,
2588 spender,
2589 } = PermitFixture::new();
2590 let value = U256::from(1000);
2591 let deadline = U256::ZERO;
2593
2594 StorageCtx::enter(&mut storage, || {
2595 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2596 let call =
2597 make_permit_call(signer, spender, token.address, value, U256::ZERO, deadline);
2598
2599 let result = token.permit(call);
2600
2601 assert!(matches!(
2602 result,
2603 Err(TempoPrecompileError::TIP20(TIP20Error::PermitExpired(_)))
2604 ));
2605
2606 Ok(())
2607 })
2608 }
2609
2610 #[test]
2611 fn test_permit_invalid_signature() -> eyre::Result<()> {
2612 let mut storage = setup_t2_storage();
2613 let admin = Address::random();
2614 let owner = Address::random();
2615 let spender = Address::random();
2616 let value = U256::from(1000);
2617 let deadline = U256::MAX;
2618
2619 StorageCtx::enter(&mut storage, || {
2620 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2621
2622 let result = token.permit(ITIP20::permitCall {
2624 owner,
2625 spender,
2626 value,
2627 deadline,
2628 v: 27,
2629 r: B256::ZERO,
2630 s: B256::ZERO,
2631 });
2632
2633 assert!(matches!(
2634 result,
2635 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
2636 ));
2637
2638 Ok(())
2639 })
2640 }
2641
2642 #[test]
2643 fn test_permit_wrong_signer() -> eyre::Result<()> {
2644 let PermitFixture {
2645 mut storage,
2646 admin,
2647 ref signer,
2648 spender,
2649 } = PermitFixture::new();
2650 let wrong_owner = Address::random(); let value = U256::from(1000);
2652 let deadline = U256::MAX;
2653
2654 StorageCtx::enter(&mut storage, || {
2655 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2656
2657 let (v, r, s) = sign_permit(
2659 signer,
2660 "Test",
2661 token.address,
2662 spender,
2663 value,
2664 U256::ZERO,
2665 deadline,
2666 );
2667
2668 let result = token.permit(ITIP20::permitCall {
2669 owner: wrong_owner, spender,
2671 value,
2672 deadline,
2673 v,
2674 r,
2675 s,
2676 });
2677
2678 assert!(matches!(
2679 result,
2680 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
2681 ));
2682
2683 Ok(())
2684 })
2685 }
2686
2687 #[test]
2688 fn test_permit_replay_protection() -> eyre::Result<()> {
2689 let PermitFixture {
2690 mut storage,
2691 admin,
2692 ref signer,
2693 spender,
2694 } = PermitFixture::new();
2695 let value = U256::from(1000);
2696
2697 StorageCtx::enter(&mut storage, || {
2698 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2699 let call =
2700 make_permit_call(signer, spender, token.address, value, U256::ZERO, U256::MAX);
2701
2702 token.permit(call.clone())?;
2704
2705 let result = token.permit(call);
2707
2708 assert!(matches!(
2709 result,
2710 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
2711 ));
2712
2713 Ok(())
2714 })
2715 }
2716
2717 #[test]
2718 fn test_permit_nonce_tracking() -> eyre::Result<()> {
2719 let PermitFixture {
2720 mut storage,
2721 admin,
2722 ref signer,
2723 spender,
2724 } = PermitFixture::new();
2725 let owner = signer.address();
2726
2727 StorageCtx::enter(&mut storage, || {
2728 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2729
2730 assert_eq!(token.nonces(ITIP20::noncesCall { owner })?, U256::ZERO);
2732
2733 for i in 0u64..3 {
2735 let nonce = U256::from(i);
2736 let value = U256::from(100 * (i + 1));
2737 let call =
2738 make_permit_call(signer, spender, token.address, value, nonce, U256::MAX);
2739 token.permit(call)?;
2740
2741 assert_eq!(
2742 token.nonces(ITIP20::noncesCall { owner })?,
2743 U256::from(i + 1)
2744 );
2745 }
2746
2747 Ok(())
2748 })
2749 }
2750
2751 #[test]
2752 fn test_permit_works_when_paused() -> eyre::Result<()> {
2753 let PermitFixture {
2754 mut storage,
2755 admin,
2756 ref signer,
2757 spender,
2758 } = PermitFixture::new();
2759 let owner = signer.address();
2760 let value = U256::from(1000);
2761
2762 StorageCtx::enter(&mut storage, || {
2763 let mut token = TIP20Setup::create("Test", "TST", admin)
2764 .with_role(admin, *PAUSE_ROLE)
2765 .apply()?;
2766
2767 token.pause(admin, ITIP20::pauseCall {})?;
2769 assert!(token.paused()?);
2770
2771 let call =
2772 make_permit_call(signer, spender, token.address, value, U256::ZERO, U256::MAX);
2773
2774 token.permit(call)?;
2776
2777 assert_eq!(
2778 token.allowance(ITIP20::allowanceCall { owner, spender })?,
2779 value
2780 );
2781
2782 Ok(())
2783 })
2784 }
2785
2786 #[test]
2787 fn test_permit_domain_separator() -> eyre::Result<()> {
2788 let PermitFixture {
2789 mut storage, admin, ..
2790 } = PermitFixture::new();
2791
2792 StorageCtx::enter(&mut storage, || {
2793 let token = TIP20Setup::create("Test", "TST", admin).apply()?;
2794
2795 let ds = token.domain_separator()?;
2796 let expected = compute_domain_separator("Test", token.address);
2797 assert_eq!(ds, expected);
2798
2799 Ok(())
2800 })
2801 }
2802
2803 #[test]
2804 fn test_permit_max_allowance() -> eyre::Result<()> {
2805 let PermitFixture {
2806 mut storage,
2807 admin,
2808 ref signer,
2809 spender,
2810 } = PermitFixture::new();
2811 let owner = signer.address();
2812
2813 StorageCtx::enter(&mut storage, || {
2814 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2815 let call = make_permit_call(
2816 signer,
2817 spender,
2818 token.address,
2819 U256::MAX,
2820 U256::ZERO,
2821 U256::MAX,
2822 );
2823 token.permit(call)?;
2824
2825 assert_eq!(
2826 token.allowance(ITIP20::allowanceCall { owner, spender })?,
2827 U256::MAX
2828 );
2829
2830 Ok(())
2831 })
2832 }
2833
2834 #[test]
2835 fn test_permit_allowance_override() -> eyre::Result<()> {
2836 let PermitFixture {
2837 mut storage,
2838 admin,
2839 ref signer,
2840 spender,
2841 } = PermitFixture::new();
2842 let owner = signer.address();
2843
2844 StorageCtx::enter(&mut storage, || {
2845 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2846
2847 let call = make_permit_call(
2849 signer,
2850 spender,
2851 token.address,
2852 U256::from(1000),
2853 U256::ZERO,
2854 U256::MAX,
2855 );
2856 token.permit(call)?;
2857 assert_eq!(
2858 token.allowance(ITIP20::allowanceCall { owner, spender })?,
2859 U256::from(1000)
2860 );
2861
2862 let call = make_permit_call(
2864 signer,
2865 spender,
2866 token.address,
2867 U256::ZERO,
2868 U256::from(1),
2869 U256::MAX,
2870 );
2871 token.permit(call)?;
2872 assert_eq!(
2873 token.allowance(ITIP20::allowanceCall { owner, spender })?,
2874 U256::ZERO
2875 );
2876
2877 Ok(())
2878 })
2879 }
2880
2881 #[test]
2882 fn test_permit_invalid_v_values() -> eyre::Result<()> {
2883 let PermitFixture {
2884 mut storage,
2885 admin,
2886 spender,
2887 ..
2888 } = PermitFixture::new();
2889
2890 StorageCtx::enter(&mut storage, || {
2891 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2892
2893 for v in [0u8, 1] {
2894 let result = token.permit(ITIP20::permitCall {
2895 owner: admin,
2896 spender,
2897 value: U256::from(1000),
2898 deadline: U256::MAX,
2899 v,
2900 r: B256::ZERO,
2901 s: B256::ZERO,
2902 });
2903
2904 assert!(
2905 matches!(
2906 result,
2907 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
2908 ),
2909 "v={v} should revert with InvalidSignature"
2910 );
2911 }
2912
2913 Ok(())
2914 })
2915 }
2916
2917 #[test]
2918 fn test_permit_zero_address_recovery_reverts() -> eyre::Result<()> {
2919 let PermitFixture {
2920 mut storage,
2921 admin,
2922 spender,
2923 ..
2924 } = PermitFixture::new();
2925
2926 StorageCtx::enter(&mut storage, || {
2927 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
2928
2929 let result = token.permit(ITIP20::permitCall {
2930 owner: Address::ZERO,
2931 spender,
2932 value: U256::from(1000),
2933 deadline: U256::MAX,
2934 v: 27,
2935 r: B256::ZERO,
2936 s: B256::ZERO,
2937 });
2938
2939 assert!(matches!(
2940 result,
2941 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
2942 ));
2943
2944 Ok(())
2945 })
2946 }
2947
2948 #[test]
2949 fn test_permit_domain_separator_changes_with_chain_id() -> eyre::Result<()> {
2950 let PermitFixture { admin, .. } = PermitFixture::new();
2951
2952 let mut storage_a = setup_t2_storage();
2953 let mut storage_b =
2954 HashMapStorageProvider::new_with_spec(CHAIN_ID + 1, TempoHardfork::T2);
2955
2956 let ds_a = StorageCtx::enter(&mut storage_a, || {
2957 TIP20Setup::create("Test", "TST", admin)
2958 .apply()?
2959 .domain_separator()
2960 })?;
2961
2962 let ds_b = StorageCtx::enter(&mut storage_b, || {
2963 TIP20Setup::create("Test", "TST", admin)
2964 .apply()?
2965 .domain_separator()
2966 })?;
2967
2968 assert_ne!(
2969 ds_a, ds_b,
2970 "domain separator must change when chainId changes"
2971 );
2972
2973 Ok(())
2974 }
2975 }
2976}