Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 13375af

Browse files
authored
stake-pool: Add last epoch values for APR calculation (#2491)
* stake-pool: Add last epoch values for APR calculation * Bump versions for release
1 parent 06563bf commit 13375af

File tree

13 files changed

+114
-83
lines changed

13 files changed

+114
-83
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

stake-pool/cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ homepage = "https://spl.solana.com/stake-pool"
66
license = "Apache-2.0"
77
name = "spl-stake-pool-cli"
88
repository = "https://github.com/solana-labs/solana-program-library"
9-
version = "0.6.0"
9+
version = "0.6.1"
1010

1111
[dependencies]
1212
borsh = "0.9"

stake-pool/cli/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,7 @@ fn command_list(config: &Config, stake_pool_address: &Pubkey) -> CommandResult {
967967
}
968968
println!(
969969
"Total Pool Stake: {}{}",
970-
Sol(stake_pool.total_stake_lamports),
970+
Sol(stake_pool.total_lamports),
971971
if stake_pool.last_update_epoch != epoch_info.epoch {
972972
" [UPDATE REQUIRED]"
973973
} else {
@@ -1542,7 +1542,7 @@ fn command_list_all_pools(config: &Config) -> CommandResult {
15421542
"Address: {}\tManager: {}\tLamports: {}\tPool tokens: {}\tValidators: {}",
15431543
address,
15441544
stake_pool.manager,
1545-
stake_pool.total_stake_lamports,
1545+
stake_pool.total_lamports,
15461546
stake_pool.pool_token_supply,
15471547
validator_list.validators.len()
15481548
);

stake-pool/program/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "spl-stake-pool"
3-
version = "0.6.0"
3+
version = "0.6.1"
44
description = "Solana Program Library Stake Pool"
55
authors = ["Solana Maintainers <[email protected]>"]
66
repository = "https://github.com/solana-labs/solana-program-library"

stake-pool/program/src/processor.rs

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -607,8 +607,7 @@ impl Processor {
607607
let stake_state = try_from_slice_unchecked::<stake_program::StakeState>(
608608
&reserve_stake_info.data.borrow(),
609609
)?;
610-
let total_stake_lamports = if let stake_program::StakeState::Initialized(meta) = stake_state
611-
{
610+
let total_lamports = if let stake_program::StakeState::Initialized(meta) = stake_state {
612611
if meta.lockup != stake_program::Lockup::default() {
613612
msg!("Reserve stake account has some lockup");
614613
return Err(StakePoolError::WrongStakeState.into());
@@ -653,7 +652,7 @@ impl Processor {
653652
stake_pool.manager_fee_account = *manager_fee_info.key;
654653
stake_pool.token_program_id = *token_program_info.key;
655654
stake_pool.last_update_epoch = Clock::get()?.epoch;
656-
stake_pool.total_stake_lamports = total_stake_lamports;
655+
stake_pool.total_lamports = total_lamports;
657656
stake_pool.epoch_fee = epoch_fee;
658657
stake_pool.next_epoch_fee = None;
659658
stake_pool.preferred_deposit_validator_vote_address = None;
@@ -1614,30 +1613,31 @@ impl Processor {
16141613
return Err(StakePoolError::InvalidState.into());
16151614
}
16161615

1617-
let previous_lamports = stake_pool.total_stake_lamports;
1616+
let previous_lamports = stake_pool.total_lamports;
1617+
let previous_pool_token_supply = stake_pool.pool_token_supply;
16181618
let reserve_stake = try_from_slice_unchecked::<stake_program::StakeState>(
16191619
&reserve_stake_info.data.borrow(),
16201620
)?;
1621-
let mut total_stake_lamports =
1622-
if let stake_program::StakeState::Initialized(meta) = reserve_stake {
1623-
reserve_stake_info
1624-
.lamports()
1625-
.checked_sub(minimum_reserve_lamports(&meta))
1626-
.ok_or(StakePoolError::CalculationFailure)?
1627-
} else {
1628-
msg!("Reserve stake account in unknown state, aborting");
1629-
return Err(StakePoolError::WrongStakeState.into());
1630-
};
1621+
let mut total_lamports = if let stake_program::StakeState::Initialized(meta) = reserve_stake
1622+
{
1623+
reserve_stake_info
1624+
.lamports()
1625+
.checked_sub(minimum_reserve_lamports(&meta))
1626+
.ok_or(StakePoolError::CalculationFailure)?
1627+
} else {
1628+
msg!("Reserve stake account in unknown state, aborting");
1629+
return Err(StakePoolError::WrongStakeState.into());
1630+
};
16311631
for validator_stake_record in validator_list.iter::<ValidatorStakeInfo>() {
16321632
if validator_stake_record.last_update_epoch < clock.epoch {
16331633
return Err(StakePoolError::StakeListOutOfDate.into());
16341634
}
1635-
total_stake_lamports = total_stake_lamports
1635+
total_lamports = total_lamports
16361636
.checked_add(validator_stake_record.stake_lamports())
16371637
.ok_or(StakePoolError::CalculationFailure)?;
16381638
}
16391639

1640-
let reward_lamports = total_stake_lamports.saturating_sub(previous_lamports);
1640+
let reward_lamports = total_lamports.saturating_sub(previous_lamports);
16411641

16421642
// If the manager fee info is invalid, they don't deserve to receive the fee.
16431643
let fee = if stake_pool.check_manager_fee_info(manager_fee_info).is_ok() {
@@ -1659,11 +1659,6 @@ impl Processor {
16591659
stake_pool.stake_withdraw_bump_seed,
16601660
fee,
16611661
)?;
1662-
1663-
stake_pool.pool_token_supply = stake_pool
1664-
.pool_token_supply
1665-
.checked_add(fee)
1666-
.ok_or(StakePoolError::CalculationFailure)?;
16671662
}
16681663

16691664
if stake_pool.last_update_epoch < clock.epoch {
@@ -1680,8 +1675,10 @@ impl Processor {
16801675
stake_pool.next_sol_withdrawal_fee = None;
16811676
}
16821677
stake_pool.last_update_epoch = clock.epoch;
1678+
stake_pool.last_epoch_total_lamports = previous_lamports;
1679+
stake_pool.last_epoch_pool_token_supply = previous_pool_token_supply;
16831680
}
1684-
stake_pool.total_stake_lamports = total_stake_lamports;
1681+
stake_pool.total_lamports = total_lamports;
16851682

16861683
let pool_mint = Mint::unpack_from_slice(&pool_mint_info.data.borrow())?;
16871684
stake_pool.pool_token_supply = pool_mint.supply;
@@ -1974,8 +1971,8 @@ impl Processor {
19741971
.ok_or(StakePoolError::CalculationFailure)?;
19751972
// We treat the extra lamports as though they were
19761973
// transferred directly to the reserve stake account.
1977-
stake_pool.total_stake_lamports = stake_pool
1978-
.total_stake_lamports
1974+
stake_pool.total_lamports = stake_pool
1975+
.total_lamports
19791976
.checked_add(all_deposit_lamports)
19801977
.ok_or(StakePoolError::CalculationFailure)?;
19811978
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
@@ -2121,8 +2118,8 @@ impl Processor {
21212118
.pool_token_supply
21222119
.checked_add(new_pool_tokens)
21232120
.ok_or(StakePoolError::CalculationFailure)?;
2124-
stake_pool.total_stake_lamports = stake_pool
2125-
.total_stake_lamports
2121+
stake_pool.total_lamports = stake_pool
2122+
.total_lamports
21262123
.checked_add(deposit_lamports)
21272124
.ok_or(StakePoolError::CalculationFailure)?;
21282125
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
@@ -2345,8 +2342,8 @@ impl Processor {
23452342
.pool_token_supply
23462343
.checked_sub(pool_tokens_burnt)
23472344
.ok_or(StakePoolError::CalculationFailure)?;
2348-
stake_pool.total_stake_lamports = stake_pool
2349-
.total_stake_lamports
2345+
stake_pool.total_lamports = stake_pool
2346+
.total_lamports
23502347
.checked_sub(withdraw_lamports)
23512348
.ok_or(StakePoolError::CalculationFailure)?;
23522349
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
@@ -2500,8 +2497,8 @@ impl Processor {
25002497
.pool_token_supply
25012498
.checked_sub(pool_tokens_burnt)
25022499
.ok_or(StakePoolError::CalculationFailure)?;
2503-
stake_pool.total_stake_lamports = stake_pool
2504-
.total_stake_lamports
2500+
stake_pool.total_lamports = stake_pool
2501+
.total_lamports
25052502
.checked_sub(withdraw_lamports)
25062503
.ok_or(StakePoolError::CalculationFailure)?;
25072504
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;

stake-pool/program/src/state.rs

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,12 @@ pub struct StakePool {
8686
/// Total stake under management.
8787
/// Note that if `last_update_epoch` does not match the current epoch then
8888
/// this field may not be accurate
89-
pub total_stake_lamports: u64,
89+
pub total_lamports: u64,
9090

9191
/// Total supply of pool tokens (should always match the supply in the Pool Mint)
9292
pub pool_token_supply: u64,
9393

94-
/// Last epoch the `total_stake_lamports` field was updated
94+
/// Last epoch the `total_lamports` field was updated
9595
pub last_update_epoch: u64,
9696

9797
/// Lockup that all stakes in the pool must have
@@ -146,18 +146,24 @@ pub struct StakePool {
146146

147147
/// Future SOL withdrawal fee, to be set for the following epoch
148148
pub next_sol_withdrawal_fee: Option<Fee>,
149+
150+
/// Last epoch's total pool tokens, used only for APR estimation
151+
pub last_epoch_pool_token_supply: u64,
152+
153+
/// Last epoch's total lamports, used only for APR estimation
154+
pub last_epoch_total_lamports: u64,
149155
}
150156
impl StakePool {
151157
/// calculate the pool tokens that should be minted for a deposit of `stake_lamports`
152158
#[inline]
153159
pub fn calc_pool_tokens_for_deposit(&self, stake_lamports: u64) -> Option<u64> {
154-
if self.total_stake_lamports == 0 || self.pool_token_supply == 0 {
160+
if self.total_lamports == 0 || self.pool_token_supply == 0 {
155161
return Some(stake_lamports);
156162
}
157163
u64::try_from(
158164
(stake_lamports as u128)
159165
.checked_mul(self.pool_token_supply as u128)?
160-
.checked_div(self.total_stake_lamports as u128)?,
166+
.checked_div(self.total_lamports as u128)?,
161167
)
162168
.ok()
163169
}
@@ -168,7 +174,7 @@ impl StakePool {
168174
// `checked_ceil_div` returns `None` for a 0 quotient result, but in this
169175
// case, a return of 0 is valid for small amounts of pool tokens. So
170176
// we check for that separately
171-
let numerator = (pool_tokens as u128).checked_mul(self.total_stake_lamports as u128)?;
177+
let numerator = (pool_tokens as u128).checked_mul(self.total_lamports as u128)?;
172178
let denominator = self.pool_token_supply as u128;
173179
if numerator < denominator || denominator == 0 {
174180
Some(0)
@@ -227,22 +233,21 @@ impl StakePool {
227233
/// Calculate the fee in pool tokens that goes to the manager
228234
///
229235
/// This function assumes that `reward_lamports` has not already been added
230-
/// to the stake pool's `total_stake_lamports`
236+
/// to the stake pool's `total_lamports`
231237
#[inline]
232238
pub fn calc_epoch_fee_amount(&self, reward_lamports: u64) -> Option<u64> {
233239
if reward_lamports == 0 {
234240
return Some(0);
235241
}
236-
let total_stake_lamports =
237-
(self.total_stake_lamports as u128).checked_add(reward_lamports as u128)?;
242+
let total_lamports = (self.total_lamports as u128).checked_add(reward_lamports as u128)?;
238243
let fee_lamports = self.epoch_fee.apply(reward_lamports)?;
239-
if total_stake_lamports == fee_lamports || self.pool_token_supply == 0 {
244+
if total_lamports == fee_lamports || self.pool_token_supply == 0 {
240245
Some(reward_lamports)
241246
} else {
242247
u64::try_from(
243248
(self.pool_token_supply as u128)
244249
.checked_mul(fee_lamports)?
245-
.checked_div(total_stake_lamports.checked_sub(fee_lamports)?)?,
250+
.checked_div(total_lamports.checked_sub(fee_lamports)?)?,
246251
)
247252
.ok()
248253
}
@@ -816,7 +821,10 @@ mod test {
816821
solana_program::borsh::{
817822
get_instance_packed_len, get_packed_len, try_from_slice_unchecked,
818823
},
819-
solana_program::native_token::LAMPORTS_PER_SOL,
824+
solana_program::{
825+
clock::{DEFAULT_SLOTS_PER_EPOCH, DEFAULT_S_PER_SLOT, SECONDS_PER_DAY},
826+
native_token::LAMPORTS_PER_SOL,
827+
},
820828
};
821829

822830
fn uninitialized_validator_list() -> ValidatorList {
@@ -992,11 +1000,11 @@ mod test {
9921000
}
9931001

9941002
prop_compose! {
995-
fn total_stake_and_rewards()(total_stake_lamports in 1..u64::MAX)(
996-
total_stake_lamports in Just(total_stake_lamports),
997-
rewards in 0..=total_stake_lamports,
1003+
fn total_stake_and_rewards()(total_lamports in 1..u64::MAX)(
1004+
total_lamports in Just(total_lamports),
1005+
rewards in 0..=total_lamports,
9981006
) -> (u64, u64) {
999-
(total_stake_lamports - rewards, rewards)
1007+
(total_lamports - rewards, rewards)
10001008
}
10011009
}
10021010

@@ -1008,15 +1016,15 @@ mod test {
10081016
denominator: 10,
10091017
};
10101018
let mut stake_pool = StakePool {
1011-
total_stake_lamports: 100 * LAMPORTS_PER_SOL,
1019+
total_lamports: 100 * LAMPORTS_PER_SOL,
10121020
pool_token_supply: 100 * LAMPORTS_PER_SOL,
10131021
epoch_fee,
10141022
..StakePool::default()
10151023
};
10161024
let reward_lamports = 10 * LAMPORTS_PER_SOL;
10171025
let pool_token_fee = stake_pool.calc_epoch_fee_amount(reward_lamports).unwrap();
10181026

1019-
stake_pool.total_stake_lamports += reward_lamports;
1027+
stake_pool.total_lamports += reward_lamports;
10201028
stake_pool.pool_token_supply += pool_token_fee;
10211029

10221030
let fee_lamports = stake_pool
@@ -1042,7 +1050,7 @@ mod test {
10421050
#[test]
10431051
fn divide_by_zero_fee() {
10441052
let stake_pool = StakePool {
1045-
total_stake_lamports: 0,
1053+
total_lamports: 0,
10461054
epoch_fee: Fee {
10471055
numerator: 1,
10481056
denominator: 10,
@@ -1054,22 +1062,44 @@ mod test {
10541062
assert_eq!(fee, rewards);
10551063
}
10561064

1065+
#[test]
1066+
fn approximate_apr_calculation() {
1067+
// 8% / year means roughly .044% / epoch
1068+
let stake_pool = StakePool {
1069+
last_epoch_total_lamports: 100_000,
1070+
last_epoch_pool_token_supply: 100_000,
1071+
total_lamports: 100_044,
1072+
pool_token_supply: 100_000,
1073+
..StakePool::default()
1074+
};
1075+
let pool_token_value =
1076+
stake_pool.total_lamports as f64 / stake_pool.pool_token_supply as f64;
1077+
let last_epoch_pool_token_value = stake_pool.last_epoch_total_lamports as f64
1078+
/ stake_pool.last_epoch_pool_token_supply as f64;
1079+
let epoch_rate = pool_token_value / last_epoch_pool_token_value - 1.0;
1080+
const SECONDS_PER_EPOCH: f64 = DEFAULT_SLOTS_PER_EPOCH as f64 * DEFAULT_S_PER_SLOT;
1081+
const EPOCHS_PER_YEAR: f64 = SECONDS_PER_DAY as f64 * 365.25 / SECONDS_PER_EPOCH;
1082+
const EPSILON: f64 = 0.00001;
1083+
let yearly_rate = epoch_rate * EPOCHS_PER_YEAR;
1084+
assert!((yearly_rate - 0.080355).abs() < EPSILON);
1085+
}
1086+
10571087
proptest! {
10581088
#[test]
10591089
fn fee_calculation(
10601090
(numerator, denominator) in fee(),
1061-
(total_stake_lamports, reward_lamports) in total_stake_and_rewards(),
1091+
(total_lamports, reward_lamports) in total_stake_and_rewards(),
10621092
) {
10631093
let epoch_fee = Fee { denominator, numerator };
10641094
let mut stake_pool = StakePool {
1065-
total_stake_lamports,
1066-
pool_token_supply: total_stake_lamports,
1095+
total_lamports,
1096+
pool_token_supply: total_lamports,
10671097
epoch_fee,
10681098
..StakePool::default()
10691099
};
10701100
let pool_token_fee = stake_pool.calc_epoch_fee_amount(reward_lamports).unwrap();
10711101

1072-
stake_pool.total_stake_lamports += reward_lamports;
1102+
stake_pool.total_lamports += reward_lamports;
10731103
stake_pool.pool_token_supply += pool_token_fee;
10741104

10751105
let fee_lamports = stake_pool.calc_lamports_withdraw_amount(pool_token_fee).unwrap();
@@ -1082,7 +1112,7 @@ mod test {
10821112
// since we do two "flooring" conversions, the max epsilon should be
10831113
// correct up to 2 lamports (one for each floor division), plus a
10841114
// correction for huge discrepancies between rewards and total stake
1085-
let epsilon = 2 + reward_lamports / total_stake_lamports;
1115+
let epsilon = 2 + reward_lamports / total_lamports;
10861116
assert!(max_fee_lamports - fee_lamports <= epsilon,
10871117
"Max expected fee in lamports {}, actually receive {}, epsilon {}",
10881118
max_fee_lamports, fee_lamports, epsilon);
@@ -1102,16 +1132,16 @@ mod test {
11021132
proptest! {
11031133
#[test]
11041134
fn deposit_and_withdraw(
1105-
(total_stake_lamports, pool_token_supply, deposit_stake) in total_tokens_and_deposit()
1135+
(total_lamports, pool_token_supply, deposit_stake) in total_tokens_and_deposit()
11061136
) {
11071137
let mut stake_pool = StakePool {
1108-
total_stake_lamports,
1138+
total_lamports,
11091139
pool_token_supply,
11101140
..StakePool::default()
11111141
};
11121142
let deposit_result = stake_pool.calc_pool_tokens_for_deposit(deposit_stake).unwrap();
11131143
prop_assume!(deposit_result > 0);
1114-
stake_pool.total_stake_lamports += deposit_stake;
1144+
stake_pool.total_lamports += deposit_stake;
11151145
stake_pool.pool_token_supply += deposit_result;
11161146
let withdraw_result = stake_pool.calc_lamports_withdraw_amount(deposit_result).unwrap();
11171147
assert!(withdraw_result <= deposit_stake);

0 commit comments

Comments
 (0)