stake: Allow initialized stakes to be below the min delegation (#24670)
* stake: Allow initialized stakes to be below the min delegation * Add PR number in feature * Fixup RPC subscription test * Address feedback pt 1 * Address feedback pt 2 * Update FrozenAbi Digest * Address feedback: no new error type, more comments
This commit is contained in:
parent
b2ecfa6252
commit
326e53be97
|
@ -5730,6 +5730,7 @@ dependencies = [
|
|||
name = "solana-stake-program"
|
||||
version = "1.11.0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"bincode",
|
||||
"log",
|
||||
"num-derive",
|
||||
|
|
|
@ -26,6 +26,7 @@ solana-vote-program = { path = "../vote", version = "=1.11.0" }
|
|||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5.0"
|
||||
proptest = "1.0"
|
||||
solana-logger = { path = "../../logger", version = "=1.11.0" }
|
||||
|
||||
|
|
|
@ -187,6 +187,7 @@ pub fn process_instruction(
|
|||
&stake_history,
|
||||
&config,
|
||||
&signers,
|
||||
&invoke_context.feature_set,
|
||||
)
|
||||
}
|
||||
Ok(StakeInstruction::Split(lamports)) => {
|
||||
|
@ -456,6 +457,7 @@ mod tests {
|
|||
authorized_from, create_stake_history_from_delegations, from, new_stake, stake_from,
|
||||
Delegation, Meta, Stake, StakeState,
|
||||
},
|
||||
assert_matches::assert_matches,
|
||||
bincode::serialize,
|
||||
solana_program_runtime::{
|
||||
invoke_context::mock_process_instruction, sysvar_cache::SysvarCache,
|
||||
|
@ -3246,31 +3248,17 @@ mod tests {
|
|||
},
|
||||
];
|
||||
|
||||
// should pass, withdrawing account down to minimum balance
|
||||
// should pass, withdrawing initialized account down to minimum balance
|
||||
process_instruction(
|
||||
&serialize(&StakeInstruction::Withdraw(
|
||||
stake_lamports - minimum_delegation,
|
||||
))
|
||||
.unwrap(),
|
||||
&serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(),
|
||||
transaction_accounts.clone(),
|
||||
instruction_accounts.clone(),
|
||||
Ok(()),
|
||||
);
|
||||
|
||||
// should fail, withdrawing account down to only rent-exempt reserve
|
||||
process_instruction(
|
||||
&serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(),
|
||||
transaction_accounts.clone(),
|
||||
instruction_accounts.clone(),
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
);
|
||||
|
||||
// should fail, withdrawal that would leave less than rent-exempt reserve
|
||||
process_instruction(
|
||||
&serialize(&StakeInstruction::Withdraw(
|
||||
stake_lamports + minimum_delegation,
|
||||
))
|
||||
.unwrap(),
|
||||
&serialize(&StakeInstruction::Withdraw(stake_lamports + 1)).unwrap(),
|
||||
transaction_accounts.clone(),
|
||||
instruction_accounts.clone(),
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
|
@ -3675,13 +3663,11 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// Ensure that `initialize()` respects the minimum 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
|
||||
/// Ensure that `initialize()` respects the minimum balance requirements
|
||||
/// - Assert 1: accounts with a balance equal-to the rent exemption initialize OK
|
||||
/// - Assert 2: accounts with a balance less-than the rent exemption do not initialize
|
||||
#[test]
|
||||
fn test_initialize_minimum_stake_delegation() {
|
||||
let feature_set = FeatureSet::all_enabled();
|
||||
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
|
||||
fn test_initialize_minimum_balance() {
|
||||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of());
|
||||
let stake_address = solana_sdk::pubkey::new_rand();
|
||||
|
@ -3702,18 +3688,14 @@ mod tests {
|
|||
is_writable: false,
|
||||
},
|
||||
];
|
||||
for (stake_delegation, expected_result) in [
|
||||
(minimum_delegation, Ok(())),
|
||||
for (lamports, expected_result) in [
|
||||
(rent_exempt_reserve, Ok(())),
|
||||
(
|
||||
minimum_delegation - 1,
|
||||
rent_exempt_reserve - 1,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
] {
|
||||
let stake_account = AccountSharedData::new(
|
||||
stake_delegation + rent_exempt_reserve,
|
||||
StakeState::size_of(),
|
||||
&id(),
|
||||
);
|
||||
let stake_account = AccountSharedData::new(lamports, StakeState::size_of(), &id());
|
||||
process_instruction(
|
||||
&instruction_data,
|
||||
vec![
|
||||
|
@ -3731,7 +3713,7 @@ mod tests {
|
|||
|
||||
/// Ensure that `delegate()` respects the minimum delegation requirements
|
||||
/// - Assert 1: delegating an amount equal-to the minimum delegates OK
|
||||
/// - Assert 2: delegating an amount less-than the minimum delegates OK
|
||||
/// - Assert 2: delegating an amount less-than the minimum fails
|
||||
/// Also test both asserts above over both StakeState::{Initialized and Stake}, since the logic
|
||||
/// is slightly different for the variants.
|
||||
///
|
||||
|
@ -3783,7 +3765,7 @@ mod tests {
|
|||
];
|
||||
for (stake_delegation, expected_result) in [
|
||||
(minimum_delegation, Ok(())),
|
||||
(minimum_delegation - 1, Ok(())),
|
||||
(minimum_delegation - 1, Err(StakeError::InsufficientStake)),
|
||||
] {
|
||||
for stake_state in &[
|
||||
StakeState::Initialized(meta),
|
||||
|
@ -3815,7 +3797,7 @@ mod tests {
|
|||
),
|
||||
],
|
||||
instruction_accounts.clone(),
|
||||
expected_result.clone(),
|
||||
expected_result.clone().map_err(|e| e.into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3862,32 +3844,38 @@ mod tests {
|
|||
is_writable: false,
|
||||
},
|
||||
];
|
||||
for (source_stake_delegation, dest_stake_delegation, expected_result) in [
|
||||
(minimum_delegation, minimum_delegation, Ok(())),
|
||||
for (source_reserve, dest_reserve, expected_result) in [
|
||||
(rent_exempt_reserve, rent_exempt_reserve, Ok(())),
|
||||
(
|
||||
minimum_delegation,
|
||||
minimum_delegation - 1,
|
||||
rent_exempt_reserve,
|
||||
rent_exempt_reserve - 1,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
(
|
||||
minimum_delegation - 1,
|
||||
minimum_delegation,
|
||||
rent_exempt_reserve - 1,
|
||||
rent_exempt_reserve,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
(
|
||||
minimum_delegation - 1,
|
||||
minimum_delegation - 1,
|
||||
rent_exempt_reserve - 1,
|
||||
rent_exempt_reserve - 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 mut source_starting_balance = source_reserve + dest_reserve;
|
||||
for (delegation, source_stake_state) in &[
|
||||
(0, StakeState::Initialized(source_meta)),
|
||||
(
|
||||
minimum_delegation,
|
||||
just_stake(
|
||||
source_meta,
|
||||
minimum_delegation * 2 + source_starting_balance - rent_exempt_reserve,
|
||||
),
|
||||
),
|
||||
] {
|
||||
source_starting_balance += delegation * 2;
|
||||
let source_account = AccountSharedData::new_data_with_space(
|
||||
source_starting_balance,
|
||||
source_stake_state,
|
||||
|
@ -3896,10 +3884,7 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
process_instruction(
|
||||
&serialize(&StakeInstruction::Split(
|
||||
dest_stake_delegation + rent_exempt_reserve,
|
||||
))
|
||||
.unwrap(),
|
||||
&serialize(&StakeInstruction::Split(dest_reserve + delegation)).unwrap(),
|
||||
vec![
|
||||
(source_address, source_account),
|
||||
(dest_address, dest_account.clone()),
|
||||
|
@ -3953,19 +3938,22 @@ mod tests {
|
|||
is_writable: false,
|
||||
},
|
||||
];
|
||||
for (stake_delegation, expected_result) in [
|
||||
(minimum_delegation, Ok(())),
|
||||
for (reserve, expected_result) in [
|
||||
(rent_exempt_reserve, Ok(())),
|
||||
(
|
||||
minimum_delegation - 1,
|
||||
rent_exempt_reserve - 1,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
] {
|
||||
for source_stake_state in &[
|
||||
StakeState::Initialized(source_meta),
|
||||
just_stake(source_meta, stake_delegation),
|
||||
for (stake_delegation, source_stake_state) in &[
|
||||
(0, StakeState::Initialized(source_meta)),
|
||||
(
|
||||
minimum_delegation,
|
||||
just_stake(source_meta, minimum_delegation),
|
||||
),
|
||||
] {
|
||||
let source_account = AccountSharedData::new_data_with_space(
|
||||
stake_delegation + rent_exempt_reserve,
|
||||
stake_delegation + reserve,
|
||||
source_stake_state,
|
||||
StakeState::size_of(),
|
||||
&id(),
|
||||
|
@ -3988,12 +3976,11 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ensure that `split()` correctly handles prefunded destination accounts. When a destination
|
||||
/// account already has funds, ensure the minimum split amount reduces accordingly.
|
||||
/// Ensure that `split()` correctly handles prefunded destination accounts from
|
||||
/// initialized stakes. When a destination account already has funds, ensure
|
||||
/// the minimum split amount reduces accordingly.
|
||||
#[test]
|
||||
fn test_split_destination_minimum_stake_delegation() {
|
||||
let feature_set = FeatureSet::all_enabled();
|
||||
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
|
||||
fn test_initialized_split_destination_minimum_balance() {
|
||||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of());
|
||||
let source_address = Pubkey::new_unique();
|
||||
|
@ -4014,6 +4001,111 @@ mod tests {
|
|||
is_writable: false,
|
||||
},
|
||||
];
|
||||
let source_balance = u64::MAX;
|
||||
let source_stake_state = StakeState::Initialized(source_meta);
|
||||
for (destination_starting_balance, split_amount, expected_result) in [
|
||||
// split amount must be non zero
|
||||
(
|
||||
rent_exempt_reserve,
|
||||
0,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
// any split amount is OK when destination account is already fully funded
|
||||
(rent_exempt_reserve, 1, Ok(())),
|
||||
// if destination is only short by 1 lamport, then split amount can be 1 lamport
|
||||
(rent_exempt_reserve - 1, 1, Ok(())),
|
||||
// destination short by 2 lamports, so 1 isn't enough (non-zero split amount)
|
||||
(
|
||||
rent_exempt_reserve - 2,
|
||||
1,
|
||||
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 - 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 - 2,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
// destination has zero lamports, so split must be at least rent exempt reserve plus
|
||||
// minimum delegation
|
||||
(0, rent_exempt_reserve, Ok(())),
|
||||
// destination has zero lamports, but split amount is less than rent exempt reserve
|
||||
// plus minimum delegation
|
||||
(
|
||||
0,
|
||||
rent_exempt_reserve - 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_account = AccountSharedData::new_data_with_space(
|
||||
source_balance,
|
||||
&source_stake_state,
|
||||
StakeState::size_of(),
|
||||
&id(),
|
||||
)
|
||||
.unwrap();
|
||||
let dest_account = AccountSharedData::new_data_with_space(
|
||||
destination_starting_balance,
|
||||
&StakeState::Uninitialized,
|
||||
StakeState::size_of(),
|
||||
&id(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure that `split()` correctly handles prefunded destination accounts from
|
||||
/// delegated stakes. When a destination account already has funds, ensure
|
||||
/// the minimum split amount reduces accordingly.
|
||||
#[test]
|
||||
fn test_stake_split_destination_minimum_delegation() {
|
||||
let feature_set = FeatureSet::all_enabled();
|
||||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of());
|
||||
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,
|
||||
},
|
||||
];
|
||||
// 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;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
|
||||
let source_stake_state = just_stake(source_meta, source_stake_delegation);
|
||||
for (destination_starting_balance, split_amount, expected_result) in [
|
||||
// split amount must be non zero
|
||||
(
|
||||
|
@ -4036,7 +4128,7 @@ mod tests {
|
|||
// destination is rent exempt, but split amount less than minimum delegation
|
||||
(
|
||||
rent_exempt_reserve,
|
||||
minimum_delegation - 1,
|
||||
minimum_delegation.saturating_sub(1), // when minimum is 0, this blows up!
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
// destination is not rent exempt, so split enough for rent and minimum delegation
|
||||
|
@ -4068,65 +4160,54 @@ mod tests {
|
|||
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,
|
||||
StakeState::size_of(),
|
||||
&id(),
|
||||
)
|
||||
.unwrap();
|
||||
let dest_account = AccountSharedData::new_data_with_space(
|
||||
destination_starting_balance,
|
||||
&StakeState::Uninitialized,
|
||||
StakeState::size_of(),
|
||||
&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_delegation,);
|
||||
} else {
|
||||
panic!("destination state must be StakeStake::Stake after successful split when source is also StakeState::Stake!");
|
||||
}
|
||||
}
|
||||
let source_account = AccountSharedData::new_data_with_space(
|
||||
source_balance,
|
||||
&source_stake_state,
|
||||
StakeState::size_of(),
|
||||
&id(),
|
||||
)
|
||||
.unwrap();
|
||||
let dest_account = AccountSharedData::new_data_with_space(
|
||||
destination_starting_balance,
|
||||
&StakeState::Uninitialized,
|
||||
StakeState::size_of(),
|
||||
&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() {
|
||||
assert_matches!(accounts[0].state().unwrap(), StakeState::Stake(_, _));
|
||||
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_delegation,);
|
||||
} else {
|
||||
panic!("destination state must be StakeStake::Stake after successful split when source is also StakeState::Stake!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4182,13 +4263,13 @@ mod tests {
|
|||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
] {
|
||||
for stake_state in &[
|
||||
StakeState::Initialized(meta),
|
||||
just_stake(meta, starting_stake_delegation),
|
||||
for (stake_delegation, stake_state) in &[
|
||||
(0, StakeState::Initialized(meta)),
|
||||
(minimum_delegation, just_stake(meta, minimum_delegation)),
|
||||
] {
|
||||
let rewards_balance = 123;
|
||||
let stake_account = AccountSharedData::new_data_with_space(
|
||||
starting_stake_delegation + rent_exempt_reserve + rewards_balance,
|
||||
stake_delegation + rent_exempt_reserve + rewards_balance,
|
||||
stake_state,
|
||||
StakeState::size_of(),
|
||||
&id(),
|
||||
|
@ -4616,8 +4697,6 @@ mod tests {
|
|||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of());
|
||||
let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled());
|
||||
let minimum_balance = rent_exempt_reserve + minimum_delegation;
|
||||
let stake_lamports = minimum_balance * 2;
|
||||
let stake_address = solana_sdk::pubkey::new_rand();
|
||||
let split_to_address = solana_sdk::pubkey::new_rand();
|
||||
let split_to_account = AccountSharedData::new_data_with_space(
|
||||
|
@ -4646,10 +4725,14 @@ mod tests {
|
|||
};
|
||||
|
||||
// test splitting both an Initialized stake and a Staked stake
|
||||
for state in &[
|
||||
StakeState::Initialized(meta),
|
||||
just_stake(meta, stake_lamports - rent_exempt_reserve),
|
||||
for (minimum_balance, state) in &[
|
||||
(rent_exempt_reserve, StakeState::Initialized(meta)),
|
||||
(
|
||||
rent_exempt_reserve + minimum_delegation,
|
||||
just_stake(meta, minimum_delegation * 2 + rent_exempt_reserve),
|
||||
),
|
||||
] {
|
||||
let stake_lamports = minimum_balance * 2;
|
||||
let stake_account = AccountSharedData::new_data_with_space(
|
||||
stake_lamports,
|
||||
state,
|
||||
|
@ -4668,7 +4751,7 @@ mod tests {
|
|||
|
||||
// not enough to make a non-zero stake account
|
||||
process_instruction(
|
||||
&serialize(&StakeInstruction::Split(rent_exempt_reserve)).unwrap(),
|
||||
&serialize(&StakeInstruction::Split(minimum_balance - 1)).unwrap(),
|
||||
transaction_accounts.clone(),
|
||||
instruction_accounts.clone(),
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
|
@ -4677,7 +4760,7 @@ mod tests {
|
|||
// doesn't leave enough for initial stake to be non-zero
|
||||
process_instruction(
|
||||
&serialize(&StakeInstruction::Split(
|
||||
stake_lamports - rent_exempt_reserve,
|
||||
stake_lamports - minimum_balance + 1,
|
||||
))
|
||||
.unwrap(),
|
||||
transaction_accounts.clone(),
|
||||
|
@ -4686,7 +4769,7 @@ mod tests {
|
|||
);
|
||||
|
||||
// split account already has way enough lamports
|
||||
transaction_accounts[1].1.set_lamports(minimum_balance);
|
||||
transaction_accounts[1].1.set_lamports(*minimum_balance);
|
||||
let accounts = process_instruction(
|
||||
&serialize(&StakeInstruction::Split(stake_lamports - minimum_balance)).unwrap(),
|
||||
transaction_accounts,
|
||||
|
@ -4709,7 +4792,7 @@ mod tests {
|
|||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(accounts[0].lamports(), minimum_balance,);
|
||||
assert_eq!(accounts[0].lamports(), *minimum_balance,);
|
||||
assert_eq!(accounts[1].lamports(), stake_lamports,);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,8 @@ use {
|
|||
account_utils::StateMut,
|
||||
clock::{Clock, Epoch},
|
||||
feature_set::{
|
||||
stake_merge_with_unmatched_credits_observed, stake_split_uses_rent_sysvar, FeatureSet,
|
||||
stake_allow_zero_undelegated_amount, stake_merge_with_unmatched_credits_observed,
|
||||
stake_split_uses_rent_sysvar, FeatureSet,
|
||||
},
|
||||
instruction::{checked_add, InstructionError},
|
||||
pubkey::Pubkey,
|
||||
|
@ -453,8 +454,13 @@ pub fn initialize(
|
|||
}
|
||||
if let StakeState::Uninitialized = stake_account.get_state()? {
|
||||
let rent_exempt_reserve = rent.minimum_balance(stake_account.get_data().len());
|
||||
let minimum_delegation = crate::get_minimum_delegation(feature_set);
|
||||
let minimum_balance = rent_exempt_reserve + minimum_delegation;
|
||||
// when removing this feature, remove `minimum_balance` and just use `rent_exempt_reserve`
|
||||
let minimum_balance = if feature_set.is_active(&stake_allow_zero_undelegated_amount::id()) {
|
||||
rent_exempt_reserve
|
||||
} else {
|
||||
let minimum_delegation = crate::get_minimum_delegation(feature_set);
|
||||
rent_exempt_reserve + minimum_delegation
|
||||
};
|
||||
|
||||
if stake_account.get_lamports() >= minimum_balance {
|
||||
stake_account.set_state(&StakeState::Initialized(Meta {
|
||||
|
@ -558,6 +564,7 @@ pub fn delegate(
|
|||
stake_history: &StakeHistory,
|
||||
config: &Config,
|
||||
signers: &HashSet<Pubkey>,
|
||||
feature_set: &FeatureSet,
|
||||
) -> Result<(), InstructionError> {
|
||||
let vote_account =
|
||||
instruction_context.try_borrow_account(transaction_context, vote_account_index)?;
|
||||
|
@ -574,7 +581,7 @@ pub fn delegate(
|
|||
StakeState::Initialized(meta) => {
|
||||
meta.authorized.check(signers, StakeAuthorize::Staker)?;
|
||||
let ValidatedDelegatedInfo { stake_amount } =
|
||||
validate_delegated_amount(&stake_account, &meta)?;
|
||||
validate_delegated_amount(&stake_account, &meta, feature_set)?;
|
||||
let stake = new_stake(
|
||||
stake_amount,
|
||||
&vote_pubkey,
|
||||
|
@ -587,7 +594,7 @@ pub fn delegate(
|
|||
StakeState::Stake(meta, mut stake) => {
|
||||
meta.authorized.check(signers, StakeAuthorize::Staker)?;
|
||||
let ValidatedDelegatedInfo { stake_amount } =
|
||||
validate_delegated_amount(&stake_account, &meta)?;
|
||||
validate_delegated_amount(&stake_account, &meta, feature_set)?;
|
||||
redelegate(
|
||||
&mut stake,
|
||||
stake_amount,
|
||||
|
@ -669,6 +676,7 @@ pub fn split(
|
|||
match stake_state {
|
||||
StakeState::Stake(meta, mut stake) => {
|
||||
meta.authorized.check(signers, StakeAuthorize::Staker)?;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
||||
let validated_split_info = validate_split_amount(
|
||||
invoke_context,
|
||||
transaction_context,
|
||||
|
@ -678,6 +686,7 @@ pub fn split(
|
|||
lamports,
|
||||
&meta,
|
||||
Some(&stake),
|
||||
minimum_delegation,
|
||||
)?;
|
||||
|
||||
// split the stake, subtract rent_exempt_balance unless
|
||||
|
@ -725,6 +734,14 @@ pub fn split(
|
|||
}
|
||||
StakeState::Initialized(meta) => {
|
||||
meta.authorized.check(signers, StakeAuthorize::Staker)?;
|
||||
let additional_required_lamports = if invoke_context
|
||||
.feature_set
|
||||
.is_active(&stake_allow_zero_undelegated_amount::id())
|
||||
{
|
||||
0
|
||||
} else {
|
||||
crate::get_minimum_delegation(&invoke_context.feature_set)
|
||||
};
|
||||
let validated_split_info = validate_split_amount(
|
||||
invoke_context,
|
||||
transaction_context,
|
||||
|
@ -734,6 +751,7 @@ pub fn split(
|
|||
lamports,
|
||||
&meta,
|
||||
None,
|
||||
additional_required_lamports,
|
||||
)?;
|
||||
let mut split_meta = meta;
|
||||
split_meta.rent_exempt_reserve = validated_split_info.destination_rent_exempt_reserve;
|
||||
|
@ -877,11 +895,15 @@ pub fn withdraw(
|
|||
StakeState::Initialized(meta) => {
|
||||
meta.authorized
|
||||
.check(&signers, StakeAuthorize::Withdrawer)?;
|
||||
// stake accounts must have a balance >= rent_exempt_reserve + minimum_stake_delegation
|
||||
let reserve = checked_add(
|
||||
meta.rent_exempt_reserve,
|
||||
crate::get_minimum_delegation(feature_set),
|
||||
)?;
|
||||
// stake accounts must have a balance >= rent_exempt_reserve
|
||||
let reserve = if feature_set.is_active(&stake_allow_zero_undelegated_amount::id()) {
|
||||
meta.rent_exempt_reserve
|
||||
} else {
|
||||
checked_add(
|
||||
meta.rent_exempt_reserve,
|
||||
crate::get_minimum_delegation(feature_set),
|
||||
)?
|
||||
};
|
||||
|
||||
(meta.lockup, reserve, false)
|
||||
}
|
||||
|
@ -1000,10 +1022,22 @@ struct ValidatedDelegatedInfo {
|
|||
fn validate_delegated_amount(
|
||||
account: &BorrowedAccount,
|
||||
meta: &Meta,
|
||||
feature_set: &FeatureSet,
|
||||
) -> Result<ValidatedDelegatedInfo, InstructionError> {
|
||||
let stake_amount = account
|
||||
.get_lamports()
|
||||
.saturating_sub(meta.rent_exempt_reserve); // can't stake the rent
|
||||
|
||||
// Previously, `initialize` checked that the stake account balance met
|
||||
// the minimum delegation amount.
|
||||
// With the `stake_allow_zero_undelegated_amount` feature, stake accounts
|
||||
// may be initialized with a lower balance, so check the minimum in this
|
||||
// function, on delegation.
|
||||
if feature_set.is_active(&stake_allow_zero_undelegated_amount::id())
|
||||
&& stake_amount < crate::get_minimum_delegation(feature_set)
|
||||
{
|
||||
return Err(StakeError::InsufficientStake.into());
|
||||
}
|
||||
Ok(ValidatedDelegatedInfo { stake_amount })
|
||||
}
|
||||
|
||||
|
@ -1028,6 +1062,7 @@ fn validate_split_amount(
|
|||
lamports: u64,
|
||||
source_meta: &Meta,
|
||||
source_stake: Option<&Stake>,
|
||||
additional_required_lamports: u64,
|
||||
) -> Result<ValidatedSplitInfo, InstructionError> {
|
||||
let source_account =
|
||||
instruction_context.try_borrow_account(transaction_context, source_account_index)?;
|
||||
|
@ -1054,10 +1089,9 @@ fn validate_split_amount(
|
|||
// EITHER at least the minimum balance, OR zero (in this case the source
|
||||
// account is transferring all lamports to new destination account, and the source
|
||||
// account will be closed)
|
||||
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
||||
let source_minimum_balance = source_meta
|
||||
.rent_exempt_reserve
|
||||
.saturating_add(minimum_delegation);
|
||||
.saturating_add(additional_required_lamports);
|
||||
let source_remaining_balance = source_lamports.saturating_sub(lamports);
|
||||
if source_remaining_balance == 0 {
|
||||
// full amount is a withdrawal
|
||||
|
@ -1088,7 +1122,7 @@ fn validate_split_amount(
|
|||
)
|
||||
};
|
||||
let destination_minimum_balance =
|
||||
destination_rent_exempt_reserve.saturating_add(minimum_delegation);
|
||||
destination_rent_exempt_reserve.saturating_add(additional_required_lamports);
|
||||
let destination_balance_deficit =
|
||||
destination_minimum_balance.saturating_sub(destination_lamports);
|
||||
if lamports < destination_balance_deficit {
|
||||
|
@ -1105,7 +1139,7 @@ fn validate_split_amount(
|
|||
// account, the split amount must be at least the minimum stake delegation. So if the minimum
|
||||
// stake delegation was 10 lamports, then a split amount of 1 lamport would not meet the
|
||||
// *delegation* requirements.
|
||||
if source_stake.is_some() && lamports < minimum_delegation {
|
||||
if source_stake.is_some() && lamports < additional_required_lamports {
|
||||
return Err(InstructionError::InsufficientFunds);
|
||||
}
|
||||
|
||||
|
|
|
@ -592,8 +592,8 @@ mod tests {
|
|||
bank_forks::BankForks,
|
||||
commitment::{BlockCommitmentCache, CommitmentSlots},
|
||||
genesis_utils::{
|
||||
create_genesis_config, create_genesis_config_with_vote_accounts, GenesisConfigInfo,
|
||||
ValidatorVoteKeypairs,
|
||||
activate_all_features, create_genesis_config,
|
||||
create_genesis_config_with_vote_accounts, GenesisConfigInfo, ValidatorVoteKeypairs,
|
||||
},
|
||||
vote_transaction::VoteTransaction,
|
||||
},
|
||||
|
@ -604,6 +604,7 @@ mod tests {
|
|||
hash::Hash,
|
||||
message::Message,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
signature::{Keypair, Signer},
|
||||
stake::{
|
||||
self, instruction as stake_instruction,
|
||||
|
@ -825,10 +826,12 @@ mod tests {
|
|||
#[serial]
|
||||
fn test_account_subscribe() {
|
||||
let GenesisConfigInfo {
|
||||
genesis_config,
|
||||
mut genesis_config,
|
||||
mint_keypair: alice,
|
||||
..
|
||||
} = create_genesis_config(10_000_000_000);
|
||||
genesis_config.rent = Rent::default();
|
||||
activate_all_features(&mut genesis_config);
|
||||
|
||||
let new_stake_authority = solana_sdk::pubkey::new_rand();
|
||||
let stake_authority = Keypair::new();
|
||||
|
@ -875,10 +878,7 @@ mod tests {
|
|||
let balance = {
|
||||
let bank = bank_forks.read().unwrap().working_bank();
|
||||
let rent = &bank.rent_collector().rent;
|
||||
let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of());
|
||||
let minimum_delegation =
|
||||
solana_stake_program::get_minimum_delegation(&bank.feature_set);
|
||||
rent_exempt_reserve + minimum_delegation
|
||||
rent.minimum_balance(StakeState::size_of())
|
||||
};
|
||||
|
||||
let tx = system_transaction::transfer(&alice, &from.pubkey(), balance, blockhash);
|
||||
|
@ -928,7 +928,13 @@ mod tests {
|
|||
serde_json::from_str::<serde_json::Value>(&response).unwrap(),
|
||||
);
|
||||
|
||||
let tx = system_transaction::transfer(&alice, &stake_authority.pubkey(), 1, blockhash);
|
||||
let balance = {
|
||||
let bank = bank_forks.read().unwrap().working_bank();
|
||||
let rent = &bank.rent_collector().rent;
|
||||
rent.minimum_balance(0)
|
||||
};
|
||||
let tx =
|
||||
system_transaction::transfer(&alice, &stake_authority.pubkey(), balance, blockhash);
|
||||
process_transaction_and_notify(&bank_forks, &tx, &rpc_subscriptions, 1).unwrap();
|
||||
sleep(Duration::from_millis(200));
|
||||
let ix = stake_instruction::authorize(
|
||||
|
|
|
@ -379,6 +379,10 @@ pub mod default_units_per_instruction {
|
|||
solana_sdk::declare_id!("J2QdYx8crLbTVK8nur1jeLsmc3krDbfjoxoea2V1Uy5Q");
|
||||
}
|
||||
|
||||
pub mod stake_allow_zero_undelegated_amount {
|
||||
solana_sdk::declare_id!("sTKz343FM8mqtyGvYWvbLpTThw3ixRM4Xk8QvZ985mw");
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
/// Map of feature identifiers to user-visible description
|
||||
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
|
||||
|
@ -468,6 +472,7 @@ lazy_static! {
|
|||
(spl_token_v3_4_0::id(), "SPL Token Program version 3.4.0 release #24740"),
|
||||
(spl_associated_token_account_v1_1_0::id(), "SPL Associated Token Account Program version 1.1.0 release #24741"),
|
||||
(default_units_per_instruction::id(), "Default max tx-wide compute units calculated per instruction"),
|
||||
(stake_allow_zero_undelegated_amount::id(), "Allow zero-lamport undelegated amount for initialized stakes #24670")
|
||||
/*************** ADD NEW FEATURES HERE ***************/
|
||||
]
|
||||
.iter()
|
||||
|
|
Loading…
Reference in New Issue