* Prevent withrawing Initialized stake account to zero stake (#17366)
(cherry picked from commit 91f2b6185e
)
# Conflicts:
# programs/stake/src/stake_instruction.rs
* Fix conflicts
Co-authored-by: Tyera Eulberg <teulberg@gmail.com>
Co-authored-by: Tyera Eulberg <tyera@solana.com>
This commit is contained in:
@ -606,6 +606,7 @@ pub fn process_instruction(
|
||||
&from_keyed_account::<StakeHistory>(next_keyed_account(keyed_accounts)?)?,
|
||||
next_keyed_account(keyed_accounts)?,
|
||||
keyed_accounts.next(),
|
||||
invoke_context.is_feature_active(&feature_set::stake_program_v4::id()),
|
||||
)
|
||||
}
|
||||
StakeInstruction::Deactivate => me.deactivate(
|
||||
|
@ -881,6 +881,7 @@ pub trait StakeAccount {
|
||||
stake_history: &StakeHistory,
|
||||
withdraw_authority: &KeyedAccount,
|
||||
custodian: Option<&KeyedAccount>,
|
||||
prevent_withdraw_to_zero: bool,
|
||||
) -> Result<(), InstructionError>;
|
||||
}
|
||||
|
||||
@ -1215,6 +1216,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
|
||||
stake_history: &StakeHistory,
|
||||
withdraw_authority: &KeyedAccount,
|
||||
custodian: Option<&KeyedAccount>,
|
||||
prevent_withdraw_to_zero: bool,
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut signers = HashSet::new();
|
||||
let withdraw_authority_pubkey = withdraw_authority
|
||||
@ -1244,8 +1246,13 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
|
||||
StakeState::Initialized(meta) => {
|
||||
meta.authorized
|
||||
.check(&signers, StakeAuthorize::Withdrawer)?;
|
||||
let reserve = if prevent_withdraw_to_zero {
|
||||
checked_add(meta.rent_exempt_reserve, 1)? // stake accounts must have a balance > rent_exempt_reserve
|
||||
} else {
|
||||
meta.rent_exempt_reserve
|
||||
};
|
||||
|
||||
(meta.lockup, meta.rent_exempt_reserve, false)
|
||||
(meta.lockup, reserve, false)
|
||||
}
|
||||
StakeState::Uninitialized => {
|
||||
if !signers.contains(&self.unsigned_key()) {
|
||||
@ -3087,6 +3094,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&to_keyed_account, // unsigned account as withdraw authority
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Err(InstructionError::MissingRequiredSignature)
|
||||
);
|
||||
@ -3102,6 +3110,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
@ -3137,6 +3146,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Err(InstructionError::InsufficientFunds)
|
||||
);
|
||||
@ -3178,6 +3188,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
@ -3195,6 +3206,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Err(InstructionError::InsufficientFunds)
|
||||
);
|
||||
@ -3214,6 +3226,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Err(InstructionError::InsufficientFunds)
|
||||
);
|
||||
@ -3228,6 +3241,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
@ -3284,6 +3298,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&authority_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
);
|
||||
@ -3355,6 +3370,7 @@ mod tests {
|
||||
&stake_history,
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Err(InstructionError::InsufficientFunds)
|
||||
);
|
||||
@ -3384,6 +3400,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
);
|
||||
@ -3426,6 +3443,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Err(StakeError::LockupInForce.into())
|
||||
);
|
||||
@ -3441,6 +3459,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
Some(&custodian_keyed_account),
|
||||
true,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
@ -3459,6 +3478,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
@ -3502,6 +3522,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Err(StakeError::LockupInForce.into())
|
||||
);
|
||||
@ -3516,6 +3537,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
Some(&custodian_keyed_account),
|
||||
true,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
@ -3523,6 +3545,102 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_withdraw_rent_exempt() {
|
||||
let stake_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let clock = Clock::default();
|
||||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let stake = 42;
|
||||
let stake_account = AccountSharedData::new_ref_data_with_space(
|
||||
stake + rent_exempt_reserve,
|
||||
&StakeState::Initialized(Meta {
|
||||
rent_exempt_reserve,
|
||||
..Meta::auto(&stake_pubkey)
|
||||
}),
|
||||
std::mem::size_of::<StakeState>(),
|
||||
&id(),
|
||||
)
|
||||
.expect("stake_account");
|
||||
|
||||
let to = solana_sdk::pubkey::new_rand();
|
||||
let to_account = AccountSharedData::new_ref(1, 0, &system_program::id());
|
||||
let to_keyed_account = KeyedAccount::new(&to, false, &to_account);
|
||||
|
||||
// Withdrawing account down to only rent-exempt reserve should succeed before feature, and
|
||||
// fail after
|
||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
|
||||
assert_eq!(
|
||||
stake_keyed_account.withdraw(
|
||||
stake,
|
||||
&to_keyed_account,
|
||||
&clock,
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
false,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
stake_account.borrow_mut().lamports += stake; // top up account
|
||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
|
||||
assert_eq!(
|
||||
stake_keyed_account.withdraw(
|
||||
stake,
|
||||
&to_keyed_account,
|
||||
&clock,
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Err(InstructionError::InsufficientFunds)
|
||||
);
|
||||
|
||||
// Withdrawal that would leave less than rent-exempt reserve should fail
|
||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
|
||||
assert_eq!(
|
||||
stake_keyed_account.withdraw(
|
||||
stake + 1,
|
||||
&to_keyed_account,
|
||||
&clock,
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
false,
|
||||
),
|
||||
Err(InstructionError::InsufficientFunds)
|
||||
);
|
||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
|
||||
assert_eq!(
|
||||
stake_keyed_account.withdraw(
|
||||
stake + 1,
|
||||
&to_keyed_account,
|
||||
&clock,
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Err(InstructionError::InsufficientFunds)
|
||||
);
|
||||
|
||||
// Withdrawal of complete account should succeed
|
||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
|
||||
assert_eq!(
|
||||
stake_keyed_account.withdraw(
|
||||
stake + rent_exempt_reserve,
|
||||
&to_keyed_account,
|
||||
&clock,
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stake_state_redeem_rewards() {
|
||||
let mut vote_state = VoteState::default();
|
||||
@ -3959,6 +4077,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account, // old signer
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Err(InstructionError::MissingRequiredSignature)
|
||||
);
|
||||
@ -3975,6 +4094,7 @@ mod tests {
|
||||
&StakeHistory::default(),
|
||||
&stake_keyed_account2,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
@ -5934,6 +6054,7 @@ mod tests {
|
||||
&stake_history,
|
||||
&stake_keyed_account,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let expected_balance = rent_exempt_reserve + initial_lamports - withdraw_lamports;
|
||||
|
Reference in New Issue
Block a user