1use crate::{
10 error::{Result, TempoPrecompileError},
11 storage::Handler,
12 tip20::TIP20Token,
13};
14use alloy::primitives::{Address, U256, uint};
15use tempo_contracts::precompiles::{ITIP20, TIP20Error, TIP20Event};
16use tempo_precompiles_macros::Storable;
17
18pub const ACC_PRECISION: U256 = uint!(1000000000000000000_U256);
20
21impl TIP20Token {
22 pub fn distribute_reward(
34 &mut self,
35 msg_sender: Address,
36 call: ITIP20::distributeRewardCall,
37 ) -> Result<()> {
38 self.check_not_paused()?;
39 let token_address = self.address;
40
41 if call.amount == U256::ZERO {
42 return Err(TIP20Error::invalid_amount().into());
43 }
44
45 self.ensure_transfer_authorized(msg_sender, token_address)?;
46 self.check_and_update_spending_limit(msg_sender, call.amount)?;
47
48 self._transfer(msg_sender, token_address, call.amount)?;
49
50 let opted_in_supply = U256::from(self.get_opted_in_supply()?);
51 if opted_in_supply.is_zero() {
52 return Err(TIP20Error::no_opted_in_supply().into());
53 }
54
55 let delta_rpt = call
56 .amount
57 .checked_mul(ACC_PRECISION)
58 .and_then(|v| v.checked_div(opted_in_supply))
59 .ok_or(TempoPrecompileError::under_overflow())?;
60 let current_rpt = self.get_global_reward_per_token()?;
61 let new_rpt = current_rpt
62 .checked_add(delta_rpt)
63 .ok_or(TempoPrecompileError::under_overflow())?;
64 self.set_global_reward_per_token(new_rpt)?;
65
66 self.emit_event(TIP20Event::RewardDistributed(ITIP20::RewardDistributed {
68 funder: msg_sender,
69 amount: call.amount,
70 }))?;
71
72 Ok(())
73 }
74
75 pub fn update_rewards(&mut self, holder: Address) -> Result<Address> {
82 let mut info = self.user_reward_info[holder].read()?;
83
84 let cached_delegate = info.reward_recipient;
85
86 let global_reward_per_token = self.get_global_reward_per_token()?;
87 let reward_per_token_delta = global_reward_per_token
88 .checked_sub(info.reward_per_token)
89 .ok_or(TempoPrecompileError::under_overflow())?;
90
91 if reward_per_token_delta != U256::ZERO {
92 if cached_delegate != Address::ZERO {
93 let holder_balance = self.get_balance(holder)?;
94 let reward = holder_balance
95 .checked_mul(reward_per_token_delta)
96 .and_then(|v| v.checked_div(ACC_PRECISION))
97 .ok_or(TempoPrecompileError::under_overflow())?;
98
99 if cached_delegate == holder {
101 info.reward_balance = info
102 .reward_balance
103 .checked_add(reward)
104 .ok_or(TempoPrecompileError::under_overflow())?;
105 } else {
106 let mut delegate_info = self.user_reward_info[cached_delegate].read()?;
107 delegate_info.reward_balance = delegate_info
108 .reward_balance
109 .checked_add(reward)
110 .ok_or(TempoPrecompileError::under_overflow())?;
111 self.user_reward_info[cached_delegate].write(delegate_info)?;
112 }
113 }
114 info.reward_per_token = global_reward_per_token;
115 self.user_reward_info[holder].write(info)?;
116 }
117
118 Ok(cached_delegate)
119 }
120
121 pub fn set_reward_recipient(
130 &mut self,
131 msg_sender: Address,
132 call: ITIP20::setRewardRecipientCall,
133 ) -> Result<()> {
134 self.check_not_paused()?;
135 if call.recipient != Address::ZERO {
136 self.ensure_transfer_authorized(msg_sender, call.recipient)?;
137 }
138
139 let from_delegate = self.update_rewards(msg_sender)?;
140
141 let holder_balance = self.get_balance(msg_sender)?;
142
143 if from_delegate != Address::ZERO {
144 if call.recipient == Address::ZERO {
145 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
146 .checked_sub(holder_balance)
147 .ok_or(TempoPrecompileError::under_overflow())?;
148 self.set_opted_in_supply(
149 opted_in_supply
150 .try_into()
151 .map_err(|_| TempoPrecompileError::under_overflow())?,
152 )?;
153 }
154 } else if call.recipient != Address::ZERO {
155 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
156 .checked_add(holder_balance)
157 .ok_or(TempoPrecompileError::under_overflow())?;
158 self.set_opted_in_supply(
159 opted_in_supply
160 .try_into()
161 .map_err(|_| TempoPrecompileError::under_overflow())?,
162 )?;
163 }
164
165 let mut info = self.user_reward_info[msg_sender].read()?;
166 info.reward_recipient = call.recipient;
167 self.user_reward_info[msg_sender].write(info)?;
168
169 self.emit_event(TIP20Event::RewardRecipientSet(ITIP20::RewardRecipientSet {
171 holder: msg_sender,
172 recipient: call.recipient,
173 }))?;
174
175 Ok(())
176 }
177
178 pub fn claim_rewards(&mut self, msg_sender: Address) -> Result<U256> {
187 self.check_not_paused()?;
188 self.ensure_transfer_authorized(self.address, msg_sender)?;
189
190 self.update_rewards(msg_sender)?;
191
192 let mut info = self.user_reward_info[msg_sender].read()?;
193 let amount = info.reward_balance;
194 let contract_address = self.address;
195 let contract_balance = self.get_balance(contract_address)?;
196 let max_amount = amount.min(contract_balance);
197
198 let reward_recipient = info.reward_recipient;
199 info.reward_balance = amount
200 .checked_sub(max_amount)
201 .ok_or(TempoPrecompileError::under_overflow())?;
202 self.user_reward_info[msg_sender].write(info)?;
203
204 if max_amount > U256::ZERO {
205 let new_contract_balance = contract_balance
206 .checked_sub(max_amount)
207 .ok_or(TempoPrecompileError::under_overflow())?;
208 self.set_balance(contract_address, new_contract_balance)?;
209
210 let recipient_balance = self
211 .get_balance(msg_sender)?
212 .checked_add(max_amount)
213 .ok_or(TempoPrecompileError::under_overflow())?;
214 self.set_balance(msg_sender, recipient_balance)?;
215
216 if reward_recipient != Address::ZERO {
217 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
218 .checked_add(max_amount)
219 .ok_or(TempoPrecompileError::under_overflow())?;
220 self.set_opted_in_supply(
221 opted_in_supply
222 .try_into()
223 .map_err(|_| TempoPrecompileError::under_overflow())?,
224 )?;
225 }
226
227 self.emit_event(TIP20Event::Transfer(ITIP20::Transfer {
228 from: contract_address,
229 to: msg_sender,
230 amount: max_amount,
231 }))?;
232 }
233
234 Ok(max_amount)
235 }
236
237 pub fn get_global_reward_per_token(&self) -> Result<U256> {
239 self.global_reward_per_token.read()
240 }
241
242 fn set_global_reward_per_token(&mut self, value: U256) -> Result<()> {
244 self.global_reward_per_token.write(value)
245 }
246
247 pub fn get_opted_in_supply(&self) -> Result<u128> {
249 self.opted_in_supply.read()
250 }
251
252 pub fn set_opted_in_supply(&mut self, value: u128) -> Result<()> {
254 self.opted_in_supply.write(value)
255 }
256
257 pub fn handle_rewards_on_transfer(
259 &mut self,
260 from: Address,
261 to: Address,
262 amount: U256,
263 ) -> Result<()> {
264 let from_delegate = self.update_rewards(from)?;
265 let to_delegate = self.update_rewards(to)?;
266
267 if !from_delegate.is_zero() {
268 if to_delegate.is_zero() {
269 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
270 .checked_sub(amount)
271 .ok_or(TempoPrecompileError::under_overflow())?;
272 self.set_opted_in_supply(
273 opted_in_supply
274 .try_into()
275 .map_err(|_| TempoPrecompileError::under_overflow())?,
276 )?;
277 }
278 } else if !to_delegate.is_zero() {
279 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
280 .checked_add(amount)
281 .ok_or(TempoPrecompileError::under_overflow())?;
282 self.set_opted_in_supply(
283 opted_in_supply
284 .try_into()
285 .map_err(|_| TempoPrecompileError::under_overflow())?,
286 )?;
287 }
288
289 Ok(())
290 }
291
292 pub fn handle_rewards_on_mint(&mut self, to: Address, amount: U256) -> Result<()> {
294 let to_delegate = self.update_rewards(to)?;
295
296 if !to_delegate.is_zero() {
297 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
298 .checked_add(amount)
299 .ok_or(TempoPrecompileError::under_overflow())?;
300 self.set_opted_in_supply(
301 opted_in_supply
302 .try_into()
303 .map_err(|_| TempoPrecompileError::under_overflow())?,
304 )?;
305 }
306
307 Ok(())
308 }
309
310 pub fn get_user_reward_info(&self, account: Address) -> Result<UserRewardInfo> {
312 self.user_reward_info[account].read()
313 }
314
315 pub fn get_pending_rewards(&self, account: Address) -> Result<u128> {
324 let info = self.user_reward_info[account].read()?;
325
326 let mut pending = info.reward_balance;
328
329 if info.reward_recipient == account {
331 let holder_balance = self.get_balance(account)?;
332 if holder_balance > U256::ZERO {
333 let global_reward_per_token = self.get_global_reward_per_token()?;
334 let reward_per_token_delta = global_reward_per_token
335 .checked_sub(info.reward_per_token)
336 .ok_or(TempoPrecompileError::under_overflow())?;
337
338 if reward_per_token_delta > U256::ZERO {
339 let accrued = holder_balance
340 .checked_mul(reward_per_token_delta)
341 .and_then(|v| v.checked_div(ACC_PRECISION))
342 .ok_or(TempoPrecompileError::under_overflow())?;
343 pending = pending
344 .checked_add(accrued)
345 .ok_or(TempoPrecompileError::under_overflow())?;
346 }
347 }
348 }
349
350 pending
351 .try_into()
352 .map_err(|_| TempoPrecompileError::under_overflow())
353 }
354}
355
356#[derive(Debug, Clone, Storable)]
358pub struct UserRewardInfo {
359 pub reward_recipient: Address,
361 pub reward_per_token: U256,
363 pub reward_balance: U256,
365}
366
367impl From<UserRewardInfo> for ITIP20::UserRewardInfo {
368 fn from(value: UserRewardInfo) -> Self {
369 Self {
370 rewardRecipient: value.reward_recipient,
371 rewardPerToken: value.reward_per_token,
372 rewardBalance: value.reward_balance,
373 }
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use crate::{
381 error::TempoPrecompileError,
382 storage::{StorageCtx, hashmap::HashMapStorageProvider},
383 test_util::TIP20Setup,
384 tip403_registry::TIP403Registry,
385 };
386 use alloy::primitives::{Address, U256};
387 use tempo_contracts::precompiles::{ITIP403Registry, TIP20Error};
388
389 #[test]
390 fn test_set_reward_recipient() -> eyre::Result<()> {
391 let mut storage = HashMapStorageProvider::new(1);
392 let admin = Address::random();
393 let alice = Address::random();
394 let amount = U256::random() % U256::from(u128::MAX);
395
396 StorageCtx::enter(&mut storage, || {
397 let mut token = TIP20Setup::create("Test", "TST", admin)
398 .with_issuer(admin)
399 .with_mint(alice, amount)
400 .apply()?;
401
402 token
403 .set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
404
405 let info = token.user_reward_info[alice].read()?;
406 assert_eq!(info.reward_recipient, alice);
407 assert_eq!(token.get_opted_in_supply()?, amount.to::<u128>());
408 assert_eq!(info.reward_per_token, U256::ZERO);
409
410 token.set_reward_recipient(
411 alice,
412 ITIP20::setRewardRecipientCall {
413 recipient: Address::ZERO,
414 },
415 )?;
416
417 let info = token.user_reward_info[alice].read()?;
418 assert_eq!(info.reward_recipient, Address::ZERO);
419 assert_eq!(token.get_opted_in_supply()?, 0u128);
420 assert_eq!(info.reward_per_token, U256::ZERO);
421
422 Ok(())
423 })
424 }
425
426 #[test]
427 fn test_distribute_reward() -> eyre::Result<()> {
428 let mut storage = HashMapStorageProvider::new(1);
429 let admin = Address::random();
430 let alice = Address::random();
431 let amount = U256::from(1000);
432 let reward_amount = amount / U256::from(10);
433
434 StorageCtx::enter(&mut storage, || {
435 let mut token = TIP20Setup::create("Test", "TST", admin)
436 .with_issuer(admin)
437 .with_mint(alice, amount)
438 .with_mint(admin, reward_amount)
439 .apply()?;
440
441 token
442 .set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
443
444 token.distribute_reward(
446 admin,
447 ITIP20::distributeRewardCall {
448 amount: reward_amount,
449 },
450 )?;
451
452 let expected_rpt = reward_amount * ACC_PRECISION / amount;
454 assert_eq!(token.get_global_reward_per_token()?, expected_rpt);
455
456 assert_eq!(token.get_balance(token.address)?, reward_amount);
458 assert_eq!(token.get_balance(admin)?, U256::ZERO);
459
460 token.update_rewards(alice)?;
462 let info = token.get_user_reward_info(alice)?;
463 assert_eq!(info.reward_balance, reward_amount);
464
465 let claimed = token.claim_rewards(alice)?;
467 assert_eq!(claimed, reward_amount);
468 assert_eq!(token.get_balance(alice)?, amount + reward_amount);
469 assert_eq!(token.get_balance(token.address)?, U256::ZERO);
470
471 token.mint(
473 admin,
474 ITIP20::mintCall {
475 to: admin,
476 amount: U256::from(1),
477 },
478 )?;
479 let result =
480 token.distribute_reward(admin, ITIP20::distributeRewardCall { amount: U256::ZERO });
481 assert!(result.is_err());
482
483 Ok(())
484 })
485 }
486
487 #[test]
488 fn test_get_pending_rewards() -> eyre::Result<()> {
489 let mut storage = HashMapStorageProvider::new(1);
490 let admin = Address::random();
491 let alice = Address::random();
492
493 StorageCtx::enter(&mut storage, || {
494 let alice_balance = U256::from(1000e18);
495 let reward_amount = U256::from(100e18);
496
497 let mut token = TIP20Setup::create("Test", "TST", admin)
498 .with_issuer(admin)
499 .with_mint(alice, alice_balance)
500 .with_mint(admin, reward_amount)
501 .apply()?;
502
503 token
504 .set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
505
506 let pending_before = token.get_pending_rewards(alice)?;
508 assert_eq!(pending_before, 0u128);
509
510 token.distribute_reward(
512 admin,
513 ITIP20::distributeRewardCall {
514 amount: reward_amount,
515 },
516 )?;
517
518 let pending_after = token.get_pending_rewards(alice)?;
520 assert_eq!(U256::from(pending_after), reward_amount);
521
522 let user_info = token.get_user_reward_info(alice)?;
524 assert_eq!(
525 user_info.reward_balance,
526 U256::ZERO,
527 "get_pending_rewards should not modify state"
528 );
529
530 Ok(())
531 })
532 }
533
534 #[test]
535 fn test_get_pending_rewards_includes_stored_balance() -> eyre::Result<()> {
536 let mut storage = HashMapStorageProvider::new(1);
537 let admin = Address::random();
538 let alice = Address::random();
539
540 StorageCtx::enter(&mut storage, || {
541 let alice_balance = U256::from(1000e18);
542 let reward_amount = U256::from(50e18);
543
544 let mut token = TIP20Setup::create("Test", "TST", admin)
545 .with_issuer(admin)
546 .with_mint(alice, alice_balance)
547 .with_mint(admin, reward_amount * U256::from(2))
548 .apply()?;
549
550 token
551 .set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
552
553 token.distribute_reward(
555 admin,
556 ITIP20::distributeRewardCall {
557 amount: reward_amount,
558 },
559 )?;
560
561 token.update_rewards(alice)?;
563 let user_info = token.get_user_reward_info(alice)?;
564 assert_eq!(user_info.reward_balance, reward_amount);
565
566 token.distribute_reward(
568 admin,
569 ITIP20::distributeRewardCall {
570 amount: reward_amount,
571 },
572 )?;
573
574 let pending = token.get_pending_rewards(alice)?;
576 assert_eq!(U256::from(pending), reward_amount * U256::from(2));
577
578 Ok(())
579 })
580 }
581
582 #[test]
583 fn test_get_pending_rewards_with_delegation() -> eyre::Result<()> {
584 let mut storage = HashMapStorageProvider::new(1);
585 let admin = Address::random();
586 let alice = Address::random();
587 let bob = Address::random();
588
589 StorageCtx::enter(&mut storage, || {
590 let alice_balance = U256::from(1000e18);
591 let reward_amount = U256::from(100e18);
592
593 let mut token = TIP20Setup::create("Test", "TST", admin)
594 .with_issuer(admin)
595 .with_mint(alice, alice_balance)
596 .with_mint(admin, reward_amount)
597 .apply()?;
598
599 token.set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: bob })?;
601
602 token.distribute_reward(
604 admin,
605 ITIP20::distributeRewardCall {
606 amount: reward_amount,
607 },
608 )?;
609
610 let alice_pending = token.get_pending_rewards(alice)?;
612 assert_eq!(alice_pending, 0u128);
613
614 let bob_pending_before_update = token.get_pending_rewards(bob)?;
618 assert_eq!(bob_pending_before_update, 0u128);
619
620 token.update_rewards(alice)?;
622 let bob_pending_after_update = token.get_pending_rewards(bob)?;
623 assert_eq!(U256::from(bob_pending_after_update), reward_amount);
624
625 Ok(())
626 })
627 }
628
629 #[test]
630 fn test_get_pending_rewards_not_opted_in() -> eyre::Result<()> {
631 let mut storage = HashMapStorageProvider::new(1);
632 let admin = Address::random();
633 let alice = Address::random();
634 let bob = Address::random();
635
636 StorageCtx::enter(&mut storage, || {
637 let balance = U256::from(1000e18);
638 let reward_amount = U256::from(100e18);
639
640 let mut token = TIP20Setup::create("Test", "TST", admin)
641 .with_issuer(admin)
642 .with_mint(alice, balance)
643 .with_mint(bob, balance)
644 .with_mint(admin, reward_amount)
645 .apply()?;
646
647 token
649 .set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
650
651 token.distribute_reward(
653 admin,
654 ITIP20::distributeRewardCall {
655 amount: reward_amount,
656 },
657 )?;
658
659 let alice_pending = token.get_pending_rewards(alice)?;
661 assert_eq!(U256::from(alice_pending), reward_amount);
662
663 let bob_pending = token.get_pending_rewards(bob)?;
665 assert_eq!(bob_pending, 0u128);
666
667 Ok(())
668 })
669 }
670
671 #[test]
672 fn test_claim_rewards_unauthorized() -> eyre::Result<()> {
673 let mut storage = HashMapStorageProvider::new(1);
674 let admin = Address::random();
675 let alice = Address::random();
676
677 StorageCtx::enter(&mut storage, || {
678 let mut registry = TIP403Registry::new();
679 registry.initialize()?;
680
681 let policy_id = registry.create_policy(
682 admin,
683 ITIP403Registry::createPolicyCall {
684 admin,
685 policyType: ITIP403Registry::PolicyType::BLACKLIST,
686 },
687 )?;
688
689 registry.modify_policy_blacklist(
690 admin,
691 ITIP403Registry::modifyPolicyBlacklistCall {
692 policyId: policy_id,
693 account: alice,
694 restricted: true,
695 },
696 )?;
697
698 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
699
700 token.change_transfer_policy_id(
701 admin,
702 ITIP20::changeTransferPolicyIdCall {
703 newPolicyId: policy_id,
704 },
705 )?;
706
707 let err = token.claim_rewards(alice).unwrap_err();
708 assert!(
709 matches!(
710 err,
711 TempoPrecompileError::TIP20(TIP20Error::PolicyForbids(_))
712 ),
713 "Expected PolicyForbids error, got: {err:?}"
714 );
715
716 Ok(())
717 })
718 }
719}