Add GetMinimumDelegation stake program instruction (#24020)

This commit is contained in:
Brooks Prumo 2022-04-02 00:11:10 -05:00 committed by GitHub
parent 792bbf75ab
commit 2af6753808
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 178 additions and 92 deletions

View File

@ -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
}

View File

@ -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(),

View File

@ -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();

View File

@ -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};

View File

@ -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;

View File

@ -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()

View File

@ -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(),
}),
}
}