Skip to content

Commit

Permalink
stake-pool: Assess fee as a percentage of rewards (#1597)
Browse files Browse the repository at this point in the history
* stake-pool: Collect fee every epoch as proportion of rewards

* Add more complete tests

* Update docs
  • Loading branch information
joncinque committed Apr 15, 2021
1 parent d3e26d0 commit 71e5e55
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 199 deletions.
10 changes: 5 additions & 5 deletions docs/src/stake-pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ active, the stake pool manager adds it to the stake pool.
At this point, users can participate with deposits. They must delegate a stake
account to the one of the validators in the stake pool. Once it's active, the
user can deposit their stake into the pool in exchange for SPL staking derivatives
representing their fractional ownership in pool. A percentage of the user's
deposit goes to the pool manager as a fee.
representing their fractional ownership in pool. A percentage of the rewards
earned by the pool goes to the pool manager as a fee.

Over time, as the stake pool accrues staking rewards, the user's fractional
ownership will be worth more than their initial deposit. Whenever the user chooses,
Expand Down Expand Up @@ -153,9 +153,9 @@ The identifier for the SPL token for staking derivatives is
over the mint.

The pool creator's fee account identifier is
`3xvXPfQi2SaTkqPV9A7BQwh4GyTe2ZPasfoaCBCnTAJ5`. When users deposit warmed up
stake accounts into the stake pool, the program will transfer 3% of their
contribution into this account in the form of SPL token staking derivatives.
`3xvXPfQi2SaTkqPV9A7BQwh4GyTe2ZPasfoaCBCnTAJ5`. Every epoch, as stake accounts
in the stake pool earn rewards, the program will mint SPL token staking derivatives
equal to 3% of the gains on that epoch into this account.

#### Create a validator stake account

Expand Down
7 changes: 6 additions & 1 deletion stake-pool/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,6 @@ fn command_deposit(
&stake,
&validator_stake_account,
&token_receiver,
&stake_pool.manager_fee_account,
&stake_pool.pool_mint,
&spl_token::id(),
)?,
Expand Down Expand Up @@ -689,10 +688,16 @@ fn command_update(config: &Config, stake_pool_address: &Pubkey) -> CommandResult
)?);
}

let (withdraw_authority, _) =
find_withdraw_authority_program_address(&spl_stake_pool::id(), &stake_pool_address);

instructions.push(spl_stake_pool::instruction::update_stake_pool_balance(
&spl_stake_pool::id(),
stake_pool_address,
&stake_pool.validator_list,
&withdraw_authority,
&stake_pool.manager_fee_account,
&stake_pool.pool_mint,
)?);

// TODO: A faster solution would be to send all the `update_validator_list_balance` instructions concurrently
Expand Down
21 changes: 15 additions & 6 deletions stake-pool/program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use {
},
};

/// Fee rate as a ratio, minted on deposit
/// Fee rate as a ratio, minted on `UpdateStakePoolBalance` as a proportion of
/// the rewards
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)]
pub struct Fee {
Expand Down Expand Up @@ -41,7 +42,7 @@ pub enum StakePoolInstruction {
/// 8. `[]` Rent sysvar
/// 9. `[]` Token program id
Initialize {
/// Deposit fee assessed
/// Fee assessed as percentage of perceived rewards
#[allow(dead_code)] // but it's not
fee: Fee,
/// Maximum expected number of validators
Expand Down Expand Up @@ -171,7 +172,11 @@ pub enum StakePoolInstruction {
/// 0. `[w]` Stake pool
/// 1. `[]` Validator stake list storage account
/// 2. `[]` Reserve stake account
/// 3. `[]` Sysvar clock account
/// 3. `[]` Stake pool withdraw authority
/// 4. `[w]` Account to receive pool fee tokens
/// 5. `[w]` Pool mint account
/// 6. `[]` Sysvar clock account
/// 7. `[]` Pool token program
UpdateStakePoolBalance,

/// Deposit some stake into the pool. The output is a "pool" token representing ownership
Expand All @@ -184,7 +189,6 @@ pub enum StakePoolInstruction {
/// 4. `[w]` Stake account to join the pool (withdraw should be set to stake pool deposit)
/// 5. `[w]` Validator stake account for the stake account to be merged with
/// 6. `[w]` User account to receive pool tokens
/// 7. `[w]` Account to receive pool fee tokens
/// 8. `[w]` Pool token mint account
/// 9. '[]' Sysvar clock account (required)
/// 10. '[]' Sysvar stake history account
Expand Down Expand Up @@ -397,11 +401,18 @@ pub fn update_stake_pool_balance(
program_id: &Pubkey,
stake_pool: &Pubkey,
validator_list_storage: &Pubkey,
withdraw_authority: &Pubkey,
manager_fee_account: &Pubkey,
stake_pool_mint: &Pubkey,
) -> Result<Instruction, ProgramError> {
let accounts = vec![
AccountMeta::new(*stake_pool, false),
AccountMeta::new(*validator_list_storage, false),
AccountMeta::new_readonly(*withdraw_authority, false),
AccountMeta::new(*manager_fee_account, false),
AccountMeta::new(*stake_pool_mint, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
];
Ok(Instruction {
program_id: *program_id,
Expand All @@ -420,7 +431,6 @@ pub fn deposit(
stake_to_join: &Pubkey,
validator_stake_accont: &Pubkey,
pool_tokens_to: &Pubkey,
pool_fee_to: &Pubkey,
pool_mint: &Pubkey,
token_program_id: &Pubkey,
) -> Result<Instruction, ProgramError> {
Expand All @@ -432,7 +442,6 @@ pub fn deposit(
AccountMeta::new(*stake_to_join, false),
AccountMeta::new(*validator_stake_accont, false),
AccountMeta::new(*pool_tokens_to, false),
AccountMeta::new(*pool_fee_to, false),
AccountMeta::new(*pool_mint, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
Expand Down
74 changes: 43 additions & 31 deletions stake-pool/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ impl Processor {
stake_pool.check_authority_deposit(deposit_info.key, program_id, stake_pool_info.key)?;

stake_pool.check_staker(staker_info)?;
stake_pool.check_mint(pool_mint_info)?;

if stake_pool.last_update_epoch < clock.epoch {
return Err(StakePoolError::StakeListAndPoolOutOfDate.into());
Expand All @@ -473,9 +474,6 @@ impl Processor {
if stake_pool.token_program_id != *token_program_info.key {
return Err(ProgramError::IncorrectProgramId);
}
if stake_pool.pool_mint != *pool_mint_info.key {
return Err(StakePoolError::WrongPoolMint.into());
}

if *validator_list_info.key != stake_pool.validator_list {
return Err(StakePoolError::InvalidValidatorStakeList.into());
Expand Down Expand Up @@ -586,9 +584,7 @@ impl Processor {
if stake_pool.token_program_id != *token_program_info.key {
return Err(ProgramError::IncorrectProgramId);
}
if stake_pool.pool_mint != *pool_mint_info.key {
return Err(StakePoolError::WrongPoolMint.into());
}
stake_pool.check_mint(pool_mint_info)?;

if *validator_list_info.key != stake_pool.validator_list {
return Err(StakePoolError::InvalidValidatorStakeList.into());
Expand Down Expand Up @@ -703,31 +699,44 @@ impl Processor {

/// Processes `UpdateStakePoolBalance` instruction.
fn process_update_stake_pool_balance(
_program_id: &Pubkey,
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let stake_pool_info = next_account_info(account_info_iter)?;
let validator_list_info = next_account_info(account_info_iter)?;
let withdraw_info = next_account_info(account_info_iter)?;
let manager_fee_info = next_account_info(account_info_iter)?;
let pool_mint_info = next_account_info(account_info_iter)?;
let clock_info = next_account_info(account_info_iter)?;
let clock = &Clock::from_account_info(clock_info)?;
let token_program_info = next_account_info(account_info_iter)?;

let mut stake_pool = StakePool::try_from_slice(&stake_pool_info.data.borrow())?;
if !stake_pool.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
stake_pool.check_mint(pool_mint_info)?;
stake_pool.check_authority_withdraw(withdraw_info.key, program_id, stake_pool_info.key)?;
if stake_pool.manager_fee_account != *manager_fee_info.key {
return Err(StakePoolError::InvalidFeeAccount.into());
}

if *validator_list_info.key != stake_pool.validator_list {
return Err(StakePoolError::InvalidValidatorStakeList.into());
}
if stake_pool.token_program_id != *token_program_info.key {
return Err(ProgramError::IncorrectProgramId);
}

let validator_list =
try_from_slice_unchecked::<ValidatorList>(&validator_list_info.data.borrow())?;
if !validator_list.is_valid() {
return Err(StakePoolError::InvalidState.into());
}

let mut total_stake_lamports: u64 = 0;
let previous_lamports = stake_pool.total_stake_lamports;
let mut total_stake_lamports = 0;
for validator_stake_record in validator_list.validators {
if validator_stake_record.last_update_epoch < clock.epoch {
return Err(StakePoolError::StakeListOutOfDate.into());
Expand All @@ -736,6 +745,29 @@ impl Processor {
}

stake_pool.total_stake_lamports = total_stake_lamports;

let reward_lamports = total_stake_lamports.saturating_sub(previous_lamports);
let fee = stake_pool
.calc_fee_amount(reward_lamports)
.ok_or(StakePoolError::CalculationFailure)?;

if fee > 0 {
Self::token_mint_to(
stake_pool_info.key,
token_program_info.clone(),
pool_mint_info.clone(),
manager_fee_info.clone(),
withdraw_info.clone(),
AUTHORITY_WITHDRAW,
stake_pool.withdraw_bump_seed,
fee,
)?;

stake_pool.pool_token_supply = stake_pool
.pool_token_supply
.checked_add(fee)
.ok_or(StakePoolError::CalculationFailure)?;
}
stake_pool.last_update_epoch = clock.epoch;
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;

Expand Down Expand Up @@ -782,7 +814,6 @@ impl Processor {
let stake_info = next_account_info(account_info_iter)?;
let validator_stake_account_info = next_account_info(account_info_iter)?;
let dest_user_info = next_account_info(account_info_iter)?;
let manager_fee_info = next_account_info(account_info_iter)?;
let pool_mint_info = next_account_info(account_info_iter)?;
let clock_info = next_account_info(account_info_iter)?;
let clock = &Clock::from_account_info(clock_info)?;
Expand All @@ -804,10 +835,8 @@ impl Processor {

stake_pool.check_authority_withdraw(withdraw_info.key, program_id, stake_pool_info.key)?;
stake_pool.check_authority_deposit(deposit_info.key, program_id, stake_pool_info.key)?;
stake_pool.check_mint(pool_mint_info)?;

if stake_pool.manager_fee_account != *manager_fee_info.key {
return Err(StakePoolError::InvalidFeeAccount.into());
}
if stake_pool.token_program_id != *token_program_info.key {
return Err(ProgramError::IncorrectProgramId);
}
Expand Down Expand Up @@ -838,14 +867,6 @@ impl Processor {
.calc_pool_tokens_for_deposit(stake_lamports)
.ok_or(StakePoolError::CalculationFailure)?;

let fee_pool_tokens = stake_pool
.calc_fee_amount(new_pool_tokens)
.ok_or(StakePoolError::CalculationFailure)?;

let user_pool_tokens = new_pool_tokens
.checked_sub(fee_pool_tokens)
.ok_or(StakePoolError::CalculationFailure)?;

Self::stake_authorize(
stake_pool_info.key,
stake_info.clone(),
Expand Down Expand Up @@ -890,19 +911,9 @@ impl Processor {
withdraw_info.clone(),
AUTHORITY_WITHDRAW,
stake_pool.withdraw_bump_seed,
user_pool_tokens,
new_pool_tokens,
)?;

Self::token_mint_to(
stake_pool_info.key,
token_program_info.clone(),
pool_mint_info.clone(),
manager_fee_info.clone(),
withdraw_info.clone(),
AUTHORITY_WITHDRAW,
stake_pool.withdraw_bump_seed,
fee_pool_tokens,
)?;
stake_pool.pool_token_supply += new_pool_tokens;
stake_pool.total_stake_lamports += stake_lamports;
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
Expand Down Expand Up @@ -943,6 +954,7 @@ impl Processor {
}

stake_pool.check_authority_withdraw(withdraw_info.key, program_id, stake_pool_info.key)?;
stake_pool.check_mint(pool_mint_info)?;

if stake_pool.token_program_id != *token_program_info.key {
return Err(ProgramError::IncorrectProgramId);
Expand Down
12 changes: 11 additions & 1 deletion stake-pool/program/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,11 @@ impl StakePool {
.ok()
}
/// calculate the fee in pool tokens that goes to the manager
pub fn calc_fee_amount(&self, pool_amount: u64) -> Option<u64> {
pub fn calc_fee_amount(&self, reward_lamports: u64) -> Option<u64> {
if self.fee.denominator == 0 {
return Some(0);
}
let pool_amount = self.calc_pool_tokens_for_deposit(reward_lamports)?;
u64::try_from(
(pool_amount as u128)
.checked_mul(self.fee.numerator as u128)?
Expand Down Expand Up @@ -174,6 +175,15 @@ impl StakePool {
)
}

/// Check staker validity and signature
pub(crate) fn check_mint(&self, mint_info: &AccountInfo) -> Result<(), ProgramError> {
if *mint_info.key != self.pool_mint {
Err(StakePoolError::WrongPoolMint.into())
} else {
Ok(())
}
}

/// Check manager validity and signature
pub(crate) fn check_manager(&self, manager_info: &AccountInfo) -> Result<(), ProgramError> {
if *manager_info.key != self.manager {
Expand Down
Loading

0 comments on commit 71e5e55

Please sign in to comment.