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

Commit f7384c1

Browse files
authored
stake-pool: Make it impossible to leech value (#2218)
1 parent b558cbe commit f7384c1

File tree

6 files changed

+82
-79
lines changed

6 files changed

+82
-79
lines changed

Cargo.lock

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

stake-pool/program/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ num_enum = "0.5.3"
2020
serde = "1.0.127"
2121
serde_derive = "1.0.103"
2222
solana-program = "1.7.7"
23+
spl-math = { version = "0.1", path = "../../libraries/math", features = [ "no-entrypoint" ] }
2324
spl-token = { version = "3.2", path = "../../token/program", features = [ "no-entrypoint" ] }
2425
thiserror = "1.0"
2526
bincode = "1.3.1"

stake-pool/program/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ pub enum StakePoolError {
102102
/// Proposed fee increase exceeds stipulated ratio
103103
#[error("FeeIncreaseTooHigh")]
104104
FeeIncreaseTooHigh,
105+
/// Not enough pool tokens provided to withdraw stake with one lamport
106+
#[error("WithdrawalTooSmall")]
107+
WithdrawalTooSmall,
108+
/// Not enough lamports provided for deposit to result in one pool token
109+
#[error("DepositTooSmall")]
110+
DepositTooSmall,
105111
}
106112
impl From<StakePoolError> for ProgramError {
107113
fn from(e: StakePoolError) -> Self {

stake-pool/program/src/processor.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1858,6 +1858,10 @@ impl Processor {
18581858
.calc_pool_tokens_for_deposit(all_deposit_lamports)
18591859
.ok_or(StakePoolError::CalculationFailure)?;
18601860

1861+
if new_pool_tokens == 0 {
1862+
return Err(StakePoolError::DepositTooSmall.into());
1863+
}
1864+
18611865
Self::token_mint_to(
18621866
stake_pool_info.key,
18631867
token_program_info.clone(),
@@ -1978,6 +1982,10 @@ impl Processor {
19781982
.calc_lamports_withdraw_amount(pool_tokens_burnt)
19791983
.ok_or(StakePoolError::CalculationFailure)?;
19801984

1985+
if withdraw_lamports == 0 {
1986+
return Err(StakePoolError::WithdrawalTooSmall.into());
1987+
}
1988+
19811989
let has_active_stake = validator_list
19821990
.find::<ValidatorStakeInfo>(
19831991
&0u64.to_le_bytes(),
@@ -2426,6 +2434,8 @@ impl PrintProgramError for StakePoolError {
24262434
StakePoolError::IncorrectWithdrawVoteAddress => msg!("Error: The provided withdraw stake account is not the preferred deposit vote account"),
24272435
StakePoolError::InvalidMintFreezeAuthority => msg!("Error: The mint has an invalid freeze authority"),
24282436
StakePoolError::FeeIncreaseTooHigh => msg!("Error: The fee cannot increase by a factor exceeding the stipulated ratio"),
2437+
StakePoolError::WithdrawalTooSmall => msg!("Error: Not enough pool tokens provided to withdraw 1-lamport stake"),
2438+
StakePoolError::DepositTooSmall => msg!("Error: Not enough lamports provided for deposit to result in one pool token"),
24292439
}
24302440
}
24312441
}

stake-pool/program/src/state.rs

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use {
1414
program_pack::{Pack, Sealed},
1515
pubkey::{Pubkey, PUBKEY_BYTES},
1616
},
17+
spl_math::checked_ceil_div::CheckedCeilDiv,
1718
std::convert::TryFrom,
1819
};
1920

@@ -135,12 +136,10 @@ impl StakePool {
135136

136137
/// calculate lamports amount on withdrawal
137138
pub fn calc_lamports_withdraw_amount(&self, pool_tokens: u64) -> Option<u64> {
138-
u64::try_from(
139-
(pool_tokens as u128)
140-
.checked_mul(self.total_stake_lamports as u128)?
141-
.checked_div(self.pool_token_supply as u128)?,
142-
)
143-
.ok()
139+
let (quotient, _) = (pool_tokens as u128)
140+
.checked_mul(self.total_stake_lamports as u128)?
141+
.checked_ceil_div(self.pool_token_supply as u128)?;
142+
u64::try_from(quotient).ok()
144143
}
145144

146145
/// calculate pool tokens to be deducted as withdrawal fees
@@ -159,12 +158,16 @@ impl StakePool {
159158
let total_stake_lamports =
160159
(self.total_stake_lamports as u128).checked_add(reward_lamports as u128)?;
161160
let fee_lamports = self.fee.apply(reward_lamports)?;
162-
u64::try_from(
163-
(self.pool_token_supply as u128)
164-
.checked_mul(fee_lamports)?
165-
.checked_div(total_stake_lamports.checked_sub(fee_lamports)?)?,
166-
)
167-
.ok()
161+
if total_stake_lamports == fee_lamports || self.pool_token_supply == 0 {
162+
Some(reward_lamports)
163+
} else {
164+
u64::try_from(
165+
(self.pool_token_supply as u128)
166+
.checked_mul(fee_lamports)?
167+
.checked_div(total_stake_lamports.checked_sub(fee_lamports)?)?,
168+
)
169+
.ok()
170+
}
168171
}
169172

170173
/// Checks that the withdraw or deposit authority is valid
@@ -777,7 +780,22 @@ mod test {
777780
let fee_lamports = stake_pool
778781
.calc_lamports_withdraw_amount(pool_token_fee)
779782
.unwrap();
780-
assert_eq!(fee_lamports, LAMPORTS_PER_SOL - 1); // lose 1 lamport of precision
783+
assert_eq!(fee_lamports, LAMPORTS_PER_SOL);
784+
}
785+
786+
#[test]
787+
fn divide_by_zero_fee() {
788+
let stake_pool = StakePool {
789+
total_stake_lamports: 0,
790+
fee: Fee {
791+
numerator: 1,
792+
denominator: 10,
793+
},
794+
..StakePool::default()
795+
};
796+
let rewards = 10;
797+
let fee = stake_pool.calc_fee_amount(rewards).unwrap();
798+
assert_eq!(fee, rewards);
781799
}
782800

783801
proptest! {
@@ -814,4 +832,33 @@ mod test {
814832
max_fee_lamports, fee_lamports, epsilon);
815833
}
816834
}
835+
836+
prop_compose! {
837+
fn total_tokens_and_deposit()(total_lamports in 1..u64::MAX)(
838+
total_lamports in Just(total_lamports),
839+
pool_token_supply in 1..=total_lamports,
840+
deposit_lamports in 1..total_lamports,
841+
) -> (u64, u64, u64) {
842+
(total_lamports - deposit_lamports, pool_token_supply.saturating_sub(deposit_lamports).max(1), deposit_lamports)
843+
}
844+
}
845+
846+
proptest! {
847+
#[test]
848+
fn deposit_and_withdraw(
849+
(total_stake_lamports, pool_token_supply, deposit_stake) in total_tokens_and_deposit()
850+
) {
851+
let mut stake_pool = StakePool {
852+
total_stake_lamports,
853+
pool_token_supply,
854+
..StakePool::default()
855+
};
856+
let deposit_result = stake_pool.calc_pool_tokens_for_deposit(deposit_stake).unwrap();
857+
prop_assume!(deposit_result > 0);
858+
stake_pool.total_stake_lamports += deposit_stake;
859+
stake_pool.pool_token_supply += deposit_result;
860+
let withdraw_result = stake_pool.calc_lamports_withdraw_amount(deposit_result).unwrap();
861+
assert!(withdraw_result <= deposit_stake);
862+
}
863+
}
817864
}

stake-pool/program/tests/withdraw.rs

Lines changed: 4 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -448,10 +448,10 @@ async fn fail_with_unknown_validator() {
448448
recent_blockhash,
449449
stake_pool_accounts,
450450
_,
451-
_,
451+
deposit_info,
452452
user_transfer_authority,
453453
user_stake_recipient,
454-
_,
454+
tokens_to_withdraw,
455455
) = setup().await;
456456

457457
let validator_stake_account =
@@ -475,80 +475,18 @@ async fn fail_with_unknown_validator() {
475475
)
476476
.await;
477477

478-
let user_pool_account = Keypair::new();
479-
let user = Keypair::new();
480-
create_token_account(
481-
&mut banks_client,
482-
&payer,
483-
&recent_blockhash,
484-
&user_pool_account,
485-
&stake_pool_accounts.pool_mint.pubkey(),
486-
&user.pubkey(),
487-
)
488-
.await
489-
.unwrap();
490-
491-
let user = Keypair::new();
492-
// make stake account
493-
let user_stake = Keypair::new();
494-
let lockup = stake_program::Lockup::default();
495-
let authorized = stake_program::Authorized {
496-
staker: stake_pool_accounts.deposit_authority,
497-
withdrawer: stake_pool_accounts.deposit_authority,
498-
};
499-
create_independent_stake_account(
500-
&mut banks_client,
501-
&payer,
502-
&recent_blockhash,
503-
&user_stake,
504-
&authorized,
505-
&lockup,
506-
TEST_STAKE_AMOUNT,
507-
)
508-
.await;
509-
// make pool token account
510-
let user_pool_account = Keypair::new();
511-
create_token_account(
512-
&mut banks_client,
513-
&payer,
514-
&recent_blockhash,
515-
&user_pool_account,
516-
&stake_pool_accounts.pool_mint.pubkey(),
517-
&user.pubkey(),
518-
)
519-
.await
520-
.unwrap();
521-
522-
let user_pool_account = user_pool_account.pubkey();
523-
let pool_tokens = get_token_balance(&mut banks_client, &user_pool_account).await;
524-
525-
let tokens_to_burn = pool_tokens / 4;
526-
527-
// Delegate tokens for burning
528-
delegate_tokens(
529-
&mut banks_client,
530-
&payer,
531-
&recent_blockhash,
532-
&user_pool_account,
533-
&user,
534-
&user_transfer_authority.pubkey(),
535-
tokens_to_burn,
536-
)
537-
.await;
538-
539478
let new_authority = Pubkey::new_unique();
540-
541479
let transaction_error = stake_pool_accounts
542480
.withdraw_stake(
543481
&mut banks_client,
544482
&payer,
545483
&recent_blockhash,
546484
&user_stake_recipient.pubkey(),
547485
&user_transfer_authority,
548-
&user_pool_account,
486+
&deposit_info.pool_account.pubkey(),
549487
&validator_stake_account.stake_account,
550488
&new_authority,
551-
tokens_to_burn,
489+
tokens_to_withdraw,
552490
)
553491
.await
554492
.unwrap()

0 commit comments

Comments
 (0)