tempo_precompiles/tip20/
rewards.rs

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    /// Starts a new reward stream for the token contract.
16    ///
17    /// This function allows an authorized user to fund a reward stream that distributes
18    /// tokens to opted-in recipients either immediately if seconds=0, or over the specified
19    /// duration.
20    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            // Emit reward scheduled event for immediate payout
53            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            // Scheduled rewards are disabled post-Moderato hardfork
67            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 the stream has not been added before, add it to the registry
111            if current_decrease.is_zero() {
112                let mut registry = TIP20RewardsRegistry::new(self.storage);
113                registry.add_stream(self.address, end_time)?;
114            }
115            // Emit reward scheduled event for streaming reward
116            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    /// Accrues rewards based on elapsed time since last update.
132    ///
133    /// This function calculates and updates the reward per token stored based on
134    /// the total reward rate and the time elapsed since the last update.
135    /// Only processes rewards if there is an opted-in supply.
136    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        // NOTE(rusowsky): first limb = u64, so it should be fine.
145        // however, it would be easier to always work with U256, since
146        // there is no possible slot packing in this slot (surrounded by U256)
147        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    /// Updates and accumulates accrued rewards for a specific token holder.
171    ///
172    /// This function calculates the rewards earned by a holder based on their
173    /// balance and the reward per token difference since their last update.
174    /// Rewards are accumulated in the delegated recipient's rewardBalance.
175    /// Returns the holder's delegated recipient address.
176    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                // Add reward to delegate's balance (or holder's own balance if self-delegated)
195                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    /// Sets or changes the reward recipient for a token holder.
217    ///
218    /// This function allows a token holder to designate who should receive their
219    /// share of rewards. Setting to zero address opts out of rewards.
220    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        // Emit reward recipient set event
264        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    /// Cancels an active reward stream and refunds remaining tokens.
277    ///
278    /// This function allows the funder of a reward stream to cancel it early,
279    /// stopping future reward distribution and refunding unused tokens.
280    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        // Remove from registry when all streams at this end_time are cancelled (Moderato+)
336        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    /// Finalizes expired reward streams by updating the total reward rate.
397    ///
398    /// This function is called to clean up streams that have reached their end time,
399    /// reducing the total reward per second rate by the amount of the expired streams.
400    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    /// Claims accumulated rewards for a recipient.
425    ///
426    /// This function allows a reward recipient to claim their accumulated rewards
427    /// and receive them as token transfers to their own balance.
428    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    /// Gets the next available stream ID (minimum 1).
486    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    /// Sets the next stream ID counter.
493    fn set_next_stream_id(&mut self, value: u64) -> Result<()> {
494        self.sstore_next_stream_id(value)
495    }
496
497    /// Gets the accumulated global reward per token.
498    fn get_global_reward_per_token(&mut self) -> Result<U256> {
499        self.sload_global_reward_per_token()
500    }
501
502    /// Sets the accumulated global reward per token in storage.
503    fn set_global_reward_per_token(&mut self, value: U256) -> Result<()> {
504        self.sstore_global_reward_per_token(value)
505    }
506
507    /// Gets the timestamp of the last reward update from storage.
508    fn get_last_update_time(&mut self) -> Result<u64> {
509        self.sload_last_update_time()
510    }
511
512    /// Sets the timestamp of the last reward update in storage.
513    fn set_last_update_time(&mut self, value: u64) -> Result<()> {
514        self.sstore_last_update_time(value)
515    }
516
517    /// Gets the total supply of tokens opted into rewards from storage.
518    pub fn get_opted_in_supply(&mut self) -> Result<u128> {
519        self.sload_opted_in_supply()
520    }
521
522    /// Sets the total supply of tokens opted into rewards in storage.
523    pub fn set_opted_in_supply(&mut self, value: u128) -> Result<()> {
524        self.sstore_opted_in_supply(value)
525    }
526
527    /// Gets the scheduled rate decrease at a specific time from storage.
528    fn get_scheduled_rate_decrease_at(&mut self, end_time: u128) -> Result<U256> {
529        self.sload_scheduled_rate_decrease(end_time)
530    }
531
532    /// Sets the scheduled rate decrease at a specific time in storage.
533    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    /// Gets the total reward per second rate from storage.
538    pub fn get_total_reward_per_second(&mut self) -> Result<U256> {
539        self.sload_total_reward_per_second()
540    }
541
542    /// Sets the total reward per second rate in storage.
543    fn set_total_reward_per_second(&mut self, value: U256) -> Result<()> {
544        self.sstore_total_reward_per_second(value)
545    }
546
547    /// Handles reward accounting for both sender and receiver during token transfers.
548    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    /// Handles reward accounting when tokens are minted to an address.
583    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    /// Retrieves a reward stream by its ID.
601    pub fn get_stream(&mut self, stream_id: u64) -> Result<RewardStream> {
602        self.sload_streams(stream_id)
603    }
604
605    /// Retrieves user reward information for a given account.
606    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    /// Creates a new RewardStream instance.
629    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        // Distribute the reward immediately
852        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        // Advance the timestamp to simulate time passing
976        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        // Mint tokens to Alice and have her opt in as reward recipient
1010        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        // Mint reward tokens to admin
1022        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        // Start immediate reward
1032        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        // Mint tokens to Alice and have her opt in as reward recipient
1061        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        // Mint reward tokens to admin
1073        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        // Start streaming reward for 20 seconds
1083        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        // Simulate 10 blocks
1094        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        // Test with Moderato hardfork - when cancelling the last stream at an end_time,
1182        // the token should be removed from the registry
1183        // Note that we start with the hardfork at pre-moderato so that scheduled rewards are still
1184        // enabled
1185        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        // Setup and start stream in a scope to release the borrow
1191        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        // Update to Moderato to assert post hardfork cancellation behavior
1218        storage.set_spec(TempoHardfork::Moderato);
1219        // Verify the token is in the registry before cancellation
1220        {
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        // Cancel the stream
1230        {
1231            let mut token = TIP20Token::new(1, &mut storage);
1232            token.cancel_reward(admin, ITIP20::cancelRewardCall { id: stream_id })?;
1233        }
1234
1235        // Verify the token is removed from the registry (post-Moderato behavior)
1236        {
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        // Test with Adagio (pre-Moderato) - token should NOT be removed from registry
1251        // even when all streams are cancelled (for consensus compatibility)
1252        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        // Setup and start stream in a scope to release the borrow
1258        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        // Verify the token is in the registry before cancellation
1285        {
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        // Cancel the stream
1295        {
1296            let mut token = TIP20Token::new(1, &mut storage);
1297            token.cancel_reward(admin, ITIP20::cancelRewardCall { id: stream_id })?;
1298        }
1299
1300        // Pre-Moderato: token should NOT be removed from registry
1301        {
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        // Start at adagio hardfork so reward streams are enabled
1365        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        // create a blacklist policy before token setup
1372        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        // setup token with the blacklist policy and start a reward stream
1384        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        // blacklist the token address
1420        {
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        // attempt to cancel the rewards
1433        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}