Add GetMinimumDelegation stake program instruction (#24020)
This commit is contained in:
parent
792bbf75ab
commit
2af6753808
|
@ -1,11 +1,11 @@
|
|||
#![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(min_specialization))]
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
use solana_sdk::genesis_config::GenesisConfig;
|
||||
#[deprecated(
|
||||
since = "1.8.0",
|
||||
note = "Please use `solana_sdk::stake::program::id` or `solana_program::stake::program::id` instead"
|
||||
)]
|
||||
pub use solana_sdk::stake::program::{check_id, id};
|
||||
use solana_sdk::{feature_set::FeatureSet, genesis_config::GenesisConfig};
|
||||
|
||||
pub mod config;
|
||||
pub mod stake_instruction;
|
||||
|
@ -14,3 +14,15 @@ pub mod stake_state;
|
|||
pub fn add_genesis_accounts(genesis_config: &mut GenesisConfig) -> u64 {
|
||||
config::add_genesis_account(genesis_config)
|
||||
}
|
||||
|
||||
/// The minimum stake amount that can be delegated, in lamports.
|
||||
/// NOTE: This is also used to calculate the minimum balance of a stake account, which is the
|
||||
/// rent exempt reserve _plus_ the minimum stake delegation.
|
||||
#[inline(always)]
|
||||
pub(crate) fn get_minimum_delegation(_feature_set: &FeatureSet) -> u64 {
|
||||
// If/when the minimum delegation amount is changed, the `feature_set` parameter will be used
|
||||
// to chose the correct value. And since the MINIMUM_STAKE_DELEGATION constant cannot be
|
||||
// removed, use it here as to not duplicate magic constants.
|
||||
#[allow(deprecated)]
|
||||
solana_sdk::stake::MINIMUM_STAKE_DELEGATION
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ pub fn process_instruction(
|
|||
match limited_deserialize(data)? {
|
||||
StakeInstruction::Initialize(authorized, lockup) => {
|
||||
let rent = get_sysvar_with_account_check::rent(invoke_context, instruction_context, 1)?;
|
||||
me.initialize(&authorized, &lockup, &rent)
|
||||
me.initialize(&authorized, &lockup, &rent, &invoke_context.feature_set)
|
||||
}
|
||||
StakeInstruction::Authorize(authorized_pubkey, stake_authorize) => {
|
||||
instruction_context.check_number_of_instruction_accounts(3)?;
|
||||
|
@ -183,6 +183,7 @@ pub fn process_instruction(
|
|||
&stake_history,
|
||||
keyed_account_at_index(keyed_accounts, first_instruction_account + 4)?,
|
||||
keyed_account_at_index(keyed_accounts, first_instruction_account + 5).ok(),
|
||||
&invoke_context.feature_set,
|
||||
)
|
||||
}
|
||||
StakeInstruction::Deactivate => {
|
||||
|
@ -213,7 +214,12 @@ pub fn process_instruction(
|
|||
|
||||
let rent =
|
||||
get_sysvar_with_account_check::rent(invoke_context, instruction_context, 1)?;
|
||||
me.initialize(&authorized, &Lockup::default(), &rent)
|
||||
me.initialize(
|
||||
&authorized,
|
||||
&Lockup::default(),
|
||||
&rent,
|
||||
&invoke_context.feature_set,
|
||||
)
|
||||
} else {
|
||||
Err(InstructionError::InvalidInstructionData)
|
||||
}
|
||||
|
@ -311,6 +317,20 @@ pub fn process_instruction(
|
|||
Err(InstructionError::InvalidInstructionData)
|
||||
}
|
||||
}
|
||||
StakeInstruction::GetMinimumDelegation => {
|
||||
let feature_set = invoke_context.feature_set.as_ref();
|
||||
if !feature_set.is_active(
|
||||
&feature_set::add_get_minimum_delegation_instruction_to_stake_program::id(),
|
||||
) {
|
||||
return Err(InstructionError::InvalidInstructionData);
|
||||
}
|
||||
|
||||
let minimum_delegation = crate::get_minimum_delegation(feature_set);
|
||||
let minimum_delegation = Vec::from(minimum_delegation.to_le_bytes());
|
||||
invoke_context
|
||||
.transaction_context
|
||||
.set_return_data(id(), minimum_delegation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -330,6 +350,7 @@ mod tests {
|
|||
account::{self, AccountSharedData, ReadableAccount, WritableAccount},
|
||||
account_utils::StateMut,
|
||||
clock::{Epoch, UnixTimestamp},
|
||||
feature_set::FeatureSet,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
|
@ -337,7 +358,6 @@ mod tests {
|
|||
config as stake_config,
|
||||
instruction::{self, LockupArgs},
|
||||
state::{Authorized, Lockup, StakeAuthorize},
|
||||
MINIMUM_STAKE_DELEGATION,
|
||||
},
|
||||
system_program,
|
||||
sysvar::{self, stake_history::StakeHistory},
|
||||
|
@ -665,7 +685,8 @@ mod tests {
|
|||
let config_address = stake_config::id();
|
||||
let config_account = config::create_account(0, &stake_config::Config::default());
|
||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let withdrawal_amount = rent_exempt_reserve + MINIMUM_STAKE_DELEGATION;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled());
|
||||
let withdrawal_amount = rent_exempt_reserve + minimum_delegation;
|
||||
|
||||
// gets the "is_empty()" check
|
||||
process_instruction(
|
||||
|
@ -880,6 +901,7 @@ mod tests {
|
|||
let rent_address = sysvar::rent::id();
|
||||
let rent_account = account::create_account_shared_data_for_test(&rent);
|
||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled());
|
||||
|
||||
// Test InitializeChecked with non-signing withdrawer
|
||||
let mut instruction =
|
||||
|
@ -892,7 +914,7 @@ mod tests {
|
|||
|
||||
// Test InitializeChecked with withdrawer signer
|
||||
let stake_account = AccountSharedData::new(
|
||||
rent_exempt_reserve + MINIMUM_STAKE_DELEGATION,
|
||||
rent_exempt_reserve + minimum_delegation,
|
||||
std::mem::size_of::<crate::stake_state::StakeState>(),
|
||||
&id(),
|
||||
);
|
||||
|
@ -1214,7 +1236,8 @@ mod tests {
|
|||
fn test_stake_initialize() {
|
||||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let stake_lamports = rent_exempt_reserve + MINIMUM_STAKE_DELEGATION;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled());
|
||||
let stake_lamports = rent_exempt_reserve + minimum_delegation;
|
||||
let stake_address = solana_sdk::pubkey::new_rand();
|
||||
let stake_account =
|
||||
AccountSharedData::new(stake_lamports, std::mem::size_of::<StakeState>(), &id());
|
||||
|
@ -2345,7 +2368,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_split() {
|
||||
let stake_address = solana_sdk::pubkey::new_rand();
|
||||
let stake_lamports = MINIMUM_STAKE_DELEGATION * 2;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled());
|
||||
let stake_lamports = minimum_delegation * 2;
|
||||
let split_to_address = solana_sdk::pubkey::new_rand();
|
||||
let split_to_account = AccountSharedData::new_data_with_space(
|
||||
0,
|
||||
|
@ -2451,7 +2475,8 @@ mod tests {
|
|||
let authority_address = solana_sdk::pubkey::new_rand();
|
||||
let custodian_address = solana_sdk::pubkey::new_rand();
|
||||
let stake_address = solana_sdk::pubkey::new_rand();
|
||||
let stake_lamports = MINIMUM_STAKE_DELEGATION;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled());
|
||||
let stake_lamports = minimum_delegation;
|
||||
let stake_account = AccountSharedData::new_data_with_space(
|
||||
stake_lamports,
|
||||
&StakeState::Uninitialized,
|
||||
|
@ -2979,7 +3004,8 @@ mod tests {
|
|||
let stake_address = solana_sdk::pubkey::new_rand();
|
||||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let stake_lamports = 7 * MINIMUM_STAKE_DELEGATION;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled());
|
||||
let stake_lamports = 7 * minimum_delegation;
|
||||
let stake_account = AccountSharedData::new_data_with_space(
|
||||
stake_lamports + rent_exempt_reserve,
|
||||
&StakeState::Initialized(Meta {
|
||||
|
@ -3034,7 +3060,7 @@ mod tests {
|
|||
// should pass, withdrawing account down to minimum balance
|
||||
process_instruction(
|
||||
&serialize(&StakeInstruction::Withdraw(
|
||||
stake_lamports - MINIMUM_STAKE_DELEGATION,
|
||||
stake_lamports - minimum_delegation,
|
||||
))
|
||||
.unwrap(),
|
||||
transaction_accounts.clone(),
|
||||
|
@ -3053,7 +3079,7 @@ mod tests {
|
|||
// should fail, withdrawal that would leave less than rent-exempt reserve
|
||||
process_instruction(
|
||||
&serialize(&StakeInstruction::Withdraw(
|
||||
stake_lamports + MINIMUM_STAKE_DELEGATION,
|
||||
stake_lamports + minimum_delegation,
|
||||
))
|
||||
.unwrap(),
|
||||
transaction_accounts.clone(),
|
||||
|
@ -3195,7 +3221,8 @@ mod tests {
|
|||
let custodian_address = solana_sdk::pubkey::new_rand();
|
||||
let authorized_address = solana_sdk::pubkey::new_rand();
|
||||
let stake_address = solana_sdk::pubkey::new_rand();
|
||||
let stake_lamports = MINIMUM_STAKE_DELEGATION;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled());
|
||||
let stake_lamports = minimum_delegation;
|
||||
let stake_account = AccountSharedData::new_data_with_space(
|
||||
stake_lamports,
|
||||
&StakeState::Uninitialized,
|
||||
|
@ -3458,11 +3485,13 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// Ensure that `initialize()` respects the MINIMUM_STAKE_DELEGATION requirements
|
||||
/// 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
|
||||
#[test]
|
||||
fn test_initialize_minimum_stake_delegation() {
|
||||
let feature_set = FeatureSet::all_enabled();
|
||||
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
|
||||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let stake_address = solana_sdk::pubkey::new_rand();
|
||||
|
@ -3484,9 +3513,9 @@ mod tests {
|
|||
},
|
||||
];
|
||||
for (stake_delegation, expected_result) in [
|
||||
(MINIMUM_STAKE_DELEGATION, Ok(())),
|
||||
(minimum_delegation, Ok(())),
|
||||
(
|
||||
MINIMUM_STAKE_DELEGATION - 1,
|
||||
minimum_delegation - 1,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
] {
|
||||
|
@ -3510,19 +3539,21 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ensure that `delegate()` respects the MINIMUM_STAKE_DELEGATION requirements
|
||||
/// 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
|
||||
/// 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
|
||||
/// the minimum 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 feature_set = FeatureSet::all_enabled();
|
||||
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
|
||||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let stake_address = solana_sdk::pubkey::new_rand();
|
||||
|
@ -3561,8 +3592,8 @@ mod tests {
|
|||
},
|
||||
];
|
||||
for (stake_delegation, expected_result) in [
|
||||
(MINIMUM_STAKE_DELEGATION, Ok(())),
|
||||
(MINIMUM_STAKE_DELEGATION - 1, Ok(())),
|
||||
(minimum_delegation, Ok(())),
|
||||
(minimum_delegation - 1, Ok(())),
|
||||
] {
|
||||
for stake_state in &[
|
||||
StakeState::Initialized(meta),
|
||||
|
@ -3600,7 +3631,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ensure that `split()` respects the MINIMUM_STAKE_DELEGATION requirements. This applies to
|
||||
/// Ensure that `split()` respects the minimum 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:
|
||||
///
|
||||
|
@ -3612,6 +3643,8 @@ mod tests {
|
|||
/// LT | LT | Err
|
||||
#[test]
|
||||
fn test_split_minimum_stake_delegation() {
|
||||
let feature_set = FeatureSet::all_enabled();
|
||||
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
|
||||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let source_address = Pubkey::new_unique();
|
||||
|
@ -3640,20 +3673,20 @@ mod tests {
|
|||
},
|
||||
];
|
||||
for (source_stake_delegation, dest_stake_delegation, expected_result) in [
|
||||
(MINIMUM_STAKE_DELEGATION, MINIMUM_STAKE_DELEGATION, Ok(())),
|
||||
(minimum_delegation, minimum_delegation, Ok(())),
|
||||
(
|
||||
MINIMUM_STAKE_DELEGATION,
|
||||
MINIMUM_STAKE_DELEGATION - 1,
|
||||
minimum_delegation,
|
||||
minimum_delegation - 1,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
(
|
||||
MINIMUM_STAKE_DELEGATION - 1,
|
||||
MINIMUM_STAKE_DELEGATION,
|
||||
minimum_delegation - 1,
|
||||
minimum_delegation,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
(
|
||||
MINIMUM_STAKE_DELEGATION - 1,
|
||||
MINIMUM_STAKE_DELEGATION - 1,
|
||||
minimum_delegation - 1,
|
||||
minimum_delegation - 1,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
] {
|
||||
|
@ -3692,7 +3725,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ensure that splitting the full amount from an account respects the MINIMUM_STAKE_DELEGATION
|
||||
/// Ensure that splitting the full amount from an account respects the minimum 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
|
||||
|
@ -3701,6 +3734,8 @@ mod tests {
|
|||
/// delegation is not OK
|
||||
#[test]
|
||||
fn test_split_full_amount_minimum_stake_delegation() {
|
||||
let feature_set = FeatureSet::all_enabled();
|
||||
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
|
||||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let source_address = Pubkey::new_unique();
|
||||
|
@ -3729,9 +3764,9 @@ mod tests {
|
|||
},
|
||||
];
|
||||
for (stake_delegation, expected_result) in [
|
||||
(MINIMUM_STAKE_DELEGATION, Ok(())),
|
||||
(minimum_delegation, Ok(())),
|
||||
(
|
||||
MINIMUM_STAKE_DELEGATION - 1,
|
||||
minimum_delegation - 1,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
] {
|
||||
|
@ -3767,6 +3802,8 @@ mod tests {
|
|||
/// 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);
|
||||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let source_address = Pubkey::new_unique();
|
||||
|
@ -3790,66 +3827,54 @@ mod tests {
|
|||
for (destination_starting_balance, split_amount, expected_result) in [
|
||||
// split amount must be non zero
|
||||
(
|
||||
rent_exempt_reserve + MINIMUM_STAKE_DELEGATION,
|
||||
rent_exempt_reserve + minimum_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(())),
|
||||
(rent_exempt_reserve + minimum_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(()),
|
||||
),
|
||||
(rent_exempt_reserve + minimum_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,
|
||||
rent_exempt_reserve + minimum_delegation - 2,
|
||||
1,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
// destination is rent exempt, so split enough for minimum delegation
|
||||
(rent_exempt_reserve, MINIMUM_STAKE_DELEGATION, Ok(())),
|
||||
(rent_exempt_reserve, minimum_delegation, Ok(())),
|
||||
// destination is rent exempt, but split amount less than minimum delegation
|
||||
(
|
||||
rent_exempt_reserve,
|
||||
MINIMUM_STAKE_DELEGATION - 1,
|
||||
minimum_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(()),
|
||||
),
|
||||
(rent_exempt_reserve - 1, minimum_delegation + 1, Ok(())),
|
||||
// destination is not rent exempt, but split amount only for minimum delegation
|
||||
(
|
||||
rent_exempt_reserve - 1,
|
||||
MINIMUM_STAKE_DELEGATION,
|
||||
minimum_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(()),
|
||||
),
|
||||
(1, rent_exempt_reserve + minimum_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,
|
||||
rent_exempt_reserve + minimum_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(())),
|
||||
(0, rent_exempt_reserve + minimum_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,
|
||||
rent_exempt_reserve + minimum_delegation - 1,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
] {
|
||||
|
@ -3907,7 +3932,7 @@ mod tests {
|
|||
expected_destination_stake_delegation,
|
||||
destination_stake.delegation.stake
|
||||
);
|
||||
assert!(destination_stake.delegation.stake >= MINIMUM_STAKE_DELEGATION,);
|
||||
assert!(destination_stake.delegation.stake >= minimum_delegation,);
|
||||
} else {
|
||||
panic!("destination state must be StakeStake::Stake after successful split when source is also StakeState::Stake!");
|
||||
}
|
||||
|
@ -3917,11 +3942,13 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ensure that `withdraw()` respects the MINIMUM_STAKE_DELEGATION requirements
|
||||
/// Ensure that `withdraw()` respects the minimum 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 feature_set = FeatureSet::all_enabled();
|
||||
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
|
||||
let rent = Rent::default();
|
||||
let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let stake_address = solana_sdk::pubkey::new_rand();
|
||||
|
@ -3957,11 +3984,11 @@ mod tests {
|
|||
is_writable: false,
|
||||
},
|
||||
];
|
||||
let starting_stake_delegation = MINIMUM_STAKE_DELEGATION;
|
||||
let starting_stake_delegation = minimum_delegation;
|
||||
for (ending_stake_delegation, expected_result) in [
|
||||
(MINIMUM_STAKE_DELEGATION, Ok(())),
|
||||
(minimum_delegation, Ok(())),
|
||||
(
|
||||
MINIMUM_STAKE_DELEGATION - 1,
|
||||
minimum_delegation - 1,
|
||||
Err(InstructionError::InsufficientFunds),
|
||||
),
|
||||
] {
|
||||
|
@ -4023,11 +4050,13 @@ mod tests {
|
|||
/// 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 feature_set = FeatureSet::all_enabled();
|
||||
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
|
||||
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,
|
||||
rent_exempt_reserve + minimum_delegation,
|
||||
std::mem::size_of::<StakeState>(),
|
||||
&id(),
|
||||
);
|
||||
|
@ -4150,7 +4179,7 @@ mod tests {
|
|||
account::create_account_shared_data_for_test(&clock),
|
||||
);
|
||||
let withdraw_amount =
|
||||
accounts[0].lamports() - (rent_exempt_reserve + MINIMUM_STAKE_DELEGATION - 1);
|
||||
accounts[0].lamports() - (rent_exempt_reserve + minimum_delegation - 1);
|
||||
process_instruction(
|
||||
&serialize(&StakeInstruction::Withdraw(withdraw_amount)).unwrap(),
|
||||
transaction_accounts.clone(),
|
||||
|
|
|
@ -14,7 +14,9 @@ use {
|
|||
account::{AccountSharedData, ReadableAccount, WritableAccount},
|
||||
account_utils::{State, StateMut},
|
||||
clock::{Clock, Epoch},
|
||||
feature_set::{stake_merge_with_unmatched_credits_observed, stake_split_uses_rent_sysvar},
|
||||
feature_set::{
|
||||
stake_merge_with_unmatched_credits_observed, stake_split_uses_rent_sysvar, FeatureSet,
|
||||
},
|
||||
instruction::{checked_add, InstructionError},
|
||||
keyed_account::KeyedAccount,
|
||||
pubkey::Pubkey,
|
||||
|
@ -23,7 +25,6 @@ use {
|
|||
config::Config,
|
||||
instruction::{LockupArgs, StakeError},
|
||||
program::id,
|
||||
MINIMUM_STAKE_DELEGATION,
|
||||
},
|
||||
stake_history::{StakeHistory, StakeHistoryEntry},
|
||||
},
|
||||
|
@ -370,6 +371,7 @@ pub trait StakeAccount {
|
|||
authorized: &Authorized,
|
||||
lockup: &Lockup,
|
||||
rent: &Rent,
|
||||
feature_set: &FeatureSet,
|
||||
) -> Result<(), InstructionError>;
|
||||
fn authorize(
|
||||
&self,
|
||||
|
@ -429,6 +431,7 @@ pub trait StakeAccount {
|
|||
stake_history: &StakeHistory,
|
||||
withdraw_authority: &KeyedAccount,
|
||||
custodian: Option<&KeyedAccount>,
|
||||
feature_set: &FeatureSet,
|
||||
) -> Result<(), InstructionError>;
|
||||
}
|
||||
|
||||
|
@ -438,13 +441,15 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
|
|||
authorized: &Authorized,
|
||||
lockup: &Lockup,
|
||||
rent: &Rent,
|
||||
feature_set: &FeatureSet,
|
||||
) -> Result<(), InstructionError> {
|
||||
if self.data_len()? != std::mem::size_of::<StakeState>() {
|
||||
return Err(InstructionError::InvalidAccountData);
|
||||
}
|
||||
if let StakeState::Uninitialized = self.state()? {
|
||||
let rent_exempt_reserve = rent.minimum_balance(self.data_len()?);
|
||||
let minimum_balance = rent_exempt_reserve + MINIMUM_STAKE_DELEGATION;
|
||||
let minimum_delegation = crate::get_minimum_delegation(feature_set);
|
||||
let minimum_balance = rent_exempt_reserve + minimum_delegation;
|
||||
|
||||
if self.lamports()? >= minimum_balance {
|
||||
self.set_state(&StakeState::Initialized(Meta {
|
||||
|
@ -760,6 +765,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
|
|||
stake_history: &StakeHistory,
|
||||
withdraw_authority: &KeyedAccount,
|
||||
custodian: Option<&KeyedAccount>,
|
||||
feature_set: &FeatureSet,
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut signers = HashSet::new();
|
||||
let withdraw_authority_pubkey = withdraw_authority
|
||||
|
@ -788,7 +794,10 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
|
|||
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, MINIMUM_STAKE_DELEGATION)?;
|
||||
let reserve = checked_add(
|
||||
meta.rent_exempt_reserve,
|
||||
crate::get_minimum_delegation(feature_set),
|
||||
)?;
|
||||
|
||||
(meta.lockup, reserve, false)
|
||||
}
|
||||
|
@ -887,9 +896,10 @@ 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_STAKE_DELEGATION);
|
||||
.saturating_add(minimum_delegation);
|
||||
let source_remaining_balance = source_lamports.saturating_sub(lamports);
|
||||
if source_remaining_balance == 0 {
|
||||
// full amount is a withdrawal
|
||||
|
@ -920,7 +930,7 @@ fn validate_split_amount(
|
|||
)
|
||||
};
|
||||
let destination_minimum_balance =
|
||||
destination_rent_exempt_reserve.saturating_add(MINIMUM_STAKE_DELEGATION);
|
||||
destination_rent_exempt_reserve.saturating_add(minimum_delegation);
|
||||
let destination_balance_deficit =
|
||||
destination_minimum_balance.saturating_sub(destination_lamports);
|
||||
if lamports < destination_balance_deficit {
|
||||
|
@ -928,7 +938,7 @@ fn validate_split_amount(
|
|||
}
|
||||
|
||||
// If the source account is already staked, the destination will end up staked as well. Verify
|
||||
// the destination account's delegation amount is at least MINIMUM_STAKE_DELEGATION.
|
||||
// the destination account's delegation amount is at least the minimum delegation.
|
||||
//
|
||||
// The *delegation* requirements are different than the *balance* requirements. If the
|
||||
// destination account is prefunded with a balance of `rent exempt reserve + minimum stake
|
||||
|
@ -937,7 +947,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_STAKE_DELEGATION {
|
||||
if source_stake.is_some() && lamports < minimum_delegation {
|
||||
return Err(InstructionError::InsufficientFunds);
|
||||
}
|
||||
|
||||
|
@ -2525,8 +2535,9 @@ mod tests {
|
|||
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>());
|
||||
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
||||
let stake_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let stake_lamports = (rent_exempt_reserve + MINIMUM_STAKE_DELEGATION) * 2;
|
||||
let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2;
|
||||
let stake_account = AccountSharedData::new_ref_data_with_space(
|
||||
stake_lamports,
|
||||
&StakeState::Uninitialized,
|
||||
|
@ -2554,7 +2565,7 @@ mod tests {
|
|||
&invoke_context,
|
||||
stake_lamports / 2,
|
||||
&split_stake_keyed_account,
|
||||
&HashSet::default() // no signers
|
||||
&HashSet::default(), // no signers
|
||||
),
|
||||
Err(InstructionError::MissingRequiredSignature)
|
||||
);
|
||||
|
@ -2677,8 +2688,9 @@ mod tests {
|
|||
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>());
|
||||
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
||||
let stake_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let stake_lamports = (rent_exempt_reserve + MINIMUM_STAKE_DELEGATION) * 2;
|
||||
let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2;
|
||||
let stake_account = AccountSharedData::new_ref_data_with_space(
|
||||
stake_lamports,
|
||||
&StakeState::Stake(
|
||||
|
@ -2723,7 +2735,8 @@ mod tests {
|
|||
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>());
|
||||
let minimum_balance = rent_exempt_reserve + MINIMUM_STAKE_DELEGATION;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
||||
let minimum_balance = rent_exempt_reserve + minimum_delegation;
|
||||
let stake_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let split_stake_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let stake_lamports = minimum_balance * 2;
|
||||
|
@ -2767,7 +2780,7 @@ mod tests {
|
|||
&invoke_context,
|
||||
rent_exempt_reserve,
|
||||
&split_stake_keyed_account,
|
||||
&signers
|
||||
&signers,
|
||||
),
|
||||
Err(InstructionError::InsufficientFunds)
|
||||
);
|
||||
|
@ -2778,7 +2791,7 @@ mod tests {
|
|||
&invoke_context,
|
||||
stake_lamports - rent_exempt_reserve,
|
||||
&split_stake_keyed_account,
|
||||
&signers
|
||||
&signers,
|
||||
),
|
||||
Err(InstructionError::InsufficientFunds)
|
||||
);
|
||||
|
@ -2793,7 +2806,7 @@ mod tests {
|
|||
&invoke_context,
|
||||
stake_lamports - minimum_balance,
|
||||
&split_stake_keyed_account,
|
||||
&signers
|
||||
&signers,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
|
@ -2832,7 +2845,8 @@ mod tests {
|
|||
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 + MINIMUM_STAKE_DELEGATION) * 2;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
||||
let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2;
|
||||
|
||||
let split_stake_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let signers = vec![stake_pubkey].into_iter().collect();
|
||||
|
@ -2851,8 +2865,8 @@ mod tests {
|
|||
0,
|
||||
rent_exempt_reserve - 1,
|
||||
rent_exempt_reserve,
|
||||
rent_exempt_reserve + MINIMUM_STAKE_DELEGATION - 1,
|
||||
rent_exempt_reserve + MINIMUM_STAKE_DELEGATION,
|
||||
rent_exempt_reserve + minimum_delegation - 1,
|
||||
rent_exempt_reserve + minimum_delegation,
|
||||
];
|
||||
for initial_balance in split_lamport_balances {
|
||||
let split_stake_account = AccountSharedData::new_ref_data_with_space(
|
||||
|
@ -2952,7 +2966,8 @@ mod tests {
|
|||
let source_larger_rent_exempt_reserve =
|
||||
rent.minimum_balance(std::mem::size_of::<StakeState>() + 100);
|
||||
let split_rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let stake_lamports = (source_larger_rent_exempt_reserve + MINIMUM_STAKE_DELEGATION) * 2;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
||||
let stake_lamports = (source_larger_rent_exempt_reserve + minimum_delegation) * 2;
|
||||
|
||||
let split_stake_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let signers = vec![stake_pubkey].into_iter().collect();
|
||||
|
@ -2975,8 +2990,8 @@ mod tests {
|
|||
0,
|
||||
split_rent_exempt_reserve - 1,
|
||||
split_rent_exempt_reserve,
|
||||
split_rent_exempt_reserve + MINIMUM_STAKE_DELEGATION - 1,
|
||||
split_rent_exempt_reserve + MINIMUM_STAKE_DELEGATION,
|
||||
split_rent_exempt_reserve + minimum_delegation - 1,
|
||||
split_rent_exempt_reserve + minimum_delegation,
|
||||
];
|
||||
for initial_balance in split_lamport_balances {
|
||||
let split_stake_account = AccountSharedData::new_ref_data_with_space(
|
||||
|
@ -3154,7 +3169,8 @@ mod tests {
|
|||
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 + MINIMUM_STAKE_DELEGATION;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
||||
let stake_lamports = rent_exempt_reserve + minimum_delegation;
|
||||
|
||||
let split_stake_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let signers = vec![stake_pubkey].into_iter().collect();
|
||||
|
@ -3247,7 +3263,8 @@ mod tests {
|
|||
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 + MINIMUM_STAKE_DELEGATION;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
||||
let stake_lamports = rent_exempt_reserve + minimum_delegation;
|
||||
|
||||
let split_stake_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let signers = vec![stake_pubkey].into_iter().collect();
|
||||
|
@ -3266,8 +3283,8 @@ mod tests {
|
|||
0,
|
||||
rent_exempt_reserve - 1,
|
||||
rent_exempt_reserve,
|
||||
rent_exempt_reserve + MINIMUM_STAKE_DELEGATION - 1,
|
||||
rent_exempt_reserve + MINIMUM_STAKE_DELEGATION,
|
||||
rent_exempt_reserve + minimum_delegation - 1,
|
||||
rent_exempt_reserve + minimum_delegation,
|
||||
];
|
||||
for initial_balance in split_lamport_balances {
|
||||
let split_stake_account = AccountSharedData::new_ref_data_with_space(
|
||||
|
@ -3336,7 +3353,8 @@ mod tests {
|
|||
let source_rent_exempt_reserve =
|
||||
rent.minimum_balance(std::mem::size_of::<StakeState>() + 100);
|
||||
let split_rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::<StakeState>());
|
||||
let stake_lamports = source_rent_exempt_reserve + MINIMUM_STAKE_DELEGATION;
|
||||
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
||||
let stake_lamports = source_rent_exempt_reserve + minimum_delegation;
|
||||
|
||||
let split_stake_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let signers = vec![stake_pubkey].into_iter().collect();
|
||||
|
|
|
@ -222,6 +222,15 @@ pub enum StakeInstruction {
|
|||
/// 1. `[SIGNER]` Lockup authority or withdraw authority
|
||||
/// 2. Optional: `[SIGNER]` New lockup authority
|
||||
SetLockupChecked(LockupCheckedArgs),
|
||||
|
||||
/// Get the minimum stake delegation, in lamports
|
||||
///
|
||||
/// # Account references
|
||||
/// None
|
||||
///
|
||||
/// The minimum delegation will be returned via the transaction context's returndata.
|
||||
/// Use `get_return_data()` to retrieve the result.
|
||||
GetMinimumDelegation,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||
|
@ -678,6 +687,14 @@ pub fn set_lockup_checked(
|
|||
)
|
||||
}
|
||||
|
||||
pub fn get_minimum_delegation() -> Instruction {
|
||||
Instruction::new_with_bincode(
|
||||
id(),
|
||||
&StakeInstruction::GetMinimumDelegation,
|
||||
Vec::default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {super::*, crate::instruction::InstructionError};
|
||||
|
|
|
@ -6,7 +6,8 @@ pub mod program {
|
|||
crate::declare_id!("Stake11111111111111111111111111111111111111");
|
||||
}
|
||||
|
||||
/// The minimum stake amount that can be delegated, in lamports.
|
||||
/// NOTE: This is also used to calculate the minimum balance of a stake account, which is the
|
||||
/// rent exempt reserve _plus_ the minimum stake delegation.
|
||||
#[deprecated(
|
||||
since = "1.10.6",
|
||||
note = "This constant may be outdated, please use `solana_stake_program::get_minimum_delegation` instead"
|
||||
)]
|
||||
pub const MINIMUM_STAKE_DELEGATION: u64 = 1;
|
||||
|
|
|
@ -339,6 +339,10 @@ pub mod stake_split_uses_rent_sysvar {
|
|||
solana_sdk::declare_id!("FQnc7U4koHqWgRvFaBJjZnV8VPg6L6wWK33yJeDp4yvV");
|
||||
}
|
||||
|
||||
pub mod add_get_minimum_delegation_instruction_to_stake_program {
|
||||
solana_sdk::declare_id!("St8k9dVXP97xT6faW24YmRSYConLbhsMJA4TJTBLmMT");
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
/// Map of feature identifiers to user-visible description
|
||||
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
|
||||
|
@ -418,6 +422,7 @@ lazy_static! {
|
|||
(disable_deprecated_loader::id(), "disable the deprecated BPF loader"),
|
||||
(check_slice_translation_size::id(), "check size when translating slices"),
|
||||
(stake_split_uses_rent_sysvar::id(), "stake split instruction uses rent sysvar"),
|
||||
(add_get_minimum_delegation_instruction_to_stake_program::id(), "add GetMinimumDelegation instruction to stake program"),
|
||||
/*************** ADD NEW FEATURES HERE ***************/
|
||||
]
|
||||
.iter()
|
||||
|
|
|
@ -3,7 +3,7 @@ use {
|
|||
check_num_accounts, ParsableProgram, ParseInstructionError, ParsedInstructionEnum,
|
||||
},
|
||||
bincode::deserialize,
|
||||
serde_json::{json, Map},
|
||||
serde_json::{json, Map, Value},
|
||||
solana_sdk::{
|
||||
instruction::CompiledInstruction, message::AccountKeys,
|
||||
stake::instruction::StakeInstruction,
|
||||
|
@ -269,6 +269,10 @@ pub fn parse_stake(
|
|||
}),
|
||||
})
|
||||
}
|
||||
StakeInstruction::GetMinimumDelegation => Ok(ParsedInstructionEnum {
|
||||
instruction_type: "getMinimumDelegation".to_string(),
|
||||
info: Value::default(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue