1use crate::{
2 TIP20_REWARDS_REGISTRY_ADDRESS,
3 error::{Result, TempoPrecompileError},
4 storage::PrecompileStorageProvider,
5 tip20::TIP20Token,
6 tip20_rewards_registry::TIP20RewardsRegistry,
7};
8use alloy::primitives::{Address, IntoLogData, U256, uint};
9use tempo_contracts::precompiles::{ITIP20, TIP20Error, TIP20Event};
10use tempo_precompiles_macros::Storable;
11
12pub const ACC_PRECISION: U256 = uint!(1000000000000000000_U256);
13
14impl<'a, S: PrecompileStorageProvider> TIP20Token<'a, S> {
15 pub fn start_reward(
21 &mut self,
22 msg_sender: Address,
23 call: ITIP20::startRewardCall,
24 ) -> Result<u64> {
25 self.check_not_paused()?;
26 let token_address = self.address;
27 self.ensure_transfer_authorized(msg_sender, token_address)?;
28
29 if call.amount == U256::ZERO {
30 return Err(TIP20Error::invalid_amount().into());
31 }
32
33 self._transfer(msg_sender, token_address, call.amount)?;
34
35 if call.secs == 0 {
36 let opted_in_supply = U256::from(self.get_opted_in_supply()?);
37 if opted_in_supply.is_zero() {
38 return Err(TIP20Error::no_opted_in_supply().into());
39 }
40
41 let delta_rpt = call
42 .amount
43 .checked_mul(ACC_PRECISION)
44 .and_then(|v| v.checked_div(opted_in_supply))
45 .ok_or(TempoPrecompileError::under_overflow())?;
46 let current_rpt = self.get_global_reward_per_token()?;
47 let new_rpt = current_rpt
48 .checked_add(delta_rpt)
49 .ok_or(TempoPrecompileError::under_overflow())?;
50 self.set_global_reward_per_token(new_rpt)?;
51
52 self.storage.emit_event(
54 self.address,
55 TIP20Event::RewardScheduled(ITIP20::RewardScheduled {
56 funder: msg_sender,
57 id: 0,
58 amount: call.amount,
59 durationSeconds: 0,
60 })
61 .into_log_data(),
62 )?;
63
64 Ok(0)
65 } else {
66 if self.storage.spec().is_moderato() {
68 return Err(TIP20Error::scheduled_rewards_disabled().into());
69 }
70
71 let rate = call
72 .amount
73 .checked_mul(ACC_PRECISION)
74 .and_then(|v| v.checked_div(U256::from(call.secs)))
75 .ok_or(TempoPrecompileError::under_overflow())?;
76 let stream_id = self.get_next_stream_id()?;
77 let next_stream_id = stream_id
78 .checked_add(1)
79 .ok_or(TempoPrecompileError::under_overflow())?;
80 self.set_next_stream_id(next_stream_id)?;
81
82 let current_total = self.get_total_reward_per_second()?;
83 let new_total = current_total
84 .checked_add(rate)
85 .ok_or(TempoPrecompileError::under_overflow())?;
86 self.set_total_reward_per_second(new_total)?;
87
88 let current_time = self.storage.timestamp().to::<u128>();
89 let end_time = current_time
90 .checked_add(call.secs as u128)
91 .ok_or(TempoPrecompileError::under_overflow())?;
92
93 self.sstore_streams(
94 stream_id,
95 RewardStream::new(
96 msg_sender,
97 current_time as u64,
98 end_time as u64,
99 rate,
100 call.amount,
101 ),
102 )?;
103
104 let current_decrease = self.get_scheduled_rate_decrease_at(end_time)?;
105 let new_decrease = current_decrease
106 .checked_add(rate)
107 .ok_or(TempoPrecompileError::under_overflow())?;
108 self.set_scheduled_rate_decrease_at(end_time, new_decrease)?;
109
110 if current_decrease.is_zero() {
112 let mut registry = TIP20RewardsRegistry::new(self.storage);
113 registry.add_stream(self.address, end_time)?;
114 }
115 self.storage.emit_event(
117 self.address,
118 TIP20Event::RewardScheduled(ITIP20::RewardScheduled {
119 funder: msg_sender,
120 id: stream_id,
121 amount: call.amount,
122 durationSeconds: call.secs,
123 })
124 .into_log_data(),
125 )?;
126
127 Ok(stream_id)
128 }
129 }
130
131 pub fn accrue(&mut self, accrue_to_timestamp: U256) -> Result<()> {
137 let elapsed = accrue_to_timestamp
138 .checked_sub(U256::from(self.get_last_update_time()?))
139 .ok_or(TempoPrecompileError::under_overflow())?;
140 if elapsed.is_zero() {
141 return Ok(());
142 }
143
144 self.set_last_update_time(accrue_to_timestamp.to::<u64>())?;
148
149 let opted_in_supply = U256::from(self.get_opted_in_supply()?);
150 if opted_in_supply == U256::ZERO {
151 return Ok(());
152 }
153
154 let total_reward_per_second = self.get_total_reward_per_second()?;
155 if total_reward_per_second > U256::ZERO {
156 let delta_rpt = total_reward_per_second
157 .checked_mul(elapsed)
158 .and_then(|v| v.checked_div(opted_in_supply))
159 .ok_or(TempoPrecompileError::under_overflow())?;
160 let current_rpt = self.get_global_reward_per_token()?;
161 let new_rpt = current_rpt
162 .checked_add(delta_rpt)
163 .ok_or(TempoPrecompileError::under_overflow())?;
164 self.set_global_reward_per_token(new_rpt)?;
165 }
166
167 Ok(())
168 }
169
170 pub fn update_rewards(&mut self, holder: Address) -> Result<Address> {
177 let mut info = self.sload_user_reward_info(holder)?;
178
179 let cached_delegate = info.reward_recipient;
180
181 let global_reward_per_token = self.get_global_reward_per_token()?;
182 let reward_per_token_delta = global_reward_per_token
183 .checked_sub(info.reward_per_token)
184 .ok_or(TempoPrecompileError::under_overflow())?;
185
186 if reward_per_token_delta != U256::ZERO {
187 if cached_delegate != Address::ZERO {
188 let holder_balance = self.get_balance(holder)?;
189 let reward = holder_balance
190 .checked_mul(reward_per_token_delta)
191 .and_then(|v| v.checked_div(ACC_PRECISION))
192 .ok_or(TempoPrecompileError::under_overflow())?;
193
194 if cached_delegate == holder {
196 info.reward_balance = info
197 .reward_balance
198 .checked_add(reward)
199 .ok_or(TempoPrecompileError::under_overflow())?;
200 } else {
201 let mut delegate_info = self.sload_user_reward_info(cached_delegate)?;
202 delegate_info.reward_balance = delegate_info
203 .reward_balance
204 .checked_add(reward)
205 .ok_or(TempoPrecompileError::under_overflow())?;
206 self.sstore_user_reward_info(cached_delegate, delegate_info)?;
207 }
208 }
209 info.reward_per_token = global_reward_per_token;
210 self.sstore_user_reward_info(holder, info)?;
211 }
212
213 Ok(cached_delegate)
214 }
215
216 pub fn set_reward_recipient(
221 &mut self,
222 msg_sender: Address,
223 call: ITIP20::setRewardRecipientCall,
224 ) -> Result<()> {
225 self.check_not_paused()?;
226 if call.recipient != Address::ZERO {
227 self.ensure_transfer_authorized(msg_sender, call.recipient)?;
228 }
229
230 let timestamp = self.storage.timestamp();
231 self.accrue(timestamp)?;
232
233 let from_delegate = self.update_rewards(msg_sender)?;
234
235 let holder_balance = self.get_balance(msg_sender)?;
236
237 if from_delegate != Address::ZERO {
238 if call.recipient == Address::ZERO {
239 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
240 .checked_sub(holder_balance)
241 .ok_or(TempoPrecompileError::under_overflow())?;
242 self.set_opted_in_supply(
243 opted_in_supply
244 .try_into()
245 .map_err(|_| TempoPrecompileError::under_overflow())?,
246 )?;
247 }
248 } else if call.recipient != Address::ZERO {
249 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
250 .checked_add(holder_balance)
251 .ok_or(TempoPrecompileError::under_overflow())?;
252 self.set_opted_in_supply(
253 opted_in_supply
254 .try_into()
255 .map_err(|_| TempoPrecompileError::under_overflow())?,
256 )?;
257 }
258
259 let mut info = self.sload_user_reward_info(msg_sender)?;
260 info.reward_recipient = call.recipient;
261 self.sstore_user_reward_info(msg_sender, info)?;
262
263 self.storage.emit_event(
265 self.address,
266 TIP20Event::RewardRecipientSet(ITIP20::RewardRecipientSet {
267 holder: msg_sender,
268 recipient: call.recipient,
269 })
270 .into_log_data(),
271 )?;
272
273 Ok(())
274 }
275
276 pub fn cancel_reward(
281 &mut self,
282 msg_sender: Address,
283 call: ITIP20::cancelRewardCall,
284 ) -> Result<U256> {
285 let stream_id = call.id;
286 let stream = self.sload_streams(stream_id)?;
287
288 if stream.funder.is_zero() {
289 return Err(TIP20Error::stream_inactive().into());
290 }
291
292 if stream.funder != msg_sender {
293 return Err(TIP20Error::not_stream_funder().into());
294 }
295
296 let current_time = self.storage.timestamp();
297 if current_time >= stream.end_time {
298 return Err(TIP20Error::stream_inactive().into());
299 }
300
301 self.accrue(current_time)?;
302
303 let elapsed = if current_time > U256::from(stream.start_time) {
304 current_time
305 .checked_sub(U256::from(stream.start_time))
306 .ok_or(TempoPrecompileError::under_overflow())?
307 } else {
308 U256::ZERO
309 };
310
311 let mut distributed = stream
312 .rate_per_second_scaled
313 .checked_mul(elapsed)
314 .and_then(|v| v.checked_div(ACC_PRECISION))
315 .ok_or(TempoPrecompileError::under_overflow())?;
316 distributed = distributed.min(stream.amount_total);
317 let refund = stream
318 .amount_total
319 .checked_sub(distributed)
320 .ok_or(TempoPrecompileError::under_overflow())?;
321
322 let total_rps = self
323 .get_total_reward_per_second()?
324 .checked_sub(stream.rate_per_second_scaled)
325 .ok_or(TempoPrecompileError::under_overflow())?;
326 self.set_total_reward_per_second(total_rps)?;
327
328 let end_time = stream.end_time as u128;
329 let new_rate = self
330 .get_scheduled_rate_decrease_at(end_time)?
331 .checked_sub(stream.rate_per_second_scaled)
332 .ok_or(TempoPrecompileError::under_overflow())?;
333 self.set_scheduled_rate_decrease_at(end_time, new_rate)?;
334
335 if self.storage.spec().is_moderato() && new_rate == U256::ZERO {
337 let mut registry = TIP20RewardsRegistry::new(self.storage);
338 registry.remove_stream(self.address, end_time)?;
339 }
340
341 self.clear_streams(stream_id)?;
342
343 let mut actual_refund = U256::ZERO;
344 if refund > U256::ZERO && self.is_transfer_authorized(stream.funder, self.address)? {
345 let funder_delegate = self.update_rewards(stream.funder)?;
346 if funder_delegate != Address::ZERO {
347 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
348 .checked_add(refund)
349 .ok_or(TempoPrecompileError::under_overflow())?;
350 self.set_opted_in_supply(
351 opted_in_supply
352 .try_into()
353 .map_err(|_| TempoPrecompileError::under_overflow())?,
354 )?;
355 }
356
357 let contract_address = self.address;
358 let contract_balance = self
359 .get_balance(contract_address)?
360 .checked_sub(refund)
361 .ok_or(TempoPrecompileError::under_overflow())?;
362 self.set_balance(contract_address, contract_balance)?;
363
364 let funder_balance = self
365 .get_balance(stream.funder)?
366 .checked_add(refund)
367 .ok_or(TempoPrecompileError::under_overflow())?;
368 self.set_balance(stream.funder, funder_balance)?;
369
370 self.storage.emit_event(
371 self.address,
372 TIP20Event::Transfer(ITIP20::Transfer {
373 from: contract_address,
374 to: stream.funder,
375 amount: refund,
376 })
377 .into_log_data(),
378 )?;
379
380 actual_refund = refund;
381 }
382
383 self.storage.emit_event(
384 self.address,
385 TIP20Event::RewardCanceled(ITIP20::RewardCanceled {
386 funder: stream.funder,
387 id: stream_id,
388 refund: actual_refund,
389 })
390 .into_log_data(),
391 )?;
392
393 Ok(actual_refund)
394 }
395
396 pub fn finalize_streams(&mut self, msg_sender: Address, end_time: u128) -> Result<()> {
401 if msg_sender != TIP20_REWARDS_REGISTRY_ADDRESS {
402 return Err(TIP20Error::unauthorized().into());
403 }
404
405 let rate_decrease = self.get_scheduled_rate_decrease_at(end_time)?;
406
407 if rate_decrease == U256::ZERO {
408 return Ok(());
409 }
410
411 self.accrue(U256::from(end_time))?;
412
413 let total_rps = self
414 .get_total_reward_per_second()?
415 .checked_sub(rate_decrease)
416 .ok_or(TempoPrecompileError::under_overflow())?;
417 self.set_total_reward_per_second(total_rps)?;
418
419 self.set_scheduled_rate_decrease_at(end_time, U256::ZERO)?;
420
421 Ok(())
422 }
423
424 pub fn claim_rewards(&mut self, msg_sender: Address) -> Result<U256> {
429 self.check_not_paused()?;
430 self.ensure_transfer_authorized(msg_sender, msg_sender)?;
431
432 let timestamp = self.storage.timestamp();
433 self.accrue(timestamp)?;
434 self.update_rewards(msg_sender)?;
435
436 let mut info = self.sload_user_reward_info(msg_sender)?;
437 let amount = info.reward_balance;
438 let contract_address = self.address;
439 let contract_balance = self.get_balance(contract_address)?;
440 let max_amount = amount.min(contract_balance);
441
442 let reward_recipient = info.reward_recipient;
443 info.reward_balance = amount
444 .checked_sub(max_amount)
445 .ok_or(TempoPrecompileError::under_overflow())?;
446 self.sstore_user_reward_info(msg_sender, info)?;
447
448 if max_amount > U256::ZERO {
449 let new_contract_balance = contract_balance
450 .checked_sub(max_amount)
451 .ok_or(TempoPrecompileError::under_overflow())?;
452 self.set_balance(contract_address, new_contract_balance)?;
453
454 let recipient_balance = self
455 .get_balance(msg_sender)?
456 .checked_add(max_amount)
457 .ok_or(TempoPrecompileError::under_overflow())?;
458 self.set_balance(msg_sender, recipient_balance)?;
459
460 if reward_recipient != Address::ZERO {
461 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
462 .checked_add(max_amount)
463 .ok_or(TempoPrecompileError::under_overflow())?;
464 self.set_opted_in_supply(
465 opted_in_supply
466 .try_into()
467 .map_err(|_| TempoPrecompileError::under_overflow())?,
468 )?;
469 }
470
471 self.storage.emit_event(
472 self.address,
473 TIP20Event::Transfer(ITIP20::Transfer {
474 from: contract_address,
475 to: msg_sender,
476 amount: max_amount,
477 })
478 .into_log_data(),
479 )?;
480 }
481
482 Ok(max_amount)
483 }
484
485 pub fn get_next_stream_id(&mut self) -> Result<u64> {
487 let id = self.sload_next_stream_id()?;
488
489 Ok(id.max(1))
490 }
491
492 fn set_next_stream_id(&mut self, value: u64) -> Result<()> {
494 self.sstore_next_stream_id(value)
495 }
496
497 fn get_global_reward_per_token(&mut self) -> Result<U256> {
499 self.sload_global_reward_per_token()
500 }
501
502 fn set_global_reward_per_token(&mut self, value: U256) -> Result<()> {
504 self.sstore_global_reward_per_token(value)
505 }
506
507 fn get_last_update_time(&mut self) -> Result<u64> {
509 self.sload_last_update_time()
510 }
511
512 fn set_last_update_time(&mut self, value: u64) -> Result<()> {
514 self.sstore_last_update_time(value)
515 }
516
517 pub fn get_opted_in_supply(&mut self) -> Result<u128> {
519 self.sload_opted_in_supply()
520 }
521
522 pub fn set_opted_in_supply(&mut self, value: u128) -> Result<()> {
524 self.sstore_opted_in_supply(value)
525 }
526
527 fn get_scheduled_rate_decrease_at(&mut self, end_time: u128) -> Result<U256> {
529 self.sload_scheduled_rate_decrease(end_time)
530 }
531
532 fn set_scheduled_rate_decrease_at(&mut self, end_time: u128, value: U256) -> Result<()> {
534 self.sstore_scheduled_rate_decrease(end_time, value)
535 }
536
537 pub fn get_total_reward_per_second(&mut self) -> Result<U256> {
539 self.sload_total_reward_per_second()
540 }
541
542 fn set_total_reward_per_second(&mut self, value: U256) -> Result<()> {
544 self.sstore_total_reward_per_second(value)
545 }
546
547 pub fn handle_rewards_on_transfer(
549 &mut self,
550 from: Address,
551 to: Address,
552 amount: U256,
553 ) -> Result<()> {
554 let from_delegate = self.update_rewards(from)?;
555 let to_delegate = self.update_rewards(to)?;
556
557 if !from_delegate.is_zero() {
558 if to_delegate.is_zero() {
559 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
560 .checked_sub(amount)
561 .ok_or(TempoPrecompileError::under_overflow())?;
562 self.set_opted_in_supply(
563 opted_in_supply
564 .try_into()
565 .map_err(|_| TempoPrecompileError::under_overflow())?,
566 )?;
567 }
568 } else if !to_delegate.is_zero() {
569 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
570 .checked_add(amount)
571 .ok_or(TempoPrecompileError::under_overflow())?;
572 self.set_opted_in_supply(
573 opted_in_supply
574 .try_into()
575 .map_err(|_| TempoPrecompileError::under_overflow())?,
576 )?;
577 }
578
579 Ok(())
580 }
581
582 pub fn handle_rewards_on_mint(&mut self, to: Address, amount: U256) -> Result<()> {
584 let to_delegate = self.update_rewards(to)?;
585
586 if !to_delegate.is_zero() {
587 let opted_in_supply = U256::from(self.get_opted_in_supply()?)
588 .checked_add(amount)
589 .ok_or(TempoPrecompileError::under_overflow())?;
590 self.set_opted_in_supply(
591 opted_in_supply
592 .try_into()
593 .map_err(|_| TempoPrecompileError::under_overflow())?,
594 )?;
595 }
596
597 Ok(())
598 }
599
600 pub fn get_stream(&mut self, stream_id: u64) -> Result<RewardStream> {
602 self.sload_streams(stream_id)
603 }
604
605 pub fn get_user_reward_info(&mut self, account: Address) -> Result<UserRewardInfo> {
607 self.sload_user_reward_info(account)
608 }
609}
610
611#[derive(Debug, Clone, Storable)]
612pub struct UserRewardInfo {
613 pub reward_recipient: Address,
614 pub reward_per_token: U256,
615 pub reward_balance: U256,
616}
617
618#[derive(Debug, Clone, Storable)]
619pub struct RewardStream {
620 funder: Address,
621 start_time: u64,
622 end_time: u64,
623 rate_per_second_scaled: U256,
624 amount_total: U256,
625}
626
627impl RewardStream {
628 pub fn new(
630 funder: Address,
631 start_time: u64,
632 end_time: u64,
633 rate_per_second_scaled: U256,
634 amount_total: U256,
635 ) -> Self {
636 Self {
637 funder,
638 start_time,
639 end_time,
640 rate_per_second_scaled,
641 amount_total,
642 }
643 }
644}
645
646impl From<RewardStream> for ITIP20::RewardStream {
647 fn from(value: RewardStream) -> Self {
648 Self {
649 funder: value.funder,
650 startTime: value.start_time,
651 endTime: value.end_time,
652 ratePerSecondScaled: value.rate_per_second_scaled,
653 amountTotal: value.amount_total,
654 }
655 }
656}
657
658impl From<UserRewardInfo> for ITIP20::UserRewardInfo {
659 fn from(value: UserRewardInfo) -> Self {
660 Self {
661 rewardRecipient: value.reward_recipient,
662 rewardPerToken: value.reward_per_token,
663 rewardBalance: value.reward_balance,
664 }
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671 use crate::{
672 PATH_USD_ADDRESS,
673 storage::hashmap::HashMapStorageProvider,
674 tip20::{ISSUER_ROLE, tests::initialize_path_usd},
675 tip20_rewards_registry::TIP20RewardsRegistry,
676 tip403_registry::TIP403Registry,
677 };
678 use alloy::primitives::{Address, U256};
679 use tempo_chainspec::hardfork::TempoHardfork;
680 use tempo_contracts::precompiles::ITIP403Registry;
681
682 #[test]
683 fn test_start_reward_pre_moderato() -> eyre::Result<()> {
684 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
685 let current_time = storage.timestamp().to::<u64>();
686 let admin = Address::random();
687
688 initialize_path_usd(&mut storage, admin)?;
689 let mut token = TIP20Token::new(1, &mut storage);
690 token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
691
692 token.grant_role_internal(admin, *ISSUER_ROLE)?;
693
694 let mint_amount = U256::from(1000e18);
695 token.mint(
696 admin,
697 ITIP20::mintCall {
698 to: admin,
699 amount: mint_amount,
700 },
701 )?;
702
703 let reward_amount = U256::from(100e18);
704 let stream_id = token.start_reward(
705 admin,
706 ITIP20::startRewardCall {
707 amount: reward_amount,
708 secs: 10,
709 },
710 )?;
711 assert_eq!(stream_id, 1);
712
713 let token_address = token.address;
714 let balance = token.get_balance(token_address)?;
715 assert_eq!(balance, reward_amount);
716
717 let stream = token.get_stream(stream_id)?;
718 assert_eq!(stream.funder, admin);
719 assert_eq!(stream.start_time, current_time);
720 assert_eq!(stream.end_time, current_time + 10);
721
722 let total_reward_per_second = token.get_total_reward_per_second()?;
723 let expected_rate = (reward_amount * ACC_PRECISION) / U256::from(10);
724 assert_eq!(total_reward_per_second, expected_rate);
725
726 let global_reward_per_token = token.get_global_reward_per_token()?;
727 assert_eq!(global_reward_per_token, U256::ZERO);
728
729 Ok(())
730 }
731
732 #[test]
733 fn test_set_reward_recipient() -> eyre::Result<()> {
734 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
735 let admin = Address::random();
736 let alice = Address::random();
737
738 initialize_path_usd(&mut storage, admin)?;
739 let mut token = TIP20Token::new(1, &mut storage);
740 token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
741
742 token.grant_role_internal(admin, *ISSUER_ROLE)?;
743
744 let amount = U256::from(1000e18);
745 token.mint(admin, ITIP20::mintCall { to: alice, amount })?;
746
747 token.set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
748
749 let info = token.sload_user_reward_info(alice)?;
750 assert_eq!(info.reward_recipient, alice);
751 assert_eq!(token.get_opted_in_supply()?, amount.to::<u128>());
752 assert_eq!(info.reward_per_token, U256::ZERO);
753
754 token.set_reward_recipient(
755 alice,
756 ITIP20::setRewardRecipientCall {
757 recipient: Address::ZERO,
758 },
759 )?;
760
761 let info = token.sload_user_reward_info(alice)?;
762 assert_eq!(info.reward_recipient, Address::ZERO);
763 assert_eq!(token.get_opted_in_supply()?, 0u128);
764 assert_eq!(info.reward_per_token, U256::ZERO);
765
766 Ok(())
767 }
768
769 #[test]
770 fn test_cancel_reward_pre_moderato() -> eyre::Result<()> {
771 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
772 let admin = Address::random();
773
774 initialize_path_usd(&mut storage, admin)?;
775 let mut token = TIP20Token::new(1, &mut storage);
776 token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
777
778 token.grant_role_internal(admin, *ISSUER_ROLE)?;
779
780 let mint_amount = U256::from(1000e18);
781 token.mint(
782 admin,
783 ITIP20::mintCall {
784 to: admin,
785 amount: mint_amount,
786 },
787 )?;
788
789 let reward_amount = U256::from(100e18);
790 let stream_id = token.start_reward(
791 admin,
792 ITIP20::startRewardCall {
793 amount: reward_amount,
794 secs: 10,
795 },
796 )?;
797
798 let remaining = token.cancel_reward(admin, ITIP20::cancelRewardCall { id: stream_id })?;
799
800 let total_after = token.get_total_reward_per_second()?;
801 assert_eq!(total_after, U256::ZERO);
802 assert_eq!(remaining, reward_amount);
803
804 let stream = token.get_stream(stream_id)?;
805 assert!(stream.funder.is_zero());
806 assert_eq!(stream.start_time, 0);
807 assert_eq!(stream.end_time, 0);
808 assert_eq!(stream.rate_per_second_scaled, U256::ZERO);
809
810 let global_reward_per_token = token.get_global_reward_per_token()?;
811 assert_eq!(global_reward_per_token, U256::ZERO);
812
813 let opted_in_supply = token.get_opted_in_supply()?;
814 assert_eq!(opted_in_supply, 0u128);
815
816 Ok(())
817 }
818
819 #[test]
820 fn test_update_rewards_pre_moderato() -> eyre::Result<()> {
821 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
822 let admin = Address::random();
823 let alice = Address::random();
824
825 initialize_path_usd(&mut storage, admin)?;
826 let mut token = TIP20Token::new(1, &mut storage);
827 token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
828
829 token.grant_role_internal(admin, *ISSUER_ROLE)?;
830
831 let mint_amount = U256::from(1000e18);
832 token.mint(
833 admin,
834 ITIP20::mintCall {
835 to: alice,
836 amount: mint_amount,
837 },
838 )?;
839
840 token.set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
841
842 let reward_amount = U256::from(100e18);
843 token.mint(
844 admin,
845 ITIP20::mintCall {
846 to: admin,
847 amount: reward_amount,
848 },
849 )?;
850
851 token.start_reward(
853 admin,
854 ITIP20::startRewardCall {
855 amount: reward_amount,
856 secs: 0,
857 },
858 )?;
859
860 token.update_rewards(alice)?;
861 let info_after = token.sload_user_reward_info(alice)?;
862 let global_rpt_after = token.get_global_reward_per_token()?;
863
864 assert_eq!(info_after.reward_per_token, global_rpt_after);
865
866 Ok(())
867 }
868
869 #[test]
870 fn test_accrue_pre_moderato() -> eyre::Result<()> {
871 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
872 let admin = Address::random();
873 let alice = Address::random();
874
875 initialize_path_usd(&mut storage, admin)?;
876 let mut token = TIP20Token::new(1, &mut storage);
877 token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
878
879 token.grant_role_internal(admin, *ISSUER_ROLE)?;
880
881 let mint_amount = U256::from(1000e18);
882 token.mint(
883 admin,
884 ITIP20::mintCall {
885 to: alice,
886 amount: mint_amount,
887 },
888 )?;
889
890 token.set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
891
892 let reward_amount = U256::from(100e18);
893 token.mint(
894 admin,
895 ITIP20::mintCall {
896 to: admin,
897 amount: reward_amount,
898 },
899 )?;
900
901 token.start_reward(
902 admin,
903 ITIP20::startRewardCall {
904 amount: reward_amount,
905 secs: 100,
906 },
907 )?;
908
909 let rpt_before = token.get_global_reward_per_token()?;
910 let last_update_before = token.get_last_update_time()?;
911
912 let timestamp = token.storage.timestamp();
913 token.accrue(timestamp)?;
914
915 let rpt_after = token.get_global_reward_per_token()?;
916 let last_update_after = token.get_last_update_time()?;
917
918 assert!(rpt_after >= rpt_before);
919 assert!(last_update_after >= last_update_before);
920
921 let total_reward_per_second = token.get_total_reward_per_second()?;
922 let expected_rate = (reward_amount * ACC_PRECISION) / U256::from(100);
923 assert_eq!(total_reward_per_second, expected_rate);
924
925 assert_eq!(token.get_opted_in_supply()?, mint_amount.to::<u128>());
926 let info = token.sload_user_reward_info(alice)?;
927 assert_eq!(info.reward_per_token, U256::ZERO);
928 Ok(())
929 }
930
931 #[test]
932 fn test_finalize_streams_pre_moderato() -> eyre::Result<()> {
933 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
934 let current_time = storage.timestamp().to::<u128>();
935 let admin = Address::random();
936 let alice = Address::random();
937
938 initialize_path_usd(&mut storage, admin)?;
939 let mut token = TIP20Token::new(1, &mut storage);
940 token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
941
942 token.grant_role_internal(admin, *ISSUER_ROLE)?;
943
944 let mint_amount = U256::from(1000e18);
945 token.mint(
946 admin,
947 ITIP20::mintCall {
948 to: alice,
949 amount: mint_amount,
950 },
951 )?;
952
953 token.set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
954
955 let reward_amount = U256::from(100e18);
956 token.mint(
957 admin,
958 ITIP20::mintCall {
959 to: admin,
960 amount: reward_amount,
961 },
962 )?;
963
964 let stream_duration = 10u32;
965 token.start_reward(
966 admin,
967 ITIP20::startRewardCall {
968 amount: reward_amount,
969 secs: stream_duration,
970 },
971 )?;
972
973 let end_time = current_time + stream_duration as u128;
974
975 token.storage.set_timestamp(U256::from(end_time));
977
978 let total_before = token.get_total_reward_per_second()?;
979 token.finalize_streams(
980 TIP20_REWARDS_REGISTRY_ADDRESS,
981 token.storage.timestamp().to::<u128>(),
982 )?;
983 let total_after = token.get_total_reward_per_second()?;
984
985 assert!(total_after < total_before);
986
987 let global_rpt = token.get_global_reward_per_token()?;
988 assert!(global_rpt > U256::ZERO);
989
990 token.update_rewards(alice)?;
991 let info = token.sload_user_reward_info(alice)?;
992 assert_eq!(info.reward_per_token, global_rpt);
993
994 Ok(())
995 }
996
997 #[test]
998 fn test_start_reward_duration_0() -> eyre::Result<()> {
999 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1000 let admin = Address::random();
1001 let alice = Address::random();
1002
1003 initialize_path_usd(&mut storage, admin)?;
1004 let mut token = TIP20Token::new(1, &mut storage);
1005 token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1006
1007 token.grant_role_internal(admin, *ISSUER_ROLE)?;
1008
1009 let mint_amount = U256::from(1000e18);
1011 token.mint(
1012 admin,
1013 ITIP20::mintCall {
1014 to: alice,
1015 amount: mint_amount,
1016 },
1017 )?;
1018
1019 token.set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
1020
1021 let reward_amount = U256::from(100e18);
1023 token.mint(
1024 admin,
1025 ITIP20::mintCall {
1026 to: admin,
1027 amount: reward_amount,
1028 },
1029 )?;
1030
1031 let id = token.start_reward(
1033 admin,
1034 ITIP20::startRewardCall {
1035 amount: reward_amount,
1036 secs: 0,
1037 },
1038 )?;
1039
1040 assert_eq!(id, 0);
1041
1042 let total_reward_per_second = token.get_total_reward_per_second()?;
1043 assert_eq!(total_reward_per_second, U256::ZERO);
1044
1045 Ok(())
1046 }
1047
1048 #[test]
1049 fn test_reward_distribution_pro_rata_pre_moderato() -> eyre::Result<()> {
1050 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1051 let admin = Address::random();
1052 let alice = Address::random();
1053
1054 initialize_path_usd(&mut storage, admin)?;
1055 let mut token = TIP20Token::new(1, &mut storage);
1056 token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1057
1058 token.grant_role_internal(admin, *ISSUER_ROLE)?;
1059
1060 let mint_amount = U256::from(1000e18);
1062 token.mint(
1063 admin,
1064 ITIP20::mintCall {
1065 to: alice,
1066 amount: mint_amount,
1067 },
1068 )?;
1069
1070 token.set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
1071
1072 let reward_amount = U256::from(100e18);
1074 token.mint(
1075 admin,
1076 ITIP20::mintCall {
1077 to: admin,
1078 amount: reward_amount,
1079 },
1080 )?;
1081
1082 let stream_id = token.start_reward(
1084 admin,
1085 ITIP20::startRewardCall {
1086 amount: reward_amount,
1087 secs: 20,
1088 },
1089 )?;
1090
1091 assert_eq!(stream_id, 1);
1092
1093 let current_timestamp = token.storage.timestamp();
1095 token
1096 .storage
1097 .set_timestamp(current_timestamp + uint!(10_U256));
1098
1099 token.finalize_streams(
1100 TIP20_REWARDS_REGISTRY_ADDRESS,
1101 token.storage.timestamp().to::<u128>(),
1102 )?;
1103
1104 token
1105 .storage
1106 .set_timestamp(current_timestamp + uint!(20_U256));
1107
1108 token.finalize_streams(
1109 TIP20_REWARDS_REGISTRY_ADDRESS,
1110 token.storage.timestamp().to::<u128>(),
1111 )?;
1112
1113 let total_reward_per_second = token.get_total_reward_per_second()?;
1114 assert_eq!(total_reward_per_second, U256::ZERO);
1115
1116 Ok(())
1117 }
1118
1119 #[test]
1120 fn test_claim_rewards_pre_moderato() -> eyre::Result<()> {
1121 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1122 let admin = Address::random();
1123 let alice = Address::random();
1124 let funder = Address::random();
1125
1126 initialize_path_usd(&mut storage, admin)?;
1127 let mut token = TIP20Token::new(1, &mut storage);
1128 token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1129
1130 token.grant_role_internal(admin, *ISSUER_ROLE)?;
1131
1132 let alice_balance = U256::from(1000e18);
1133 token.mint(
1134 admin,
1135 ITIP20::mintCall {
1136 to: alice,
1137 amount: alice_balance,
1138 },
1139 )?;
1140
1141 token.set_reward_recipient(alice, ITIP20::setRewardRecipientCall { recipient: alice })?;
1142 assert_eq!(token.get_opted_in_supply()?, alice_balance.to::<u128>());
1143
1144 let reward_amount = U256::from(100e18);
1145 token.mint(
1146 admin,
1147 ITIP20::mintCall {
1148 to: funder,
1149 amount: reward_amount,
1150 },
1151 )?;
1152
1153 token.start_reward(
1154 funder,
1155 ITIP20::startRewardCall {
1156 amount: reward_amount,
1157 secs: 100,
1158 },
1159 )?;
1160
1161 let current_time = token.storage.timestamp();
1162 token.storage.set_timestamp(current_time + U256::from(50));
1163
1164 let alice_balance_before_claim = token.get_balance(alice)?;
1165 let claimed_amount = token.claim_rewards(alice)?;
1166
1167 assert!(claimed_amount > U256::ZERO);
1168 assert_eq!(
1169 token.get_balance(alice)?,
1170 alice_balance_before_claim + claimed_amount
1171 );
1172
1173 let alice_info = token.sload_user_reward_info(alice)?;
1174 assert_eq!(alice_info.reward_balance, U256::ZERO);
1175
1176 Ok(())
1177 }
1178
1179 #[test]
1180 fn test_cancel_reward_removes_from_registry_post_moderato() -> eyre::Result<()> {
1181 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1186 let admin = Address::random();
1187
1188 initialize_path_usd(&mut storage, admin)?;
1189
1190 let (stream_id, end_time) = {
1192 let mut token = TIP20Token::new(1, &mut storage);
1193 token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1194 token.grant_role_internal(admin, *ISSUER_ROLE)?;
1195
1196 let mint_amount = U256::from(1000e18);
1197 token.mint(
1198 admin,
1199 ITIP20::mintCall {
1200 to: admin,
1201 amount: mint_amount,
1202 },
1203 )?;
1204
1205 let reward_amount = U256::from(100e18);
1206 let stream_id = token.start_reward(
1207 admin,
1208 ITIP20::startRewardCall {
1209 amount: reward_amount,
1210 secs: 10,
1211 },
1212 )?;
1213 let stream = token.get_stream(stream_id)?;
1214 (stream_id, stream.end_time as u128)
1215 };
1216
1217 storage.set_spec(TempoHardfork::Moderato);
1219 {
1221 let mut registry = TIP20RewardsRegistry::new(&mut storage);
1222 let count_before = registry.get_stream_count_at(end_time)?;
1223 assert_eq!(
1224 count_before, 1,
1225 "Registry should have 1 stream before cancellation"
1226 );
1227 }
1228
1229 {
1231 let mut token = TIP20Token::new(1, &mut storage);
1232 token.cancel_reward(admin, ITIP20::cancelRewardCall { id: stream_id })?;
1233 }
1234
1235 {
1237 let mut registry = TIP20RewardsRegistry::new(&mut storage);
1238 let count_after = registry.get_stream_count_at(end_time)?;
1239 assert_eq!(
1240 count_after, 0,
1241 "Post-Moderato: Registry should have 0 streams after cancelling the last stream"
1242 );
1243 }
1244
1245 Ok(())
1246 }
1247
1248 #[test]
1249 fn test_cancel_reward_does_not_remove_from_registry_pre_moderato() -> eyre::Result<()> {
1250 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1253 let admin = Address::random();
1254
1255 initialize_path_usd(&mut storage, admin)?;
1256
1257 let (stream_id, end_time) = {
1259 let mut token = TIP20Token::new(1, &mut storage);
1260 token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
1261 token.grant_role_internal(admin, *ISSUER_ROLE)?;
1262
1263 let mint_amount = U256::from(1000e18);
1264 token.mint(
1265 admin,
1266 ITIP20::mintCall {
1267 to: admin,
1268 amount: mint_amount,
1269 },
1270 )?;
1271
1272 let reward_amount = U256::from(100e18);
1273 let stream_id = token.start_reward(
1274 admin,
1275 ITIP20::startRewardCall {
1276 amount: reward_amount,
1277 secs: 10,
1278 },
1279 )?;
1280 let stream = token.get_stream(stream_id)?;
1281 (stream_id, stream.end_time as u128)
1282 };
1283
1284 {
1286 let mut registry = TIP20RewardsRegistry::new(&mut storage);
1287 let count_before = registry.get_stream_count_at(end_time)?;
1288 assert_eq!(
1289 count_before, 1,
1290 "Registry should have 1 stream before cancellation"
1291 );
1292 }
1293
1294 {
1296 let mut token = TIP20Token::new(1, &mut storage);
1297 token.cancel_reward(admin, ITIP20::cancelRewardCall { id: stream_id })?;
1298 }
1299
1300 {
1302 let mut registry = TIP20RewardsRegistry::new(&mut storage);
1303 let count_after = registry.get_stream_count_at(end_time)?;
1304 assert_eq!(
1305 count_after, 1,
1306 "Pre-Moderato: Registry should still have 1 stream (not removed for consensus compatibility)"
1307 );
1308 }
1309
1310 Ok(())
1311 }
1312
1313 #[test]
1314 fn test_scheduled_rewards_disabled_post_moderato() -> eyre::Result<()> {
1315 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
1316 let admin = Address::random();
1317
1318 initialize_path_usd(&mut storage, admin)?;
1319
1320 let mut token = TIP20Token::new(1, &mut storage);
1321 token.initialize(
1322 "TestToken",
1323 "TEST",
1324 "USD",
1325 PATH_USD_ADDRESS,
1326 admin,
1327 Address::ZERO,
1328 )?;
1329
1330 token.grant_role_internal(admin, *ISSUER_ROLE)?;
1331
1332 let mint_amount = U256::from(1000e18);
1333 token.mint(
1334 admin,
1335 ITIP20::mintCall {
1336 to: admin,
1337 amount: mint_amount,
1338 },
1339 )?;
1340
1341 let reward_amount = U256::from(100e18);
1342 let result = token.start_reward(
1343 admin,
1344 ITIP20::startRewardCall {
1345 amount: reward_amount,
1346 secs: 10,
1347 },
1348 );
1349
1350 assert!(result.is_err());
1351 let error = result.unwrap_err();
1352 assert!(matches!(
1353 error,
1354 TempoPrecompileError::TIP20(TIP20Error::ScheduledRewardsDisabled(_))
1355 ));
1356
1357 Ok(())
1358 }
1359
1360 #[test]
1361 fn test_cancel_reward_ensure_tip403_is_not_blacklisted() -> eyre::Result<()> {
1362 const STREAM_DURATION: u32 = 10;
1363
1364 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
1366 let current_timestamp = storage.timestamp();
1367 let admin = Address::random();
1368
1369 initialize_path_usd(&mut storage, admin)?;
1370
1371 let policy_id = {
1373 let mut tip403_registry = TIP403Registry::new(&mut storage);
1374 tip403_registry.create_policy(
1375 admin,
1376 ITIP403Registry::createPolicyCall {
1377 admin,
1378 policyType: ITIP403Registry::PolicyType::BLACKLIST,
1379 },
1380 )?
1381 };
1382
1383 let mut token = TIP20Token::new(1, &mut storage);
1385 token.initialize(
1386 "TestToken",
1387 "TEST",
1388 "USD",
1389 PATH_USD_ADDRESS,
1390 admin,
1391 Address::ZERO,
1392 )?;
1393 token.grant_role_internal(admin, *ISSUER_ROLE)?;
1394 token.change_transfer_policy_id(
1395 admin,
1396 ITIP20::changeTransferPolicyIdCall {
1397 newPolicyId: policy_id,
1398 },
1399 )?;
1400
1401 let mint_amount = U256::from(1000e18);
1402 token.mint(
1403 admin,
1404 ITIP20::mintCall {
1405 to: admin,
1406 amount: mint_amount,
1407 },
1408 )?;
1409
1410 let reward_amount = U256::from(100e18);
1411 let stream_id = token.start_reward(
1412 admin,
1413 ITIP20::startRewardCall {
1414 amount: reward_amount,
1415 secs: STREAM_DURATION,
1416 },
1417 )?;
1418
1419 {
1421 let mut tip403_registry = TIP403Registry::new(token.storage);
1422 tip403_registry.modify_policy_blacklist(
1423 admin,
1424 ITIP403Registry::modifyPolicyBlacklistCall {
1425 policyId: policy_id,
1426 account: token.address,
1427 restricted: true,
1428 },
1429 )?;
1430 }
1431
1432 storage.set_timestamp(current_timestamp + U256::from(STREAM_DURATION - 1));
1434 let mut token = TIP20Token::new(1, &mut storage);
1435 let refund = token.cancel_reward(admin, ITIP20::cancelRewardCall { id: stream_id })?;
1436 assert!(matches!(refund, U256::ZERO), "non-zero refund: {refund}");
1437
1438 Ok(())
1439 }
1440}