quarry_mine/
payroll.rs

1//! Calculates token distribution rates.
2
3use crate::{Miner, Quarry};
4use anchor_lang::prelude::*;
5use spl_math::uint::U192;
6use std::cmp;
7use vipers::prelude::*;
8
9/// Number of seconds in a year.
10pub const SECONDS_PER_YEAR: u128 = 86_400 * 365;
11
12/// Number of decimal points of precision that `rewards_per_token_stored` uses.
13pub const PRECISION_MULTIPLIER: u128 = u64::MAX as u128;
14
15/// Calculator for amount of tokens to pay out.
16#[derive(Debug)]
17pub struct Payroll {
18    /// Timestamp of when rewards should end.
19    pub famine_ts: i64,
20    /// Timestamp of the last update.
21    pub last_checkpoint_ts: i64,
22
23    /// Amount of tokens to issue per year.
24    pub annual_rewards_rate: u64,
25
26    /// Amount of tokens to issue per staked token,
27    /// multiplied by u64::MAX for precision.
28    pub rewards_per_token_stored: u128,
29
30    /// Total number of tokens deposited into the [Quarry].
31    pub total_tokens_deposited: u64,
32}
33
34impl From<Quarry> for Payroll {
35    /// Create a [Payroll] from a [Quarry].
36    fn from(quarry: Quarry) -> Self {
37        Self::new(
38            quarry.famine_ts,
39            quarry.last_update_ts,
40            quarry.annual_rewards_rate,
41            quarry.rewards_per_token_stored,
42            quarry.total_tokens_deposited,
43        )
44    }
45}
46
47impl Payroll {
48    /// Creates a new [Payroll].
49    pub fn new(
50        famine_ts: i64,
51        last_checkpoint_ts: i64,
52        annual_rewards_rate: u64,
53        rewards_per_token_stored: u128,
54        total_tokens_deposited: u64,
55    ) -> Self {
56        Self {
57            famine_ts,
58            last_checkpoint_ts,
59            annual_rewards_rate,
60            rewards_per_token_stored,
61            total_tokens_deposited,
62        }
63    }
64
65    /// Calculates the amount of rewards to pay out for each staked token.
66    /// https://github.com/Synthetixio/synthetix/blob/4b9b2ee09b38638de6fe1c38dbe4255a11ebed86/contracts/StakingRewards.sol#L62
67    fn calculate_reward_per_token_unsafe(&self, current_ts: i64) -> Option<u128> {
68        if self.total_tokens_deposited == 0 {
69            Some(self.rewards_per_token_stored)
70        } else {
71            let time_worked = self.compute_time_worked(current_ts)?;
72
73            let reward = U192::from(time_worked)
74                .checked_mul(PRECISION_MULTIPLIER.into())?
75                .checked_mul(self.annual_rewards_rate.into())?
76                .checked_div(SECONDS_PER_YEAR.into())?
77                .checked_div(self.total_tokens_deposited.into())?;
78
79            let precise_reward: u128 = reward.try_into().ok()?;
80
81            self.rewards_per_token_stored.checked_add(precise_reward)
82        }
83    }
84
85    /// Calculates the amount of rewards to pay for each staked token, performing safety checks.
86    pub fn calculate_reward_per_token(&self, current_ts: i64) -> Result<u128> {
87        invariant!(current_ts >= self.last_checkpoint_ts, InvalidTimestamp);
88        Ok(unwrap_int!(
89            self.calculate_reward_per_token_unsafe(current_ts)
90        ))
91    }
92
93    /// Calculates the amount of rewards earned for the given number of staked tokens.
94    /// https://github.com/Synthetixio/synthetix/blob/4b9b2ee09b38638de6fe1c38dbe4255a11ebed86/contracts/StakingRewards.sol#L72
95    fn calculate_rewards_earned_unsafe(
96        &self,
97        current_ts: i64,
98        tokens_deposited: u64,
99        rewards_per_token_paid: u128,
100        rewards_earned: u64,
101    ) -> Option<u128> {
102        let net_new_rewards = self
103            .calculate_reward_per_token_unsafe(current_ts)?
104            .checked_sub(rewards_per_token_paid)?;
105        let rewards_earned = U192::from(tokens_deposited)
106            .checked_mul(net_new_rewards.into())?
107            .checked_div(PRECISION_MULTIPLIER.into())?
108            .checked_add(rewards_earned.into())?;
109
110        let precise_rewards_earned: u128 = rewards_earned.try_into().ok()?;
111        Some(precise_rewards_earned)
112    }
113
114    /// Calculates the amount of rewards earned for the given number of staked tokens, with safety checks.
115    /// <https://github.com/Synthetixio/synthetix/blob/4b9b2ee09b38638de6fe1c38dbe4255a11ebed86/contracts/StakingRewards.sol#L72>
116    pub fn calculate_rewards_earned(
117        &self,
118        current_ts: i64,
119        tokens_deposited: u64,
120        rewards_per_token_paid: u128,
121        rewards_earned: u64,
122    ) -> Result<u128> {
123        invariant!(
124            tokens_deposited <= self.total_tokens_deposited,
125            NotEnoughTokens
126        );
127        invariant!(current_ts >= self.last_checkpoint_ts, InvalidTimestamp);
128        let result = unwrap_int!(self.calculate_rewards_earned_unsafe(
129            current_ts,
130            tokens_deposited,
131            rewards_per_token_paid,
132            rewards_earned,
133        ),);
134        Ok(result)
135    }
136
137    fn calculate_claimable_upper_bound_unsafe(
138        &self,
139        current_ts: i64,
140        rewards_per_token_paid: u128,
141    ) -> Option<U192> {
142        let time_worked = self.compute_time_worked(current_ts)?;
143
144        let quarry_rewards_accrued = U192::from(time_worked)
145            .checked_mul(self.annual_rewards_rate.into())?
146            .checked_div(SECONDS_PER_YEAR.into())?;
147
148        let net_rewards_per_token = self
149            .rewards_per_token_stored
150            .checked_sub(rewards_per_token_paid)?;
151        let net_quarry_rewards = U192::from(net_rewards_per_token)
152            .checked_mul(self.total_tokens_deposited.into())?
153            .checked_div(PRECISION_MULTIPLIER.into())?;
154
155        quarry_rewards_accrued.checked_add(net_quarry_rewards)
156    }
157
158    /// Sanity check on the amount of rewards to be claimed by the miner.
159    pub fn sanity_check(
160        &self,
161        current_ts: i64,
162        amount_claimable: u64,
163        miner: &Miner,
164    ) -> Result<()> {
165        let rewards_upperbound =
166            unwrap_int!(self
167                .calculate_claimable_upper_bound_unsafe(current_ts, miner.rewards_per_token_paid,));
168        let amount_claimable_less_already_earned =
169            unwrap_int!(amount_claimable.checked_sub(miner.rewards_earned));
170
171        if rewards_upperbound < amount_claimable_less_already_earned.into() {
172            msg!(
173                "current_ts: {}, rewards_upperbound: {}, amount_claimable: {}, payroll: {:?}, miner: {:?}",
174                current_ts,
175                rewards_upperbound,
176                amount_claimable,
177                self,
178                miner,
179            );
180            invariant!(
181                rewards_upperbound + 1 >= amount_claimable.into(), // Allow off by one.
182                UpperboundExceeded
183            );
184        }
185
186        Ok(())
187    }
188
189    /// Gets the latest time rewards were being distributed.
190    pub fn last_time_reward_applicable(&self, current_ts: i64) -> i64 {
191        cmp::min(current_ts, self.famine_ts)
192    }
193
194    /// Calculates the amount of seconds the [Payroll] should have applied rewards for.
195    fn compute_time_worked(&self, current_ts: i64) -> Option<i64> {
196        Some(cmp::max(
197            0,
198            self.last_time_reward_applicable(current_ts)
199                .checked_sub(self.last_checkpoint_ts)?,
200        ))
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use crate::MAX_ANNUAL_REWARDS_RATE;
207    /// Maximum seconds elapsed between two checkpoints.
208    /// [i32::MAX] corresponds to about 70 years.
209    const MAX_SECONDS_BETWEEN_CHECKPOINTS: i64 = i32::MAX as i64;
210    const MAX_TOTAL_TOKENS: u64 = 1_000_000_000_000_000;
211
212    use super::*;
213    use num_traits::ToPrimitive;
214    use proptest::prelude::*;
215
216    macro_rules! assert_percent_delta {
217        ($x:expr, $y:expr, $d:expr) => {
218            let delta = if $x > $y {
219                $x - $y
220            } else if $y > $x {
221                $y - $x
222            } else {
223                0
224            };
225            let delta_f = if delta == 0 && $y == 0 {
226                0.0_f64
227            } else {
228                (delta as f64) / ($y as f64)
229            };
230            assert!(
231                delta_f < $d,
232                "Delta {} > {}; left: {}, right: {}",
233                delta_f,
234                $d,
235                $x,
236                $y
237            );
238        };
239    }
240
241    prop_compose! {
242        pub fn part_and_total_small()(
243            total in 0..u64::MAX
244        )(
245            // use a really small number here
246            part in 0..cmp::min(100_000_u64, total),
247            total in Just(total)
248        ) -> (u64, u64) {
249           (part, total)
250       }
251    }
252
253    prop_compose! {
254        pub fn part_and_total()(
255            total in 0..MAX_TOTAL_TOKENS
256        )(
257            // use a really small number here
258            part in 0..total,
259            total in Just(total)
260        ) -> (u64, u64) {
261           (part, total)
262       }
263    }
264
265    proptest! {
266        /// Precision errors should not be over EPSILON.
267        #[test]
268        fn test_accumulated_precision_errors_epsilon(
269            num_updates in 1..100_i64,
270            (final_ts, initial_ts) in total_and_intermediate_ts(),
271            annual_rewards_rate in 0..=MAX_ANNUAL_REWARDS_RATE,
272            (my_tokens_deposited, total_tokens_deposited) in part_and_total_small()
273        ) {
274            const EPSILON: f64 = 0.0001;
275
276            let mut rewards_per_token_stored: u128 = 0;
277            let mut last_checkpoint_ts = initial_ts;
278            for i in 0..=num_updates {
279                let payroll = Payroll::new(
280                    i64::MAX,
281                    last_checkpoint_ts,
282                    annual_rewards_rate,
283                    rewards_per_token_stored,
284                    total_tokens_deposited
285                );
286                let current_ts = initial_ts + (((final_ts - initial_ts) as u128) * (i as u128) / (num_updates as u128)).to_i64().unwrap();
287                rewards_per_token_stored = payroll.calculate_reward_per_token(current_ts).unwrap();
288                last_checkpoint_ts = current_ts;
289            }
290
291            let payroll = Payroll::new(
292                i64::MAX,
293                last_checkpoint_ts,
294                annual_rewards_rate,
295                rewards_per_token_stored,
296                total_tokens_deposited
297            );
298            let rewards_earned = payroll.calculate_rewards_earned(
299                final_ts,
300                my_tokens_deposited,
301                0_u128,
302                0
303            ).unwrap();
304
305            let expected_rewards_earned = U192::from(annual_rewards_rate)
306                * U192::from(final_ts - initial_ts)
307                * U192::from(my_tokens_deposited)
308                / U192::from(SECONDS_PER_YEAR)
309                / U192::from(total_tokens_deposited);
310
311            assert_percent_delta!(expected_rewards_earned.as_u128(), rewards_earned, EPSILON);
312        }
313    }
314
315    proptest! {
316        #[test]
317        fn test_sanity_check(
318            annual_rewards_rate in 0..=MAX_ANNUAL_REWARDS_RATE,
319            rewards_already_earned in u64::MIN..MAX_TOTAL_TOKENS,
320            (rewards_per_token_paid, rewards_per_token_stored) in part_and_total(),
321            (current_ts, last_checkpoint_ts) in total_and_intermediate_ts(),
322            (my_tokens_deposited, total_tokens_deposited) in part_and_total()
323        ) {
324            let payroll = Payroll::new(
325                i64::MAX,
326                last_checkpoint_ts,
327                annual_rewards_rate,
328                rewards_per_token_stored as u128,
329                total_tokens_deposited
330            );
331
332            let amount_claimable_less_already_earned = payroll.calculate_rewards_earned(current_ts, my_tokens_deposited, rewards_per_token_paid.into(), rewards_already_earned).unwrap() - rewards_already_earned as u128;
333            let upperbound = payroll.calculate_claimable_upper_bound_unsafe(current_ts, rewards_per_token_paid.into()).unwrap();
334
335            assert!(upperbound >= amount_claimable_less_already_earned.into(), "amount_claimable_less_already_earned: {}, upperbound: {}", amount_claimable_less_already_earned, upperbound);
336        }
337    }
338
339    #[test]
340    fn test_sanity_check_off_by_one_case() {
341        // FIXME: Find out why sometimes upperbound can be off by one.
342        let total_tokens_deposited = 1_000_000;
343        let annual_rewards_rate = 365_000_000_000_000;
344        let rewards_per_token_stored: u128 = 576247267536447296791024;
345
346        let last_checkpoint_ts = 0;
347        let payroll = Payroll::new(
348            i64::MAX,
349            last_checkpoint_ts,
350            annual_rewards_rate,
351            rewards_per_token_stored,
352            total_tokens_deposited,
353        );
354
355        let current_ts = 6;
356        let rewards_earned = payroll
357            .calculate_rewards_earned(current_ts, total_tokens_deposited, 0, 0)
358            .unwrap();
359        let upperbound = payroll
360            .calculate_claimable_upper_bound_unsafe(current_ts, 0)
361            .unwrap();
362
363        assert_eq!(
364            upperbound + 1,
365            rewards_earned.into(),
366            "rewards_earned: {}, upperbound: {}",
367            rewards_earned,
368            upperbound
369        );
370    }
371
372    proptest! {
373        #[test]
374        fn test_wpt_with_zero_annual_rewards_rate(
375            famine_ts in 0..i64::MAX,
376            (current_ts, last_checkpoint_ts) in total_and_intermediate_ts(),
377            rewards_per_token_stored in u64::MIN..u64::MAX,
378            total_tokens_deposited in u64::MIN..u64::MAX,
379        ) {
380            let payroll = Payroll::new(famine_ts, last_checkpoint_ts, 0, rewards_per_token_stored.into(), total_tokens_deposited);
381            assert_eq!(payroll.calculate_reward_per_token(current_ts).unwrap(), rewards_per_token_stored.into())
382        }
383    }
384
385    proptest! {
386        #[test]
387        fn test_wpt_when_famine(
388            famine_ts in 0..i64::MAX,
389            (current_ts, last_checkpoint_ts) in total_and_intermediate_ts(),
390            annual_rewards_rate in 1..u64::MAX,
391            rewards_per_token_stored in u64::MIN..u64::MAX,
392            total_tokens_deposited in u64::MIN..u64::MAX,
393        ) {
394            let payroll = Payroll::new(
395                famine_ts, last_checkpoint_ts, annual_rewards_rate,
396                rewards_per_token_stored.into(), total_tokens_deposited
397            );
398            prop_assume!(famine_ts < current_ts && famine_ts < last_checkpoint_ts);
399            assert_eq!(payroll.calculate_reward_per_token(current_ts).unwrap(), rewards_per_token_stored.into())
400        }
401    }
402
403    proptest! {
404        #[test]
405        fn test_rewards_earned_when_zero_tokens_deposited(
406            famine_ts in 0..i64::MAX,
407            (current_ts, last_checkpoint_ts) in total_and_intermediate_ts(),
408            annual_rewards_rate in 0..u64::MAX,
409            rewards_per_token_stored in u64::MIN..u64::MAX,
410            total_tokens_deposited in u64::MIN..u64::MAX,
411            rewards_per_token_paid in u64::MIN..u64::MAX,
412            rewards_earned in u64::MIN..u64::MAX,
413        ) {
414            let payroll = Payroll::new(famine_ts, last_checkpoint_ts, annual_rewards_rate, rewards_per_token_stored.into(), total_tokens_deposited);
415            prop_assume!(payroll.calculate_reward_per_token(current_ts).unwrap() >= rewards_per_token_paid.into());
416            assert_eq!(payroll.calculate_rewards_earned(current_ts, 0, rewards_per_token_paid.into(), rewards_earned).unwrap(), rewards_earned.into())
417        }
418    }
419
420    prop_compose! {
421        pub fn total_and_intermediate_ts()(
422          elapsed_seconds in 0..MAX_SECONDS_BETWEEN_CHECKPOINTS,
423          last_checkpoint_ts in 0..(i64::MAX - MAX_SECONDS_BETWEEN_CHECKPOINTS),
424        ) -> (i64, i64) {
425          (last_checkpoint_ts + elapsed_seconds, last_checkpoint_ts)
426       }
427    }
428}