Clean & check stake (#15363)

* Add failing test

* Fix test

* Clean up redendant if case

* Demonstrate withdrawal boundaries

* Update test to fail on conditions that should be acceptable

* Fix test

* Add test for larger stake source

* Mirror changes for undelegated accounts

* Extra stake checks

* Split accounts must be the right size

Co-authored-by: Stephen Akridge <sakridge@gmail.com>
This commit is contained in:
Tyera Eulberg 2021-02-16 20:42:46 -07:00 committed by GitHub
parent b821a5d8dd
commit 51c27dcc1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 31 additions and 100 deletions

View File

@ -1057,6 +1057,9 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
if split.owner()? != id() {
return Err(InstructionError::IncorrectProgramId);
}
if split.data_len()? != std::mem::size_of::<StakeState>() {
return Err(InstructionError::InvalidAccountData);
}
if let StakeState::Uninitialized = split.state()? {
// verify enough account lamports
@ -1073,17 +1076,12 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
split.data_len()? as u64,
);
// verify enough lamports for rent in new split account
if lamports < split_rent_exempt_reserve.saturating_sub(split.lamports()?)
// verify full withdrawal can cover rent in new split account
|| (lamports < split_rent_exempt_reserve && lamports == self.lamports()?)
// verify enough lamports for rent and more than 0 stake in new split account
if lamports <= split_rent_exempt_reserve.saturating_sub(split.lamports()?)
// if not full withdrawal
|| (lamports != self.lamports()?
// verify more than 0 stake left in previous stake
&& (lamports + meta.rent_exempt_reserve >= self.lamports()?
// and verify more than 0 stake in new split account
|| lamports
<= split_rent_exempt_reserve.saturating_sub(split.lamports()?)))
&& checked_add(lamports, meta.rent_exempt_reserve)? >= self.lamports()?)
{
return Err(InstructionError::InsufficientFunds);
}
@ -1106,11 +1104,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
// different sizes.
let remaining_stake_delta =
lamports.saturating_sub(meta.rent_exempt_reserve);
let split_stake_amount = std::cmp::min(
lamports - split_rent_exempt_reserve,
remaining_stake_delta,
);
(remaining_stake_delta, split_stake_amount)
(remaining_stake_delta, remaining_stake_delta)
} else {
// Otherwise, the new split stake should reflect the entire split
// requested, less any lamports needed to cover the split_rent_exempt_reserve
@ -1134,15 +1128,12 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
split.data_len()? as u64,
);
// enough lamports for rent in new stake
if lamports < split_rent_exempt_reserve
// if not full withdrawal
|| (lamports != self.lamports()?
// verify more than 0 stake left in previous stake
&& (lamports + meta.rent_exempt_reserve >= self.lamports()?
// and verify more than 0 stake in new split account
|| lamports
<= split_rent_exempt_reserve.saturating_sub(split.lamports()?)))
// enough lamports for rent and more than 0 stake in new split account
if lamports <= split_rent_exempt_reserve.saturating_sub(split.lamports()?)
// if not full withdrawal
|| (lamports != self.lamports()?
// verify more than 0 stake left in previous stake
&& checked_add(lamports, meta.rent_exempt_reserve)? >= self.lamports()?)
{
return Err(InstructionError::InsufficientFunds);
}
@ -1429,16 +1420,18 @@ impl MergeKind {
(Self::Inactive(_, _), Self::Inactive(_, _)) => None,
(Self::Inactive(_, _), Self::ActivationEpoch(_, _)) => None,
(Self::ActivationEpoch(meta, mut stake), Self::Inactive(_, source_lamports)) => {
stake.delegation.stake += source_lamports;
stake.delegation.stake = checked_add(stake.delegation.stake, source_lamports)?;
Some(StakeState::Stake(meta, stake))
}
(
Self::ActivationEpoch(meta, mut stake),
Self::ActivationEpoch(source_meta, source_stake),
) => {
let source_lamports =
source_meta.rent_exempt_reserve + source_stake.delegation.stake;
stake.delegation.stake += source_lamports;
let source_lamports = checked_add(
source_meta.rent_exempt_reserve,
source_stake.delegation.stake,
)?;
stake.delegation.stake = checked_add(stake.delegation.stake, source_lamports)?;
Some(StakeState::Stake(meta, stake))
}
(Self::FullyActive(meta, mut stake), Self::FullyActive(_, source_stake)) => {
@ -1446,7 +1439,8 @@ impl MergeKind {
// protect against the magic activation loophole. It will
// instead be moved into the destination account as extra,
// withdrawable `lamports`
stake.delegation.stake += source_stake.delegation.stake;
stake.delegation.stake =
checked_add(stake.delegation.stake, source_stake.delegation.stake)?;
Some(StakeState::Stake(meta, stake))
}
_ => return Err(StakeError::MergeMismatch.into()),
@ -4738,7 +4732,7 @@ mod tests {
}
#[test]
fn test_split_to_larger_account_edge_case() {
fn test_split_to_larger_account() {
let stake_pubkey = solana_sdk::pubkey::new_rand();
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
@ -4792,77 +4786,15 @@ mod tests {
.expect("stake_account");
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
// should return error when initial_balance < expected_rent_exempt_reserve
let split_attempt =
// should always return error when splitting to larger account
let split_result =
stake_keyed_account.split(split_amount, &split_stake_keyed_account, &signers);
if initial_balance < expected_rent_exempt_reserve {
assert_eq!(split_attempt, Err(InstructionError::InsufficientFunds));
} else {
assert_eq!(split_attempt, Ok(()));
}
}
}
assert_eq!(split_result, Err(InstructionError::InvalidAccountData));
#[test]
fn test_split_100_percent_of_source_to_larger_account_edge_case() {
let stake_pubkey = solana_sdk::pubkey::new_rand();
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
let stake_lamports = rent_exempt_reserve + 1;
let split_stake_pubkey = solana_sdk::pubkey::new_rand();
let signers = vec![stake_pubkey].into_iter().collect();
let meta = Meta {
authorized: Authorized::auto(&stake_pubkey),
rent_exempt_reserve,
..Meta::default()
};
let state = StakeState::Stake(
meta,
Stake::just_stake(stake_lamports - rent_exempt_reserve),
);
let expected_rent_exempt_reserve = calculate_split_rent_exempt_reserve(
meta.rent_exempt_reserve,
std::mem::size_of::<StakeState>() as u64,
std::mem::size_of::<StakeState>() as u64 + 100,
);
assert!(expected_rent_exempt_reserve > stake_lamports);
let split_lamport_balances = vec![
0,
1,
expected_rent_exempt_reserve,
expected_rent_exempt_reserve + 1,
];
for initial_balance in split_lamport_balances {
let split_stake_account = Account::new_ref_data_with_space(
initial_balance,
&StakeState::Uninitialized,
std::mem::size_of::<StakeState>() + 100,
&id(),
)
.expect("stake_account");
let split_stake_keyed_account =
KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account);
let stake_account = Account::new_ref_data_with_space(
stake_lamports,
&state,
std::mem::size_of::<StakeState>(),
&id(),
)
.expect("stake_account");
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
// should return error
assert_eq!(
stake_keyed_account.split(stake_lamports, &split_stake_keyed_account, &signers),
Err(InstructionError::InsufficientFunds)
);
// Splitting 100% of source should not make a difference
let split_result =
stake_keyed_account.split(stake_lamports, &split_stake_keyed_account, &signers);
assert_eq!(split_result, Err(InstructionError::InvalidAccountData));
}
}
@ -5052,8 +4984,7 @@ mod tests {
Stake::just_stake(stake_lamports - rent_exempt_reserve),
),
] {
// Test that splitting to a larger account with greater rent-exempt requirement fails
// if split amount is too small
// Test that splitting to a larger account fails
let split_stake_account = Account::new_ref_data_with_space(
0,
&StakeState::Uninitialized,
@ -5075,7 +5006,7 @@ mod tests {
assert_eq!(
stake_keyed_account.split(stake_lamports, &split_stake_keyed_account, &signers),
Err(InstructionError::InsufficientFunds)
Err(InstructionError::InvalidAccountData)
);
// Test that splitting from a larger account to a smaller one works.