Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions programs/stake/src/stake_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<StakeState>());

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::<StakeState>(),
&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::<StakeState>(),
&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
Expand Down