diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 722994ddb2..83abcb02ce 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -606,6 +606,7 @@ pub fn process_instruction( &from_keyed_account::(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( diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index bb54eb9d13..49ff658c03 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -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::()); + 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::(), + &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;