1use anchor_lang::prelude::*;
4use vipers::prelude::*;
5
6use crate::{payroll::Payroll, Miner, Quarry, Rewarder};
7use num_traits::cast::ToPrimitive;
8
9pub enum StakeAction {
11 Stake,
13 Withdraw,
15}
16
17impl Quarry {
18 pub fn update_rewards_internal(
20 &mut self,
21 current_ts: i64,
22 rewarder: &Rewarder,
23 payroll: &Payroll,
24 ) -> Result<()> {
25 let updated_rewards_per_token_stored = payroll.calculate_reward_per_token(current_ts)?;
26 self.rewards_per_token_stored = updated_rewards_per_token_stored;
28 self.annual_rewards_rate =
29 rewarder.compute_quarry_annual_rewards_rate(self.rewards_share)?;
30 self.last_update_ts = payroll.last_time_reward_applicable(current_ts);
31
32 Ok(())
33 }
34
35 pub fn update_rewards_and_miner(
38 &mut self,
39 miner: &mut Miner,
40 rewarder: &Rewarder,
41 current_ts: i64,
42 ) -> Result<()> {
43 let payroll: Payroll = (*self).into();
44 self.update_rewards_internal(current_ts, rewarder, &payroll)?;
45
46 let updated_rewards_earned = unwrap_int!(payroll
47 .calculate_rewards_earned(
48 current_ts,
49 miner.balance,
50 miner.rewards_per_token_paid,
51 miner.rewards_earned,
52 )?
53 .to_u64());
54
55 payroll.sanity_check(current_ts, updated_rewards_earned, miner)?;
56 miner.rewards_earned = updated_rewards_earned;
58 miner.rewards_per_token_paid = self.rewards_per_token_stored;
59
60 Ok(())
61 }
62
63 pub fn process_stake_action_internal(
65 &mut self,
66 action: StakeAction,
67 current_ts: i64,
68 rewarder: &Rewarder,
69 miner: &mut Miner,
70 amount: u64,
71 ) -> Result<()> {
72 self.update_rewards_and_miner(miner, rewarder, current_ts)?;
73 match action {
74 StakeAction::Stake => {
75 miner.balance = unwrap_int!(miner.balance.checked_add(amount));
76 self.total_tokens_deposited =
77 unwrap_int!(self.total_tokens_deposited.checked_add(amount));
78 }
79 StakeAction::Withdraw => {
80 miner.balance = unwrap_int!(miner.balance.checked_sub(amount));
81 self.total_tokens_deposited =
82 unwrap_int!(self.total_tokens_deposited.checked_sub(amount));
83 }
84 }
85
86 Ok(())
87 }
88}
89
90#[cfg(test)]
91#[allow(clippy::unwrap_used)]
92mod tests {
93 use super::*;
94 use crate::{payroll::PRECISION_MULTIPLIER, quarry::StakeAction};
95
96 const SECONDS_PER_DAY: u64 = 86_400;
97 const DEFAULT_TOKEN_DECIMALS: u8 = 6;
98
99 pub struct MinerVault {
100 balance: u64,
101 }
102
103 fn sim_claim(
104 current_ts: i64,
105 rewarder: &Rewarder,
106 quarry: &mut Quarry,
107 _vault: &mut MinerVault,
108 miner: &mut Miner,
109 ) -> u64 {
110 quarry
111 .update_rewards_and_miner(miner, rewarder, current_ts)
112 .unwrap();
113 let amount_claimable = miner.rewards_earned;
114 miner.rewards_earned = 0;
115
116 amount_claimable
117 }
118
119 fn sim_stake(
120 current_ts: i64,
121 rewarder: &Rewarder,
122 quarry: &mut Quarry,
123 vault: &mut MinerVault,
124 miner: &mut Miner,
125 amount: u64,
126 ) {
127 quarry
128 .process_stake_action_internal(StakeAction::Stake, current_ts, rewarder, miner, amount)
129 .unwrap();
130 vault.balance += amount;
131 }
132
133 fn sim_withdraw(
134 current_ts: i64,
135 rewarder: &Rewarder,
136 quarry: &mut Quarry,
137 vault: &mut MinerVault,
138 miner: &mut Miner,
139 amount: u64,
140 ) {
141 quarry
142 .process_stake_action_internal(
143 StakeAction::Withdraw,
144 current_ts,
145 rewarder,
146 miner,
147 amount,
148 )
149 .unwrap();
150 vault.balance -= amount;
151 }
152
153 fn to_unit(amt: u64) -> u64 {
154 amt * 1_000_000
155 }
156
157 #[test]
158 fn test_lifecycle_one_miner() {
159 let quarry = &mut Quarry::default();
160 quarry.famine_ts = i64::MAX;
161 quarry.rewards_share = 100;
162 quarry.token_mint_decimals = DEFAULT_TOKEN_DECIMALS;
163 let miner_vault = &mut MinerVault { balance: 0 };
164
165 let daily_rewards_rate = to_unit(5_000);
166 let annual_rewards_rate = daily_rewards_rate * 365;
167 let rewarder = Rewarder {
168 bump: 254,
169 annual_rewards_rate,
170 num_quarries: 1,
171 total_rewards_shares: quarry.rewards_share,
172 ..Default::default()
173 };
174
175 let miner = &mut Miner::default();
176
177 let mut current_ts: i64 = 0;
178 let total_to_stake = to_unit(500);
179
180 sim_stake(
182 current_ts,
183 &rewarder,
184 quarry,
185 miner_vault,
186 miner,
187 total_to_stake,
188 );
189 assert!(quarry.annual_rewards_rate > 0);
190 assert_eq!(miner_vault.balance, total_to_stake);
191
192 current_ts += SECONDS_PER_DAY as i64 * 6;
194 let expected_rewards_earned = daily_rewards_rate * 6;
195
196 let withdraw_amount = to_unit(250);
198 sim_withdraw(
199 current_ts,
200 &rewarder,
201 quarry,
202 miner_vault,
203 miner,
204 withdraw_amount,
205 );
206 assert!(quarry.rewards_per_token_stored > 0);
207 assert_eq!(
208 miner.rewards_earned,
209 (miner.rewards_per_token_paid * (total_to_stake as u128) / PRECISION_MULTIPLIER)
210 .to_u64()
211 .unwrap()
212 );
213 assert_eq!(miner.rewards_earned, expected_rewards_earned);
214 assert_eq!(miner_vault.balance, total_to_stake - withdraw_amount);
215
216 let expected_rewards_earned = miner.rewards_earned;
218 assert_eq!(
219 sim_claim(current_ts, &rewarder, quarry, miner_vault, miner),
220 expected_rewards_earned
221 );
222 assert_eq!(
224 sim_claim(current_ts, &rewarder, quarry, miner_vault, miner),
225 0
226 );
227
228 current_ts += SECONDS_PER_DAY as i64 * 6;
230
231 sim_withdraw(
233 current_ts,
234 &rewarder,
235 quarry,
236 miner_vault,
237 miner,
238 withdraw_amount,
239 );
240 assert_eq!(miner_vault.balance, 0);
241
242 assert_eq!(
244 sim_claim(current_ts, &rewarder, quarry, miner_vault, miner),
245 expected_rewards_earned
246 );
247
248 current_ts += SECONDS_PER_DAY as i64 * 6;
250
251 assert_eq!(
253 sim_claim(current_ts, &rewarder, quarry, miner_vault, miner),
254 0
255 );
256 }
257
258 #[test]
259 fn test_lifecycle_two_miners() {
260 let quarry = &mut Quarry::default();
261 quarry.famine_ts = i64::MAX;
262 quarry.rewards_share = 100;
263 quarry.token_mint_decimals = DEFAULT_TOKEN_DECIMALS;
264 let miner_vault_one = &mut MinerVault { balance: 0 };
265 let miner_vault_two = &mut MinerVault { balance: 0 };
266
267 let daily_rewards_rate = to_unit(5_000);
268 let annual_rewards_rate = daily_rewards_rate * 365;
269 let rewarder = Rewarder {
270 bump: 254,
271 annual_rewards_rate,
272 num_quarries: 1,
273 total_rewards_shares: quarry.rewards_share,
274 ..Default::default()
275 };
276 let miner_one = &mut Miner::default();
277 let miner_two = &mut Miner::default();
278
279 let mut current_ts: i64 = 0;
280 let total_to_stake = to_unit(500);
281
282 sim_stake(
284 current_ts,
285 &rewarder,
286 quarry,
287 miner_vault_one,
288 miner_one,
289 total_to_stake,
290 );
291 assert_eq!(miner_vault_one.balance, total_to_stake);
292 assert_eq!(miner_one.balance, miner_vault_one.balance);
293 sim_stake(
294 current_ts,
295 &rewarder,
296 quarry,
297 miner_vault_two,
298 miner_two,
299 total_to_stake,
300 );
301 assert_eq!(miner_vault_two.balance, total_to_stake);
302 assert_eq!(miner_two.balance, miner_vault_two.balance);
303 assert!(quarry.annual_rewards_rate > 0);
304
305 current_ts += SECONDS_PER_DAY as i64 * 3;
307
308 sim_withdraw(
310 current_ts,
311 &rewarder,
312 quarry,
313 miner_vault_two,
314 miner_two,
315 total_to_stake,
316 );
317 assert!(quarry.rewards_per_token_stored > 0);
318 assert_eq!(
319 miner_two.rewards_earned,
320 (miner_two.rewards_per_token_paid * (total_to_stake as u128) / PRECISION_MULTIPLIER)
321 .to_u64()
322 .unwrap()
323 );
324 assert_eq!(miner_vault_two.balance, 0);
325 assert_eq!(miner_two.balance, miner_vault_two.balance);
326
327 current_ts += SECONDS_PER_DAY as i64 * 3;
329
330 let total_distributed = daily_rewards_rate * 6; let expected_miner_one_rewards_earned = total_distributed * 3 / 4;
333 let expected_miner_two_rewards_earned = total_distributed / 4;
334 assert_eq!(
335 sim_claim(current_ts, &rewarder, quarry, miner_vault_one, miner_one),
336 expected_miner_one_rewards_earned
337 );
338 assert_eq!(
339 sim_claim(current_ts, &rewarder, quarry, miner_vault_two, miner_two),
340 expected_miner_two_rewards_earned
341 );
342
343 current_ts += SECONDS_PER_DAY as i64 * 6;
345
346 let expected_miner_one_rewards_earned = daily_rewards_rate * 6;
348 let expected_miner_two_rewards_earned = 0;
349 assert_eq!(
350 sim_claim(current_ts, &rewarder, quarry, miner_vault_one, miner_one),
351 expected_miner_one_rewards_earned
352 );
353 assert_eq!(
354 sim_claim(current_ts, &rewarder, quarry, miner_vault_two, miner_two),
355 expected_miner_two_rewards_earned
356 );
357
358 sim_stake(
360 current_ts,
361 &rewarder,
362 quarry,
363 miner_vault_two,
364 miner_two,
365 total_to_stake,
366 );
367 assert_eq!(miner_vault_two.balance, total_to_stake);
368 assert_eq!(miner_two.balance, miner_vault_two.balance);
369
370 current_ts += SECONDS_PER_DAY as i64 * 6;
372
373 let expected_miner_one_rewards_earned = expected_miner_one_rewards_earned / 2;
375 let expected_miner_two_rewards_earned = expected_miner_one_rewards_earned;
376 assert_eq!(
377 sim_claim(current_ts, &rewarder, quarry, miner_vault_one, miner_one),
378 expected_miner_one_rewards_earned
379 );
380 assert_eq!(
381 sim_claim(current_ts, &rewarder, quarry, miner_vault_two, miner_two),
382 expected_miner_two_rewards_earned
383 );
384 }
385}