1use crate::{Miner, Quarry};
4use anchor_lang::prelude::*;
5use spl_math::uint::U192;
6use std::cmp;
7use vipers::prelude::*;
8
9pub const SECONDS_PER_YEAR: u128 = 86_400 * 365;
11
12pub const PRECISION_MULTIPLIER: u128 = u64::MAX as u128;
14
15#[derive(Debug)]
17pub struct Payroll {
18 pub famine_ts: i64,
20 pub last_checkpoint_ts: i64,
22
23 pub annual_rewards_rate: u64,
25
26 pub rewards_per_token_stored: u128,
29
30 pub total_tokens_deposited: u64,
32}
33
34impl From<Quarry> for Payroll {
35 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 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 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 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 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 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 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(), UpperboundExceeded
183 );
184 }
185
186 Ok(())
187 }
188
189 pub fn last_time_reward_applicable(&self, current_ts: i64) -> i64 {
191 cmp::min(current_ts, self.famine_ts)
192 }
193
194 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 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 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 part in 0..total,
259 total in Just(total)
260 ) -> (u64, u64) {
261 (part, total)
262 }
263 }
264
265 proptest! {
266 #[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 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}