Skip to content

Commit

Permalink
Add GetMinimumDelegation stake program instruction (solana-labs#24020)
Browse files Browse the repository at this point in the history
  • Loading branch information
brooksprumo authored Apr 2, 2022
1 parent 792bbf7 commit 2af6753
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 92 deletions.
14 changes: 13 additions & 1 deletion programs/stake/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
}
151 changes: 90 additions & 61 deletions programs/stake/src/stake_instruction.rs

Large diffs are not rendered by default.

70 changes: 44 additions & 26 deletions programs/stake/src/stake_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,7 +25,6 @@ use {
config::Config,
instruction::{LockupArgs, StakeError},
program::id,
MINIMUM_STAKE_DELEGATION,
},
stake_history::{StakeHistory, StakeHistoryEntry},
},
Expand Down Expand Up @@ -370,6 +371,7 @@ pub trait StakeAccount {
authorized: &Authorized,
lockup: &Lockup,
rent: &Rent,
feature_set: &FeatureSet,
) -> Result<(), InstructionError>;
fn authorize(
&self,
Expand Down Expand Up @@ -429,6 +431,7 @@ pub trait StakeAccount {
stake_history: &StakeHistory,
withdraw_authority: &KeyedAccount,
custodian: Option<&KeyedAccount>,
feature_set: &FeatureSet,
) -> Result<(), InstructionError>;
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -920,15 +930,15 @@ 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 {
return Err(InstructionError::InsufficientFunds);
}

// 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
Expand All @@ -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);
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2767,7 +2780,7 @@ mod tests {
&invoke_context,
rent_exempt_reserve,
&split_stake_keyed_account,
&signers
&signers,
),
Err(InstructionError::InsufficientFunds)
);
Expand All @@ -2778,7 +2791,7 @@ mod tests {
&invoke_context,
stake_lamports - rent_exempt_reserve,
&split_stake_keyed_account,
&signers
&signers,
),
Err(InstructionError::InsufficientFunds)
);
Expand All @@ -2793,7 +2806,7 @@ mod tests {
&invoke_context,
stake_lamports - minimum_balance,
&split_stake_keyed_account,
&signers
&signers,
),
Ok(())
);
Expand Down Expand Up @@ -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();
Expand All @@ -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(
Expand Down Expand Up @@ -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();
Expand All @@ -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(
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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(
Expand Down Expand Up @@ -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();
Expand Down
17 changes: 17 additions & 0 deletions sdk/program/src/stake/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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};
Expand Down
7 changes: 4 additions & 3 deletions sdk/program/src/stake/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
5 changes: 5 additions & 0 deletions sdk/src/feature_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> = [
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 5 additions & 1 deletion transaction-status/src/parse_stake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -269,6 +269,10 @@ pub fn parse_stake(
}),
})
}
StakeInstruction::GetMinimumDelegation => Ok(ParsedInstructionEnum {
instruction_type: "getMinimumDelegation".to_string(),
info: Value::default(),
}),
}
}

Expand Down

0 comments on commit 2af6753

Please sign in to comment.