stake-pool: Add last epoch values for APR calculation (#2491)
* stake-pool: Add last epoch values for APR calculation * Bump versions for release
This commit is contained in:
parent
06563bf879
commit
13375afff2
|
@ -3779,7 +3779,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spl-stake-pool"
|
name = "spl-stake-pool"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayref",
|
"arrayref",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
@ -3801,7 +3801,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spl-stake-pool-cli"
|
name = "spl-stake-pool-cli"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"borsh",
|
"borsh",
|
||||||
|
|
|
@ -6,7 +6,7 @@ homepage = "https://spl.solana.com/stake-pool"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
name = "spl-stake-pool-cli"
|
name = "spl-stake-pool-cli"
|
||||||
repository = "https://github.com/solana-labs/solana-program-library"
|
repository = "https://github.com/solana-labs/solana-program-library"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
borsh = "0.9"
|
borsh = "0.9"
|
||||||
|
|
|
@ -967,7 +967,7 @@ fn command_list(config: &Config, stake_pool_address: &Pubkey) -> CommandResult {
|
||||||
}
|
}
|
||||||
println!(
|
println!(
|
||||||
"Total Pool Stake: {}{}",
|
"Total Pool Stake: {}{}",
|
||||||
Sol(stake_pool.total_stake_lamports),
|
Sol(stake_pool.total_lamports),
|
||||||
if stake_pool.last_update_epoch != epoch_info.epoch {
|
if stake_pool.last_update_epoch != epoch_info.epoch {
|
||||||
" [UPDATE REQUIRED]"
|
" [UPDATE REQUIRED]"
|
||||||
} else {
|
} else {
|
||||||
|
@ -1542,7 +1542,7 @@ fn command_list_all_pools(config: &Config) -> CommandResult {
|
||||||
"Address: {}\tManager: {}\tLamports: {}\tPool tokens: {}\tValidators: {}",
|
"Address: {}\tManager: {}\tLamports: {}\tPool tokens: {}\tValidators: {}",
|
||||||
address,
|
address,
|
||||||
stake_pool.manager,
|
stake_pool.manager,
|
||||||
stake_pool.total_stake_lamports,
|
stake_pool.total_lamports,
|
||||||
stake_pool.pool_token_supply,
|
stake_pool.pool_token_supply,
|
||||||
validator_list.validators.len()
|
validator_list.validators.len()
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "spl-stake-pool"
|
name = "spl-stake-pool"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
description = "Solana Program Library Stake Pool"
|
description = "Solana Program Library Stake Pool"
|
||||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||||
repository = "https://github.com/solana-labs/solana-program-library"
|
repository = "https://github.com/solana-labs/solana-program-library"
|
||||||
|
|
|
@ -607,8 +607,7 @@ impl Processor {
|
||||||
let stake_state = try_from_slice_unchecked::<stake_program::StakeState>(
|
let stake_state = try_from_slice_unchecked::<stake_program::StakeState>(
|
||||||
&reserve_stake_info.data.borrow(),
|
&reserve_stake_info.data.borrow(),
|
||||||
)?;
|
)?;
|
||||||
let total_stake_lamports = if let stake_program::StakeState::Initialized(meta) = stake_state
|
let total_lamports = if let stake_program::StakeState::Initialized(meta) = stake_state {
|
||||||
{
|
|
||||||
if meta.lockup != stake_program::Lockup::default() {
|
if meta.lockup != stake_program::Lockup::default() {
|
||||||
msg!("Reserve stake account has some lockup");
|
msg!("Reserve stake account has some lockup");
|
||||||
return Err(StakePoolError::WrongStakeState.into());
|
return Err(StakePoolError::WrongStakeState.into());
|
||||||
|
@ -653,7 +652,7 @@ impl Processor {
|
||||||
stake_pool.manager_fee_account = *manager_fee_info.key;
|
stake_pool.manager_fee_account = *manager_fee_info.key;
|
||||||
stake_pool.token_program_id = *token_program_info.key;
|
stake_pool.token_program_id = *token_program_info.key;
|
||||||
stake_pool.last_update_epoch = Clock::get()?.epoch;
|
stake_pool.last_update_epoch = Clock::get()?.epoch;
|
||||||
stake_pool.total_stake_lamports = total_stake_lamports;
|
stake_pool.total_lamports = total_lamports;
|
||||||
stake_pool.epoch_fee = epoch_fee;
|
stake_pool.epoch_fee = epoch_fee;
|
||||||
stake_pool.next_epoch_fee = None;
|
stake_pool.next_epoch_fee = None;
|
||||||
stake_pool.preferred_deposit_validator_vote_address = None;
|
stake_pool.preferred_deposit_validator_vote_address = None;
|
||||||
|
@ -1614,12 +1613,13 @@ impl Processor {
|
||||||
return Err(StakePoolError::InvalidState.into());
|
return Err(StakePoolError::InvalidState.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let previous_lamports = stake_pool.total_stake_lamports;
|
let previous_lamports = stake_pool.total_lamports;
|
||||||
|
let previous_pool_token_supply = stake_pool.pool_token_supply;
|
||||||
let reserve_stake = try_from_slice_unchecked::<stake_program::StakeState>(
|
let reserve_stake = try_from_slice_unchecked::<stake_program::StakeState>(
|
||||||
&reserve_stake_info.data.borrow(),
|
&reserve_stake_info.data.borrow(),
|
||||||
)?;
|
)?;
|
||||||
let mut total_stake_lamports =
|
let mut total_lamports = if let stake_program::StakeState::Initialized(meta) = reserve_stake
|
||||||
if let stake_program::StakeState::Initialized(meta) = reserve_stake {
|
{
|
||||||
reserve_stake_info
|
reserve_stake_info
|
||||||
.lamports()
|
.lamports()
|
||||||
.checked_sub(minimum_reserve_lamports(&meta))
|
.checked_sub(minimum_reserve_lamports(&meta))
|
||||||
|
@ -1632,12 +1632,12 @@ impl Processor {
|
||||||
if validator_stake_record.last_update_epoch < clock.epoch {
|
if validator_stake_record.last_update_epoch < clock.epoch {
|
||||||
return Err(StakePoolError::StakeListOutOfDate.into());
|
return Err(StakePoolError::StakeListOutOfDate.into());
|
||||||
}
|
}
|
||||||
total_stake_lamports = total_stake_lamports
|
total_lamports = total_lamports
|
||||||
.checked_add(validator_stake_record.stake_lamports())
|
.checked_add(validator_stake_record.stake_lamports())
|
||||||
.ok_or(StakePoolError::CalculationFailure)?;
|
.ok_or(StakePoolError::CalculationFailure)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let reward_lamports = total_stake_lamports.saturating_sub(previous_lamports);
|
let reward_lamports = total_lamports.saturating_sub(previous_lamports);
|
||||||
|
|
||||||
// If the manager fee info is invalid, they don't deserve to receive the fee.
|
// If the manager fee info is invalid, they don't deserve to receive the fee.
|
||||||
let fee = if stake_pool.check_manager_fee_info(manager_fee_info).is_ok() {
|
let fee = if stake_pool.check_manager_fee_info(manager_fee_info).is_ok() {
|
||||||
|
@ -1659,11 +1659,6 @@ impl Processor {
|
||||||
stake_pool.stake_withdraw_bump_seed,
|
stake_pool.stake_withdraw_bump_seed,
|
||||||
fee,
|
fee,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
stake_pool.pool_token_supply = stake_pool
|
|
||||||
.pool_token_supply
|
|
||||||
.checked_add(fee)
|
|
||||||
.ok_or(StakePoolError::CalculationFailure)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if stake_pool.last_update_epoch < clock.epoch {
|
if stake_pool.last_update_epoch < clock.epoch {
|
||||||
|
@ -1680,8 +1675,10 @@ impl Processor {
|
||||||
stake_pool.next_sol_withdrawal_fee = None;
|
stake_pool.next_sol_withdrawal_fee = None;
|
||||||
}
|
}
|
||||||
stake_pool.last_update_epoch = clock.epoch;
|
stake_pool.last_update_epoch = clock.epoch;
|
||||||
|
stake_pool.last_epoch_total_lamports = previous_lamports;
|
||||||
|
stake_pool.last_epoch_pool_token_supply = previous_pool_token_supply;
|
||||||
}
|
}
|
||||||
stake_pool.total_stake_lamports = total_stake_lamports;
|
stake_pool.total_lamports = total_lamports;
|
||||||
|
|
||||||
let pool_mint = Mint::unpack_from_slice(&pool_mint_info.data.borrow())?;
|
let pool_mint = Mint::unpack_from_slice(&pool_mint_info.data.borrow())?;
|
||||||
stake_pool.pool_token_supply = pool_mint.supply;
|
stake_pool.pool_token_supply = pool_mint.supply;
|
||||||
|
@ -1974,8 +1971,8 @@ impl Processor {
|
||||||
.ok_or(StakePoolError::CalculationFailure)?;
|
.ok_or(StakePoolError::CalculationFailure)?;
|
||||||
// We treat the extra lamports as though they were
|
// We treat the extra lamports as though they were
|
||||||
// transferred directly to the reserve stake account.
|
// transferred directly to the reserve stake account.
|
||||||
stake_pool.total_stake_lamports = stake_pool
|
stake_pool.total_lamports = stake_pool
|
||||||
.total_stake_lamports
|
.total_lamports
|
||||||
.checked_add(all_deposit_lamports)
|
.checked_add(all_deposit_lamports)
|
||||||
.ok_or(StakePoolError::CalculationFailure)?;
|
.ok_or(StakePoolError::CalculationFailure)?;
|
||||||
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
|
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
|
||||||
|
@ -2121,8 +2118,8 @@ impl Processor {
|
||||||
.pool_token_supply
|
.pool_token_supply
|
||||||
.checked_add(new_pool_tokens)
|
.checked_add(new_pool_tokens)
|
||||||
.ok_or(StakePoolError::CalculationFailure)?;
|
.ok_or(StakePoolError::CalculationFailure)?;
|
||||||
stake_pool.total_stake_lamports = stake_pool
|
stake_pool.total_lamports = stake_pool
|
||||||
.total_stake_lamports
|
.total_lamports
|
||||||
.checked_add(deposit_lamports)
|
.checked_add(deposit_lamports)
|
||||||
.ok_or(StakePoolError::CalculationFailure)?;
|
.ok_or(StakePoolError::CalculationFailure)?;
|
||||||
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
|
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
|
||||||
|
@ -2345,8 +2342,8 @@ impl Processor {
|
||||||
.pool_token_supply
|
.pool_token_supply
|
||||||
.checked_sub(pool_tokens_burnt)
|
.checked_sub(pool_tokens_burnt)
|
||||||
.ok_or(StakePoolError::CalculationFailure)?;
|
.ok_or(StakePoolError::CalculationFailure)?;
|
||||||
stake_pool.total_stake_lamports = stake_pool
|
stake_pool.total_lamports = stake_pool
|
||||||
.total_stake_lamports
|
.total_lamports
|
||||||
.checked_sub(withdraw_lamports)
|
.checked_sub(withdraw_lamports)
|
||||||
.ok_or(StakePoolError::CalculationFailure)?;
|
.ok_or(StakePoolError::CalculationFailure)?;
|
||||||
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
|
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
|
||||||
|
@ -2500,8 +2497,8 @@ impl Processor {
|
||||||
.pool_token_supply
|
.pool_token_supply
|
||||||
.checked_sub(pool_tokens_burnt)
|
.checked_sub(pool_tokens_burnt)
|
||||||
.ok_or(StakePoolError::CalculationFailure)?;
|
.ok_or(StakePoolError::CalculationFailure)?;
|
||||||
stake_pool.total_stake_lamports = stake_pool
|
stake_pool.total_lamports = stake_pool
|
||||||
.total_stake_lamports
|
.total_lamports
|
||||||
.checked_sub(withdraw_lamports)
|
.checked_sub(withdraw_lamports)
|
||||||
.ok_or(StakePoolError::CalculationFailure)?;
|
.ok_or(StakePoolError::CalculationFailure)?;
|
||||||
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
|
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
|
||||||
|
|
|
@ -86,12 +86,12 @@ pub struct StakePool {
|
||||||
/// Total stake under management.
|
/// Total stake under management.
|
||||||
/// Note that if `last_update_epoch` does not match the current epoch then
|
/// Note that if `last_update_epoch` does not match the current epoch then
|
||||||
/// this field may not be accurate
|
/// this field may not be accurate
|
||||||
pub total_stake_lamports: u64,
|
pub total_lamports: u64,
|
||||||
|
|
||||||
/// Total supply of pool tokens (should always match the supply in the Pool Mint)
|
/// Total supply of pool tokens (should always match the supply in the Pool Mint)
|
||||||
pub pool_token_supply: u64,
|
pub pool_token_supply: u64,
|
||||||
|
|
||||||
/// Last epoch the `total_stake_lamports` field was updated
|
/// Last epoch the `total_lamports` field was updated
|
||||||
pub last_update_epoch: u64,
|
pub last_update_epoch: u64,
|
||||||
|
|
||||||
/// Lockup that all stakes in the pool must have
|
/// Lockup that all stakes in the pool must have
|
||||||
|
@ -146,18 +146,24 @@ pub struct StakePool {
|
||||||
|
|
||||||
/// Future SOL withdrawal fee, to be set for the following epoch
|
/// Future SOL withdrawal fee, to be set for the following epoch
|
||||||
pub next_sol_withdrawal_fee: Option<Fee>,
|
pub next_sol_withdrawal_fee: Option<Fee>,
|
||||||
|
|
||||||
|
/// Last epoch's total pool tokens, used only for APR estimation
|
||||||
|
pub last_epoch_pool_token_supply: u64,
|
||||||
|
|
||||||
|
/// Last epoch's total lamports, used only for APR estimation
|
||||||
|
pub last_epoch_total_lamports: u64,
|
||||||
}
|
}
|
||||||
impl StakePool {
|
impl StakePool {
|
||||||
/// calculate the pool tokens that should be minted for a deposit of `stake_lamports`
|
/// calculate the pool tokens that should be minted for a deposit of `stake_lamports`
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn calc_pool_tokens_for_deposit(&self, stake_lamports: u64) -> Option<u64> {
|
pub fn calc_pool_tokens_for_deposit(&self, stake_lamports: u64) -> Option<u64> {
|
||||||
if self.total_stake_lamports == 0 || self.pool_token_supply == 0 {
|
if self.total_lamports == 0 || self.pool_token_supply == 0 {
|
||||||
return Some(stake_lamports);
|
return Some(stake_lamports);
|
||||||
}
|
}
|
||||||
u64::try_from(
|
u64::try_from(
|
||||||
(stake_lamports as u128)
|
(stake_lamports as u128)
|
||||||
.checked_mul(self.pool_token_supply as u128)?
|
.checked_mul(self.pool_token_supply as u128)?
|
||||||
.checked_div(self.total_stake_lamports as u128)?,
|
.checked_div(self.total_lamports as u128)?,
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
@ -168,7 +174,7 @@ impl StakePool {
|
||||||
// `checked_ceil_div` returns `None` for a 0 quotient result, but in this
|
// `checked_ceil_div` returns `None` for a 0 quotient result, but in this
|
||||||
// case, a return of 0 is valid for small amounts of pool tokens. So
|
// case, a return of 0 is valid for small amounts of pool tokens. So
|
||||||
// we check for that separately
|
// we check for that separately
|
||||||
let numerator = (pool_tokens as u128).checked_mul(self.total_stake_lamports as u128)?;
|
let numerator = (pool_tokens as u128).checked_mul(self.total_lamports as u128)?;
|
||||||
let denominator = self.pool_token_supply as u128;
|
let denominator = self.pool_token_supply as u128;
|
||||||
if numerator < denominator || denominator == 0 {
|
if numerator < denominator || denominator == 0 {
|
||||||
Some(0)
|
Some(0)
|
||||||
|
@ -227,22 +233,21 @@ impl StakePool {
|
||||||
/// Calculate the fee in pool tokens that goes to the manager
|
/// Calculate the fee in pool tokens that goes to the manager
|
||||||
///
|
///
|
||||||
/// This function assumes that `reward_lamports` has not already been added
|
/// This function assumes that `reward_lamports` has not already been added
|
||||||
/// to the stake pool's `total_stake_lamports`
|
/// to the stake pool's `total_lamports`
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn calc_epoch_fee_amount(&self, reward_lamports: u64) -> Option<u64> {
|
pub fn calc_epoch_fee_amount(&self, reward_lamports: u64) -> Option<u64> {
|
||||||
if reward_lamports == 0 {
|
if reward_lamports == 0 {
|
||||||
return Some(0);
|
return Some(0);
|
||||||
}
|
}
|
||||||
let total_stake_lamports =
|
let total_lamports = (self.total_lamports as u128).checked_add(reward_lamports as u128)?;
|
||||||
(self.total_stake_lamports as u128).checked_add(reward_lamports as u128)?;
|
|
||||||
let fee_lamports = self.epoch_fee.apply(reward_lamports)?;
|
let fee_lamports = self.epoch_fee.apply(reward_lamports)?;
|
||||||
if total_stake_lamports == fee_lamports || self.pool_token_supply == 0 {
|
if total_lamports == fee_lamports || self.pool_token_supply == 0 {
|
||||||
Some(reward_lamports)
|
Some(reward_lamports)
|
||||||
} else {
|
} else {
|
||||||
u64::try_from(
|
u64::try_from(
|
||||||
(self.pool_token_supply as u128)
|
(self.pool_token_supply as u128)
|
||||||
.checked_mul(fee_lamports)?
|
.checked_mul(fee_lamports)?
|
||||||
.checked_div(total_stake_lamports.checked_sub(fee_lamports)?)?,
|
.checked_div(total_lamports.checked_sub(fee_lamports)?)?,
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
@ -816,7 +821,10 @@ mod test {
|
||||||
solana_program::borsh::{
|
solana_program::borsh::{
|
||||||
get_instance_packed_len, get_packed_len, try_from_slice_unchecked,
|
get_instance_packed_len, get_packed_len, try_from_slice_unchecked,
|
||||||
},
|
},
|
||||||
solana_program::native_token::LAMPORTS_PER_SOL,
|
solana_program::{
|
||||||
|
clock::{DEFAULT_SLOTS_PER_EPOCH, DEFAULT_S_PER_SLOT, SECONDS_PER_DAY},
|
||||||
|
native_token::LAMPORTS_PER_SOL,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn uninitialized_validator_list() -> ValidatorList {
|
fn uninitialized_validator_list() -> ValidatorList {
|
||||||
|
@ -992,11 +1000,11 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
prop_compose! {
|
prop_compose! {
|
||||||
fn total_stake_and_rewards()(total_stake_lamports in 1..u64::MAX)(
|
fn total_stake_and_rewards()(total_lamports in 1..u64::MAX)(
|
||||||
total_stake_lamports in Just(total_stake_lamports),
|
total_lamports in Just(total_lamports),
|
||||||
rewards in 0..=total_stake_lamports,
|
rewards in 0..=total_lamports,
|
||||||
) -> (u64, u64) {
|
) -> (u64, u64) {
|
||||||
(total_stake_lamports - rewards, rewards)
|
(total_lamports - rewards, rewards)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1008,7 +1016,7 @@ mod test {
|
||||||
denominator: 10,
|
denominator: 10,
|
||||||
};
|
};
|
||||||
let mut stake_pool = StakePool {
|
let mut stake_pool = StakePool {
|
||||||
total_stake_lamports: 100 * LAMPORTS_PER_SOL,
|
total_lamports: 100 * LAMPORTS_PER_SOL,
|
||||||
pool_token_supply: 100 * LAMPORTS_PER_SOL,
|
pool_token_supply: 100 * LAMPORTS_PER_SOL,
|
||||||
epoch_fee,
|
epoch_fee,
|
||||||
..StakePool::default()
|
..StakePool::default()
|
||||||
|
@ -1016,7 +1024,7 @@ mod test {
|
||||||
let reward_lamports = 10 * LAMPORTS_PER_SOL;
|
let reward_lamports = 10 * LAMPORTS_PER_SOL;
|
||||||
let pool_token_fee = stake_pool.calc_epoch_fee_amount(reward_lamports).unwrap();
|
let pool_token_fee = stake_pool.calc_epoch_fee_amount(reward_lamports).unwrap();
|
||||||
|
|
||||||
stake_pool.total_stake_lamports += reward_lamports;
|
stake_pool.total_lamports += reward_lamports;
|
||||||
stake_pool.pool_token_supply += pool_token_fee;
|
stake_pool.pool_token_supply += pool_token_fee;
|
||||||
|
|
||||||
let fee_lamports = stake_pool
|
let fee_lamports = stake_pool
|
||||||
|
@ -1042,7 +1050,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn divide_by_zero_fee() {
|
fn divide_by_zero_fee() {
|
||||||
let stake_pool = StakePool {
|
let stake_pool = StakePool {
|
||||||
total_stake_lamports: 0,
|
total_lamports: 0,
|
||||||
epoch_fee: Fee {
|
epoch_fee: Fee {
|
||||||
numerator: 1,
|
numerator: 1,
|
||||||
denominator: 10,
|
denominator: 10,
|
||||||
|
@ -1054,22 +1062,44 @@ mod test {
|
||||||
assert_eq!(fee, rewards);
|
assert_eq!(fee, rewards);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn approximate_apr_calculation() {
|
||||||
|
// 8% / year means roughly .044% / epoch
|
||||||
|
let stake_pool = StakePool {
|
||||||
|
last_epoch_total_lamports: 100_000,
|
||||||
|
last_epoch_pool_token_supply: 100_000,
|
||||||
|
total_lamports: 100_044,
|
||||||
|
pool_token_supply: 100_000,
|
||||||
|
..StakePool::default()
|
||||||
|
};
|
||||||
|
let pool_token_value =
|
||||||
|
stake_pool.total_lamports as f64 / stake_pool.pool_token_supply as f64;
|
||||||
|
let last_epoch_pool_token_value = stake_pool.last_epoch_total_lamports as f64
|
||||||
|
/ stake_pool.last_epoch_pool_token_supply as f64;
|
||||||
|
let epoch_rate = pool_token_value / last_epoch_pool_token_value - 1.0;
|
||||||
|
const SECONDS_PER_EPOCH: f64 = DEFAULT_SLOTS_PER_EPOCH as f64 * DEFAULT_S_PER_SLOT;
|
||||||
|
const EPOCHS_PER_YEAR: f64 = SECONDS_PER_DAY as f64 * 365.25 / SECONDS_PER_EPOCH;
|
||||||
|
const EPSILON: f64 = 0.00001;
|
||||||
|
let yearly_rate = epoch_rate * EPOCHS_PER_YEAR;
|
||||||
|
assert!((yearly_rate - 0.080355).abs() < EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
proptest! {
|
proptest! {
|
||||||
#[test]
|
#[test]
|
||||||
fn fee_calculation(
|
fn fee_calculation(
|
||||||
(numerator, denominator) in fee(),
|
(numerator, denominator) in fee(),
|
||||||
(total_stake_lamports, reward_lamports) in total_stake_and_rewards(),
|
(total_lamports, reward_lamports) in total_stake_and_rewards(),
|
||||||
) {
|
) {
|
||||||
let epoch_fee = Fee { denominator, numerator };
|
let epoch_fee = Fee { denominator, numerator };
|
||||||
let mut stake_pool = StakePool {
|
let mut stake_pool = StakePool {
|
||||||
total_stake_lamports,
|
total_lamports,
|
||||||
pool_token_supply: total_stake_lamports,
|
pool_token_supply: total_lamports,
|
||||||
epoch_fee,
|
epoch_fee,
|
||||||
..StakePool::default()
|
..StakePool::default()
|
||||||
};
|
};
|
||||||
let pool_token_fee = stake_pool.calc_epoch_fee_amount(reward_lamports).unwrap();
|
let pool_token_fee = stake_pool.calc_epoch_fee_amount(reward_lamports).unwrap();
|
||||||
|
|
||||||
stake_pool.total_stake_lamports += reward_lamports;
|
stake_pool.total_lamports += reward_lamports;
|
||||||
stake_pool.pool_token_supply += pool_token_fee;
|
stake_pool.pool_token_supply += pool_token_fee;
|
||||||
|
|
||||||
let fee_lamports = stake_pool.calc_lamports_withdraw_amount(pool_token_fee).unwrap();
|
let fee_lamports = stake_pool.calc_lamports_withdraw_amount(pool_token_fee).unwrap();
|
||||||
|
@ -1082,7 +1112,7 @@ mod test {
|
||||||
// since we do two "flooring" conversions, the max epsilon should be
|
// since we do two "flooring" conversions, the max epsilon should be
|
||||||
// correct up to 2 lamports (one for each floor division), plus a
|
// correct up to 2 lamports (one for each floor division), plus a
|
||||||
// correction for huge discrepancies between rewards and total stake
|
// correction for huge discrepancies between rewards and total stake
|
||||||
let epsilon = 2 + reward_lamports / total_stake_lamports;
|
let epsilon = 2 + reward_lamports / total_lamports;
|
||||||
assert!(max_fee_lamports - fee_lamports <= epsilon,
|
assert!(max_fee_lamports - fee_lamports <= epsilon,
|
||||||
"Max expected fee in lamports {}, actually receive {}, epsilon {}",
|
"Max expected fee in lamports {}, actually receive {}, epsilon {}",
|
||||||
max_fee_lamports, fee_lamports, epsilon);
|
max_fee_lamports, fee_lamports, epsilon);
|
||||||
|
@ -1102,16 +1132,16 @@ mod test {
|
||||||
proptest! {
|
proptest! {
|
||||||
#[test]
|
#[test]
|
||||||
fn deposit_and_withdraw(
|
fn deposit_and_withdraw(
|
||||||
(total_stake_lamports, pool_token_supply, deposit_stake) in total_tokens_and_deposit()
|
(total_lamports, pool_token_supply, deposit_stake) in total_tokens_and_deposit()
|
||||||
) {
|
) {
|
||||||
let mut stake_pool = StakePool {
|
let mut stake_pool = StakePool {
|
||||||
total_stake_lamports,
|
total_lamports,
|
||||||
pool_token_supply,
|
pool_token_supply,
|
||||||
..StakePool::default()
|
..StakePool::default()
|
||||||
};
|
};
|
||||||
let deposit_result = stake_pool.calc_pool_tokens_for_deposit(deposit_stake).unwrap();
|
let deposit_result = stake_pool.calc_pool_tokens_for_deposit(deposit_stake).unwrap();
|
||||||
prop_assume!(deposit_result > 0);
|
prop_assume!(deposit_result > 0);
|
||||||
stake_pool.total_stake_lamports += deposit_stake;
|
stake_pool.total_lamports += deposit_stake;
|
||||||
stake_pool.pool_token_supply += deposit_result;
|
stake_pool.pool_token_supply += deposit_result;
|
||||||
let withdraw_result = stake_pool.calc_lamports_withdraw_amount(deposit_result).unwrap();
|
let withdraw_result = stake_pool.calc_lamports_withdraw_amount(deposit_result).unwrap();
|
||||||
assert!(withdraw_result <= deposit_stake);
|
assert!(withdraw_result <= deposit_stake);
|
||||||
|
|
|
@ -203,8 +203,8 @@ async fn success() {
|
||||||
let post_stake_pool =
|
let post_stake_pool =
|
||||||
try_from_slice_unchecked::<state::StakePool>(post_stake_pool.data.as_slice()).unwrap();
|
try_from_slice_unchecked::<state::StakePool>(post_stake_pool.data.as_slice()).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
post_stake_pool.total_stake_lamports,
|
post_stake_pool.total_lamports,
|
||||||
pre_stake_pool.total_stake_lamports + stake_lamports
|
pre_stake_pool.total_lamports + stake_lamports
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
post_stake_pool.pool_token_supply,
|
post_stake_pool.pool_token_supply,
|
||||||
|
@ -374,8 +374,8 @@ async fn success_with_extra_stake_lamports() {
|
||||||
let post_stake_pool =
|
let post_stake_pool =
|
||||||
try_from_slice_unchecked::<state::StakePool>(post_stake_pool.data.as_slice()).unwrap();
|
try_from_slice_unchecked::<state::StakePool>(post_stake_pool.data.as_slice()).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
post_stake_pool.total_stake_lamports,
|
post_stake_pool.total_lamports,
|
||||||
pre_stake_pool.total_stake_lamports + extra_lamports + stake_lamports
|
pre_stake_pool.total_lamports + extra_lamports + stake_lamports
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
post_stake_pool.pool_token_supply,
|
post_stake_pool.pool_token_supply,
|
||||||
|
|
|
@ -103,8 +103,8 @@ async fn success() {
|
||||||
let post_stake_pool =
|
let post_stake_pool =
|
||||||
try_from_slice_unchecked::<state::StakePool>(post_stake_pool.data.as_slice()).unwrap();
|
try_from_slice_unchecked::<state::StakePool>(post_stake_pool.data.as_slice()).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
post_stake_pool.total_stake_lamports,
|
post_stake_pool.total_lamports,
|
||||||
pre_stake_pool.total_stake_lamports + TEST_STAKE_AMOUNT
|
pre_stake_pool.total_lamports + TEST_STAKE_AMOUNT
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
post_stake_pool.pool_token_supply,
|
post_stake_pool.pool_token_supply,
|
||||||
|
|
|
@ -69,7 +69,7 @@ async fn setup(
|
||||||
pool_mint: stake_pool_accounts.pool_mint.pubkey(),
|
pool_mint: stake_pool_accounts.pool_mint.pubkey(),
|
||||||
manager_fee_account: stake_pool_accounts.pool_fee_account.pubkey(),
|
manager_fee_account: stake_pool_accounts.pool_fee_account.pubkey(),
|
||||||
token_program_id: spl_token::id(),
|
token_program_id: spl_token::id(),
|
||||||
total_stake_lamports: 0,
|
total_lamports: 0,
|
||||||
pool_token_supply: 0,
|
pool_token_supply: 0,
|
||||||
last_update_epoch: 0,
|
last_update_epoch: 0,
|
||||||
lockup: stake_program::Lockup::default(),
|
lockup: stake_program::Lockup::default(),
|
||||||
|
@ -87,6 +87,8 @@ async fn setup(
|
||||||
sol_withdraw_authority: None,
|
sol_withdraw_authority: None,
|
||||||
sol_withdrawal_fee: Fee::default(),
|
sol_withdrawal_fee: Fee::default(),
|
||||||
next_sol_withdrawal_fee: None,
|
next_sol_withdrawal_fee: None,
|
||||||
|
last_epoch_pool_token_supply: 0,
|
||||||
|
last_epoch_total_lamports: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut validator_list = ValidatorList::new(max_validators);
|
let mut validator_list = ValidatorList::new(max_validators);
|
||||||
|
@ -168,7 +170,7 @@ async fn setup(
|
||||||
transient_seed_suffix_end: 0,
|
transient_seed_suffix_end: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
stake_pool.total_stake_lamports += active_stake_lamports;
|
stake_pool.total_lamports += active_stake_lamports;
|
||||||
stake_pool.pool_token_supply += active_stake_lamports;
|
stake_pool.pool_token_supply += active_stake_lamports;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ async fn success() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let stake_pool = try_from_slice_unchecked::<StakePool>(stake_pool.data.as_slice()).unwrap();
|
let stake_pool = try_from_slice_unchecked::<StakePool>(stake_pool.data.as_slice()).unwrap();
|
||||||
assert_eq!(pre_balance, stake_pool.total_stake_lamports);
|
assert_eq!(pre_balance, stake_pool.total_lamports);
|
||||||
|
|
||||||
let pre_token_supply = get_token_supply(
|
let pre_token_supply = get_token_supply(
|
||||||
&mut context.banks_client,
|
&mut context.banks_client,
|
||||||
|
@ -138,7 +138,7 @@ async fn success() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let stake_pool = try_from_slice_unchecked::<StakePool>(stake_pool.data.as_slice()).unwrap();
|
let stake_pool = try_from_slice_unchecked::<StakePool>(stake_pool.data.as_slice()).unwrap();
|
||||||
assert_eq!(post_balance, stake_pool.total_stake_lamports);
|
assert_eq!(post_balance, stake_pool.total_lamports);
|
||||||
|
|
||||||
let post_fee = get_token_balance(
|
let post_fee = get_token_balance(
|
||||||
&mut context.banks_client,
|
&mut context.banks_client,
|
||||||
|
@ -162,6 +162,8 @@ async fn success() {
|
||||||
assert_eq!(expected_fee, actual_fee);
|
assert_eq!(expected_fee, actual_fee);
|
||||||
|
|
||||||
assert_eq!(pool_token_supply, stake_pool.pool_token_supply);
|
assert_eq!(pool_token_supply, stake_pool.pool_token_supply);
|
||||||
|
assert_eq!(pre_token_supply, stake_pool.last_epoch_pool_token_supply);
|
||||||
|
assert_eq!(pre_balance, stake_pool.last_epoch_total_lamports);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -180,7 +182,7 @@ async fn success_ignoring_extra_lamports() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let stake_pool = try_from_slice_unchecked::<StakePool>(stake_pool.data.as_slice()).unwrap();
|
let stake_pool = try_from_slice_unchecked::<StakePool>(stake_pool.data.as_slice()).unwrap();
|
||||||
assert_eq!(pre_balance, stake_pool.total_stake_lamports);
|
assert_eq!(pre_balance, stake_pool.total_lamports);
|
||||||
|
|
||||||
let pre_token_supply = get_token_supply(
|
let pre_token_supply = get_token_supply(
|
||||||
&mut context.banks_client,
|
&mut context.banks_client,
|
||||||
|
|
|
@ -214,7 +214,7 @@ async fn success() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let stake_pool = try_from_slice_unchecked::<StakePool>(&stake_pool_info.data).unwrap();
|
let stake_pool = try_from_slice_unchecked::<StakePool>(&stake_pool_info.data).unwrap();
|
||||||
assert_eq!(new_lamports, stake_pool.total_stake_lamports);
|
assert_eq!(new_lamports, stake_pool.total_lamports);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -282,7 +282,7 @@ async fn merge_into_reserve() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let stake_pool = try_from_slice_unchecked::<StakePool>(&stake_pool_info.data).unwrap();
|
let stake_pool = try_from_slice_unchecked::<StakePool>(&stake_pool_info.data).unwrap();
|
||||||
assert_eq!(expected_lamports, stake_pool.total_stake_lamports);
|
assert_eq!(expected_lamports, stake_pool.total_lamports);
|
||||||
|
|
||||||
println!("Warp one more epoch so the stakes deactivate");
|
println!("Warp one more epoch so the stakes deactivate");
|
||||||
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
|
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
|
||||||
|
@ -325,7 +325,7 @@ async fn merge_into_reserve() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let stake_pool = try_from_slice_unchecked::<StakePool>(&stake_pool_info.data).unwrap();
|
let stake_pool = try_from_slice_unchecked::<StakePool>(&stake_pool_info.data).unwrap();
|
||||||
assert_eq!(expected_lamports, stake_pool.total_stake_lamports);
|
assert_eq!(expected_lamports, stake_pool.total_lamports);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -389,7 +389,7 @@ async fn merge_into_validator_stake() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let stake_pool = try_from_slice_unchecked::<StakePool>(&stake_pool_info.data).unwrap();
|
let stake_pool = try_from_slice_unchecked::<StakePool>(&stake_pool_info.data).unwrap();
|
||||||
assert_eq!(expected_lamports, stake_pool.total_stake_lamports);
|
assert_eq!(expected_lamports, stake_pool.total_lamports);
|
||||||
|
|
||||||
// Warp one more epoch so the stakes activate, ready to merge
|
// Warp one more epoch so the stakes activate, ready to merge
|
||||||
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
|
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
|
||||||
|
@ -422,7 +422,7 @@ async fn merge_into_validator_stake() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let stake_pool = try_from_slice_unchecked::<StakePool>(&stake_pool_info.data).unwrap();
|
let stake_pool = try_from_slice_unchecked::<StakePool>(&stake_pool_info.data).unwrap();
|
||||||
assert_eq!(current_lamports, stake_pool.total_stake_lamports);
|
assert_eq!(current_lamports, stake_pool.total_lamports);
|
||||||
|
|
||||||
// Check that transient accounts are gone
|
// Check that transient accounts are gone
|
||||||
for stake_account in &stake_accounts {
|
for stake_account in &stake_accounts {
|
||||||
|
@ -803,7 +803,7 @@ async fn success_ignoring_hijacked_transient_stake() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let stake_pool = try_from_slice_unchecked::<StakePool>(&stake_pool_info.data).unwrap();
|
let stake_pool = try_from_slice_unchecked::<StakePool>(&stake_pool_info.data).unwrap();
|
||||||
assert_eq!(pre_lamports, stake_pool.total_stake_lamports);
|
assert_eq!(pre_lamports, stake_pool.total_lamports);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
|
@ -245,8 +245,8 @@ async fn _success(test_type: SuccessTestType) {
|
||||||
};
|
};
|
||||||
let tokens_burnt = tokens_to_withdraw - tokens_withdrawal_fee;
|
let tokens_burnt = tokens_to_withdraw - tokens_withdrawal_fee;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_pool.total_stake_lamports,
|
stake_pool.total_lamports,
|
||||||
stake_pool_before.total_stake_lamports - tokens_burnt
|
stake_pool_before.total_lamports - tokens_burnt
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_pool.pool_token_supply,
|
stake_pool.pool_token_supply,
|
||||||
|
|
|
@ -119,8 +119,8 @@ async fn success() {
|
||||||
let amount_withdrawn_minus_fee =
|
let amount_withdrawn_minus_fee =
|
||||||
pool_tokens - stake_pool_accounts.calculate_withdrawal_fee(pool_tokens);
|
pool_tokens - stake_pool_accounts.calculate_withdrawal_fee(pool_tokens);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
post_stake_pool.total_stake_lamports,
|
post_stake_pool.total_lamports,
|
||||||
pre_stake_pool.total_stake_lamports - amount_withdrawn_minus_fee
|
pre_stake_pool.total_lamports - amount_withdrawn_minus_fee
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
post_stake_pool.pool_token_supply,
|
post_stake_pool.pool_token_supply,
|
||||||
|
|
Loading…
Reference in New Issue