quarry_mine/
quarry.rs

1//! Quarry-related math and helpers.
2
3use anchor_lang::prelude::*;
4use vipers::prelude::*;
5
6use crate::{payroll::Payroll, Miner, Quarry, Rewarder};
7use num_traits::cast::ToPrimitive;
8
9/// An action for a user to take on the staking pool.
10pub enum StakeAction {
11    /// Stake into a [Quarry].
12    Stake,
13    /// Withdraw from the [Quarry].
14    Withdraw,
15}
16
17impl Quarry {
18    /// Updates the quarry by synchronizing its rewards rate with the rewarder.
19    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        // Update quarry struct
27        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    /// Updates the quarry and miner with the latest info.
36    /// <https://github.com/Synthetixio/synthetix/blob/aeee6b2c82588681e1f99202663346098d1866ac/contracts/StakingRewards.sol#L158>
37    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        // Update miner struct
57        miner.rewards_earned = updated_rewards_earned;
58        miner.rewards_per_token_paid = self.rewards_per_token_stored;
59
60        Ok(())
61    }
62
63    /// Processes a [StakeAction] for a [Miner],
64    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        // Stake tokens
181        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        // Fastforward time by 6 days
193        current_ts += SECONDS_PER_DAY as i64 * 6;
194        let expected_rewards_earned = daily_rewards_rate * 6;
195
196        // Withdraw half
197        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        // Claim rewards
217        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        // Should not allow double claim
223        assert_eq!(
224            sim_claim(current_ts, &rewarder, quarry, miner_vault, miner),
225            0
226        );
227
228        // Fastforward time another 6 days
229        current_ts += SECONDS_PER_DAY as i64 * 6;
230
231        // Withdraw remaining half
232        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        // Claim rewards, still the same since we're the only miner in the quarry
243        assert_eq!(
244            sim_claim(current_ts, &rewarder, quarry, miner_vault, miner),
245            expected_rewards_earned
246        );
247
248        // Fastforward time by 6 days
249        current_ts += SECONDS_PER_DAY as i64 * 6;
250
251        // Claim rewards again, should be 0 since all tokens were withdrawn
252        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        // Stake tokens
283        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        // Fastforward time by 3 days
306        current_ts += SECONDS_PER_DAY as i64 * 3;
307
308        // Miner two withdraws their stake
309        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        // Fastforward time by 3 days
328        current_ts += SECONDS_PER_DAY as i64 * 3;
329
330        // Claim rewards
331        let total_distributed = daily_rewards_rate * 6; // 6 days of rewards
332        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        // Fastforward time by 6 days
344        current_ts += SECONDS_PER_DAY as i64 * 6;
345
346        // Claim rewards
347        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        // Miner two re-stakes
359        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        // Fastforward time by 6 days
371        current_ts += SECONDS_PER_DAY as i64 * 6;
372
373        // Claim rewards
374        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}