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

Commit 61a53ab

Browse files
authored
stake-pool: Add ability to withdraw from reserve if no stake available (#1627)
1 parent 1e47030 commit 61a53ab

File tree

2 files changed

+246
-35
lines changed

2 files changed

+246
-35
lines changed

stake-pool/program/src/processor.rs

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,30 +1640,59 @@ impl Processor {
16401640
return Err(StakePoolError::InvalidState.into());
16411641
}
16421642

1643-
let (meta, stake) = get_stake_state(stake_split_from)?;
1644-
let vote_account_address = stake.delegation.voter_pubkey;
1645-
check_validator_stake_address(
1646-
program_id,
1647-
stake_pool_info.key,
1648-
stake_split_from.key,
1649-
&vote_account_address,
1650-
)?;
1651-
1652-
let validator_list_item = validator_list
1653-
.find_mut(&vote_account_address)
1654-
.ok_or(StakePoolError::ValidatorNotFound)?;
1655-
16561643
let withdraw_lamports = stake_pool
16571644
.calc_lamports_withdraw_amount(pool_tokens)
16581645
.ok_or(StakePoolError::CalculationFailure)?;
16591646

1660-
let required_lamports = minimum_stake_lamports(&meta);
1661-
let current_lamports = **stake_split_from.lamports.borrow();
1662-
let remaining_lamports = current_lamports.saturating_sub(withdraw_lamports);
1663-
if remaining_lamports < required_lamports {
1664-
msg!("Attempting to withdraw {} lamports from validator account with {} lamports, {} must remain", withdraw_lamports, current_lamports, required_lamports);
1665-
return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into());
1666-
}
1647+
let validator_list_item = if *stake_split_from.key == stake_pool.reserve_stake {
1648+
// check that the validator stake accounts have no withdrawable stake
1649+
if let Some(withdrawable_entry) = validator_list
1650+
.validators
1651+
.iter()
1652+
.find(|&&x| x.stake_lamports != 0)
1653+
{
1654+
let (validator_stake_address, _) = crate::find_stake_program_address(
1655+
&program_id,
1656+
&withdrawable_entry.vote_account_address,
1657+
stake_pool_info.key,
1658+
);
1659+
msg!("Error withdrawing from reserve: validator stake account {} has {} lamports available, please use that first.", validator_stake_address, withdrawable_entry.stake_lamports);
1660+
return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into());
1661+
}
1662+
1663+
// check that reserve has enough (should never fail, but who knows?)
1664+
let stake_state = try_from_slice_unchecked::<stake_program::StakeState>(
1665+
&stake_split_from.data.borrow(),
1666+
)?;
1667+
let meta = stake_state.meta().ok_or(StakePoolError::WrongStakeState)?;
1668+
stake_split_from
1669+
.lamports()
1670+
.checked_sub(minimum_reserve_lamports(&meta))
1671+
.ok_or(StakePoolError::StakeLamportsNotEqualToMinimum)?;
1672+
None
1673+
} else {
1674+
let (meta, stake) = get_stake_state(stake_split_from)?;
1675+
let vote_account_address = stake.delegation.voter_pubkey;
1676+
check_validator_stake_address(
1677+
program_id,
1678+
stake_pool_info.key,
1679+
stake_split_from.key,
1680+
&vote_account_address,
1681+
)?;
1682+
1683+
let validator_list_item = validator_list
1684+
.find_mut(&vote_account_address)
1685+
.ok_or(StakePoolError::ValidatorNotFound)?;
1686+
1687+
let required_lamports = minimum_stake_lamports(&meta);
1688+
let current_lamports = stake_split_from.lamports();
1689+
let remaining_lamports = current_lamports.saturating_sub(withdraw_lamports);
1690+
if remaining_lamports < required_lamports {
1691+
msg!("Attempting to withdraw {} lamports from validator account with {} lamports, {} must remain", withdraw_lamports, current_lamports, required_lamports);
1692+
return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into());
1693+
}
1694+
Some(validator_list_item)
1695+
};
16671696

16681697
Self::token_burn(
16691698
stake_pool_info.key,
@@ -1707,11 +1736,13 @@ impl Processor {
17071736
.ok_or(StakePoolError::CalculationFailure)?;
17081737
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
17091738

1710-
validator_list_item.stake_lamports = validator_list_item
1711-
.stake_lamports
1712-
.checked_sub(withdraw_lamports)
1713-
.ok_or(StakePoolError::CalculationFailure)?;
1714-
validator_list.serialize(&mut *validator_list_info.data.borrow_mut())?;
1739+
if let Some(validator_list_item) = validator_list_item {
1740+
validator_list_item.stake_lamports = validator_list_item
1741+
.stake_lamports
1742+
.checked_sub(withdraw_lamports)
1743+
.ok_or(StakePoolError::CalculationFailure)?;
1744+
validator_list.serialize(&mut *validator_list_info.data.borrow_mut())?;
1745+
}
17151746

17161747
Ok(())
17171748
}

stake-pool/program/tests/withdraw.rs

Lines changed: 190 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -396,12 +396,7 @@ async fn fail_with_wrong_validator_list() {
396396

397397
#[tokio::test]
398398
async fn fail_with_unknown_validator() {
399-
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
400-
let stake_pool_accounts = StakePoolAccounts::new();
401-
stake_pool_accounts
402-
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
403-
.await
404-
.unwrap();
399+
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, _, _, _) = setup().await;
405400

406401
let validator_stake_account =
407402
ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
@@ -502,13 +497,11 @@ async fn fail_with_unknown_validator() {
502497
tokens_to_burn,
503498
)
504499
.await
500+
.unwrap()
505501
.unwrap();
506502

507503
match transaction_error {
508-
TransportError::TransactionError(TransactionError::InstructionError(
509-
_,
510-
InstructionError::Custom(error_index),
511-
)) => {
504+
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
512505
let program_error = StakePoolError::ValidatorNotFound as u32;
513506
assert_eq!(error_index, program_error);
514507
}
@@ -790,3 +783,190 @@ async fn fail_overdraw_validator() {
790783
),
791784
);
792785
}
786+
787+
#[tokio::test]
788+
async fn success_with_reserve() {
789+
let mut context = program_test().start_with_context().await;
790+
let stake_pool_accounts = StakePoolAccounts::new();
791+
let initial_reserve_lamports = 1;
792+
stake_pool_accounts
793+
.initialize_stake_pool(
794+
&mut context.banks_client,
795+
&context.payer,
796+
&context.last_blockhash,
797+
initial_reserve_lamports,
798+
)
799+
.await
800+
.unwrap();
801+
802+
let validator_stake = simple_add_validator_to_pool(
803+
&mut context.banks_client,
804+
&context.payer,
805+
&context.last_blockhash,
806+
&stake_pool_accounts,
807+
)
808+
.await;
809+
810+
let deposit_lamports = TEST_STAKE_AMOUNT;
811+
let rent = context.banks_client.get_rent().await.unwrap();
812+
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
813+
814+
let deposit_info = simple_deposit(
815+
&mut context.banks_client,
816+
&context.payer,
817+
&context.last_blockhash,
818+
&stake_pool_accounts,
819+
&validator_stake,
820+
deposit_lamports,
821+
)
822+
.await;
823+
824+
// decrease some stake
825+
let error = stake_pool_accounts
826+
.decrease_validator_stake(
827+
&mut context.banks_client,
828+
&context.payer,
829+
&context.last_blockhash,
830+
&validator_stake.stake_account,
831+
&validator_stake.transient_stake_account,
832+
deposit_lamports - 1,
833+
)
834+
.await;
835+
assert!(error.is_none());
836+
837+
// warp forward to deactivation
838+
let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot;
839+
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
840+
context
841+
.warp_to_slot(first_normal_slot + slots_per_epoch)
842+
.unwrap();
843+
844+
// update to merge deactivated stake into reserve
845+
stake_pool_accounts
846+
.update_all(
847+
&mut context.banks_client,
848+
&context.payer,
849+
&context.last_blockhash,
850+
&[validator_stake.vote.pubkey()],
851+
false,
852+
)
853+
.await;
854+
855+
// Delegate tokens for burning during withdraw
856+
delegate_tokens(
857+
&mut context.banks_client,
858+
&context.payer,
859+
&context.last_blockhash,
860+
&deposit_info.pool_account.pubkey(),
861+
&deposit_info.authority,
862+
&stake_pool_accounts.withdraw_authority,
863+
deposit_info.pool_tokens,
864+
)
865+
.await;
866+
867+
// Withdraw directly from reserve, fail because some stake left
868+
let withdraw_destination = Keypair::new();
869+
let withdraw_destination_authority = Pubkey::new_unique();
870+
let initial_stake_lamports = create_blank_stake_account(
871+
&mut context.banks_client,
872+
&context.payer,
873+
&context.last_blockhash,
874+
&withdraw_destination,
875+
)
876+
.await;
877+
let error = stake_pool_accounts
878+
.withdraw_stake(
879+
&mut context.banks_client,
880+
&context.payer,
881+
&context.last_blockhash,
882+
&withdraw_destination.pubkey(),
883+
&deposit_info.pool_account.pubkey(),
884+
&stake_pool_accounts.reserve_stake.pubkey(),
885+
&withdraw_destination_authority,
886+
deposit_info.pool_tokens,
887+
)
888+
.await
889+
.unwrap()
890+
.unwrap();
891+
assert_eq!(
892+
error,
893+
TransactionError::InstructionError(
894+
0,
895+
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
896+
)
897+
);
898+
899+
// decrease rest of stake
900+
let error = stake_pool_accounts
901+
.decrease_validator_stake(
902+
&mut context.banks_client,
903+
&context.payer,
904+
&context.last_blockhash,
905+
&validator_stake.stake_account,
906+
&validator_stake.transient_stake_account,
907+
stake_rent + 1,
908+
)
909+
.await;
910+
assert!(error.is_none());
911+
912+
// warp forward to deactivation
913+
context
914+
.warp_to_slot(first_normal_slot + 2 * slots_per_epoch)
915+
.unwrap();
916+
917+
// update to merge deactivated stake into reserve
918+
stake_pool_accounts
919+
.update_all(
920+
&mut context.banks_client,
921+
&context.payer,
922+
&context.last_blockhash,
923+
&[validator_stake.vote.pubkey()],
924+
false,
925+
)
926+
.await;
927+
928+
// now it works
929+
let error = stake_pool_accounts
930+
.withdraw_stake(
931+
&mut context.banks_client,
932+
&context.payer,
933+
&context.last_blockhash,
934+
&withdraw_destination.pubkey(),
935+
&deposit_info.pool_account.pubkey(),
936+
&stake_pool_accounts.reserve_stake.pubkey(),
937+
&withdraw_destination_authority,
938+
deposit_info.pool_tokens,
939+
)
940+
.await;
941+
assert!(error.is_none());
942+
943+
// Check tokens burned
944+
let user_token_balance = get_token_balance(
945+
&mut context.banks_client,
946+
&deposit_info.pool_account.pubkey(),
947+
)
948+
.await;
949+
assert_eq!(user_token_balance, 0);
950+
951+
// Check reserve stake account balance
952+
let reserve_stake_account = get_account(
953+
&mut context.banks_client,
954+
&stake_pool_accounts.reserve_stake.pubkey(),
955+
)
956+
.await;
957+
let stake_state =
958+
deserialize::<stake_program::StakeState>(&reserve_stake_account.data).unwrap();
959+
let meta = stake_state.meta().unwrap();
960+
assert_eq!(
961+
initial_reserve_lamports + meta.rent_exempt_reserve,
962+
reserve_stake_account.lamports
963+
);
964+
965+
// Check user recipient stake account balance
966+
let user_stake_recipient_account =
967+
get_account(&mut context.banks_client, &withdraw_destination.pubkey()).await;
968+
assert_eq!(
969+
user_stake_recipient_account.lamports,
970+
initial_stake_lamports + deposit_info.stake_lamports + stake_rent
971+
);
972+
}

0 commit comments

Comments
 (0)