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

Commit 71e5e55

Browse files
authored
stake-pool: Assess fee as a percentage of rewards (#1597)
* stake-pool: Collect fee every epoch as proportion of rewards * Add more complete tests * Update docs
1 parent d3e26d0 commit 71e5e55

File tree

9 files changed

+367
-199
lines changed

9 files changed

+367
-199
lines changed

docs/src/stake-pool.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ active, the stake pool manager adds it to the stake pool.
5454
At this point, users can participate with deposits. They must delegate a stake
5555
account to the one of the validators in the stake pool. Once it's active, the
5656
user can deposit their stake into the pool in exchange for SPL staking derivatives
57-
representing their fractional ownership in pool. A percentage of the user's
58-
deposit goes to the pool manager as a fee.
57+
representing their fractional ownership in pool. A percentage of the rewards
58+
earned by the pool goes to the pool manager as a fee.
5959

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

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

160160
#### Create a validator stake account
161161

stake-pool/cli/src/main.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,6 @@ fn command_deposit(
561561
&stake,
562562
&validator_stake_account,
563563
&token_receiver,
564-
&stake_pool.manager_fee_account,
565564
&stake_pool.pool_mint,
566565
&spl_token::id(),
567566
)?,
@@ -689,10 +688,16 @@ fn command_update(config: &Config, stake_pool_address: &Pubkey) -> CommandResult
689688
)?);
690689
}
691690

691+
let (withdraw_authority, _) =
692+
find_withdraw_authority_program_address(&spl_stake_pool::id(), &stake_pool_address);
693+
692694
instructions.push(spl_stake_pool::instruction::update_stake_pool_balance(
693695
&spl_stake_pool::id(),
694696
stake_pool_address,
695697
&stake_pool.validator_list,
698+
&withdraw_authority,
699+
&stake_pool.manager_fee_account,
700+
&stake_pool.pool_mint,
696701
)?);
697702

698703
// TODO: A faster solution would be to send all the `update_validator_list_balance` instructions concurrently

stake-pool/program/src/instruction.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ use {
1313
},
1414
};
1515

16-
/// Fee rate as a ratio, minted on deposit
16+
/// Fee rate as a ratio, minted on `UpdateStakePoolBalance` as a proportion of
17+
/// the rewards
1718
#[repr(C)]
1819
#[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)]
1920
pub struct Fee {
@@ -41,7 +42,7 @@ pub enum StakePoolInstruction {
4142
/// 8. `[]` Rent sysvar
4243
/// 9. `[]` Token program id
4344
Initialize {
44-
/// Deposit fee assessed
45+
/// Fee assessed as percentage of perceived rewards
4546
#[allow(dead_code)] // but it's not
4647
fee: Fee,
4748
/// Maximum expected number of validators
@@ -171,7 +172,11 @@ pub enum StakePoolInstruction {
171172
/// 0. `[w]` Stake pool
172173
/// 1. `[]` Validator stake list storage account
173174
/// 2. `[]` Reserve stake account
174-
/// 3. `[]` Sysvar clock account
175+
/// 3. `[]` Stake pool withdraw authority
176+
/// 4. `[w]` Account to receive pool fee tokens
177+
/// 5. `[w]` Pool mint account
178+
/// 6. `[]` Sysvar clock account
179+
/// 7. `[]` Pool token program
175180
UpdateStakePoolBalance,
176181

177182
/// Deposit some stake into the pool. The output is a "pool" token representing ownership
@@ -184,7 +189,6 @@ pub enum StakePoolInstruction {
184189
/// 4. `[w]` Stake account to join the pool (withdraw should be set to stake pool deposit)
185190
/// 5. `[w]` Validator stake account for the stake account to be merged with
186191
/// 6. `[w]` User account to receive pool tokens
187-
/// 7. `[w]` Account to receive pool fee tokens
188192
/// 8. `[w]` Pool token mint account
189193
/// 9. '[]' Sysvar clock account (required)
190194
/// 10. '[]' Sysvar stake history account
@@ -397,11 +401,18 @@ pub fn update_stake_pool_balance(
397401
program_id: &Pubkey,
398402
stake_pool: &Pubkey,
399403
validator_list_storage: &Pubkey,
404+
withdraw_authority: &Pubkey,
405+
manager_fee_account: &Pubkey,
406+
stake_pool_mint: &Pubkey,
400407
) -> Result<Instruction, ProgramError> {
401408
let accounts = vec![
402409
AccountMeta::new(*stake_pool, false),
403410
AccountMeta::new(*validator_list_storage, false),
411+
AccountMeta::new_readonly(*withdraw_authority, false),
412+
AccountMeta::new(*manager_fee_account, false),
413+
AccountMeta::new(*stake_pool_mint, false),
404414
AccountMeta::new_readonly(sysvar::clock::id(), false),
415+
AccountMeta::new_readonly(spl_token::id(), false),
405416
];
406417
Ok(Instruction {
407418
program_id: *program_id,
@@ -420,7 +431,6 @@ pub fn deposit(
420431
stake_to_join: &Pubkey,
421432
validator_stake_accont: &Pubkey,
422433
pool_tokens_to: &Pubkey,
423-
pool_fee_to: &Pubkey,
424434
pool_mint: &Pubkey,
425435
token_program_id: &Pubkey,
426436
) -> Result<Instruction, ProgramError> {
@@ -432,7 +442,6 @@ pub fn deposit(
432442
AccountMeta::new(*stake_to_join, false),
433443
AccountMeta::new(*validator_stake_accont, false),
434444
AccountMeta::new(*pool_tokens_to, false),
435-
AccountMeta::new(*pool_fee_to, false),
436445
AccountMeta::new(*pool_mint, false),
437446
AccountMeta::new_readonly(sysvar::clock::id(), false),
438447
AccountMeta::new_readonly(sysvar::stake_history::id(), false),

stake-pool/program/src/processor.rs

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@ impl Processor {
465465
stake_pool.check_authority_deposit(deposit_info.key, program_id, stake_pool_info.key)?;
466466

467467
stake_pool.check_staker(staker_info)?;
468+
stake_pool.check_mint(pool_mint_info)?;
468469

469470
if stake_pool.last_update_epoch < clock.epoch {
470471
return Err(StakePoolError::StakeListAndPoolOutOfDate.into());
@@ -473,9 +474,6 @@ impl Processor {
473474
if stake_pool.token_program_id != *token_program_info.key {
474475
return Err(ProgramError::IncorrectProgramId);
475476
}
476-
if stake_pool.pool_mint != *pool_mint_info.key {
477-
return Err(StakePoolError::WrongPoolMint.into());
478-
}
479477

480478
if *validator_list_info.key != stake_pool.validator_list {
481479
return Err(StakePoolError::InvalidValidatorStakeList.into());
@@ -586,9 +584,7 @@ impl Processor {
586584
if stake_pool.token_program_id != *token_program_info.key {
587585
return Err(ProgramError::IncorrectProgramId);
588586
}
589-
if stake_pool.pool_mint != *pool_mint_info.key {
590-
return Err(StakePoolError::WrongPoolMint.into());
591-
}
587+
stake_pool.check_mint(pool_mint_info)?;
592588

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

704700
/// Processes `UpdateStakePoolBalance` instruction.
705701
fn process_update_stake_pool_balance(
706-
_program_id: &Pubkey,
702+
program_id: &Pubkey,
707703
accounts: &[AccountInfo],
708704
) -> ProgramResult {
709705
let account_info_iter = &mut accounts.iter();
710706
let stake_pool_info = next_account_info(account_info_iter)?;
711707
let validator_list_info = next_account_info(account_info_iter)?;
708+
let withdraw_info = next_account_info(account_info_iter)?;
709+
let manager_fee_info = next_account_info(account_info_iter)?;
710+
let pool_mint_info = next_account_info(account_info_iter)?;
712711
let clock_info = next_account_info(account_info_iter)?;
713712
let clock = &Clock::from_account_info(clock_info)?;
713+
let token_program_info = next_account_info(account_info_iter)?;
714714

715715
let mut stake_pool = StakePool::try_from_slice(&stake_pool_info.data.borrow())?;
716716
if !stake_pool.is_valid() {
717717
return Err(StakePoolError::InvalidState.into());
718718
}
719+
stake_pool.check_mint(pool_mint_info)?;
720+
stake_pool.check_authority_withdraw(withdraw_info.key, program_id, stake_pool_info.key)?;
721+
if stake_pool.manager_fee_account != *manager_fee_info.key {
722+
return Err(StakePoolError::InvalidFeeAccount.into());
723+
}
719724

720725
if *validator_list_info.key != stake_pool.validator_list {
721726
return Err(StakePoolError::InvalidValidatorStakeList.into());
722727
}
728+
if stake_pool.token_program_id != *token_program_info.key {
729+
return Err(ProgramError::IncorrectProgramId);
730+
}
723731

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

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

738747
stake_pool.total_stake_lamports = total_stake_lamports;
748+
749+
let reward_lamports = total_stake_lamports.saturating_sub(previous_lamports);
750+
let fee = stake_pool
751+
.calc_fee_amount(reward_lamports)
752+
.ok_or(StakePoolError::CalculationFailure)?;
753+
754+
if fee > 0 {
755+
Self::token_mint_to(
756+
stake_pool_info.key,
757+
token_program_info.clone(),
758+
pool_mint_info.clone(),
759+
manager_fee_info.clone(),
760+
withdraw_info.clone(),
761+
AUTHORITY_WITHDRAW,
762+
stake_pool.withdraw_bump_seed,
763+
fee,
764+
)?;
765+
766+
stake_pool.pool_token_supply = stake_pool
767+
.pool_token_supply
768+
.checked_add(fee)
769+
.ok_or(StakePoolError::CalculationFailure)?;
770+
}
739771
stake_pool.last_update_epoch = clock.epoch;
740772
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
741773

@@ -782,7 +814,6 @@ impl Processor {
782814
let stake_info = next_account_info(account_info_iter)?;
783815
let validator_stake_account_info = next_account_info(account_info_iter)?;
784816
let dest_user_info = next_account_info(account_info_iter)?;
785-
let manager_fee_info = next_account_info(account_info_iter)?;
786817
let pool_mint_info = next_account_info(account_info_iter)?;
787818
let clock_info = next_account_info(account_info_iter)?;
788819
let clock = &Clock::from_account_info(clock_info)?;
@@ -804,10 +835,8 @@ impl Processor {
804835

805836
stake_pool.check_authority_withdraw(withdraw_info.key, program_id, stake_pool_info.key)?;
806837
stake_pool.check_authority_deposit(deposit_info.key, program_id, stake_pool_info.key)?;
838+
stake_pool.check_mint(pool_mint_info)?;
807839

808-
if stake_pool.manager_fee_account != *manager_fee_info.key {
809-
return Err(StakePoolError::InvalidFeeAccount.into());
810-
}
811840
if stake_pool.token_program_id != *token_program_info.key {
812841
return Err(ProgramError::IncorrectProgramId);
813842
}
@@ -838,14 +867,6 @@ impl Processor {
838867
.calc_pool_tokens_for_deposit(stake_lamports)
839868
.ok_or(StakePoolError::CalculationFailure)?;
840869

841-
let fee_pool_tokens = stake_pool
842-
.calc_fee_amount(new_pool_tokens)
843-
.ok_or(StakePoolError::CalculationFailure)?;
844-
845-
let user_pool_tokens = new_pool_tokens
846-
.checked_sub(fee_pool_tokens)
847-
.ok_or(StakePoolError::CalculationFailure)?;
848-
849870
Self::stake_authorize(
850871
stake_pool_info.key,
851872
stake_info.clone(),
@@ -890,19 +911,9 @@ impl Processor {
890911
withdraw_info.clone(),
891912
AUTHORITY_WITHDRAW,
892913
stake_pool.withdraw_bump_seed,
893-
user_pool_tokens,
914+
new_pool_tokens,
894915
)?;
895916

896-
Self::token_mint_to(
897-
stake_pool_info.key,
898-
token_program_info.clone(),
899-
pool_mint_info.clone(),
900-
manager_fee_info.clone(),
901-
withdraw_info.clone(),
902-
AUTHORITY_WITHDRAW,
903-
stake_pool.withdraw_bump_seed,
904-
fee_pool_tokens,
905-
)?;
906917
stake_pool.pool_token_supply += new_pool_tokens;
907918
stake_pool.total_stake_lamports += stake_lamports;
908919
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
@@ -943,6 +954,7 @@ impl Processor {
943954
}
944955

945956
stake_pool.check_authority_withdraw(withdraw_info.key, program_id, stake_pool_info.key)?;
957+
stake_pool.check_mint(pool_mint_info)?;
946958

947959
if stake_pool.token_program_id != *token_program_info.key {
948960
return Err(ProgramError::IncorrectProgramId);

stake-pool/program/src/state.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,11 @@ impl StakePool {
107107
.ok()
108108
}
109109
/// calculate the fee in pool tokens that goes to the manager
110-
pub fn calc_fee_amount(&self, pool_amount: u64) -> Option<u64> {
110+
pub fn calc_fee_amount(&self, reward_lamports: u64) -> Option<u64> {
111111
if self.fee.denominator == 0 {
112112
return Some(0);
113113
}
114+
let pool_amount = self.calc_pool_tokens_for_deposit(reward_lamports)?;
114115
u64::try_from(
115116
(pool_amount as u128)
116117
.checked_mul(self.fee.numerator as u128)?
@@ -174,6 +175,15 @@ impl StakePool {
174175
)
175176
}
176177

178+
/// Check staker validity and signature
179+
pub(crate) fn check_mint(&self, mint_info: &AccountInfo) -> Result<(), ProgramError> {
180+
if *mint_info.key != self.pool_mint {
181+
Err(StakePoolError::WrongPoolMint.into())
182+
} else {
183+
Ok(())
184+
}
185+
}
186+
177187
/// Check manager validity and signature
178188
pub(crate) fn check_manager(&self, manager_info: &AccountInfo) -> Result<(), ProgramError> {
179189
if *manager_info.key != self.manager {

0 commit comments

Comments
 (0)