Skip to content

Commit f5753c0

Browse files
committed
program: Update for larger minimum required amount in accounts
1 parent ca44b89 commit f5753c0

4 files changed

Lines changed: 167 additions & 19 deletions

File tree

program/src/processor.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2895,18 +2895,24 @@ impl Processor {
28952895
let lamports_per_pool_token = stake_pool
28962896
.get_lamports_per_pool_token()
28972897
.ok_or(StakePoolError::CalculationFailure)?;
2898-
let minimum_lamports_with_tolerance =
2899-
required_lamports.saturating_add(lamports_per_pool_token);
29002898

2901-
let has_active_stake = validator_list
2899+
// Since this instruction performs a stake split on an active stake, the
2900+
// source stake needs to have at least twice the minimum delegation
2901+
// amount, so that both accounts have at least the minimum delegation
2902+
// afterwards.
2903+
let minimum_lamports_with_tolerance = required_lamports
2904+
.saturating_add(stake_minimum_delegation)
2905+
.saturating_add(lamports_per_pool_token);
2906+
2907+
let has_withdrawable_active_stake = validator_list
29022908
.find::<ValidatorStakeInfo, _>(|x| {
29032909
ValidatorStakeInfo::active_lamports_greater_than(
29042910
x,
29052911
&minimum_lamports_with_tolerance,
29062912
) && ValidatorStakeInfo::is_active(x)
29072913
})
29082914
.is_some();
2909-
let has_transient_stake = validator_list
2915+
let has_withdrawable_transient_stake = validator_list
29102916
.find::<ValidatorStakeInfo, _>(|x| {
29112917
ValidatorStakeInfo::transient_lamports_greater_than(
29122918
x,
@@ -2917,7 +2923,7 @@ impl Processor {
29172923

29182924
let validator_list_item_info = if *stake_split_from.key == stake_pool.reserve_stake {
29192925
// check that the validator stake accounts have no withdrawable stake
2920-
if has_transient_stake || has_active_stake {
2926+
if has_withdrawable_transient_stake || has_withdrawable_active_stake {
29212927
msg!("Error withdrawing from reserve: validator stake accounts have lamports available, please use those first.");
29222928
return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into());
29232929
}
@@ -2952,11 +2958,9 @@ impl Processor {
29522958
ValidatorStakeInfo::memcmp_pubkey(x, &preferred_withdraw_validator)
29532959
})
29542960
{
2955-
let available_lamports =
2956-
u64::from(preferred_validator_info.active_stake_lamports)
2957-
.saturating_sub(minimum_lamports_with_tolerance);
29582961
if preferred_withdraw_validator != vote_account_address
2959-
&& available_lamports > 0
2962+
&& u64::from(preferred_validator_info.active_stake_lamports)
2963+
>= minimum_lamports_with_tolerance
29602964
{
29612965
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));
29622966
return Err(StakePoolError::IncorrectWithdrawVoteAddress.into());
@@ -2972,7 +2976,7 @@ impl Processor {
29722976
})
29732977
.ok_or(StakePoolError::ValidatorNotFound)?;
29742978

2975-
let withdraw_source = if has_active_stake {
2979+
let withdraw_source = if has_withdrawable_active_stake {
29762980
// if there's any active stake, we must withdraw from an active
29772981
// stake account
29782982
check_validator_stake_address(
@@ -2983,7 +2987,7 @@ impl Processor {
29832987
NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()),
29842988
)?;
29852989
StakeWithdrawSource::Active
2986-
} else if has_transient_stake
2990+
} else if has_withdrawable_transient_stake
29872991
|| validator_stake_info.transient_stake_lamports != 0.into()
29882992
{
29892993
// if there's any transient stake, we must withdraw from there

program/tests/helpers/mod.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,16 @@ impl StakePoolAccounts {
921921
}
922922
}
923923

924+
pub fn new_without_fees() -> Self {
925+
Self {
926+
epoch_fee: state::Fee::default(),
927+
withdrawal_fee: state::Fee::default(),
928+
deposit_fee: state::Fee::default(),
929+
sol_deposit_fee: state::Fee::default(),
930+
..Default::default()
931+
}
932+
}
933+
924934
pub fn calculate_fee(&self, amount: u64) -> u64 {
925935
(amount * self.epoch_fee.numerator).div_ceil(self.epoch_fee.denominator)
926936
}
@@ -2606,20 +2616,18 @@ pub fn add_token_account(
26062616
program_test.add_account(*account_key, fee_account);
26072617
}
26082618

2609-
pub async fn setup_for_withdraw(
2610-
token_program_id: Pubkey,
2619+
pub async fn setup_for_withdraw_with_accounts(
2620+
stake_pool_accounts: &StakePoolAccounts,
26112621
reserve_lamports: u64,
26122622
) -> (
26132623
ProgramTestContext,
2614-
StakePoolAccounts,
26152624
ValidatorStakeAccount,
26162625
DepositStakeAccount,
26172626
Keypair,
26182627
Keypair,
26192628
u64,
26202629
) {
26212630
let mut context = program_test().start_with_context().await;
2622-
let stake_pool_accounts = StakePoolAccounts::new_with_token_program(token_program_id);
26232631
stake_pool_accounts
26242632
.initialize_stake_pool(
26252633
&mut context.banks_client,
@@ -2634,7 +2642,7 @@ pub async fn setup_for_withdraw(
26342642
&mut context.banks_client,
26352643
&context.payer,
26362644
&context.last_blockhash,
2637-
&stake_pool_accounts,
2645+
stake_pool_accounts,
26382646
None,
26392647
)
26402648
.await;
@@ -2650,7 +2658,7 @@ pub async fn setup_for_withdraw(
26502658
&mut context.banks_client,
26512659
&context.payer,
26522660
&context.last_blockhash,
2653-
&stake_pool_accounts,
2661+
stake_pool_accounts,
26542662
&validator_stake_account,
26552663
current_minimum_delegation * 3,
26562664
)
@@ -2683,6 +2691,38 @@ pub async fn setup_for_withdraw(
26832691
)
26842692
.await;
26852693

2694+
(
2695+
context,
2696+
validator_stake_account,
2697+
deposit_info,
2698+
user_transfer_authority,
2699+
user_stake_recipient,
2700+
tokens_to_withdraw,
2701+
)
2702+
}
2703+
2704+
pub async fn setup_for_withdraw(
2705+
token_program_id: Pubkey,
2706+
reserve_lamports: u64,
2707+
) -> (
2708+
ProgramTestContext,
2709+
StakePoolAccounts,
2710+
ValidatorStakeAccount,
2711+
DepositStakeAccount,
2712+
Keypair,
2713+
Keypair,
2714+
u64,
2715+
) {
2716+
let stake_pool_accounts = StakePoolAccounts::new_with_token_program(token_program_id);
2717+
let (
2718+
context,
2719+
validator_stake_account,
2720+
deposit_info,
2721+
user_transfer_authority,
2722+
user_stake_recipient,
2723+
tokens_to_withdraw,
2724+
) = setup_for_withdraw_with_accounts(&stake_pool_accounts, reserve_lamports).await;
2725+
26862726
(
26872727
context,
26882728
stake_pool_accounts,

program/tests/update_validator_list_balance.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,14 @@ async fn success_with_normal() {
201201

202202
// Simulate rewards
203203
for stake_account in &stake_accounts {
204-
context.increment_vote_account_credits(&stake_account.vote.pubkey(), 100);
204+
transfer(
205+
&mut context.banks_client,
206+
&context.payer,
207+
&context.last_blockhash,
208+
&stake_account.stake_account,
209+
1_000,
210+
)
211+
.await;
205212
}
206213

207214
// Warp one more epoch so the rewards are paid out

program/tests/withdraw_edge_cases.rs

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,10 @@ async fn fail_withdraw_from_transient() {
916916
.await
917917
.unwrap();
918918

919+
let stake_minimum_delegation =
920+
stake_get_minimum_delegation(&mut context.banks_client, &context.payer, &last_blockhash)
921+
.await;
922+
919923
let rent = context.banks_client.get_rent().await.unwrap();
920924
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake::state::StakeStateV2>());
921925

@@ -927,7 +931,7 @@ async fn fail_withdraw_from_transient() {
927931
&last_blockhash,
928932
&validator_stake_account.stake_account,
929933
&validator_stake_account.transient_stake_account,
930-
deposit_info.stake_lamports + stake_rent - 2,
934+
deposit_info.stake_lamports + stake_rent - stake_minimum_delegation - 2,
931935
validator_stake_account.transient_stake_seed,
932936
DecreaseInstruction::Reserve,
933937
)
@@ -1531,3 +1535,96 @@ async fn success_remove_preferred_validator_resets_preference() {
15311535
"User should receive all lamports from removed validator"
15321536
);
15331537
}
1538+
1539+
#[tokio::test]
1540+
async fn fail_withdrawal_minimum_in_preferred() {
1541+
let stake_pool_accounts = StakePoolAccounts::new_without_fees();
1542+
1543+
let (
1544+
mut context,
1545+
validator_stake,
1546+
deposit_info,
1547+
user_transfer_authority,
1548+
user_stake_recipient,
1549+
tokens_to_burn,
1550+
) = setup_for_withdraw_with_accounts(&stake_pool_accounts, 0).await;
1551+
1552+
stake_pool_accounts
1553+
.set_preferred_validator(
1554+
&mut context.banks_client,
1555+
&context.payer,
1556+
&context.last_blockhash,
1557+
instruction::PreferredValidatorType::Withdraw,
1558+
Some(validator_stake.vote.pubkey()),
1559+
)
1560+
.await;
1561+
1562+
// Warp forward to activation
1563+
let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot;
1564+
let slot = first_normal_slot + 1;
1565+
context.warp_to_slot(slot).unwrap();
1566+
let error = stake_pool_accounts
1567+
.update_all(
1568+
&mut context.banks_client,
1569+
&context.payer,
1570+
&context.last_blockhash,
1571+
false,
1572+
)
1573+
.await;
1574+
assert!(error.is_none());
1575+
1576+
// Withdraw some from preferred, get it down to 2x + 1
1577+
let stake_minimum_delegation = stake_get_minimum_delegation(
1578+
&mut context.banks_client,
1579+
&context.payer,
1580+
&context.last_blockhash,
1581+
)
1582+
.await;
1583+
1584+
let new_authority = Pubkey::new_unique();
1585+
let error = stake_pool_accounts
1586+
.withdraw_stake(
1587+
&mut context.banks_client,
1588+
&context.payer,
1589+
&context.last_blockhash,
1590+
&user_stake_recipient.pubkey(),
1591+
&user_transfer_authority,
1592+
&deposit_info.pool_account.pubkey(),
1593+
&validator_stake.stake_account,
1594+
&new_authority,
1595+
tokens_to_burn - stake_minimum_delegation,
1596+
)
1597+
.await;
1598+
assert!(error.is_none(), "{:?}", error);
1599+
1600+
// preferred is not empty, withdrawing from non-preferred fails
1601+
let user_stake_recipient = Keypair::new();
1602+
create_blank_stake_account(
1603+
&mut context.banks_client,
1604+
&context.payer,
1605+
&context.last_blockhash,
1606+
&user_stake_recipient,
1607+
)
1608+
.await;
1609+
let error = stake_pool_accounts
1610+
.withdraw_stake(
1611+
&mut context.banks_client,
1612+
&context.payer,
1613+
&context.last_blockhash,
1614+
&user_stake_recipient.pubkey(),
1615+
&user_transfer_authority,
1616+
&deposit_info.pool_account.pubkey(),
1617+
&validator_stake.stake_account,
1618+
&new_authority,
1619+
stake_minimum_delegation,
1620+
)
1621+
.await;
1622+
let transaction_error = error.unwrap().unwrap();
1623+
assert_eq!(
1624+
transaction_error,
1625+
TransactionError::InstructionError(
1626+
0,
1627+
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
1628+
)
1629+
);
1630+
}

0 commit comments

Comments
 (0)