diff --git a/programs/stake/src/lib.rs b/programs/stake/src/lib.rs index 59cbe78232..1041288fdb 100644 --- a/programs/stake/src/lib.rs +++ b/programs/stake/src/lib.rs @@ -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 +} diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 38e71d796e..33da4ac44d 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -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::()); - 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::()); + 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::(), &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::()); - 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::(), &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::()); - 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::()); 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::()); 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::()); 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::()); 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::()); 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::()); 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::()); 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::(), &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(), diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index bff4664714..126ea0a85f 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -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::() { 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::()); + 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::()); + 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::()); - 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::()); - 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::() + 100); let split_rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - 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::()); - 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::()); - 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::() + 100); let split_rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - 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(); diff --git a/sdk/program/src/stake/instruction.rs b/sdk/program/src/stake/instruction.rs index 5ddd92b9df..087407e226 100644 --- a/sdk/program/src/stake/instruction.rs +++ b/sdk/program/src/stake/instruction.rs @@ -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}; diff --git a/sdk/program/src/stake/mod.rs b/sdk/program/src/stake/mod.rs index c756449c06..9552baea0f 100644 --- a/sdk/program/src/stake/mod.rs +++ b/sdk/program/src/stake/mod.rs @@ -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; diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index bd0c32c71c..994eb66257 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -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 = [ @@ -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() diff --git a/transaction-status/src/parse_stake.rs b/transaction-status/src/parse_stake.rs index 043f63c522..28e8460ea7 100644 --- a/transaction-status/src/parse_stake.rs +++ b/transaction-status/src/parse_stake.rs @@ -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(), + }), } }