From fcaf01e2439b4f651d928ecb7dc8c257bf6d3061 Mon Sep 17 00:00:00 2001 From: Brooks Prumo Date: Wed, 23 Feb 2022 22:47:57 -0600 Subject: [PATCH] Add test to check destination for stake splitting (#23303) --- programs/stake/src/stake_state.rs | 145 ++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index e2a1b7fb49..4d882df716 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -6666,6 +6666,151 @@ mod tests { } } + /// Ensure that `split()` correctly handles prefunded destination accounts. When a destination + /// account already has funds, ensure the minimum split amount reduces accordingly. + #[test] + fn test_split_destination_minimum_stake_delegation() { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + + for (destination_starting_balance, split_amount, expected_result) in [ + // split amount must be non zero + ( + rent_exempt_reserve + MINIMUM_STAKE_DELEGATION, + 0, + Err(InstructionError::InsufficientFunds), + ), + // any split amount is OK when destination account is already fully funded + (rent_exempt_reserve + MINIMUM_STAKE_DELEGATION, 1, Ok(())), + // if destination is only short by 1 lamport, then split amount can be 1 lamport + ( + rent_exempt_reserve + MINIMUM_STAKE_DELEGATION - 1, + 1, + Ok(()), + ), + // destination short by 2 lamports, so 1 isn't enough (non-zero split amount) + ( + rent_exempt_reserve + MINIMUM_STAKE_DELEGATION - 2, + 1, + Err(InstructionError::InsufficientFunds), + ), + // destination is rent exempt, so split enough for minimum delegation + (rent_exempt_reserve, MINIMUM_STAKE_DELEGATION, Ok(())), + // destination is rent exempt, but split amount less than minimum delegation + ( + rent_exempt_reserve, + MINIMUM_STAKE_DELEGATION - 1, + Err(InstructionError::InsufficientFunds), + ), + // destination is not rent exempt, so split enough for rent and minimum delegation + ( + rent_exempt_reserve - 1, + MINIMUM_STAKE_DELEGATION + 1, + Ok(()), + ), + // destination is not rent exempt, but split amount only for minimum delegation + ( + rent_exempt_reserve - 1, + MINIMUM_STAKE_DELEGATION, + Err(InstructionError::InsufficientFunds), + ), + // destination has smallest non-zero balance, so can split the minimum balance + // requirements minus what destination already has + ( + 1, + rent_exempt_reserve + MINIMUM_STAKE_DELEGATION - 1, + Ok(()), + ), + // destination has smallest non-zero balance, but cannot split less than the minimum + // balance requirements minus what destination already has + ( + 1, + rent_exempt_reserve + MINIMUM_STAKE_DELEGATION - 2, + Err(InstructionError::InsufficientFunds), + ), + // destination has zero lamports, so split must be at least rent exempt reserve plus + // minimum delegation + (0, rent_exempt_reserve + MINIMUM_STAKE_DELEGATION, Ok(())), + // destination has zero lamports, but split amount is less than rent exempt reserve + // plus minimum delegation + ( + 0, + rent_exempt_reserve + MINIMUM_STAKE_DELEGATION - 1, + Err(InstructionError::InsufficientFunds), + ), + ] { + let source_pubkey = Pubkey::new_unique(); + let source_meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&source_pubkey) + }; + + // Set the source's starting balance and stake delegation amount to something large + // to ensure its post-split balance meets all the requirements + let source_balance = u64::MAX; + let source_stake_delegation = source_balance - rent_exempt_reserve; + + for source_stake_state in &[ + StakeState::Initialized(source_meta), + StakeState::Stake(source_meta, just_stake(source_stake_delegation)), + ] { + let source_account = AccountSharedData::new_ref_data_with_space( + source_balance, + &source_stake_state, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let source_keyed_account = KeyedAccount::new(&source_pubkey, true, &source_account); + + let destination_pubkey = Pubkey::new_unique(); + let destination_account = AccountSharedData::new_ref_data_with_space( + destination_starting_balance, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let destination_keyed_account = + KeyedAccount::new(&destination_pubkey, true, &destination_account); + + assert_eq!( + expected_result, + source_keyed_account.split( + split_amount, + &destination_keyed_account, + &HashSet::from([source_pubkey]), + ), + ); + + // For the expected OK cases, when the source's StakeState is Stake, then the + // destination's StakeState *must* also end up as Stake as well. Additionally, + // check to ensure the destination's delegation amount is correct. If the + // destination is already rent exempt, then the destination's stake delegation + // *must* equal the split amount. Otherwise, the split amount must first be used to + // make the destination rent exempt, and then the leftover lamports are delegated. + if expected_result.is_ok() { + if let StakeState::Stake(_, _) = source_keyed_account.state().unwrap() { + if let StakeState::Stake(_, destination_stake) = + destination_keyed_account.state().unwrap() + { + let destination_initial_rent_deficit = + rent_exempt_reserve.saturating_sub(destination_starting_balance); + let expected_destination_stake_delegation = + split_amount - destination_initial_rent_deficit; + assert_eq!( + expected_destination_stake_delegation, + destination_stake.delegation.stake + ); + } else { + panic!("destination state must be StakeStake::Stake after successful split when source is also StakeState::Stake!"); + } + } + } + } + } + } + /// Ensure that `withdraw()` respects the MINIMUM_STAKE_DELEGATION requirements /// - Assert 1: withdrawing so remaining stake is equal-to the minimum is OK /// - Assert 2: withdrawing so remaining stake is less-than the minimum is not OK