stake-pool: Add ability to withdraw from transient stakes (#1890)

* stake-pool: Add ability to withdraw from transient stakes

It's possible for a very malicious pool staker to constantly increase /
decrease the stake on validators, making it impossible for people to get
their SOL out.

Update the accounting to know how much of the stake is active and how
much is transient and allow users to withdraw from transient accounts,
but only if there's no more active stake.

* Remove mut ickiness
This commit is contained in:
Jon Cinque 2021-06-11 22:50:59 +02:00 committed by GitHub
parent de8433e815
commit ddf9efa330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 337 additions and 88 deletions

View File

@ -641,7 +641,7 @@ fn command_list(config: &Config, stake_pool_address: &Pubkey) -> CommandResult {
println!( println!(
"Validator Vote Account: {}\tBalance: {}\tLast Update Epoch: {}{}", "Validator Vote Account: {}\tBalance: {}\tLast Update Epoch: {}{}",
validator.vote_account_address, validator.vote_account_address,
Sol(validator.stake_lamports), Sol(validator.stake_lamports()),
validator.last_update_epoch, validator.last_update_epoch,
if validator.last_update_epoch != epoch_info.epoch { if validator.last_update_epoch != epoch_info.epoch {
" [UPDATE REQUIRED]" " [UPDATE REQUIRED]"

View File

@ -129,8 +129,8 @@ pub enum StakePoolInstruction {
/// 0. `[]` Stake pool /// 0. `[]` Stake pool
/// 1. `[s]` Stake pool staker /// 1. `[s]` Stake pool staker
/// 2. `[]` Stake pool withdraw authority /// 2. `[]` Stake pool withdraw authority
/// 3. `[]` Validator list /// 3. `[w]` Validator list
/// 5. `[w]` Canonical stake account to split from /// 4. `[w]` Canonical stake account to split from
/// 5. `[w]` Transient stake account to receive split /// 5. `[w]` Transient stake account to receive split
/// 6. `[]` Clock sysvar /// 6. `[]` Clock sysvar
/// 7. `[]` Rent sysvar /// 7. `[]` Rent sysvar
@ -444,7 +444,7 @@ pub fn decrease_validator_stake(
AccountMeta::new_readonly(*stake_pool, false), AccountMeta::new_readonly(*stake_pool, false),
AccountMeta::new_readonly(*staker, true), AccountMeta::new_readonly(*staker, true),
AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), AccountMeta::new_readonly(*stake_pool_withdraw_authority, false),
AccountMeta::new_readonly(*validator_list, false), AccountMeta::new(*validator_list, false),
AccountMeta::new(*validator_stake, false), AccountMeta::new(*validator_stake, false),
AccountMeta::new(*transient_stake, false), AccountMeta::new(*transient_stake, false),
AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::clock::id(), false),

View File

@ -736,7 +736,8 @@ impl Processor {
validator_list.validators.push(ValidatorStakeInfo { validator_list.validators.push(ValidatorStakeInfo {
status: StakeStatus::Active, status: StakeStatus::Active,
vote_account_address, vote_account_address,
stake_lamports: stake_lamports.saturating_sub(minimum_lamport_amount), active_stake_lamports: stake_lamports.saturating_sub(minimum_lamport_amount),
transient_stake_lamports: 0,
last_update_epoch: clock.epoch, last_update_epoch: clock.epoch,
}); });
validator_list.serialize(&mut *validator_list_info.data.borrow_mut())?; validator_list.serialize(&mut *validator_list_info.data.borrow_mut())?;
@ -916,7 +917,7 @@ impl Processor {
stake_pool.check_validator_list(validator_list_info)?; stake_pool.check_validator_list(validator_list_info)?;
check_account_owner(validator_list_info, program_id)?; check_account_owner(validator_list_info, program_id)?;
let validator_list = let mut validator_list =
try_from_slice_unchecked::<ValidatorList>(&validator_list_info.data.borrow())?; try_from_slice_unchecked::<ValidatorList>(&validator_list_info.data.borrow())?;
if !validator_list.is_valid() { if !validator_list.is_valid() {
return Err(StakePoolError::InvalidState.into()); return Err(StakePoolError::InvalidState.into());
@ -944,13 +945,15 @@ impl Processor {
&[transient_stake_bump_seed], &[transient_stake_bump_seed],
]; ];
if !validator_list.contains(&vote_account_address) { let maybe_validator_list_entry = validator_list.find_mut(&vote_account_address);
if maybe_validator_list_entry.is_none() {
msg!( msg!(
"Vote account {} not found in stake pool", "Vote account {} not found in stake pool",
vote_account_address vote_account_address
); );
return Err(StakePoolError::ValidatorNotFound.into()); return Err(StakePoolError::ValidatorNotFound.into());
} }
let mut validator_list_entry = maybe_validator_list_entry.unwrap();
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>()); let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
if lamports <= stake_rent { if lamports <= stake_rent {
@ -996,6 +999,13 @@ impl Processor {
stake_pool.withdraw_bump_seed, stake_pool.withdraw_bump_seed,
)?; )?;
validator_list_entry.active_stake_lamports = validator_list_entry
.active_stake_lamports
.checked_sub(lamports)
.ok_or(StakePoolError::CalculationFailure)?;
validator_list_entry.transient_stake_lamports = lamports;
validator_list.serialize(&mut *validator_list_info.data.borrow_mut())?;
Ok(()) Ok(())
} }
@ -1146,10 +1156,7 @@ impl Processor {
stake_pool.withdraw_bump_seed, stake_pool.withdraw_bump_seed,
)?; )?;
validator_list_entry.stake_lamports = validator_list_entry validator_list_entry.transient_stake_lamports = lamports;
.stake_lamports
.checked_add(lamports)
.ok_or(StakePoolError::CalculationFailure)?;
validator_list.serialize(&mut *validator_list_info.data.borrow_mut())?; validator_list.serialize(&mut *validator_list_info.data.borrow_mut())?;
Ok(()) Ok(())
@ -1274,7 +1281,8 @@ impl Processor {
continue; continue;
}; };
let mut stake_lamports = 0; let mut active_stake_lamports = 0;
let mut transient_stake_lamports = 0;
let validator_stake_state = try_from_slice_unchecked::<stake_program::StakeState>( let validator_stake_state = try_from_slice_unchecked::<stake_program::StakeState>(
&validator_stake_info.data.borrow(), &validator_stake_info.data.borrow(),
) )
@ -1293,7 +1301,7 @@ impl Processor {
match transient_stake_state { match transient_stake_state {
Some(stake_program::StakeState::Initialized(_meta)) => { Some(stake_program::StakeState::Initialized(_meta)) => {
if no_merge { if no_merge {
stake_lamports += transient_stake_info.lamports(); transient_stake_lamports = transient_stake_info.lamports();
} else { } else {
// merge into reserve // merge into reserve
Self::stake_merge( Self::stake_merge(
@ -1316,7 +1324,7 @@ impl Processor {
} }
Some(stake_program::StakeState::Stake(_, stake)) => { Some(stake_program::StakeState::Stake(_, stake)) => {
if no_merge { if no_merge {
stake_lamports += transient_stake_info.lamports(); transient_stake_lamports = transient_stake_info.lamports();
} else if stake.delegation.deactivation_epoch < clock.epoch { } else if stake.delegation.deactivation_epoch < clock.epoch {
// deactivated, merge into reserve // deactivated, merge into reserve
Self::stake_merge( Self::stake_merge(
@ -1355,15 +1363,15 @@ impl Processor {
)?; )?;
} else { } else {
msg!("Stake activating or just active, not ready to merge"); msg!("Stake activating or just active, not ready to merge");
stake_lamports += transient_stake_info.lamports(); transient_stake_lamports = transient_stake_info.lamports();
} }
} else { } else {
msg!("Transient stake is activating or active, but validator stake is not, need to add the validator stake account on {} back into the stake pool", stake.delegation.voter_pubkey); msg!("Transient stake is activating or active, but validator stake is not, need to add the validator stake account on {} back into the stake pool", stake.delegation.voter_pubkey);
stake_lamports += transient_stake_info.lamports(); transient_stake_lamports = transient_stake_info.lamports();
} }
} else { } else {
msg!("Transient stake not ready to be merged anywhere"); msg!("Transient stake not ready to be merged anywhere");
stake_lamports += transient_stake_info.lamports(); transient_stake_lamports = transient_stake_info.lamports();
} }
} }
None None
@ -1377,7 +1385,7 @@ impl Processor {
match validator_stake_state { match validator_stake_state {
Some(stake_program::StakeState::Stake(meta, _)) => { Some(stake_program::StakeState::Stake(meta, _)) => {
if validator_stake_record.status == StakeStatus::Active { if validator_stake_record.status == StakeStatus::Active {
stake_lamports += validator_stake_info active_stake_lamports = validator_stake_info
.lamports() .lamports()
.saturating_sub(minimum_stake_lamports(&meta)); .saturating_sub(minimum_stake_lamports(&meta));
} else { } else {
@ -1393,7 +1401,8 @@ impl Processor {
} }
validator_stake_record.last_update_epoch = clock.epoch; validator_stake_record.last_update_epoch = clock.epoch;
validator_stake_record.stake_lamports = stake_lamports; validator_stake_record.active_stake_lamports = active_stake_lamports;
validator_stake_record.transient_stake_lamports = transient_stake_lamports;
changes = true; changes = true;
} }
@ -1465,7 +1474,7 @@ impl Processor {
return Err(StakePoolError::StakeListOutOfDate.into()); return Err(StakePoolError::StakeListOutOfDate.into());
} }
total_stake_lamports = total_stake_lamports total_stake_lamports = total_stake_lamports
.checked_add(validator_stake_record.stake_lamports) .checked_add(validator_stake_record.stake_lamports())
.ok_or(StakePoolError::CalculationFailure)?; .ok_or(StakePoolError::CalculationFailure)?;
} }
@ -1674,7 +1683,7 @@ impl Processor {
"lamports post merge {}", "lamports post merge {}",
validator_stake_account_info.lamports() validator_stake_account_info.lamports()
); );
validator_list_item.stake_lamports = validator_stake_account_info validator_list_item.active_stake_lamports = validator_stake_account_info
.lamports() .lamports()
.checked_sub(minimum_stake_lamports(&meta)) .checked_sub(minimum_stake_lamports(&meta))
.ok_or(StakePoolError::CalculationFailure)?; .ok_or(StakePoolError::CalculationFailure)?;
@ -1738,19 +1747,19 @@ impl Processor {
.calc_lamports_withdraw_amount(pool_tokens) .calc_lamports_withdraw_amount(pool_tokens)
.ok_or(StakePoolError::CalculationFailure)?; .ok_or(StakePoolError::CalculationFailure)?;
let validator_list_item = if *stake_split_from.key == stake_pool.reserve_stake { let validator_list_item_info = if *stake_split_from.key == stake_pool.reserve_stake {
// check that the validator stake accounts have no withdrawable stake // check that the validator stake accounts have no withdrawable stake
if let Some(withdrawable_entry) = validator_list if let Some(withdrawable_entry) = validator_list
.validators .validators
.iter() .iter()
.find(|&&x| x.stake_lamports != 0) .find(|&&x| x.stake_lamports() != 0)
{ {
let (validator_stake_address, _) = crate::find_stake_program_address( let (validator_stake_address, _) = crate::find_stake_program_address(
&program_id, &program_id,
&withdrawable_entry.vote_account_address, &withdrawable_entry.vote_account_address,
stake_pool_info.key, stake_pool_info.key,
); );
msg!("Error withdrawing from reserve: validator stake account {} has {} lamports available, please use that first.", validator_stake_address, withdrawable_entry.stake_lamports); msg!("Error withdrawing from reserve: validator stake account {} has {} lamports available, please use that first.", validator_stake_address, withdrawable_entry.stake_lamports());
return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into()); return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into());
} }
@ -1767,12 +1776,6 @@ impl Processor {
} else { } else {
let (meta, stake) = get_stake_state(stake_split_from)?; let (meta, stake) = get_stake_state(stake_split_from)?;
let vote_account_address = stake.delegation.voter_pubkey; let vote_account_address = stake.delegation.voter_pubkey;
check_validator_stake_address(
program_id,
stake_pool_info.key,
stake_split_from.key,
&vote_account_address,
)?;
if let Some(preferred_withdraw_validator) = if let Some(preferred_withdraw_validator) =
validator_list.preferred_withdraw_validator_vote_address validator_list.preferred_withdraw_validator_vote_address
@ -1781,13 +1784,33 @@ impl Processor {
.find(&preferred_withdraw_validator) .find(&preferred_withdraw_validator)
.ok_or(StakePoolError::ValidatorNotFound)?; .ok_or(StakePoolError::ValidatorNotFound)?;
if preferred_withdraw_validator != vote_account_address if preferred_withdraw_validator != vote_account_address
&& preferred_validator_info.stake_lamports > 0 && preferred_validator_info.active_stake_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.stake_lamports); 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()); return Err(StakePoolError::IncorrectWithdrawVoteAddress.into());
} }
} }
// if there's any active stake, we must withdraw from an active
// stake account
let withdrawing_from_transient_stake = if validator_list.has_active_stake() {
check_validator_stake_address(
program_id,
stake_pool_info.key,
stake_split_from.key,
&vote_account_address,
)?;
false
} else {
check_transient_stake_address(
program_id,
stake_pool_info.key,
stake_split_from.key,
&vote_account_address,
)?;
true
};
let validator_list_item = validator_list let validator_list_item = validator_list
.find_mut(&vote_account_address) .find_mut(&vote_account_address)
.ok_or(StakePoolError::ValidatorNotFound)?; .ok_or(StakePoolError::ValidatorNotFound)?;
@ -1804,7 +1827,7 @@ impl Processor {
msg!("Attempting to withdraw {} lamports from validator account with {} lamports, {} must remain", withdraw_lamports, current_lamports, required_lamports); msg!("Attempting to withdraw {} lamports from validator account with {} lamports, {} must remain", withdraw_lamports, current_lamports, required_lamports);
return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into()); return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into());
} }
Some(validator_list_item) Some((validator_list_item, withdrawing_from_transient_stake))
}; };
Self::token_burn( Self::token_burn(
@ -1846,11 +1869,20 @@ impl Processor {
.ok_or(StakePoolError::CalculationFailure)?; .ok_or(StakePoolError::CalculationFailure)?;
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?; stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
if let Some(validator_list_item) = validator_list_item { if let Some((validator_list_item, withdrawing_from_transient_stake_account)) =
validator_list_item.stake_lamports = validator_list_item validator_list_item_info
.stake_lamports {
.checked_sub(withdraw_lamports) if withdrawing_from_transient_stake_account {
.ok_or(StakePoolError::CalculationFailure)?; validator_list_item.transient_stake_lamports = validator_list_item
.transient_stake_lamports
.checked_sub(withdraw_lamports)
.ok_or(StakePoolError::CalculationFailure)?;
} else {
validator_list_item.active_stake_lamports = validator_list_item
.active_stake_lamports
.checked_sub(withdraw_lamports)
.ok_or(StakePoolError::CalculationFailure)?;
}
validator_list.serialize(&mut *validator_list_info.data.borrow_mut())?; validator_list.serialize(&mut *validator_list_info.data.borrow_mut())?;
} }

View File

@ -328,15 +328,29 @@ pub struct ValidatorStakeInfo {
/// Validator vote account address /// Validator vote account address
pub vote_account_address: Pubkey, pub vote_account_address: Pubkey,
/// Amount of stake delegated to this validator /// Amount of active stake delegated to this validator
/// Note that if `last_update_epoch` does not match the current epoch then this field may not /// Note that if `last_update_epoch` does not match the current epoch then
/// be accurate /// this field may not be accurate
pub stake_lamports: u64, pub active_stake_lamports: u64,
/// Last epoch the `stake_lamports` field was updated /// Amount of transient stake delegated to this validator
/// Note that if `last_update_epoch` does not match the current epoch then
/// this field may not be accurate
pub transient_stake_lamports: u64,
/// Last epoch the active and transient stake lamports fields were updated
pub last_update_epoch: u64, pub last_update_epoch: u64,
} }
impl ValidatorStakeInfo {
/// Get the total lamports delegated to this validator (active and transient)
pub fn stake_lamports(&self) -> u64 {
self.active_stake_lamports
.checked_add(self.transient_stake_lamports)
.unwrap()
}
}
impl ValidatorList { impl ValidatorList {
/// Create an empty instance containing space for `max_validators` and preferred validator keys /// Create an empty instance containing space for `max_validators` and preferred validator keys
pub fn new(max_validators: u32) -> Self { pub fn new(max_validators: u32) -> Self {
@ -352,7 +366,7 @@ impl ValidatorList {
/// Calculate the number of validator entries that fit in the provided length /// Calculate the number of validator entries that fit in the provided length
pub fn calculate_max_validators(buffer_length: usize) -> usize { pub fn calculate_max_validators(buffer_length: usize) -> usize {
let header_size = 1 + 4 + 4 + 33 + 33; let header_size = 1 + 4 + 4 + 33 + 33;
buffer_length.saturating_sub(header_size) / 49 buffer_length.saturating_sub(header_size) / 57
} }
/// Check if contains validator with particular pubkey /// Check if contains validator with particular pubkey
@ -384,6 +398,11 @@ impl ValidatorList {
pub fn is_uninitialized(&self) -> bool { pub fn is_uninitialized(&self) -> bool {
self.account_type == AccountType::Uninitialized self.account_type == AccountType::Uninitialized
} }
/// Check if the list has any active stake
pub fn has_active_stake(&self) -> bool {
self.validators.iter().any(|x| x.active_stake_lamports > 0)
}
} }
/// Fee rate as a ratio, minted on `UpdateStakePoolBalance` as a proportion of /// Fee rate as a ratio, minted on `UpdateStakePoolBalance` as a proportion of
@ -407,18 +426,53 @@ mod test {
solana_program::native_token::LAMPORTS_PER_SOL, solana_program::native_token::LAMPORTS_PER_SOL,
}; };
#[test] fn uninitialized_validator_list() -> ValidatorList {
fn test_state_packing() { ValidatorList {
let max_validators = 10_000;
let size = get_instance_packed_len(&ValidatorList::new(max_validators)).unwrap();
// Not initialized
let stake_list = ValidatorList {
account_type: AccountType::Uninitialized, account_type: AccountType::Uninitialized,
preferred_deposit_validator_vote_address: None, preferred_deposit_validator_vote_address: None,
preferred_withdraw_validator_vote_address: None, preferred_withdraw_validator_vote_address: None,
max_validators: 0, max_validators: 0,
validators: vec![], validators: vec![],
}; }
}
fn test_validator_list(max_validators: u32) -> ValidatorList {
ValidatorList {
account_type: AccountType::ValidatorList,
preferred_deposit_validator_vote_address: Some(Pubkey::new_unique()),
preferred_withdraw_validator_vote_address: Some(Pubkey::new_unique()),
max_validators,
validators: vec![
ValidatorStakeInfo {
status: StakeStatus::Active,
vote_account_address: Pubkey::new_from_array([1; 32]),
active_stake_lamports: 123456789,
transient_stake_lamports: 1111111,
last_update_epoch: 987654321,
},
ValidatorStakeInfo {
status: StakeStatus::DeactivatingTransient,
vote_account_address: Pubkey::new_from_array([2; 32]),
active_stake_lamports: 998877665544,
transient_stake_lamports: 222222222,
last_update_epoch: 11223445566,
},
ValidatorStakeInfo {
status: StakeStatus::ReadyForRemoval,
vote_account_address: Pubkey::new_from_array([3; 32]),
active_stake_lamports: 0,
transient_stake_lamports: 0,
last_update_epoch: 999999999999999,
},
],
}
}
#[test]
fn state_packing() {
let max_validators = 10_000;
let size = get_instance_packed_len(&ValidatorList::new(max_validators)).unwrap();
let stake_list = uninitialized_validator_list();
let mut byte_vec = vec![0u8; size]; let mut byte_vec = vec![0u8; size];
let mut bytes = byte_vec.as_mut_slice(); let mut bytes = byte_vec.as_mut_slice();
stake_list.serialize(&mut bytes).unwrap(); stake_list.serialize(&mut bytes).unwrap();
@ -440,32 +494,7 @@ mod test {
assert_eq!(stake_list_unpacked, stake_list); assert_eq!(stake_list_unpacked, stake_list);
// With several accounts // With several accounts
let stake_list = ValidatorList { let stake_list = test_validator_list(max_validators);
account_type: AccountType::ValidatorList,
preferred_deposit_validator_vote_address: Some(Pubkey::new_unique()),
preferred_withdraw_validator_vote_address: Some(Pubkey::new_unique()),
max_validators,
validators: vec![
ValidatorStakeInfo {
status: StakeStatus::Active,
vote_account_address: Pubkey::new_from_array([1; 32]),
stake_lamports: 123456789,
last_update_epoch: 987654321,
},
ValidatorStakeInfo {
status: StakeStatus::DeactivatingTransient,
vote_account_address: Pubkey::new_from_array([2; 32]),
stake_lamports: 998877665544,
last_update_epoch: 11223445566,
},
ValidatorStakeInfo {
status: StakeStatus::ReadyForRemoval,
vote_account_address: Pubkey::new_from_array([3; 32]),
stake_lamports: 0,
last_update_epoch: 999999999999999,
},
],
};
let mut byte_vec = vec![0u8; size]; let mut byte_vec = vec![0u8; size];
let mut bytes = byte_vec.as_mut_slice(); let mut bytes = byte_vec.as_mut_slice();
stake_list.serialize(&mut bytes).unwrap(); stake_list.serialize(&mut bytes).unwrap();
@ -473,6 +502,17 @@ mod test {
assert_eq!(stake_list_unpacked, stake_list); assert_eq!(stake_list_unpacked, stake_list);
} }
#[test]
fn validator_list_active_stake() {
let max_validators = 10_000;
let mut validator_list = test_validator_list(max_validators);
assert!(validator_list.has_active_stake());
for validator in validator_list.validators.iter_mut() {
validator.active_stake_lamports = 0;
}
assert!(!validator_list.has_active_stake());
}
proptest! { proptest! {
#[test] #[test]
fn stake_list_size_calculation(test_amount in 0..=100_000_u32) { fn stake_list_size_calculation(test_amount in 0..=100_000_u32) {

View File

@ -191,8 +191,8 @@ async fn success() {
.find(&validator_stake_account.vote.pubkey()) .find(&validator_stake_account.vote.pubkey())
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
validator_stake_item.stake_lamports, validator_stake_item.stake_lamports(),
validator_stake_item_before.stake_lamports + stake_lamports validator_stake_item_before.stake_lamports() + stake_lamports
); );
// Check validator stake account actual SOL balance // Check validator stake account actual SOL balance
@ -203,8 +203,9 @@ async fn success() {
let meta = stake_state.meta().unwrap(); let meta = stake_state.meta().unwrap();
assert_eq!( assert_eq!(
validator_stake_account.lamports - minimum_stake_lamports(&meta), validator_stake_account.lamports - minimum_stake_lamports(&meta),
validator_stake_item.stake_lamports validator_stake_item.stake_lamports()
); );
assert_eq!(validator_stake_item.transient_stake_lamports, 0);
} }
#[tokio::test] #[tokio::test]

View File

@ -1124,7 +1124,7 @@ pub async fn get_validator_list_sum(
let validator_sum: u64 = validator_list let validator_sum: u64 = validator_list
.validators .validators
.iter() .iter()
.map(|info| info.stake_lamports) .map(|info| info.stake_lamports())
.sum(); .sum();
let rent = banks_client.get_rent().await.unwrap(); let rent = banks_client.get_rent().await.unwrap();
let rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>()); let rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());

View File

@ -536,8 +536,9 @@ async fn merge_transient_stake_after_remove() {
validator_list.validators[0].status, validator_list.validators[0].status,
StakeStatus::DeactivatingTransient StakeStatus::DeactivatingTransient
); );
assert_eq!(validator_list.validators[0].active_stake_lamports, 0);
assert_eq!( assert_eq!(
validator_list.validators[0].stake_lamports, validator_list.validators[0].transient_stake_lamports,
deactivated_lamports deactivated_lamports
); );
@ -569,7 +570,7 @@ async fn merge_transient_stake_after_remove() {
validator_list.validators[0].status, validator_list.validators[0].status,
StakeStatus::ReadyForRemoval StakeStatus::ReadyForRemoval
); );
assert_eq!(validator_list.validators[0].stake_lamports, 0); assert_eq!(validator_list.validators[0].stake_lamports(), 0);
let reserve_stake = context let reserve_stake = context
.banks_client .banks_client

View File

@ -91,7 +91,8 @@ async fn success() {
status: state::StakeStatus::Active, status: state::StakeStatus::Active,
vote_account_address: user_stake.vote.pubkey(), vote_account_address: user_stake.vote.pubkey(),
last_update_epoch: 0, last_update_epoch: 0,
stake_lamports: 0, active_stake_lamports: 0,
transient_stake_lamports: 0,
}] }]
} }
); );

View File

@ -529,7 +529,8 @@ async fn success_with_deactivating_transient_stake() {
status: state::StakeStatus::DeactivatingTransient, status: state::StakeStatus::DeactivatingTransient,
vote_account_address: validator_stake.vote.pubkey(), vote_account_address: validator_stake.vote.pubkey(),
last_update_epoch: 0, last_update_epoch: 0,
stake_lamports: TEST_STAKE_AMOUNT + stake_rent, active_stake_lamports: 0,
transient_stake_lamports: TEST_STAKE_AMOUNT + stake_rent,
}], }],
}; };
assert_eq!(validator_list, expected_list); assert_eq!(validator_list, expected_list);

View File

@ -181,8 +181,12 @@ async fn success() {
.find(&validator_stake_account.vote.pubkey()) .find(&validator_stake_account.vote.pubkey())
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
validator_stake_item.stake_lamports, validator_stake_item.stake_lamports(),
validator_stake_item_before.stake_lamports - tokens_to_burn validator_stake_item_before.stake_lamports() - tokens_to_burn
);
assert_eq!(
validator_stake_item.active_stake_lamports,
validator_stake_item.stake_lamports(),
); );
// Check tokens burned // Check tokens burned
@ -201,7 +205,7 @@ async fn success() {
let meta = stake_state.meta().unwrap(); let meta = stake_state.meta().unwrap();
assert_eq!( assert_eq!(
validator_stake_account.lamports - minimum_stake_lamports(&meta), validator_stake_account.lamports - minimum_stake_lamports(&meta),
validator_stake_item.stake_lamports validator_stake_item.active_stake_lamports
); );
// Check user recipient stake account balance // Check user recipient stake account balance
@ -1131,3 +1135,172 @@ async fn fail_with_wrong_preferred_withdraw() {
_ => panic!("Wrong error occurs while try to make a deposit with wrong stake program ID"), _ => panic!("Wrong error occurs while try to make a deposit with wrong stake program ID"),
} }
} }
#[tokio::test]
async fn success_withdraw_from_transient() {
let mut context = program_test().start_with_context().await;
let stake_pool_accounts = StakePoolAccounts::new();
let initial_reserve_lamports = 1;
stake_pool_accounts
.initialize_stake_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
initial_reserve_lamports,
)
.await
.unwrap();
// add a preferred withdraw validator, keep it empty, to be sure that this works
let preferred_validator = simple_add_validator_to_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
)
.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;
let validator_stake = simple_add_validator_to_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
)
.await;
let deposit_lamports = TEST_STAKE_AMOUNT;
let rent = context.banks_client.get_rent().await.unwrap();
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
let deposit_info = simple_deposit(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
&validator_stake,
deposit_lamports,
)
.await
.unwrap();
// Delegate tokens for burning during withdraw
let user_transfer_authority = Keypair::new();
delegate_tokens(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&deposit_info.pool_account.pubkey(),
&deposit_info.authority,
&user_transfer_authority.pubkey(),
deposit_info.pool_tokens,
)
.await;
// decrease minimum stake
let error = stake_pool_accounts
.decrease_validator_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
stake_rent + 1,
)
.await;
assert!(error.is_none());
let withdraw_destination = Keypair::new();
let withdraw_destination_authority = Pubkey::new_unique();
let _initial_stake_lamports = create_blank_stake_account(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&withdraw_destination,
)
.await;
// fail withdrawing from transient, still a lamport in the validator stake account
let error = stake_pool_accounts
.withdraw_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&withdraw_destination.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake.transient_stake_account,
&withdraw_destination_authority,
deposit_info.pool_tokens / 2,
)
.await
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(StakePoolError::InvalidStakeAccountAddress as u32)
)
);
// 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,
&[
preferred_validator.vote.pubkey(),
validator_stake.vote.pubkey(),
],
false,
)
.await;
// decrease rest of stake
let error = stake_pool_accounts
.decrease_validator_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
deposit_lamports - 1,
)
.await;
assert!(error.is_none());
// nothing left in the validator stake account (or any others), so withdrawing
// from the transient account is ok!
let error = stake_pool_accounts
.withdraw_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&withdraw_destination.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake.transient_stake_account,
&withdraw_destination_authority,
deposit_info.pool_tokens / 4,
)
.await;
assert!(error.is_none());
}