1use crate::{
10 error::{Result, TempoPrecompileError},
11 storage::Handler,
12 tip20::{Recipient, TIP20Token},
13};
14use alloy::primitives::{Address, U256, uint};
15use tempo_contracts::precompiles::{ITIP20, TIP20Error, TIP20Event};
16use tempo_precompiles_macros::Storable;
17use tempo_primitives::TempoAddressExt;
18
19pub const ACC_PRECISION: U256 = uint!(1000000000000000000_U256);
21
22impl TIP20Token {
23 pub fn distribute_reward(
35 &mut self,
36 msg_sender: Address,
37 call: ITIP20::distributeRewardCall,
38 ) -> Result<()> {
39 if self.storage.spec().is_t7() {
40 return Ok(());
41 }
42
43 self.check_not_paused()?;
44 let token_address = self.address;
45
46 if call.amount == U256::ZERO {
47 return Err(TIP20Error::invalid_amount().into());
48 }
49
50 self.ensure_transfer_authorized(msg_sender, token_address)?;
51 self.check_and_update_spending_limit(msg_sender, call.amount)?;
52
53 self._transfer(msg_sender, &Recipient::direct(token_address), call.amount)?;
54
55 let opted_in_supply = U256::from(self.get_opted_in_supply()?);
56 if opted_in_supply.is_zero() {
57 return Err(TIP20Error::no_opted_in_supply().into());
58 }
59
60 let delta_rpt = call
61 .amount
62 .checked_mul(ACC_PRECISION)
63 .and_then(|v| v.checked_div(opted_in_supply))
64 .ok_or(TempoPrecompileError::under_overflow())?;
65 let current_rpt = self.get_global_reward_per_token()?;
66 let new_rpt = current_rpt
67 .checked_add(delta_rpt)
68 .ok_or(TempoPrecompileError::under_overflow())?;
69 self.set_global_reward_per_token(new_rpt)?;
70
71 self.emit_event(TIP20Event::reward_distributed(msg_sender, call.amount))?;
73
74 Ok(())
75 }
76
77 pub fn update_rewards(&mut self, holder: Address) -> Result<Address> {
85 if self.storage.spec().is_t8() {
87 return Ok(Address::ZERO);
88 }
89
90 let mut info = self.user_reward_info[holder].read()?;
91
92 let cached_delegate = info.reward_recipient;
93
94 let global_reward_per_token = self.get_global_reward_per_token()?;
95 let reward_per_token_delta = global_reward_per_token
96 .checked_sub(info.reward_per_token)
97 .ok_or(TempoPrecompileError::under_overflow())?;
98
99 if reward_per_token_delta != U256::ZERO {
100 if cached_delegate != Address::ZERO {
101 let holder_balance = self.get_balance(holder)?;
102 let reward = holder_balance
103 .checked_mul(reward_per_token_delta)
104 .and_then(|v| v.checked_div(ACC_PRECISION))
105 .ok_or(TempoPrecompileError::under_overflow())?;
106
107 if cached_delegate == holder {
109 info.reward_balance = info
110 .reward_balance
111 .checked_add(reward)
112 .ok_or(TempoPrecompileError::under_overflow())?;
113 } else {
114 let mut delegate_info = self.user_reward_info[cached_delegate].read()?;
115 delegate_info.reward_balance = delegate_info
116 .reward_balance
117 .checked_add(reward)
118 .ok_or(TempoPrecompileError::under_overflow())?;
119 self.user_reward_info[cached_delegate].write(delegate_info)?;
120 }
121 }
122 info.reward_per_token = global_reward_per_token;
123 self.user_reward_info[holder].write(info)?;
124 }
125
126 Ok(cached_delegate)
127 }
128
129 pub fn set_reward_recipient(
139 &mut self,
140 msg_sender: Address,
141 call: ITIP20::setRewardRecipientCall,
142 ) -> Result<()> {
143 if self.storage.spec().is_t7() {
144 return Ok(());
145 }
146
147 self.check_not_paused()?;
148
149 if self.storage.spec().is_t3() && call.recipient.is_virtual() {
151 return Err(TIP20Error::invalid_recipient().into());
152 }
153
154 if call.recipient != Address::ZERO {
155 self.ensure_transfer_authorized(msg_sender, call.recipient)?;
156 }
157
158 let from_delegate = self.update_rewards(msg_sender)?;
159
160 let holder_balance = self.get_balance(msg_sender)?;
161
162 if from_delegate != Address::ZERO {
163 if call.recipient == Address::ZERO {
164 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
165 .checked_sub(holder_balance)
166 .ok_or(TempoPrecompileError::under_overflow())?;
167 self.set_opted_in_supply(
168 opted_in_supply
169 .try_into()
170 .map_err(|_| TempoPrecompileError::under_overflow())?,
171 )?;
172 }
173 } else if call.recipient != Address::ZERO {
174 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
175 .checked_add(holder_balance)
176 .ok_or(TempoPrecompileError::under_overflow())?;
177 self.set_opted_in_supply(
178 opted_in_supply
179 .try_into()
180 .map_err(|_| TempoPrecompileError::under_overflow())?,
181 )?;
182 }
183
184 let mut info = self.user_reward_info[msg_sender].read()?;
185 info.reward_recipient = call.recipient;
186 self.user_reward_info[msg_sender].write(info)?;
187
188 self.emit_event(TIP20Event::reward_recipient_set(msg_sender, call.recipient))?;
190
191 Ok(())
192 }
193
194 pub fn claim_rewards(&mut self, msg_sender: Address) -> Result<U256> {
203 self.check_not_paused()?;
204 self.ensure_transfer_authorized(self.address, msg_sender)?;
205
206 let reward_recipient = self.update_rewards(msg_sender)?;
208
209 let mut info = self.user_reward_info[msg_sender].read()?;
210 let amount = info.reward_balance;
211 let contract_address = self.address;
212 let contract_balance = self.get_balance(contract_address)?;
213 let max_amount = amount.min(contract_balance);
214
215 info.reward_balance = amount
216 .checked_sub(max_amount)
217 .ok_or(TempoPrecompileError::under_overflow())?;
218 self.user_reward_info[msg_sender].write(info)?;
219
220 if max_amount > U256::ZERO {
221 let new_contract_balance = contract_balance
222 .checked_sub(max_amount)
223 .ok_or(TempoPrecompileError::under_overflow())?;
224 self.set_balance(contract_address, new_contract_balance)?;
225
226 let recipient_balance = self
227 .get_balance(msg_sender)?
228 .checked_add(max_amount)
229 .ok_or(TempoPrecompileError::under_overflow())?;
230 self.set_balance(msg_sender, recipient_balance)?;
231
232 if reward_recipient != Address::ZERO {
233 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
234 .checked_add(max_amount)
235 .ok_or(TempoPrecompileError::under_overflow())?;
236 self.set_opted_in_supply(
237 opted_in_supply
238 .try_into()
239 .map_err(|_| TempoPrecompileError::under_overflow())?,
240 )?;
241 }
242
243 self.emit_event(TIP20Event::transfer(
244 contract_address,
245 msg_sender,
246 max_amount,
247 ))?;
248 }
249
250 Ok(max_amount)
251 }
252
253 pub fn get_global_reward_per_token(&self) -> Result<U256> {
255 self.global_reward_per_token.read()
256 }
257
258 fn set_global_reward_per_token(&mut self, value: U256) -> Result<()> {
260 self.global_reward_per_token.write(value)
261 }
262
263 pub fn get_opted_in_supply(&self) -> Result<u128> {
265 self.opted_in_supply.read()
266 }
267
268 pub fn set_opted_in_supply(&mut self, value: u128) -> Result<()> {
270 self.opted_in_supply.write(value)
271 }
272
273 pub fn handle_rewards_on_transfer(
275 &mut self,
276 from: Address,
277 to: Address,
278 amount: U256,
279 ) -> Result<()> {
280 let from_delegate = self.update_rewards(from)?;
281 let to_delegate = self.update_rewards(to)?;
282
283 if !from_delegate.is_zero() {
284 if to_delegate.is_zero() {
285 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
286 .checked_sub(amount)
287 .ok_or(TempoPrecompileError::under_overflow())?;
288 self.set_opted_in_supply(
289 opted_in_supply
290 .try_into()
291 .map_err(|_| TempoPrecompileError::under_overflow())?,
292 )?;
293 }
294 } else if !to_delegate.is_zero() {
295 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
296 .checked_add(amount)
297 .ok_or(TempoPrecompileError::under_overflow())?;
298 self.set_opted_in_supply(
299 opted_in_supply
300 .try_into()
301 .map_err(|_| TempoPrecompileError::under_overflow())?,
302 )?;
303 }
304
305 Ok(())
306 }
307
308 pub fn handle_rewards_on_mint(&mut self, to: Address, amount: U256) -> Result<()> {
310 let to_delegate = self.update_rewards(to)?;
311
312 if !to_delegate.is_zero() {
313 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
314 .checked_add(amount)
315 .ok_or(TempoPrecompileError::under_overflow())?;
316 self.set_opted_in_supply(
317 opted_in_supply
318 .try_into()
319 .map_err(|_| TempoPrecompileError::under_overflow())?,
320 )?;
321 }
322
323 Ok(())
324 }
325
326 pub fn get_user_reward_info(&self, account: Address) -> Result<UserRewardInfo> {
328 self.user_reward_info[account].read()
329 }
330
331 pub fn get_pending_rewards(&self, account: Address) -> Result<u128> {
340 let info = self.user_reward_info[account].read()?;
341
342 let mut pending = info.reward_balance;
344
345 if self.storage.spec().is_t8() {
347 return pending
348 .try_into()
349 .map_err(|_| TempoPrecompileError::under_overflow());
350 }
351
352 if info.reward_recipient == account {
354 let holder_balance = self.get_balance(account)?;
355 if holder_balance > U256::ZERO {
356 let global_reward_per_token = self.get_global_reward_per_token()?;
357 let reward_per_token_delta = global_reward_per_token
358 .checked_sub(info.reward_per_token)
359 .ok_or(TempoPrecompileError::under_overflow())?;
360
361 if reward_per_token_delta > U256::ZERO {
362 let accrued = holder_balance
363 .checked_mul(reward_per_token_delta)
364 .and_then(|v| v.checked_div(ACC_PRECISION))
365 .ok_or(TempoPrecompileError::under_overflow())?;
366 pending = pending
367 .checked_add(accrued)
368 .ok_or(TempoPrecompileError::under_overflow())?;
369 }
370 }
371 }
372
373 pending
374 .try_into()
375 .map_err(|_| TempoPrecompileError::under_overflow())
376 }
377}
378
379#[derive(Debug, Clone, Storable)]
381pub struct UserRewardInfo {
382 pub reward_recipient: Address,
384 pub reward_per_token: U256,
386 pub reward_balance: U256,
388}
389
390impl From<UserRewardInfo> for ITIP20::UserRewardInfo {
391 fn from(value: UserRewardInfo) -> Self {
392 Self {
393 rewardRecipient: value.reward_recipient,
394 rewardPerToken: value.reward_per_token,
395 rewardBalance: value.reward_balance,
396 }
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403 use crate::{
404 address_registry::{MasterId, UserTag},
405 error::TempoPrecompileError,
406 storage::{StorageCtx, hashmap::HashMapStorageProvider},
407 test_util::TIP20Setup,
408 tip403_registry::TIP403Registry,
409 };
410 use alloy::primitives::{Address, U256};
411 use tempo_chainspec::hardfork::TempoHardfork;
412 use tempo_contracts::precompiles::{ITIP403Registry, TIP20Error};
413
414 #[test]
415 fn test_set_reward_recipient() -> eyre::Result<()> {
416 let mut storage = HashMapStorageProvider::new(1);
417 let admin = Address::random();
418 let alice = Address::random();
419 let amount = U256::random() % U256::from(u128::MAX);
420
421 StorageCtx::enter(&mut storage, || {
422 let mut token = TIP20Setup::create("Test", "TST", admin)
423 .with_issuer(admin)
424 .with_mint(alice, amount)
425 .apply()?;
426
427 token
428 .set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
429
430 let info = token.user_reward_info[alice].read()?;
431 assert_eq!(info.reward_recipient, alice);
432 assert_eq!(token.get_opted_in_supply()?, amount.to::<u128>());
433 assert_eq!(info.reward_per_token, U256::ZERO);
434
435 token.set_reward_recipient(
436 alice,
437 ITIP20::setRewardRecipientCall {
438 recipient: Address::ZERO,
439 },
440 )?;
441
442 let info = token.user_reward_info[alice].read()?;
443 assert_eq!(info.reward_recipient, Address::ZERO);
444 assert_eq!(token.get_opted_in_supply()?, 0u128);
445 assert_eq!(info.reward_per_token, U256::ZERO);
446
447 Ok(())
448 })
449 }
450
451 #[test]
452 fn test_t7_zero_rpt_fast_path_preserves_opt_in_state() -> eyre::Result<()> {
453 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
454 let admin = Address::random();
455 let alice = Address::random();
456 let bob = Address::random();
457 let alice_balance = U256::from(1000);
458 let transfer_amount = U256::from(100);
459 let mint_amount = U256::from(100);
460 let reward_amount = U256::from(110);
461
462 StorageCtx::enter(&mut storage, || {
463 let mut token = TIP20Setup::create("Test", "TST", admin)
464 .with_issuer(admin)
465 .with_mint(alice, alice_balance)
466 .with_mint(admin, reward_amount)
467 .apply()?;
468
469 token
470 .set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
471 token.set_reward_recipient(bob, ITIP20::setRewardRecipientCall { recipient: bob })?;
472 assert_eq!(token.update_rewards(alice)?, alice);
473 assert_eq!(token.get_opted_in_supply()?, alice_balance.to::<u128>());
474
475 StorageCtx.set_spec(TempoHardfork::T7);
476
477 token.set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: bob })?;
480 assert_eq!(token.get_user_reward_info(alice)?.reward_recipient, alice);
481 assert_eq!(token.update_rewards(alice)?, alice);
482 assert_eq!(token.get_opted_in_supply()?, alice_balance.to::<u128>());
483
484 token.transfer(
487 alice,
488 ITIP20::transferCall {
489 to: bob,
490 amount: transfer_amount,
491 },
492 )?;
493 assert_eq!(token.get_opted_in_supply()?, alice_balance.to::<u128>());
494
495 token.mint(
496 admin,
497 ITIP20::mintCall {
498 to: alice,
499 amount: mint_amount,
500 },
501 )?;
502 let opted_in_supply = alice_balance + mint_amount;
503 assert_eq!(token.get_opted_in_supply()?, opted_in_supply.to::<u128>());
504
505 token.distribute_reward(admin, ITIP20::distributeRewardCall { amount: U256::ZERO })?;
506 token.distribute_reward(
507 admin,
508 ITIP20::distributeRewardCall {
509 amount: reward_amount,
510 },
511 )?;
512 assert_eq!(token.get_balance(admin)?, reward_amount);
513 assert_eq!(token.get_balance(token.address)?, U256::ZERO);
514 assert_eq!(token.get_global_reward_per_token()?, U256::ZERO);
515 assert_eq!(token.get_pending_rewards(alice)?, 0);
516 assert_eq!(token.get_pending_rewards(bob)?, 0);
517
518 Ok(())
519 })
520 }
521
522 #[test]
523 fn test_distribute_reward() -> eyre::Result<()> {
524 let mut storage = HashMapStorageProvider::new(1);
525 let admin = Address::random();
526 let alice = Address::random();
527 let amount = U256::from(1000);
528 let reward_amount = amount / U256::from(10);
529
530 StorageCtx::enter(&mut storage, || {
531 let mut token = TIP20Setup::create("Test", "TST", admin)
532 .with_issuer(admin)
533 .with_mint(alice, amount)
534 .with_mint(admin, reward_amount)
535 .apply()?;
536
537 token
538 .set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
539
540 token.distribute_reward(
542 admin,
543 ITIP20::distributeRewardCall {
544 amount: reward_amount,
545 },
546 )?;
547
548 let expected_rpt = reward_amount * ACC_PRECISION / amount;
550 assert_eq!(token.get_global_reward_per_token()?, expected_rpt);
551
552 assert_eq!(token.get_balance(token.address)?, reward_amount);
554 assert_eq!(token.get_balance(admin)?, U256::ZERO);
555
556 token.update_rewards(alice)?;
558 let info = token.get_user_reward_info(alice)?;
559 assert_eq!(info.reward_balance, reward_amount);
560
561 let claimed = token.claim_rewards(alice)?;
563 assert_eq!(claimed, reward_amount);
564 assert_eq!(token.get_balance(alice)?, amount + reward_amount);
565 assert_eq!(token.get_balance(token.address)?, U256::ZERO);
566
567 token.mint(
569 admin,
570 ITIP20::mintCall {
571 to: admin,
572 amount: U256::from(1),
573 },
574 )?;
575 let result =
576 token.distribute_reward(admin, ITIP20::distributeRewardCall { amount: U256::ZERO });
577 assert!(result.is_err());
578
579 Ok(())
580 })
581 }
582
583 #[test]
584 fn test_tip1075_t7_noops_and_t8_claims_settled_rewards() -> eyre::Result<()> {
585 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
586 let admin = Address::random();
587 let alice = Address::random();
588 let bob = Address::random();
589 let amount = U256::from(1000);
590 let reward_amount = U256::from(100);
591
592 StorageCtx::enter(&mut storage, || {
593 let mut token = TIP20Setup::create("Test", "TST", admin)
594 .with_issuer(admin)
595 .with_mint(alice, amount)
596 .with_mint(admin, reward_amount * U256::from(2))
597 .apply()?;
598
599 token
601 .set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
602 token.distribute_reward(
603 admin,
604 ITIP20::distributeRewardCall {
605 amount: reward_amount,
606 },
607 )?;
608 token.update_rewards(alice)?;
609 token.distribute_reward(
610 admin,
611 ITIP20::distributeRewardCall {
612 amount: reward_amount,
613 },
614 )?;
615 assert_eq!(token.get_opted_in_supply()?, amount.to::<u128>());
616
617 StorageCtx.set_spec(TempoHardfork::T7);
618 token.paused.write(true)?;
619
620 token.set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: bob })?;
622 assert_eq!(
623 token.user_reward_info[alice].read()?.reward_recipient,
624 alice
625 );
626
627 let rpt = token.get_global_reward_per_token()?;
629 token.distribute_reward(admin, ITIP20::distributeRewardCall { amount: U256::ZERO })?;
630 assert_eq!(token.get_global_reward_per_token()?, rpt);
631
632 StorageCtx.set_spec(TempoHardfork::T8);
634 token.paused.write(false)?;
635 let claimed = token.claim_rewards(alice)?;
636 assert_eq!(claimed, reward_amount);
637 assert_eq!(token.get_balance(alice)?, amount + reward_amount);
638 assert_eq!(token.get_opted_in_supply()?, amount.to::<u128>());
639 assert_eq!(token.get_global_reward_per_token()?, rpt);
640
641 Ok(())
642 })
643 }
644
645 #[test]
646 fn test_t8_get_pending_rewards_returns_only_stored_balance() -> eyre::Result<()> {
647 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T8);
648 let admin = Address::random();
649 let alice = Address::random();
650 let alice_balance = U256::from(1000);
651 let stored_reward = U256::from(7);
652
653 StorageCtx::enter(&mut storage, || {
654 let mut token = TIP20Setup::create("Test", "TST", admin)
655 .with_issuer(admin)
656 .with_mint(alice, alice_balance)
657 .apply()?;
658
659 token.set_global_reward_per_token(U256::from(100) * ACC_PRECISION)?;
660 token.user_reward_info[alice].write(UserRewardInfo {
661 reward_recipient: alice,
662 reward_per_token: U256::ZERO,
663 reward_balance: stored_reward,
664 })?;
665
666 assert_eq!(U256::from(token.get_pending_rewards(alice)?), stored_reward);
667
668 Ok(())
669 })
670 }
671
672 #[test]
673 fn test_get_pending_rewards() -> eyre::Result<()> {
674 let mut storage = HashMapStorageProvider::new(1);
675 let admin = Address::random();
676 let alice = Address::random();
677
678 StorageCtx::enter(&mut storage, || {
679 let alice_balance = U256::from(1000e18);
680 let reward_amount = U256::from(100e18);
681
682 let mut token = TIP20Setup::create("Test", "TST", admin)
683 .with_issuer(admin)
684 .with_mint(alice, alice_balance)
685 .with_mint(admin, reward_amount)
686 .apply()?;
687
688 token
689 .set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
690
691 let pending_before = token.get_pending_rewards(alice)?;
693 assert_eq!(pending_before, 0u128);
694
695 token.distribute_reward(
697 admin,
698 ITIP20::distributeRewardCall {
699 amount: reward_amount,
700 },
701 )?;
702
703 let pending_after = token.get_pending_rewards(alice)?;
705 assert_eq!(U256::from(pending_after), reward_amount);
706
707 let user_info = token.get_user_reward_info(alice)?;
709 assert_eq!(
710 user_info.reward_balance,
711 U256::ZERO,
712 "get_pending_rewards should not modify state"
713 );
714
715 Ok(())
716 })
717 }
718
719 #[test]
720 fn test_get_pending_rewards_includes_stored_balance() -> eyre::Result<()> {
721 let mut storage = HashMapStorageProvider::new(1);
722 let admin = Address::random();
723 let alice = Address::random();
724
725 StorageCtx::enter(&mut storage, || {
726 let alice_balance = U256::from(1000e18);
727 let reward_amount = U256::from(50e18);
728
729 let mut token = TIP20Setup::create("Test", "TST", admin)
730 .with_issuer(admin)
731 .with_mint(alice, alice_balance)
732 .with_mint(admin, reward_amount * U256::from(2))
733 .apply()?;
734
735 token
736 .set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
737
738 token.distribute_reward(
740 admin,
741 ITIP20::distributeRewardCall {
742 amount: reward_amount,
743 },
744 )?;
745
746 token.update_rewards(alice)?;
748 let user_info = token.get_user_reward_info(alice)?;
749 assert_eq!(user_info.reward_balance, reward_amount);
750
751 token.distribute_reward(
753 admin,
754 ITIP20::distributeRewardCall {
755 amount: reward_amount,
756 },
757 )?;
758
759 let pending = token.get_pending_rewards(alice)?;
761 assert_eq!(U256::from(pending), reward_amount * U256::from(2));
762
763 Ok(())
764 })
765 }
766
767 #[test]
768 fn test_get_pending_rewards_with_delegation() -> eyre::Result<()> {
769 let mut storage = HashMapStorageProvider::new(1);
770 let admin = Address::random();
771 let alice = Address::random();
772 let bob = Address::random();
773
774 StorageCtx::enter(&mut storage, || {
775 let alice_balance = U256::from(1000e18);
776 let reward_amount = U256::from(100e18);
777
778 let mut token = TIP20Setup::create("Test", "TST", admin)
779 .with_issuer(admin)
780 .with_mint(alice, alice_balance)
781 .with_mint(admin, reward_amount)
782 .apply()?;
783
784 token.set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: bob })?;
786
787 token.distribute_reward(
789 admin,
790 ITIP20::distributeRewardCall {
791 amount: reward_amount,
792 },
793 )?;
794
795 let alice_pending = token.get_pending_rewards(alice)?;
797 assert_eq!(alice_pending, 0u128);
798
799 let bob_pending_before_update = token.get_pending_rewards(bob)?;
803 assert_eq!(bob_pending_before_update, 0u128);
804
805 token.update_rewards(alice)?;
807 let bob_pending_after_update = token.get_pending_rewards(bob)?;
808 assert_eq!(U256::from(bob_pending_after_update), reward_amount);
809
810 Ok(())
811 })
812 }
813
814 #[test]
815 fn test_get_pending_rewards_not_opted_in() -> eyre::Result<()> {
816 let mut storage = HashMapStorageProvider::new(1);
817 let admin = Address::random();
818 let alice = Address::random();
819 let bob = Address::random();
820
821 StorageCtx::enter(&mut storage, || {
822 let balance = U256::from(1000e18);
823 let reward_amount = U256::from(100e18);
824
825 let mut token = TIP20Setup::create("Test", "TST", admin)
826 .with_issuer(admin)
827 .with_mint(alice, balance)
828 .with_mint(bob, balance)
829 .with_mint(admin, reward_amount)
830 .apply()?;
831
832 token
834 .set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
835
836 token.distribute_reward(
838 admin,
839 ITIP20::distributeRewardCall {
840 amount: reward_amount,
841 },
842 )?;
843
844 let alice_pending = token.get_pending_rewards(alice)?;
846 assert_eq!(U256::from(alice_pending), reward_amount);
847
848 let bob_pending = token.get_pending_rewards(bob)?;
850 assert_eq!(bob_pending, 0u128);
851
852 Ok(())
853 })
854 }
855
856 #[test]
857 fn test_claim_rewards_unauthorized() -> eyre::Result<()> {
858 let mut storage = HashMapStorageProvider::new(1);
859 let admin = Address::random();
860 let alice = Address::random();
861
862 StorageCtx::enter(&mut storage, || {
863 let mut registry = TIP403Registry::new();
864 registry.initialize()?;
865
866 let policy_id = registry.create_policy(
867 admin,
868 ITIP403Registry::createPolicyCall {
869 admin,
870 policyType: ITIP403Registry::PolicyType::BLACKLIST,
871 },
872 )?;
873
874 registry.modify_policy_blacklist(
875 admin,
876 ITIP403Registry::modifyPolicyBlacklistCall {
877 policyId: policy_id,
878 account: alice,
879 restricted: true,
880 },
881 )?;
882
883 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
884
885 token.change_transfer_policy_id(
886 admin,
887 ITIP20::changeTransferPolicyIdCall {
888 newPolicyId: policy_id,
889 },
890 )?;
891
892 let err = token.claim_rewards(alice).unwrap_err();
893 assert!(
894 matches!(
895 err,
896 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
897 ),
898 "Expected PolicyForbids error, got: {err:?}"
899 );
900
901 Ok(())
902 })
903 }
904
905 #[test]
906 fn test_set_reward_recipient_rejects_virtual_on_t3() -> eyre::Result<()> {
907 let virtual_addr = Address::new_virtual(MasterId::ZERO, UserTag::ZERO);
908
909 for hardfork in [TempoHardfork::T2, TempoHardfork::T3] {
910 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
911 let admin = Address::random();
912 let alice = Address::random();
913
914 StorageCtx::enter(&mut storage, || {
915 let mut token = TIP20Setup::create("Test", "TST", admin)
916 .with_issuer(admin)
917 .with_mint(alice, U256::from(1000))
918 .apply()?;
919
920 let result = token.set_reward_recipient(
921 alice,
922 ITIP20::setRewardRecipientCall {
923 recipient: virtual_addr,
924 },
925 );
926
927 if hardfork.is_t3() {
928 assert!(matches!(
929 result.unwrap_err(),
930 TempoPrecompileError::TIP20(TIP20Error::InvalidRecipient(_))
931 ));
932 } else {
933 assert!(result.is_ok());
935 }
936
937 Ok::<_, TempoPrecompileError>(())
938 })?;
939 }
940 Ok(())
941 }
942}