Removes KeyedAccount
from tests in stake instruction. (Part 2) (#24053)
* Migrates test_initialize_minimum_stake_delegation. * Migrates test_delegate_minimum_stake_delegation. * Migrates test_split_minimum_stake_delegation. * Migrates test_split_full_amount_minimum_stake_delegation. * Migrates test_split_destination_minimum_stake_delegation. * Migrates test_withdraw_minimum_stake_delegation. * Migrates test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation.
This commit is contained in:
committed by
GitHub
parent
0b7d0476c8
commit
8a18c48e47
@ -3457,4 +3457,738 @@ mod tests {
|
|||||||
Err(InstructionError::MissingRequiredSignature),
|
Err(InstructionError::MissingRequiredSignature),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure that `initialize()` respects the MINIMUM_STAKE_DELEGATION requirements
|
||||||
|
/// - Assert 1: accounts with a balance equal-to the minimum initialize OK
|
||||||
|
/// - Assert 2: accounts with a balance less-than the minimum do not initialize
|
||||||
|
#[test]
|
||||||
|
fn test_initialize_minimum_stake_delegation() {
|
||||||
|
let rent = Rent::default();
|
||||||
|
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||||
|
let stake_address = solana_sdk::pubkey::new_rand();
|
||||||
|
let instruction_data = serialize(&StakeInstruction::Initialize(
|
||||||
|
Authorized::auto(&stake_address),
|
||||||
|
Lockup::default(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
let instruction_accounts = vec![
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: stake_address,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: sysvar::rent::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (stake_delegation, expected_result) in [
|
||||||
|
(MINIMUM_STAKE_DELEGATION, Ok(())),
|
||||||
|
(
|
||||||
|
MINIMUM_STAKE_DELEGATION - 1,
|
||||||
|
Err(InstructionError::InsufficientFunds),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let stake_account = AccountSharedData::new(
|
||||||
|
stake_delegation + rent_exempt_reserve,
|
||||||
|
std::mem::size_of::<StakeState>(),
|
||||||
|
&id(),
|
||||||
|
);
|
||||||
|
process_instruction(
|
||||||
|
&instruction_data,
|
||||||
|
vec![
|
||||||
|
(stake_address, stake_account),
|
||||||
|
(
|
||||||
|
sysvar::rent::id(),
|
||||||
|
account::create_account_shared_data_for_test(&rent),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
instruction_accounts.clone(),
|
||||||
|
expected_result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that `delegate()` respects the MINIMUM_STAKE_DELEGATION requirements
|
||||||
|
/// - Assert 1: delegating an amount equal-to the minimum delegates OK
|
||||||
|
/// - Assert 2: delegating an amount less-than the minimum delegates OK
|
||||||
|
/// Also test both asserts above over both StakeState::{Initialized and Stake}, since the logic
|
||||||
|
/// is slightly different for the variants.
|
||||||
|
///
|
||||||
|
/// NOTE: Even though new stake accounts must have a minimum balance that is at least
|
||||||
|
/// MINIMUM_STAKE_DELEGATION (plus rent exempt reserve), the current behavior allows
|
||||||
|
/// withdrawing below the minimum delegation, then re-delegating successfully (see
|
||||||
|
/// `test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation()` for
|
||||||
|
/// more information.)
|
||||||
|
#[test]
|
||||||
|
fn test_delegate_minimum_stake_delegation() {
|
||||||
|
let rent = Rent::default();
|
||||||
|
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||||
|
let stake_address = solana_sdk::pubkey::new_rand();
|
||||||
|
let meta = Meta {
|
||||||
|
rent_exempt_reserve,
|
||||||
|
..Meta::auto(&stake_address)
|
||||||
|
};
|
||||||
|
let vote_address = solana_sdk::pubkey::new_rand();
|
||||||
|
let vote_account =
|
||||||
|
vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100);
|
||||||
|
let instruction_accounts = vec![
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: stake_address,
|
||||||
|
is_signer: true,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: vote_address,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: sysvar::clock::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: sysvar::stake_history::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: stake_config::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (stake_delegation, expected_result) in [
|
||||||
|
(MINIMUM_STAKE_DELEGATION, Ok(())),
|
||||||
|
(MINIMUM_STAKE_DELEGATION - 1, Ok(())),
|
||||||
|
] {
|
||||||
|
for stake_state in &[
|
||||||
|
StakeState::Initialized(meta),
|
||||||
|
just_stake(meta, stake_delegation),
|
||||||
|
] {
|
||||||
|
let stake_account = AccountSharedData::new_data_with_space(
|
||||||
|
stake_delegation + rent_exempt_reserve,
|
||||||
|
stake_state,
|
||||||
|
std::mem::size_of::<StakeState>(),
|
||||||
|
&id(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
process_instruction(
|
||||||
|
&serialize(&StakeInstruction::DelegateStake).unwrap(),
|
||||||
|
vec![
|
||||||
|
(stake_address, stake_account),
|
||||||
|
(vote_address, vote_account.clone()),
|
||||||
|
(
|
||||||
|
sysvar::clock::id(),
|
||||||
|
account::create_account_shared_data_for_test(&Clock::default()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
sysvar::stake_history::id(),
|
||||||
|
account::create_account_shared_data_for_test(&StakeHistory::default()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
stake_config::id(),
|
||||||
|
config::create_account(0, &stake_config::Config::default()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
instruction_accounts.clone(),
|
||||||
|
expected_result.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that `split()` respects the MINIMUM_STAKE_DELEGATION requirements. This applies to
|
||||||
|
/// both the source and destination acounts. Thus, we have four permutations possible based on
|
||||||
|
/// if each account's post-split delegation is equal-to (EQ) or less-than (LT) the minimum:
|
||||||
|
///
|
||||||
|
/// source | dest | result
|
||||||
|
/// --------+------+--------
|
||||||
|
/// EQ | EQ | Ok
|
||||||
|
/// EQ | LT | Err
|
||||||
|
/// LT | EQ | Err
|
||||||
|
/// LT | LT | Err
|
||||||
|
#[test]
|
||||||
|
fn test_split_minimum_stake_delegation() {
|
||||||
|
let rent = Rent::default();
|
||||||
|
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||||
|
let source_address = Pubkey::new_unique();
|
||||||
|
let source_meta = Meta {
|
||||||
|
rent_exempt_reserve,
|
||||||
|
..Meta::auto(&source_address)
|
||||||
|
};
|
||||||
|
let dest_address = Pubkey::new_unique();
|
||||||
|
let dest_account = AccountSharedData::new_data_with_space(
|
||||||
|
0,
|
||||||
|
&StakeState::Uninitialized,
|
||||||
|
std::mem::size_of::<StakeState>(),
|
||||||
|
&id(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let instruction_accounts = vec![
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: source_address,
|
||||||
|
is_signer: true,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: dest_address,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (source_stake_delegation, dest_stake_delegation, expected_result) in [
|
||||||
|
(MINIMUM_STAKE_DELEGATION, MINIMUM_STAKE_DELEGATION, Ok(())),
|
||||||
|
(
|
||||||
|
MINIMUM_STAKE_DELEGATION,
|
||||||
|
MINIMUM_STAKE_DELEGATION - 1,
|
||||||
|
Err(InstructionError::InsufficientFunds),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
MINIMUM_STAKE_DELEGATION - 1,
|
||||||
|
MINIMUM_STAKE_DELEGATION,
|
||||||
|
Err(InstructionError::InsufficientFunds),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
MINIMUM_STAKE_DELEGATION - 1,
|
||||||
|
MINIMUM_STAKE_DELEGATION - 1,
|
||||||
|
Err(InstructionError::InsufficientFunds),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
// The source account's starting balance is equal to *both* the source and dest
|
||||||
|
// accounts' *final* balance
|
||||||
|
let source_starting_balance =
|
||||||
|
source_stake_delegation + dest_stake_delegation + rent_exempt_reserve * 2;
|
||||||
|
for source_stake_state in &[
|
||||||
|
StakeState::Initialized(source_meta),
|
||||||
|
just_stake(source_meta, source_starting_balance - rent_exempt_reserve),
|
||||||
|
] {
|
||||||
|
let source_account = AccountSharedData::new_data_with_space(
|
||||||
|
source_starting_balance,
|
||||||
|
source_stake_state,
|
||||||
|
std::mem::size_of::<StakeState>(),
|
||||||
|
&id(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
process_instruction(
|
||||||
|
&serialize(&StakeInstruction::Split(
|
||||||
|
dest_stake_delegation + rent_exempt_reserve,
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
vec![
|
||||||
|
(source_address, source_account),
|
||||||
|
(dest_address, dest_account.clone()),
|
||||||
|
(
|
||||||
|
sysvar::rent::id(),
|
||||||
|
account::create_account_shared_data_for_test(&rent),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
instruction_accounts.clone(),
|
||||||
|
expected_result.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that splitting the full amount from an account respects the MINIMUM_STAKE_DELEGATION
|
||||||
|
/// requirements. This ensures that we are future-proofing/testing any raises to the minimum
|
||||||
|
/// delegation.
|
||||||
|
/// - Assert 1: splitting the full amount from an account that has at least the minimum
|
||||||
|
/// delegation is OK
|
||||||
|
/// - Assert 2: splitting the full amount from an account that has less than the minimum
|
||||||
|
/// delegation is not OK
|
||||||
|
#[test]
|
||||||
|
fn test_split_full_amount_minimum_stake_delegation() {
|
||||||
|
let rent = Rent::default();
|
||||||
|
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||||
|
let source_address = Pubkey::new_unique();
|
||||||
|
let source_meta = Meta {
|
||||||
|
rent_exempt_reserve,
|
||||||
|
..Meta::auto(&source_address)
|
||||||
|
};
|
||||||
|
let dest_address = Pubkey::new_unique();
|
||||||
|
let dest_account = AccountSharedData::new_data_with_space(
|
||||||
|
0,
|
||||||
|
&StakeState::Uninitialized,
|
||||||
|
std::mem::size_of::<StakeState>(),
|
||||||
|
&id(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let instruction_accounts = vec![
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: source_address,
|
||||||
|
is_signer: true,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: dest_address,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (stake_delegation, expected_result) in [
|
||||||
|
(MINIMUM_STAKE_DELEGATION, Ok(())),
|
||||||
|
(
|
||||||
|
MINIMUM_STAKE_DELEGATION - 1,
|
||||||
|
Err(InstructionError::InsufficientFunds),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
for source_stake_state in &[
|
||||||
|
StakeState::Initialized(source_meta),
|
||||||
|
just_stake(source_meta, stake_delegation),
|
||||||
|
] {
|
||||||
|
let source_account = AccountSharedData::new_data_with_space(
|
||||||
|
stake_delegation + rent_exempt_reserve,
|
||||||
|
source_stake_state,
|
||||||
|
std::mem::size_of::<StakeState>(),
|
||||||
|
&id(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
process_instruction(
|
||||||
|
&serialize(&StakeInstruction::Split(source_account.lamports())).unwrap(),
|
||||||
|
vec![
|
||||||
|
(source_address, source_account),
|
||||||
|
(dest_address, dest_account.clone()),
|
||||||
|
(
|
||||||
|
sysvar::rent::id(),
|
||||||
|
account::create_account_shared_data_for_test(&rent),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
instruction_accounts.clone(),
|
||||||
|
expected_result.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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>());
|
||||||
|
let source_address = Pubkey::new_unique();
|
||||||
|
let source_meta = Meta {
|
||||||
|
rent_exempt_reserve,
|
||||||
|
..Meta::auto(&source_address)
|
||||||
|
};
|
||||||
|
let dest_address = Pubkey::new_unique();
|
||||||
|
let instruction_accounts = vec![
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: source_address,
|
||||||
|
is_signer: true,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: dest_address,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
// 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),
|
||||||
|
just_stake(source_meta, source_stake_delegation),
|
||||||
|
] {
|
||||||
|
let source_account = AccountSharedData::new_data_with_space(
|
||||||
|
source_balance,
|
||||||
|
&source_stake_state,
|
||||||
|
std::mem::size_of::<StakeState>(),
|
||||||
|
&id(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let dest_account = AccountSharedData::new_data_with_space(
|
||||||
|
destination_starting_balance,
|
||||||
|
&StakeState::Uninitialized,
|
||||||
|
std::mem::size_of::<StakeState>(),
|
||||||
|
&id(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let accounts = process_instruction(
|
||||||
|
&serialize(&StakeInstruction::Split(split_amount)).unwrap(),
|
||||||
|
vec![
|
||||||
|
(source_address, source_account),
|
||||||
|
(dest_address, dest_account),
|
||||||
|
(
|
||||||
|
sysvar::rent::id(),
|
||||||
|
account::create_account_shared_data_for_test(&rent),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
instruction_accounts.clone(),
|
||||||
|
expected_result.clone(),
|
||||||
|
);
|
||||||
|
// 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(_, _) = accounts[0].state().unwrap() {
|
||||||
|
if let StakeState::Stake(_, destination_stake) =
|
||||||
|
accounts[1].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
|
||||||
|
);
|
||||||
|
assert!(destination_stake.delegation.stake >= MINIMUM_STAKE_DELEGATION,);
|
||||||
|
} 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
|
||||||
|
#[test]
|
||||||
|
fn test_withdraw_minimum_stake_delegation() {
|
||||||
|
let rent = Rent::default();
|
||||||
|
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||||
|
let stake_address = solana_sdk::pubkey::new_rand();
|
||||||
|
let meta = Meta {
|
||||||
|
rent_exempt_reserve,
|
||||||
|
..Meta::auto(&stake_address)
|
||||||
|
};
|
||||||
|
let recipient_address = solana_sdk::pubkey::new_rand();
|
||||||
|
let instruction_accounts = vec![
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: stake_address,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: recipient_address,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: sysvar::clock::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: sysvar::stake_history::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: stake_address,
|
||||||
|
is_signer: true,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let starting_stake_delegation = MINIMUM_STAKE_DELEGATION;
|
||||||
|
for (ending_stake_delegation, expected_result) in [
|
||||||
|
(MINIMUM_STAKE_DELEGATION, Ok(())),
|
||||||
|
(
|
||||||
|
MINIMUM_STAKE_DELEGATION - 1,
|
||||||
|
Err(InstructionError::InsufficientFunds),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
for stake_state in &[
|
||||||
|
StakeState::Initialized(meta),
|
||||||
|
just_stake(meta, starting_stake_delegation),
|
||||||
|
] {
|
||||||
|
let rewards_balance = 123;
|
||||||
|
let stake_account = AccountSharedData::new_data_with_space(
|
||||||
|
starting_stake_delegation + rent_exempt_reserve + rewards_balance,
|
||||||
|
stake_state,
|
||||||
|
std::mem::size_of::<StakeState>(),
|
||||||
|
&id(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let withdraw_amount =
|
||||||
|
(starting_stake_delegation + rewards_balance) - ending_stake_delegation;
|
||||||
|
process_instruction(
|
||||||
|
&serialize(&StakeInstruction::Withdraw(withdraw_amount)).unwrap(),
|
||||||
|
vec![
|
||||||
|
(stake_address, stake_account),
|
||||||
|
(
|
||||||
|
recipient_address,
|
||||||
|
AccountSharedData::new(rent_exempt_reserve, 0, &system_program::id()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
sysvar::clock::id(),
|
||||||
|
account::create_account_shared_data_for_test(&Clock::default()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
sysvar::rent::id(),
|
||||||
|
account::create_account_shared_data_for_test(&Rent::free()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
sysvar::stake_history::id(),
|
||||||
|
account::create_account_shared_data_for_test(&StakeHistory::default()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
stake_config::id(),
|
||||||
|
config::create_account(0, &stake_config::Config::default()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
instruction_accounts.clone(),
|
||||||
|
expected_result.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stake program currently allows delegations below the minimum stake delegation (see also
|
||||||
|
/// `test_delegate_minimum_stake_delegation()`). This is not the ultimate desired behavior,
|
||||||
|
/// but this test ensures the existing behavior is not changed inadvertently.
|
||||||
|
///
|
||||||
|
/// This test:
|
||||||
|
/// 1. Initialises a stake account (with sufficient balance for both rent and minimum delegation)
|
||||||
|
/// 2. Delegates the minimum amount
|
||||||
|
/// 3. Deactives the delegation
|
||||||
|
/// 4. Withdraws from the account such that the ending balance is *below* rent + minimum delegation
|
||||||
|
/// 5. Re-delegates, now with less than the minimum delegation, but it still succeeds
|
||||||
|
#[test]
|
||||||
|
fn test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation() {
|
||||||
|
let rent = Rent::default();
|
||||||
|
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||||
|
let stake_address = solana_sdk::pubkey::new_rand();
|
||||||
|
let stake_account = AccountSharedData::new(
|
||||||
|
rent_exempt_reserve + MINIMUM_STAKE_DELEGATION,
|
||||||
|
std::mem::size_of::<StakeState>(),
|
||||||
|
&id(),
|
||||||
|
);
|
||||||
|
let vote_address = solana_sdk::pubkey::new_rand();
|
||||||
|
let vote_account =
|
||||||
|
vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100);
|
||||||
|
let recipient_address = solana_sdk::pubkey::new_rand();
|
||||||
|
let mut clock = Clock::default();
|
||||||
|
let mut transaction_accounts = vec![
|
||||||
|
(stake_address, stake_account),
|
||||||
|
(vote_address, vote_account),
|
||||||
|
(
|
||||||
|
recipient_address,
|
||||||
|
AccountSharedData::new(rent_exempt_reserve, 0, &system_program::id()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
sysvar::clock::id(),
|
||||||
|
account::create_account_shared_data_for_test(&clock),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
sysvar::stake_history::id(),
|
||||||
|
account::create_account_shared_data_for_test(&StakeHistory::default()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
stake_config::id(),
|
||||||
|
config::create_account(0, &stake_config::Config::default()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
sysvar::rent::id(),
|
||||||
|
account::create_account_shared_data_for_test(&rent),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let instruction_accounts = vec![
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: stake_address,
|
||||||
|
is_signer: true,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: vote_address,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: sysvar::clock::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: sysvar::stake_history::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: stake_config::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let accounts = process_instruction(
|
||||||
|
&serialize(&StakeInstruction::Initialize(
|
||||||
|
Authorized::auto(&stake_address),
|
||||||
|
Lockup::default(),
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
transaction_accounts.clone(),
|
||||||
|
vec![
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: stake_address,
|
||||||
|
is_signer: true,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: sysvar::rent::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Ok(()),
|
||||||
|
);
|
||||||
|
transaction_accounts[0] = (stake_address, accounts[0].clone());
|
||||||
|
|
||||||
|
let accounts = process_instruction(
|
||||||
|
&serialize(&StakeInstruction::DelegateStake).unwrap(),
|
||||||
|
transaction_accounts.clone(),
|
||||||
|
instruction_accounts.clone(),
|
||||||
|
Ok(()),
|
||||||
|
);
|
||||||
|
transaction_accounts[0] = (stake_address, accounts[0].clone());
|
||||||
|
transaction_accounts[1] = (vote_address, accounts[1].clone());
|
||||||
|
|
||||||
|
clock.epoch += 1;
|
||||||
|
transaction_accounts[3] = (
|
||||||
|
sysvar::clock::id(),
|
||||||
|
account::create_account_shared_data_for_test(&clock),
|
||||||
|
);
|
||||||
|
let accounts = process_instruction(
|
||||||
|
&serialize(&StakeInstruction::Deactivate).unwrap(),
|
||||||
|
transaction_accounts.clone(),
|
||||||
|
vec![
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: stake_address,
|
||||||
|
is_signer: true,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: sysvar::clock::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Ok(()),
|
||||||
|
);
|
||||||
|
transaction_accounts[0] = (stake_address, accounts[0].clone());
|
||||||
|
|
||||||
|
clock.epoch += 1;
|
||||||
|
transaction_accounts[3] = (
|
||||||
|
sysvar::clock::id(),
|
||||||
|
account::create_account_shared_data_for_test(&clock),
|
||||||
|
);
|
||||||
|
let withdraw_amount =
|
||||||
|
accounts[0].lamports() - (rent_exempt_reserve + MINIMUM_STAKE_DELEGATION - 1);
|
||||||
|
process_instruction(
|
||||||
|
&serialize(&StakeInstruction::Withdraw(withdraw_amount)).unwrap(),
|
||||||
|
transaction_accounts.clone(),
|
||||||
|
vec![
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: stake_address,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: recipient_address,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: sysvar::clock::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: sysvar::stake_history::id(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: stake_address,
|
||||||
|
is_signer: true,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Ok(()),
|
||||||
|
);
|
||||||
|
|
||||||
|
process_instruction(
|
||||||
|
&serialize(&StakeInstruction::DelegateStake).unwrap(),
|
||||||
|
transaction_accounts,
|
||||||
|
instruction_accounts,
|
||||||
|
Ok(()),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1437,12 +1437,10 @@ mod tests {
|
|||||||
account::{create_account_shared_data_for_test, AccountSharedData, WritableAccount},
|
account::{create_account_shared_data_for_test, AccountSharedData, WritableAccount},
|
||||||
native_token,
|
native_token,
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
system_program,
|
|
||||||
sysvar::SysvarId,
|
sysvar::SysvarId,
|
||||||
transaction_context::TransactionContext,
|
transaction_context::TransactionContext,
|
||||||
},
|
},
|
||||||
solana_vote_program::vote_state,
|
std::iter::FromIterator,
|
||||||
std::{cell::RefCell, iter::FromIterator},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -4845,529 +4843,6 @@ mod tests {
|
|||||||
assert_eq!(new_stake.delegation.stake, delegation * 2);
|
assert_eq!(new_stake.delegation.stake, delegation * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure that `initialize()` respects the MINIMUM_STAKE_DELEGATION requirements
|
|
||||||
/// - Assert 1: accounts with a balance equal-to the minimum initialize OK
|
|
||||||
/// - Assert 2: accounts with a balance less-than the minimum do not initialize
|
|
||||||
#[test]
|
|
||||||
fn test_initialize_minimum_stake_delegation() {
|
|
||||||
for (stake_delegation, expected_result) in [
|
|
||||||
(MINIMUM_STAKE_DELEGATION, Ok(())),
|
|
||||||
(
|
|
||||||
MINIMUM_STAKE_DELEGATION - 1,
|
|
||||||
Err(InstructionError::InsufficientFunds),
|
|
||||||
),
|
|
||||||
] {
|
|
||||||
let rent = Rent::default();
|
|
||||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
|
||||||
let stake_pubkey = Pubkey::new_unique();
|
|
||||||
let stake_account = AccountSharedData::new_ref(
|
|
||||||
stake_delegation + rent_exempt_reserve,
|
|
||||||
std::mem::size_of::<StakeState>(),
|
|
||||||
&id(),
|
|
||||||
);
|
|
||||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
expected_result,
|
|
||||||
stake_keyed_account.initialize(
|
|
||||||
&Authorized::auto(&stake_pubkey),
|
|
||||||
&Lockup::default(),
|
|
||||||
&rent
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure that `delegate()` respects the MINIMUM_STAKE_DELEGATION requirements
|
|
||||||
/// - Assert 1: delegating an amount equal-to the minimum delegates OK
|
|
||||||
/// - Assert 2: delegating an amount less-than the minimum delegates OK
|
|
||||||
/// Also test both asserts above over both StakeState::{Initialized and Stake}, since the logic
|
|
||||||
/// is slightly different for the variants.
|
|
||||||
///
|
|
||||||
/// NOTE: Even though new stake accounts must have a minimum balance that is at least
|
|
||||||
/// MINIMUM_STAKE_DELEGATION (plus rent exempt reserve), the current behavior allows
|
|
||||||
/// withdrawing below the minimum delegation, then re-delegating successfully (see
|
|
||||||
/// `test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation()` for
|
|
||||||
/// more information.)
|
|
||||||
#[test]
|
|
||||||
fn test_delegate_minimum_stake_delegation() {
|
|
||||||
for (stake_delegation, expected_result) in [
|
|
||||||
(MINIMUM_STAKE_DELEGATION, Ok(())),
|
|
||||||
(MINIMUM_STAKE_DELEGATION - 1, Ok(())),
|
|
||||||
] {
|
|
||||||
let rent = Rent::default();
|
|
||||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
|
||||||
let stake_pubkey = Pubkey::new_unique();
|
|
||||||
let signers = HashSet::from([stake_pubkey]);
|
|
||||||
let meta = Meta {
|
|
||||||
rent_exempt_reserve,
|
|
||||||
..Meta::auto(&stake_pubkey)
|
|
||||||
};
|
|
||||||
|
|
||||||
for stake_state in &[
|
|
||||||
StakeState::Initialized(meta),
|
|
||||||
StakeState::Stake(meta, just_stake(stake_delegation)),
|
|
||||||
] {
|
|
||||||
let stake_account = AccountSharedData::new_ref_data_with_space(
|
|
||||||
stake_delegation + rent_exempt_reserve,
|
|
||||||
stake_state,
|
|
||||||
std::mem::size_of::<StakeState>(),
|
|
||||||
&id(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
|
|
||||||
|
|
||||||
let vote_pubkey = Pubkey::new_unique();
|
|
||||||
let vote_account = RefCell::new(vote_state::create_account(
|
|
||||||
&vote_pubkey,
|
|
||||||
&Pubkey::new_unique(),
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
));
|
|
||||||
let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
expected_result,
|
|
||||||
stake_keyed_account.delegate(
|
|
||||||
&vote_keyed_account,
|
|
||||||
&Clock::default(),
|
|
||||||
&StakeHistory::default(),
|
|
||||||
&Config::default(),
|
|
||||||
&signers,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure that `split()` respects the MINIMUM_STAKE_DELEGATION requirements. This applies to
|
|
||||||
/// both the source and destination acounts. Thus, we have four permutations possible based on
|
|
||||||
/// if each account's post-split delegation is equal-to (EQ) or less-than (LT) the minimum:
|
|
||||||
///
|
|
||||||
/// source | dest | result
|
|
||||||
/// --------+------+--------
|
|
||||||
/// EQ | EQ | Ok
|
|
||||||
/// EQ | LT | Err
|
|
||||||
/// LT | EQ | Err
|
|
||||||
/// LT | LT | Err
|
|
||||||
#[test]
|
|
||||||
fn test_split_minimum_stake_delegation() {
|
|
||||||
let mut transaction_context = create_mock_tx_context();
|
|
||||||
let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]);
|
|
||||||
for (source_stake_delegation, dest_stake_delegation, expected_result) in [
|
|
||||||
(MINIMUM_STAKE_DELEGATION, MINIMUM_STAKE_DELEGATION, Ok(())),
|
|
||||||
(
|
|
||||||
MINIMUM_STAKE_DELEGATION,
|
|
||||||
MINIMUM_STAKE_DELEGATION - 1,
|
|
||||||
Err(InstructionError::InsufficientFunds),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
MINIMUM_STAKE_DELEGATION - 1,
|
|
||||||
MINIMUM_STAKE_DELEGATION,
|
|
||||||
Err(InstructionError::InsufficientFunds),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
MINIMUM_STAKE_DELEGATION - 1,
|
|
||||||
MINIMUM_STAKE_DELEGATION - 1,
|
|
||||||
Err(InstructionError::InsufficientFunds),
|
|
||||||
),
|
|
||||||
] {
|
|
||||||
let rent = Rent::default();
|
|
||||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
|
||||||
let source_pubkey = Pubkey::new_unique();
|
|
||||||
let source_meta = Meta {
|
|
||||||
rent_exempt_reserve,
|
|
||||||
..Meta::auto(&source_pubkey)
|
|
||||||
};
|
|
||||||
// The source account's starting balance is equal to *both* the source and dest
|
|
||||||
// accounts' *final* balance
|
|
||||||
let source_starting_balance =
|
|
||||||
source_stake_delegation + dest_stake_delegation + rent_exempt_reserve * 2;
|
|
||||||
|
|
||||||
for source_stake_state in &[
|
|
||||||
StakeState::Initialized(source_meta),
|
|
||||||
StakeState::Stake(
|
|
||||||
source_meta,
|
|
||||||
just_stake(source_starting_balance - rent_exempt_reserve),
|
|
||||||
),
|
|
||||||
] {
|
|
||||||
let source_account = AccountSharedData::new_ref_data_with_space(
|
|
||||||
source_starting_balance,
|
|
||||||
source_stake_state,
|
|
||||||
std::mem::size_of::<StakeState>(),
|
|
||||||
&id(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let source_keyed_account = KeyedAccount::new(&source_pubkey, true, &source_account);
|
|
||||||
|
|
||||||
let dest_pubkey = Pubkey::new_unique();
|
|
||||||
let dest_account = AccountSharedData::new_ref_data_with_space(
|
|
||||||
0,
|
|
||||||
&StakeState::Uninitialized,
|
|
||||||
std::mem::size_of::<StakeState>(),
|
|
||||||
&id(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let dest_keyed_account = KeyedAccount::new(&dest_pubkey, true, &dest_account);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
expected_result,
|
|
||||||
source_keyed_account.split(
|
|
||||||
&invoke_context,
|
|
||||||
dest_stake_delegation + rent_exempt_reserve,
|
|
||||||
&dest_keyed_account,
|
|
||||||
&HashSet::from([source_pubkey]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure that splitting the full amount from an account respects the MINIMUM_STAKE_DELEGATION
|
|
||||||
/// requirements. This ensures that we are future-proofing/testing any raises to the minimum
|
|
||||||
/// delegation.
|
|
||||||
/// - Assert 1: splitting the full amount from an account that has at least the minimum
|
|
||||||
/// delegation is OK
|
|
||||||
/// - Assert 2: splitting the full amount from an account that has less than the minimum
|
|
||||||
/// delegation is not OK
|
|
||||||
#[test]
|
|
||||||
fn test_split_full_amount_minimum_stake_delegation() {
|
|
||||||
let mut transaction_context = create_mock_tx_context();
|
|
||||||
let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]);
|
|
||||||
for (stake_delegation, expected_result) in [
|
|
||||||
(MINIMUM_STAKE_DELEGATION, Ok(())),
|
|
||||||
(
|
|
||||||
MINIMUM_STAKE_DELEGATION - 1,
|
|
||||||
Err(InstructionError::InsufficientFunds),
|
|
||||||
),
|
|
||||||
] {
|
|
||||||
let rent = Rent::default();
|
|
||||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
|
||||||
let source_pubkey = Pubkey::new_unique();
|
|
||||||
let source_meta = Meta {
|
|
||||||
rent_exempt_reserve,
|
|
||||||
..Meta::auto(&source_pubkey)
|
|
||||||
};
|
|
||||||
|
|
||||||
for source_stake_state in &[
|
|
||||||
StakeState::Initialized(source_meta),
|
|
||||||
StakeState::Stake(source_meta, just_stake(stake_delegation)),
|
|
||||||
] {
|
|
||||||
let source_account = AccountSharedData::new_ref_data_with_space(
|
|
||||||
stake_delegation + rent_exempt_reserve,
|
|
||||||
source_stake_state,
|
|
||||||
std::mem::size_of::<StakeState>(),
|
|
||||||
&id(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let source_keyed_account = KeyedAccount::new(&source_pubkey, true, &source_account);
|
|
||||||
|
|
||||||
let dest_pubkey = Pubkey::new_unique();
|
|
||||||
let dest_account = AccountSharedData::new_ref_data_with_space(
|
|
||||||
0,
|
|
||||||
&StakeState::Uninitialized,
|
|
||||||
std::mem::size_of::<StakeState>(),
|
|
||||||
&id(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let dest_keyed_account = KeyedAccount::new(&dest_pubkey, true, &dest_account);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
expected_result,
|
|
||||||
source_keyed_account.split(
|
|
||||||
&invoke_context,
|
|
||||||
source_keyed_account.lamports().unwrap(),
|
|
||||||
&dest_keyed_account,
|
|
||||||
&HashSet::from([source_pubkey]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 mut transaction_context = create_mock_tx_context();
|
|
||||||
let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]);
|
|
||||||
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(
|
|
||||||
&invoke_context,
|
|
||||||
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
|
|
||||||
);
|
|
||||||
assert!(destination_stake.delegation.stake >= MINIMUM_STAKE_DELEGATION,);
|
|
||||||
} 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
|
|
||||||
#[test]
|
|
||||||
fn test_withdraw_minimum_stake_delegation() {
|
|
||||||
let starting_stake_delegation = MINIMUM_STAKE_DELEGATION;
|
|
||||||
for (ending_stake_delegation, expected_result) in [
|
|
||||||
(MINIMUM_STAKE_DELEGATION, Ok(())),
|
|
||||||
(
|
|
||||||
MINIMUM_STAKE_DELEGATION - 1,
|
|
||||||
Err(InstructionError::InsufficientFunds),
|
|
||||||
),
|
|
||||||
] {
|
|
||||||
let rent = Rent::default();
|
|
||||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
|
||||||
let stake_pubkey = Pubkey::new_unique();
|
|
||||||
let meta = Meta {
|
|
||||||
rent_exempt_reserve,
|
|
||||||
..Meta::auto(&stake_pubkey)
|
|
||||||
};
|
|
||||||
|
|
||||||
for stake_state in &[
|
|
||||||
StakeState::Initialized(meta),
|
|
||||||
StakeState::Stake(meta, just_stake(starting_stake_delegation)),
|
|
||||||
] {
|
|
||||||
let rewards_balance = 123;
|
|
||||||
let stake_account = AccountSharedData::new_ref_data_with_space(
|
|
||||||
starting_stake_delegation + rent_exempt_reserve + rewards_balance,
|
|
||||||
stake_state,
|
|
||||||
std::mem::size_of::<StakeState>(),
|
|
||||||
&id(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
|
|
||||||
|
|
||||||
let to_pubkey = Pubkey::new_unique();
|
|
||||||
let to_account =
|
|
||||||
AccountSharedData::new_ref(rent_exempt_reserve, 0, &system_program::id());
|
|
||||||
let to_keyed_account = KeyedAccount::new(&to_pubkey, false, &to_account);
|
|
||||||
|
|
||||||
let withdraw_amount =
|
|
||||||
(starting_stake_delegation + rewards_balance) - ending_stake_delegation;
|
|
||||||
assert_eq!(
|
|
||||||
expected_result,
|
|
||||||
stake_keyed_account.withdraw(
|
|
||||||
withdraw_amount,
|
|
||||||
&to_keyed_account,
|
|
||||||
&Clock::default(),
|
|
||||||
&StakeHistory::default(),
|
|
||||||
&stake_keyed_account,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The stake program currently allows delegations below the minimum stake delegation (see also
|
|
||||||
/// `test_delegate_minimum_stake_delegation()`). This is not the ultimate desired behavior,
|
|
||||||
/// but this test ensures the existing behavior is not changed inadvertently.
|
|
||||||
///
|
|
||||||
/// This test:
|
|
||||||
/// 1. Initialises a stake account (with sufficient balance for both rent and minimum delegation)
|
|
||||||
/// 2. Delegates the minimum amount
|
|
||||||
/// 3. Deactives the delegation
|
|
||||||
/// 4. Withdraws from the account such that the ending balance is *below* rent + minimum delegation
|
|
||||||
/// 5. Re-delegates, now with less than the minimum delegation, but it still succeeds
|
|
||||||
#[test]
|
|
||||||
fn test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation() {
|
|
||||||
let rent = Rent::default();
|
|
||||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
|
||||||
let stake_pubkey = Pubkey::new_unique();
|
|
||||||
let signers = HashSet::from([stake_pubkey]);
|
|
||||||
let stake_account = AccountSharedData::new_ref(
|
|
||||||
rent_exempt_reserve + MINIMUM_STAKE_DELEGATION,
|
|
||||||
std::mem::size_of::<StakeState>(),
|
|
||||||
&id(),
|
|
||||||
);
|
|
||||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
|
|
||||||
stake_keyed_account
|
|
||||||
.initialize(&Authorized::auto(&stake_pubkey), &Lockup::default(), &rent)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let vote_pubkey = Pubkey::new_unique();
|
|
||||||
let vote_account = RefCell::new(vote_state::create_account(
|
|
||||||
&vote_pubkey,
|
|
||||||
&Pubkey::new_unique(),
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
));
|
|
||||||
let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account);
|
|
||||||
let mut clock = Clock::default();
|
|
||||||
stake_keyed_account
|
|
||||||
.delegate(
|
|
||||||
&vote_keyed_account,
|
|
||||||
&clock,
|
|
||||||
&StakeHistory::default(),
|
|
||||||
&Config::default(),
|
|
||||||
&signers,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
clock.epoch += 1;
|
|
||||||
stake_keyed_account.deactivate(&clock, &signers).unwrap();
|
|
||||||
|
|
||||||
clock.epoch += 1;
|
|
||||||
let withdraw_amount = stake_keyed_account.lamports().unwrap()
|
|
||||||
- (rent_exempt_reserve + MINIMUM_STAKE_DELEGATION - 1);
|
|
||||||
let withdraw_pubkey = Pubkey::new_unique();
|
|
||||||
let withdraw_account =
|
|
||||||
AccountSharedData::new_ref(rent_exempt_reserve, 0, &system_program::id());
|
|
||||||
let withdraw_keyed_account = KeyedAccount::new(&withdraw_pubkey, false, &withdraw_account);
|
|
||||||
stake_keyed_account
|
|
||||||
.withdraw(
|
|
||||||
withdraw_amount,
|
|
||||||
&withdraw_keyed_account,
|
|
||||||
&clock,
|
|
||||||
&StakeHistory::default(),
|
|
||||||
&stake_keyed_account,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(stake_keyed_account
|
|
||||||
.delegate(
|
|
||||||
&vote_keyed_account,
|
|
||||||
&clock,
|
|
||||||
&StakeHistory::default(),
|
|
||||||
&Config::default(),
|
|
||||||
&signers,
|
|
||||||
)
|
|
||||||
.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
prop_compose! {
|
prop_compose! {
|
||||||
pub fn sum_within(max: u64)(total in 1..max)
|
pub fn sum_within(max: u64)(total in 1..max)
|
||||||
(intermediate in 1..total, total in Just(total))
|
(intermediate in 1..total, total in Just(total))
|
||||||
|
Reference in New Issue
Block a user