Skip to main content

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}