diff --git a/stake-pool/program/src/big_vec.rs b/stake-pool/program/src/big_vec.rs index b98a29ecb9f..6d02142c86c 100644 --- a/stake-pool/program/src/big_vec.rs +++ b/stake-pool/program/src/big_vec.rs @@ -148,14 +148,14 @@ impl<'data> BigVec<'data> { } /// Find matching data in the array - pub fn find(&self, data: &[u8], predicate: fn(&[u8], &[u8]) -> bool) -> Option<&T> { + pub fn find bool>(&self, predicate: F) -> Option<&T> { let len = self.len() as usize; let mut current = 0; let mut current_index = VEC_SIZE_BYTES; while current != len { let end_index = current_index + T::LEN; let current_slice = &self.data[current_index..end_index]; - if predicate(current_slice, data) { + if predicate(current_slice) { return Some(unsafe { &*(current_slice.as_ptr() as *const T) }); } current_index = end_index; @@ -165,18 +165,14 @@ impl<'data> BigVec<'data> { } /// Find matching data in the array - pub fn find_mut( - &mut self, - data: &[u8], - predicate: fn(&[u8], &[u8]) -> bool, - ) -> Option<&mut T> { + pub fn find_mut bool>(&mut self, predicate: F) -> Option<&mut T> { let len = self.len() as usize; let mut current = 0; let mut current_index = VEC_SIZE_BYTES; while current != len { let end_index = current_index + T::LEN; let current_slice = &self.data[current_index..end_index]; - if predicate(current_slice, data) { + if predicate(current_slice) { return Some(unsafe { &mut *(current_slice.as_ptr() as *mut T) }); } current_index = end_index; @@ -242,10 +238,7 @@ impl<'data, 'vec, T: Pack + 'data> Iterator for IterMut<'data, 'vec, T> { #[cfg(test)] mod tests { - use { - super::*, - solana_program::{program_memory::sol_memcmp, program_pack::Sealed}, - }; + use {super::*, solana_program::program_pack::Sealed}; #[derive(Debug, PartialEq)] struct TestStruct { @@ -317,11 +310,11 @@ mod tests { check_big_vec_eq(&v, &[2, 4]); } - fn find_predicate(a: &[u8], b: &[u8]) -> bool { - if a.len() != b.len() { + fn find_predicate(a: &[u8], b: u64) -> bool { + if a.len() != 8 { false } else { - sol_memcmp(a, b, a.len()) == 0 + u64::try_from_slice(&a[0..8]).unwrap() == b } } @@ -330,17 +323,14 @@ mod tests { let mut data = [0u8; 4 + 8 * 4]; let v = from_slice(&mut data, &[1, 2, 3, 4]); assert_eq!( - v.find::(&1u64.to_le_bytes(), find_predicate), + v.find::(|x| find_predicate(x, 1)), Some(&TestStruct::new(1)) ); assert_eq!( - v.find::(&4u64.to_le_bytes(), find_predicate), + v.find::(|x| find_predicate(x, 4)), Some(&TestStruct::new(4)) ); - assert_eq!( - v.find::(&5u64.to_le_bytes(), find_predicate), - None - ); + assert_eq!(v.find::(|x| find_predicate(x, 5)), None); } #[test] @@ -348,14 +338,11 @@ mod tests { let mut data = [0u8; 4 + 8 * 4]; let mut v = from_slice(&mut data, &[1, 2, 3, 4]); let mut test_struct = v - .find_mut::(&1u64.to_le_bytes(), find_predicate) + .find_mut::(|x| find_predicate(x, 1)) .unwrap(); test_struct.value = 0; check_big_vec_eq(&v, &[0, 2, 3, 4]); - assert_eq!( - v.find_mut::(&5u64.to_le_bytes(), find_predicate), - None - ); + assert_eq!(v.find_mut::(|x| find_predicate(x, 5)), None); } #[test] diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 1d0025ca670..f0a6f1021b1 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -844,10 +844,9 @@ impl Processor { if header.max_validators == validator_list.len() { return Err(ProgramError::AccountDataTooSmall); } - let maybe_validator_stake_info = validator_list.find::( - validator_vote_info.key.as_ref(), - ValidatorStakeInfo::memcmp_pubkey, - ); + let maybe_validator_stake_info = validator_list.find::(|x| { + ValidatorStakeInfo::memcmp_pubkey(x, validator_vote_info.key) + }); if maybe_validator_stake_info.is_some() { return Err(StakePoolError::ValidatorAlreadyAdded.into()); } @@ -994,10 +993,9 @@ impl Processor { let (meta, stake) = get_stake_state(stake_account_info)?; let vote_account_address = stake.delegation.voter_pubkey; - let maybe_validator_stake_info = validator_list.find_mut::( - vote_account_address.as_ref(), - ValidatorStakeInfo::memcmp_pubkey, - ); + let maybe_validator_stake_info = validator_list.find_mut::(|x| { + ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address) + }); if maybe_validator_stake_info.is_none() { msg!( "Vote account {} not found in stake pool", @@ -1154,10 +1152,9 @@ impl Processor { let (meta, stake) = get_stake_state(validator_stake_account_info)?; let vote_account_address = stake.delegation.voter_pubkey; - let maybe_validator_stake_info = validator_list.find_mut::( - vote_account_address.as_ref(), - ValidatorStakeInfo::memcmp_pubkey, - ); + let maybe_validator_stake_info = validator_list.find_mut::(|x| { + ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address) + }); if maybe_validator_stake_info.is_none() { msg!( "Vote account {} not found in stake pool", @@ -1316,10 +1313,9 @@ impl Processor { let vote_account_address = validator_vote_account_info.key; - let maybe_validator_stake_info = validator_list.find_mut::( - vote_account_address.as_ref(), - ValidatorStakeInfo::memcmp_pubkey, - ); + let maybe_validator_stake_info = validator_list.find_mut::(|x| { + ValidatorStakeInfo::memcmp_pubkey(x, vote_account_address) + }); if maybe_validator_stake_info.is_none() { msg!( "Vote account {} not found in stake pool", @@ -1481,10 +1477,9 @@ impl Processor { } if let Some(vote_account_address) = vote_account_address { - let maybe_validator_stake_info = validator_list.find::( - vote_account_address.as_ref(), - ValidatorStakeInfo::memcmp_pubkey, - ); + let maybe_validator_stake_info = validator_list.find::(|x| { + ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address) + }); match maybe_validator_stake_info { Some(vsi) => { if vsi.status != StakeStatus::Active { @@ -2031,10 +2026,9 @@ impl Processor { } let mut validator_stake_info = validator_list - .find_mut::( - vote_account_address.as_ref(), - ValidatorStakeInfo::memcmp_pubkey, - ) + .find_mut::(|x| { + ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address) + }) .ok_or(StakePoolError::ValidatorNotFound)?; check_validator_stake_address( program_id, @@ -2428,7 +2422,7 @@ impl Processor { .checked_sub(pool_tokens_fee) .ok_or(StakePoolError::CalculationFailure)?; - let withdraw_lamports = stake_pool + let mut withdraw_lamports = stake_pool .calc_lamports_withdraw_amount(pool_tokens_burnt) .ok_or(StakePoolError::CalculationFailure)?; @@ -2442,17 +2436,27 @@ impl Processor { let meta = stake_state.meta().ok_or(StakePoolError::WrongStakeState)?; let required_lamports = minimum_stake_lamports(&meta, stake_minimum_delegation); + 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 - .find::( - &required_lamports.to_le_bytes(), - ValidatorStakeInfo::active_lamports_not_equal, - ) + .find::(|x| { + ValidatorStakeInfo::active_lamports_greater_than( + x, + &minimum_lamports_with_tolerance, + ) + }) .is_some(); let has_transient_stake = validator_list - .find::( - &0u64.to_le_bytes(), - ValidatorStakeInfo::transient_lamports_not_equal, - ) + .find::(|x| { + ValidatorStakeInfo::transient_lamports_greater_than( + x, + &minimum_lamports_with_tolerance, + ) + }) .is_some(); let validator_list_item_info = if *stake_split_from.key == stake_pool.reserve_stake { @@ -2478,14 +2482,13 @@ impl Processor { stake_pool.preferred_withdraw_validator_vote_address { let preferred_validator_info = validator_list - .find::( - preferred_withdraw_validator.as_ref(), - ValidatorStakeInfo::memcmp_pubkey, - ) + .find::(|x| { + ValidatorStakeInfo::memcmp_pubkey(x, &preferred_withdraw_validator) + }) .ok_or(StakePoolError::ValidatorNotFound)?; let available_lamports = preferred_validator_info .active_stake_lamports - .saturating_sub(required_lamports); + .saturating_sub(minimum_lamports_with_tolerance); if preferred_withdraw_validator != vote_account_address && available_lamports > 0 { 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, preferred_validator_info.active_stake_lamports); return Err(StakePoolError::IncorrectWithdrawVoteAddress.into()); @@ -2493,10 +2496,9 @@ impl Processor { } let validator_stake_info = validator_list - .find_mut::( - vote_account_address.as_ref(), - ValidatorStakeInfo::memcmp_pubkey, - ) + .find_mut::(|x| { + ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address) + }) .ok_or(StakePoolError::ValidatorNotFound)?; let withdraw_source = if has_active_stake { @@ -2548,11 +2550,21 @@ impl Processor { } } StakeWithdrawSource::ValidatorRemoval => { - if withdraw_lamports != stake_split_from.lamports() { - msg!("Cannot withdraw a whole account worth {} lamports, must withdraw exactly {} lamports worth of pool tokens", - withdraw_lamports, stake_split_from.lamports()); + let split_from_lamports = stake_split_from.lamports(); + let upper_bound = split_from_lamports.saturating_add(lamports_per_pool_token); + if withdraw_lamports < split_from_lamports || withdraw_lamports > upper_bound { + msg!( + "Cannot withdraw a whole account worth {} lamports, \ + must withdraw at least {} lamports worth of pool tokens \ + with a margin of {} lamports", + withdraw_lamports, + split_from_lamports, + lamports_per_pool_token + ); return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into()); } + // truncate the lamports down to the amount in the account + withdraw_lamports = split_from_lamports; } } Some((validator_stake_info, withdraw_source)) diff --git a/stake-pool/program/src/state.rs b/stake-pool/program/src/state.rs index 4eb044dc869..ab533dcfa2a 100644 --- a/stake-pool/program/src/state.rs +++ b/stake-pool/program/src/state.rs @@ -255,6 +255,15 @@ impl StakePool { } } + /// Get the current value of pool tokens, rounded up + #[inline] + pub fn get_lamports_per_pool_token(&self) -> Option { + self.total_lamports + .checked_add(self.pool_token_supply)? + .checked_sub(1)? + .checked_div(self.pool_token_supply) + } + /// Checks that the withdraw or deposit authority is valid fn check_program_derived_authority( authority_address: &Pubkey, @@ -660,24 +669,24 @@ impl ValidatorStakeInfo { /// Performs a very cheap comparison, for checking if this validator stake /// info matches the vote account address - pub fn memcmp_pubkey(data: &[u8], vote_address_bytes: &[u8]) -> bool { + pub fn memcmp_pubkey(data: &[u8], vote_address: &Pubkey) -> bool { sol_memcmp( &data[41..41 + PUBKEY_BYTES], - vote_address_bytes, + vote_address.as_ref(), PUBKEY_BYTES, ) == 0 } - /// Performs a very cheap comparison, for checking if this validator stake - /// info does not have active lamports equal to the given bytes - pub fn active_lamports_not_equal(data: &[u8], lamports_le_bytes: &[u8]) -> bool { - sol_memcmp(&data[0..8], lamports_le_bytes, 8) != 0 + /// Performs a comparison, used to check if this validator stake + /// info has more active lamports than some limit + pub fn active_lamports_greater_than(data: &[u8], lamports: &u64) -> bool { + u64::try_from_slice(&data[0..8]).unwrap() > *lamports } - /// Performs a very cheap comparison, for checking if this validator stake - /// info does not have lamports equal to the given bytes - pub fn transient_lamports_not_equal(data: &[u8], lamports_le_bytes: &[u8]) -> bool { - sol_memcmp(&data[8..16], lamports_le_bytes, 8) != 0 + /// Performs a comparison, used to check if this validator stake + /// info has more transient lamports than some limit + pub fn transient_lamports_greater_than(data: &[u8], lamports: &u64) -> bool { + u64::try_from_slice(&data[8..16]).unwrap() > *lamports } /// Check that the validator stake info is valid diff --git a/stake-pool/program/tests/huge_pool.rs b/stake-pool/program/tests/huge_pool.rs index 66fcbfc8d53..40028e69385 100644 --- a/stake-pool/program/tests/huge_pool.rs +++ b/stake-pool/program/tests/huge_pool.rs @@ -20,7 +20,7 @@ use { }, }; -const HUGE_POOL_SIZE: u32 = 2_000; +const HUGE_POOL_SIZE: u32 = 3_300; const STAKE_AMOUNT: u64 = 200_000_000_000; async fn setup( diff --git a/stake-pool/program/tests/update_validator_list_balance.rs b/stake-pool/program/tests/update_validator_list_balance.rs index 7ace0cf8efc..827189a231a 100644 --- a/stake-pool/program/tests/update_validator_list_balance.rs +++ b/stake-pool/program/tests/update_validator_list_balance.rs @@ -96,6 +96,11 @@ async fn setup( // Warp forward so the stakes properly activate, and deposit slot += slots_per_epoch; context.warp_to_slot(slot).unwrap(); + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&context.last_blockhash) + .await + .unwrap(); stake_pool_accounts .update_all( @@ -111,12 +116,6 @@ async fn setup( ) .await; - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - for deposit_account in &mut deposit_accounts { deposit_account .deposit_stake( @@ -130,6 +129,11 @@ async fn setup( slot += slots_per_epoch; context.warp_to_slot(slot).unwrap(); + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&context.last_blockhash) + .await + .unwrap(); stake_pool_accounts .update_all( @@ -418,6 +422,11 @@ async fn merge_into_validator_stake() { // Warp just a little bit to get a new blockhash and update again context.warp_to_slot(slot + 10).unwrap(); + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&last_blockhash) + .await + .unwrap(); // Update, should not change, no merges yet let error = stake_pool_accounts diff --git a/stake-pool/program/tests/withdraw.rs b/stake-pool/program/tests/withdraw.rs index 46102400358..d66c5f95d05 100644 --- a/stake-pool/program/tests/withdraw.rs +++ b/stake-pool/program/tests/withdraw.rs @@ -678,6 +678,8 @@ async fn fail_with_not_enough_tokens() { ) .await; + // generate a new authority each time to make each transaction unique + let new_authority = Pubkey::new_unique(); let transaction_error = stake_pool_accounts .withdraw_stake( &mut context.banks_client, @@ -715,6 +717,7 @@ async fn fail_with_not_enough_tokens() { ) .await; + // generate a new authority each time to make each transaction unique let new_authority = Pubkey::new_unique(); let transaction_error = stake_pool_accounts .withdraw_stake( diff --git a/stake-pool/program/tests/withdraw_edge_cases.rs b/stake-pool/program/tests/withdraw_edge_cases.rs index b97d930064b..5c1f0a45c97 100644 --- a/stake-pool/program/tests/withdraw_edge_cases.rs +++ b/stake-pool/program/tests/withdraw_edge_cases.rs @@ -12,6 +12,7 @@ use { solana_program_test::*, solana_sdk::{signature::Signer, transaction::TransactionError}, spl_stake_pool::{error::StakePoolError, instruction, state, MINIMUM_RESERVE_LAMPORTS}, + test_case::test_case, }; #[tokio::test] @@ -87,8 +88,12 @@ async fn fail_remove_validator() { ); } +#[test_case(0; "equal")] +#[test_case(5; "big")] +#[test_case(11; "bigger")] +#[test_case(29; "biggest")] #[tokio::test] -async fn success_remove_validator() { +async fn success_remove_validator(multiple: u64) { let ( mut context, stake_pool_accounts, @@ -99,10 +104,33 @@ async fn success_remove_validator() { _, ) = setup_for_withdraw(spl_token::id()).await; + // make pool tokens very valuable, so it isn't possible to exactly get down to the minimum + transfer( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &stake_pool_accounts.reserve_stake.pubkey(), + deposit_info.stake_lamports * multiple, // each pool token is worth more than one lamport + ) + .await; + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &[validator_stake.vote.pubkey()], + false, + ) + .await; + let rent = context.banks_client.get_rent().await.unwrap(); let stake_rent = rent.minimum_balance(std::mem::size_of::()); + let stake_pool = stake_pool_accounts + .get_stake_pool(&mut context.banks_client) + .await; + let lamports_per_pool_token = stake_pool.get_lamports_per_pool_token().unwrap(); - // decrease all of stake + // decrease all of stake except for lamports_per_pool_token lamports, must be withdrawable let error = stake_pool_accounts .decrease_validator_stake( &mut context.banks_client, @@ -110,7 +138,7 @@ async fn success_remove_validator() { &context.last_blockhash, &validator_stake.stake_account, &validator_stake.transient_stake_account, - deposit_info.stake_lamports + stake_rent, + deposit_info.stake_lamports + stake_rent - lamports_per_pool_token, validator_stake.transient_stake_seed, ) .await; @@ -123,12 +151,18 @@ async fn success_remove_validator() { .warp_to_slot(first_normal_slot + slots_per_epoch) .unwrap(); + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&context.last_blockhash) + .await + .unwrap(); + // update to merge deactivated stake into reserve stake_pool_accounts .update_all( &mut context.banks_client, &context.payer, - &context.last_blockhash, + &last_blockhash, &[validator_stake.vote.pubkey()], false, ) @@ -137,13 +171,23 @@ async fn success_remove_validator() { let validator_stake_account = get_account(&mut context.banks_client, &validator_stake.stake_account).await; let remaining_lamports = validator_stake_account.lamports; + let stake_minimum_delegation = + stake_get_minimum_delegation(&mut context.banks_client, &context.payer, &last_blockhash) + .await; + // make sure it's actually more than the minimum + assert!(remaining_lamports > stake_rent + stake_minimum_delegation); + + // round up to force one more pool token if needed + let pool_tokens_post_fee = + (remaining_lamports * stake_pool.pool_token_supply + stake_pool.total_lamports - 1) + / stake_pool.total_lamports; let new_user_authority = Pubkey::new_unique(); - let pool_tokens = stake_pool_accounts.calculate_inverse_withdrawal_fee(remaining_lamports); + let pool_tokens = stake_pool_accounts.calculate_inverse_withdrawal_fee(pool_tokens_post_fee); let error = stake_pool_accounts .withdraw_stake( &mut context.banks_client, &context.payer, - &context.last_blockhash, + &last_blockhash, &user_stake_recipient.pubkey(), &user_transfer_authority, &deposit_info.pool_account.pubkey(), @@ -175,7 +219,7 @@ async fn success_remove_validator() { .cleanup_removed_validator_entries( &mut context.banks_client, &context.payer, - &context.last_blockhash, + &last_blockhash, ) .await; @@ -497,6 +541,7 @@ async fn success_and_fail_with_preferred_withdraw() { ); // success from preferred + let new_authority = Pubkey::new_unique(); let error = stake_pool_accounts .withdraw_stake( &mut context.banks_client, @@ -548,7 +593,7 @@ async fn fail_withdraw_from_transient() { let rent = context.banks_client.get_rent().await.unwrap(); let stake_rent = rent.minimum_balance(std::mem::size_of::()); - // decrease to minimum stake + 1 lamport + // decrease to minimum stake + 2 lamports let error = stake_pool_accounts .decrease_validator_stake( &mut context.banks_client, @@ -556,7 +601,7 @@ async fn fail_withdraw_from_transient() { &context.last_blockhash, &validator_stake_account.stake_account, &validator_stake_account.transient_stake_account, - deposit_info.stake_lamports + stake_rent - 1, + deposit_info.stake_lamports + stake_rent - 2, validator_stake_account.transient_stake_seed, ) .await; @@ -655,3 +700,145 @@ async fn success_withdraw_from_transient() { .await; assert!(error.is_none()); } + +#[tokio::test] +async fn success_with_small_preferred_withdraw() { + let ( + mut context, + stake_pool_accounts, + validator_stake, + deposit_info, + user_transfer_authority, + user_stake_recipient, + tokens_to_burn, + ) = setup_for_withdraw(spl_token::id()).await; + + // make pool tokens very valuable, so it isn't possible to exactly get down to the minimum + transfer( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &stake_pool_accounts.reserve_stake.pubkey(), + deposit_info.stake_lamports * 5, // each pool token is worth more than one lamport + ) + .await; + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &[validator_stake.vote.pubkey()], + false, + ) + .await; + + let preferred_validator = simple_add_validator_to_pool( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &stake_pool_accounts, + None, + ) + .await; + + stake_pool_accounts + .set_preferred_validator( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + instruction::PreferredValidatorType::Withdraw, + Some(preferred_validator.vote.pubkey()), + ) + .await; + + // add a tiny bit of stake, less than lamports per pool token to preferred validator + let rent = context.banks_client.get_rent().await.unwrap(); + let rent_exempt = rent.minimum_balance(std::mem::size_of::()); + let stake_minimum_delegation = stake_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + ) + .await; + let minimum_lamports = stake_minimum_delegation + rent_exempt; + + simple_deposit_stake( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &stake_pool_accounts, + &preferred_validator, + stake_minimum_delegation + 1, // stake_rent gets deposited too + ) + .await + .unwrap(); + + // decrease all stake except for 1 lamport + let error = stake_pool_accounts + .decrease_validator_stake( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &preferred_validator.stake_account, + &preferred_validator.transient_stake_account, + minimum_lamports, + preferred_validator.transient_stake_seed, + ) + .await; + assert!(error.is_none()); + + // warp forward to deactivation + let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; + let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; + context + .warp_to_slot(first_normal_slot + slots_per_epoch) + .unwrap(); + + // update to merge deactivated stake into reserve + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &[ + validator_stake.vote.pubkey(), + preferred_validator.vote.pubkey(), + ], + false, + ) + .await; + + // withdraw from preferred fails + 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(), + &preferred_validator.stake_account, + &new_authority, + 1, + ) + .await; + assert!(error.is_some()); + + // preferred is empty, withdrawing from non-preferred works + 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 / 6, + ) + .await; + assert!(error.is_none()); +}