Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,167 changes: 2,156 additions & 1,011 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ check-cfg = [
]

[workspace.metadata.cli]
solana = "3.1.14"
solana = "4.0.3"
4 changes: 2 additions & 2 deletions program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ solana-account = { version = "4.3.1", features = ["bincode"] }
solana-compute-budget-interface = "3.0.0"
solana-native-token = "3.0.0"
solana-program = "4.0.0"
solana-program-test = { version = "3.1.8", features = ["agave-unstable-api"] }
solana-sdk = "3.0.0"
solana-program-test = { version = "4.1.0-rc.1", features = ["agave-unstable-api"] }
solana-sdk = "4.0.0"
solana-vote-interface = { version = "6.0.0", features = ["bincode"] }
spl-token-interface = "3.0.0"
test-case = "3.3"
Expand Down
26 changes: 15 additions & 11 deletions program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2895,18 +2895,24 @@ impl Processor {
let lamports_per_pool_token = stake_pool
.get_lamports_per_pool_token()
.ok_or(StakePoolError::CalculationFailure)?;
let minimum_lamports_with_tolerance =
required_lamports.saturating_add(lamports_per_pool_token);

let has_active_stake = validator_list
// Since this instruction performs a stake split on an active stake, the
// source stake needs to have at least twice the minimum delegation
// amount, so that both accounts have at least the minimum delegation
// afterwards.
let minimum_lamports_with_tolerance = required_lamports
.saturating_add(stake_minimum_delegation)
.saturating_add(lamports_per_pool_token);

let has_withdrawable_active_stake = validator_list
.find::<ValidatorStakeInfo, _>(|x| {
ValidatorStakeInfo::active_lamports_greater_than(
x,
&minimum_lamports_with_tolerance,
) && ValidatorStakeInfo::is_active(x)
})
.is_some();
let has_transient_stake = validator_list
let has_withdrawable_transient_stake = validator_list
.find::<ValidatorStakeInfo, _>(|x| {
ValidatorStakeInfo::transient_lamports_greater_than(
x,
Expand All @@ -2917,7 +2923,7 @@ impl Processor {

let validator_list_item_info = if *stake_split_from.key == stake_pool.reserve_stake {
// check that the validator stake accounts have no withdrawable stake
if has_transient_stake || has_active_stake {
if has_withdrawable_transient_stake || has_withdrawable_active_stake {
msg!("Error withdrawing from reserve: validator stake accounts have lamports available, please use those first.");
return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into());
}
Expand Down Expand Up @@ -2952,11 +2958,9 @@ impl Processor {
ValidatorStakeInfo::memcmp_pubkey(x, &preferred_withdraw_validator)
})
{
let available_lamports =
u64::from(preferred_validator_info.active_stake_lamports)
.saturating_sub(minimum_lamports_with_tolerance);
if preferred_withdraw_validator != vote_account_address
&& available_lamports > 0
&& u64::from(preferred_validator_info.active_stake_lamports)
>= minimum_lamports_with_tolerance
Comment on lines +2962 to +2963

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think >= is correct here, since if the stake account has exactly minimum_lamports_with_tolerance, it should be possible to withdraw a stake account

{
msg!("Validator vote address {} is preferred for withdrawals, it currently has {} lamports available. Please withdraw those before using other validator stake accounts.", preferred_withdraw_validator, u64::from(preferred_validator_info.active_stake_lamports));
return Err(StakePoolError::IncorrectWithdrawVoteAddress.into());
Expand All @@ -2972,7 +2976,7 @@ impl Processor {
})
.ok_or(StakePoolError::ValidatorNotFound)?;

let withdraw_source = if has_active_stake {
let withdraw_source = if has_withdrawable_active_stake {
// if there's any active stake, we must withdraw from an active
// stake account
check_validator_stake_address(
Expand All @@ -2983,7 +2987,7 @@ impl Processor {
NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()),
)?;
StakeWithdrawSource::Active
} else if has_transient_stake
} else if has_withdrawable_transient_stake
|| validator_stake_info.transient_stake_lamports != 0.into()
{
// if there's any transient stake, we must withdraw from there
Expand Down
Binary file modified program/tests/fixtures/solana_stake_program.so
Binary file not shown.
59 changes: 48 additions & 11 deletions program/tests/helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#![allow(dead_code)]

use {
agave_feature_set::stake_raise_minimum_delegation_to_1_sol,
borsh::BorshDeserialize,
solana_compute_budget_interface::ComputeBudgetInstruction,
solana_program::{
Expand Down Expand Up @@ -55,17 +54,15 @@ const ACCOUNT_RENT_EXEMPTION: u64 = 1_000_000_000; // go with something big to b

pub fn program_test() -> ProgramTest {
let mut program_test = ProgramTest::new("spl_stake_pool", id(), processor!(Processor::process));
program_test.add_upgradeable_program_to_genesis("solana_stake_program", &stake::program::id());
program_test.deactivate_feature(stake_raise_minimum_delegation_to_1_sol::id());
program_test.add_program("solana_stake_program", stake::program::id(), None);
program_test
}

pub fn program_test_with_metadata_program() -> ProgramTest {
let mut program_test = ProgramTest::default();
program_test.add_upgradeable_program_to_genesis("solana_stake_program", &stake::program::id());
program_test.deactivate_feature(stake_raise_minimum_delegation_to_1_sol::id());
program_test.add_program("spl_stake_pool", id(), processor!(Processor::process));
program_test.add_program("mpl_token_metadata", inline_mpl_token_metadata::id(), None);
program_test.add_program("solana_stake_program", stake::program::id(), None);
program_test
}

Expand Down Expand Up @@ -924,6 +921,16 @@ impl StakePoolAccounts {
}
}

pub fn new_without_fees() -> Self {
Self {
epoch_fee: state::Fee::default(),
withdrawal_fee: state::Fee::default(),
deposit_fee: state::Fee::default(),
sol_deposit_fee: state::Fee::default(),
..Default::default()
}
}

pub fn calculate_fee(&self, amount: u64) -> u64 {
(amount * self.epoch_fee.numerator).div_ceil(self.epoch_fee.denominator)
}
Expand Down Expand Up @@ -2609,20 +2616,18 @@ pub fn add_token_account(
program_test.add_account(*account_key, fee_account);
}

pub async fn setup_for_withdraw(
token_program_id: Pubkey,
pub async fn setup_for_withdraw_with_accounts(
stake_pool_accounts: &StakePoolAccounts,
reserve_lamports: u64,
) -> (
ProgramTestContext,
StakePoolAccounts,
ValidatorStakeAccount,
DepositStakeAccount,
Keypair,
Keypair,
u64,
) {
let mut context = program_test().start_with_context().await;
let stake_pool_accounts = StakePoolAccounts::new_with_token_program(token_program_id);
stake_pool_accounts
.initialize_stake_pool(
&mut context.banks_client,
Expand All @@ -2637,7 +2642,7 @@ pub async fn setup_for_withdraw(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
stake_pool_accounts,
None,
)
.await;
Expand All @@ -2653,7 +2658,7 @@ pub async fn setup_for_withdraw(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
stake_pool_accounts,
&validator_stake_account,
current_minimum_delegation * 3,
)
Expand Down Expand Up @@ -2686,6 +2691,38 @@ pub async fn setup_for_withdraw(
)
.await;

(
context,
validator_stake_account,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_withdraw,
)
}

pub async fn setup_for_withdraw(
token_program_id: Pubkey,
reserve_lamports: u64,
) -> (
ProgramTestContext,
StakePoolAccounts,
ValidatorStakeAccount,
DepositStakeAccount,
Keypair,
Keypair,
u64,
) {
let stake_pool_accounts = StakePoolAccounts::new_with_token_program(token_program_id);
let (
context,
validator_stake_account,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_withdraw,
) = setup_for_withdraw_with_accounts(&stake_pool_accounts, reserve_lamports).await;

(
context,
stake_pool_accounts,
Expand Down
9 changes: 8 additions & 1 deletion program/tests/update_validator_list_balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,14 @@ async fn success_with_normal() {

// Simulate rewards
for stake_account in &stake_accounts {
context.increment_vote_account_credits(&stake_account.vote.pubkey(), 100);
transfer(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_account.stake_account,
1_000,
)
.await;
}

// Warp one more epoch so the rewards are paid out
Expand Down
99 changes: 98 additions & 1 deletion program/tests/withdraw_edge_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,10 @@ async fn fail_withdraw_from_transient() {
.await
.unwrap();

let stake_minimum_delegation =
stake_get_minimum_delegation(&mut context.banks_client, &context.payer, &last_blockhash)
.await;

let rent = context.banks_client.get_rent().await.unwrap();
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake::state::StakeStateV2>());

Expand All @@ -927,7 +931,7 @@ async fn fail_withdraw_from_transient() {
&last_blockhash,
&validator_stake_account.stake_account,
&validator_stake_account.transient_stake_account,
deposit_info.stake_lamports + stake_rent - 2,
deposit_info.stake_lamports + stake_rent - stake_minimum_delegation - 2,
validator_stake_account.transient_stake_seed,
DecreaseInstruction::Reserve,
)
Expand Down Expand Up @@ -1531,3 +1535,96 @@ async fn success_remove_preferred_validator_resets_preference() {
"User should receive all lamports from removed validator"
);
}

#[tokio::test]
async fn fail_withdrawal_minimum_in_preferred() {
let stake_pool_accounts = StakePoolAccounts::new_without_fees();

let (
mut context,
validator_stake,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_burn,
) = setup_for_withdraw_with_accounts(&stake_pool_accounts, 0).await;

stake_pool_accounts
.set_preferred_validator(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
instruction::PreferredValidatorType::Withdraw,
Some(validator_stake.vote.pubkey()),
)
.await;

// Warp forward to activation
let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot;
let slot = first_normal_slot + 1;
context.warp_to_slot(slot).unwrap();
let error = stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
false,
)
.await;
assert!(error.is_none());

// Withdraw some from preferred, get it down to 2x + 1
let stake_minimum_delegation = stake_get_minimum_delegation(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
)
.await;

let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.withdraw_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake.stake_account,
&new_authority,
tokens_to_burn - stake_minimum_delegation,
)
.await;
assert!(error.is_none(), "{:?}", error);

// preferred is not empty, withdrawing from non-preferred fails
let user_stake_recipient = Keypair::new();
create_blank_stake_account(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&user_stake_recipient,
)
.await;
let error = stake_pool_accounts
.withdraw_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake.stake_account,
&new_authority,
stake_minimum_delegation,
)
.await;
let transaction_error = error.unwrap().unwrap();
assert_eq!(
transaction_error,
TransactionError::InstructionError(
0,
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
)
);
}
Loading