jiminy_staking/staking.rs
1//! Staking rewards math (reward-per-token accumulator).
2//!
3//! Distributes rewards proportionally to stakers without iterating over
4//! all users. A global accumulator tracks rewards per staked unit; each
5//! user stores a debt checkpoint.
6//!
7//! The pattern:
8//! 1. Global accumulator: `reward_per_token += rewards_earned / total_staked`
9//! 2. User debt: `user.reward_debt = user.staked * global.reward_per_token`
10//! 3. Pending: `claimable = user.staked * global.reward_per_token - user.reward_debt`
11//!
12//! All values use a `PRECISION` scaling factor (1e12) to avoid precision loss
13//! when dividing small reward amounts by large total stakes.
14
15use pinocchio::error::ProgramError;
16
17/// Scaling factor for reward-per-token accumulator (1e12).
18///
19/// This provides 12 decimal places of precision. Enough for any practical
20/// staking scenario without overflowing u128 for reasonable token amounts.
21pub const REWARD_PRECISION: u128 = 1_000_000_000_000;
22
23/// Update the global reward-per-token accumulator.
24///
25/// Call this every time rewards are distributed or a user stakes/unstakes.
26///
27/// `reward_per_token` is the current accumulator value (scaled by `REWARD_PRECISION`).
28/// `rewards_since_last` is the new rewards to distribute since the last update.
29/// `total_staked` is the total amount currently staked across all users.
30///
31/// Returns the updated accumulator. If `total_staked == 0`, returns the
32/// current value unchanged (rewards are not distributed to nobody).
33///
34/// ```rust,ignore
35/// let new_rpt = update_reward_per_token(pool.reward_per_token, new_rewards, pool.total_staked)?;
36/// ```
37#[inline(always)]
38pub fn update_reward_per_token(
39 reward_per_token: u128,
40 rewards_since_last: u64,
41 total_staked: u64,
42) -> Result<u128, ProgramError> {
43 if total_staked == 0 {
44 return Ok(reward_per_token);
45 }
46 let increment = (rewards_since_last as u128)
47 .checked_mul(REWARD_PRECISION)
48 .ok_or(ProgramError::ArithmeticOverflow)?
49 / total_staked as u128;
50 reward_per_token
51 .checked_add(increment)
52 .ok_or(ProgramError::ArithmeticOverflow)
53}
54
55/// Calculate a user's pending (claimable) rewards.
56///
57/// `user_staked` is the user's staked amount.
58/// `reward_per_token` is the current global accumulator.
59/// `user_reward_debt` is the user's stored reward debt.
60///
61/// Returns the claimable reward amount as u64.
62///
63/// ```rust,ignore
64/// let claimable = pending_rewards(user.staked, pool.reward_per_token, user.reward_debt)?;
65/// ```
66#[inline(always)]
67pub fn pending_rewards(
68 user_staked: u64,
69 reward_per_token: u128,
70 user_reward_debt: u128,
71) -> Result<u64, ProgramError> {
72 let accumulated = (user_staked as u128)
73 .checked_mul(reward_per_token)
74 .ok_or(ProgramError::ArithmeticOverflow)?
75 / REWARD_PRECISION;
76 let debt_normalized = user_reward_debt / REWARD_PRECISION;
77 let pending = accumulated.saturating_sub(debt_normalized);
78 if pending > u64::MAX as u128 {
79 return Err(ProgramError::ArithmeticOverflow);
80 }
81 Ok(pending as u64)
82}
83
84/// Compute the reward debt for a user after staking or claiming.
85///
86/// Store this value in the user's account after every stake/unstake/claim.
87///
88/// ```rust,ignore
89/// user.reward_debt = update_reward_debt(user.staked, pool.reward_per_token);
90/// ```
91#[inline(always)]
92pub fn update_reward_debt(user_staked: u64, reward_per_token: u128) -> u128 {
93 (user_staked as u128) * reward_per_token
94}
95
96/// Calculate the emission rate (rewards per second).
97///
98/// ```rust,ignore
99/// let rate = emission_rate(total_rewards, duration_seconds)?;
100/// ```
101#[inline(always)]
102pub fn emission_rate(total_rewards: u64, duration_seconds: u64) -> Result<u64, ProgramError> {
103 if duration_seconds == 0 {
104 return Err(ProgramError::ArithmeticOverflow);
105 }
106 Ok(total_rewards / duration_seconds)
107}
108
109/// Calculate rewards earned since the last update, given an emission rate
110/// and elapsed time.
111///
112/// ```rust,ignore
113/// let earned = rewards_earned(rate, elapsed_seconds)?;
114/// ```
115#[inline(always)]
116pub fn rewards_earned(rate_per_second: u64, elapsed_seconds: u64) -> Result<u64, ProgramError> {
117 (rate_per_second as u128)
118 .checked_mul(elapsed_seconds as u128)
119 .ok_or(ProgramError::ArithmeticOverflow)
120 .and_then(|v| {
121 if v > u64::MAX as u128 {
122 Err(ProgramError::ArithmeticOverflow)
123 } else {
124 Ok(v as u64)
125 }
126 })
127}