Skip to content

Commit

Permalink
stake: Allow initialized stakes to be below the min delegation
Browse files Browse the repository at this point in the history
  • Loading branch information
joncinque committed Apr 26, 2022
1 parent 7de339c commit 4eadbcc
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 138 deletions.
254 changes: 130 additions & 124 deletions programs/stake/src/stake_instruction.rs

Large diffs are not rendered by default.

56 changes: 42 additions & 14 deletions programs/stake/src/stake_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ use {
account_utils::StateMut,
clock::{Clock, Epoch},
feature_set::{
stake_merge_with_unmatched_credits_observed, stake_split_uses_rent_sysvar, FeatureSet,
stake_allow_zero_undelegated_amount, stake_merge_with_unmatched_credits_observed,
stake_split_uses_rent_sysvar, FeatureSet,
},
instruction::{checked_add, InstructionError},
pubkey::Pubkey,
Expand Down Expand Up @@ -378,8 +379,13 @@ pub fn initialize(
}
if let StakeState::Uninitialized = stake_account.get_state()? {
let rent_exempt_reserve = rent.minimum_balance(stake_account.get_data().len());
let minimum_delegation = crate::get_minimum_delegation(feature_set);
let minimum_balance = rent_exempt_reserve + minimum_delegation;
// when removing this feature, remove `minimum_balance` and just use `rent_exempt_reserve`
let minimum_balance = if feature_set.is_active(&stake_allow_zero_undelegated_amount::id()) {
rent_exempt_reserve
} else {
let minimum_delegation = crate::get_minimum_delegation(feature_set);
rent_exempt_reserve + minimum_delegation
};

if stake_account.get_lamports() >= minimum_balance {
stake_account.set_state(&StakeState::Initialized(Meta {
Expand Down Expand Up @@ -483,6 +489,7 @@ pub fn delegate(
stake_history: &StakeHistory,
config: &Config,
signers: &HashSet<Pubkey>,
feature_set: &FeatureSet,
) -> Result<(), InstructionError> {
let vote_account =
instruction_context.try_borrow_account(transaction_context, vote_account_index)?;
Expand All @@ -499,7 +506,7 @@ pub fn delegate(
StakeState::Initialized(meta) => {
meta.authorized.check(signers, StakeAuthorize::Staker)?;
let ValidatedDelegatedInfo { stake_amount } =
validate_delegated_amount(&stake_account, &meta)?;
validate_delegated_amount(&stake_account, &meta, feature_set)?;
let stake = new_stake(
stake_amount,
&vote_pubkey,
Expand All @@ -512,7 +519,7 @@ pub fn delegate(
StakeState::Stake(meta, mut stake) => {
meta.authorized.check(signers, StakeAuthorize::Staker)?;
let ValidatedDelegatedInfo { stake_amount } =
validate_delegated_amount(&stake_account, &meta)?;
validate_delegated_amount(&stake_account, &meta, feature_set)?;
redelegate(
&mut stake,
stake_amount,
Expand Down Expand Up @@ -594,6 +601,7 @@ pub fn split(
match stake_state {
StakeState::Stake(meta, mut stake) => {
meta.authorized.check(signers, StakeAuthorize::Staker)?;
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
let validated_split_info = validate_split_amount(
invoke_context,
transaction_context,
Expand All @@ -603,6 +611,7 @@ pub fn split(
lamports,
&meta,
Some(&stake),
minimum_delegation,
)?;

// split the stake, subtract rent_exempt_balance unless
Expand Down Expand Up @@ -650,6 +659,14 @@ pub fn split(
}
StakeState::Initialized(meta) => {
meta.authorized.check(signers, StakeAuthorize::Staker)?;
let additional_required_lamports = if invoke_context
.feature_set
.is_active(&stake_allow_zero_undelegated_amount::id())
{
0
} else {
crate::get_minimum_delegation(&invoke_context.feature_set)
};
let validated_split_info = validate_split_amount(
invoke_context,
transaction_context,
Expand All @@ -659,6 +676,7 @@ pub fn split(
lamports,
&meta,
None,
additional_required_lamports,
)?;
let mut split_meta = meta;
split_meta.rent_exempt_reserve = validated_split_info.destination_rent_exempt_reserve;
Expand Down Expand Up @@ -802,11 +820,15 @@ pub fn withdraw(
StakeState::Initialized(meta) => {
meta.authorized
.check(&signers, StakeAuthorize::Withdrawer)?;
// stake accounts must have a balance >= rent_exempt_reserve + minimum_stake_delegation
let reserve = checked_add(
meta.rent_exempt_reserve,
crate::get_minimum_delegation(feature_set),
)?;
// stake accounts must have a balance >= rent_exempt_reserve
let reserve = if feature_set.is_active(&stake_allow_zero_undelegated_amount::id()) {
meta.rent_exempt_reserve
} else {
checked_add(
meta.rent_exempt_reserve,
crate::get_minimum_delegation(feature_set),
)?
};

(meta.lockup, reserve, false)
}
Expand Down Expand Up @@ -925,10 +947,16 @@ struct ValidatedDelegatedInfo {
fn validate_delegated_amount(
account: &BorrowedAccount,
meta: &Meta,
feature_set: &FeatureSet,
) -> Result<ValidatedDelegatedInfo, InstructionError> {
let stake_amount = account
.get_lamports()
.saturating_sub(meta.rent_exempt_reserve); // can't stake the rent
if feature_set.is_active(&stake_allow_zero_undelegated_amount::id())
&& stake_amount < crate::get_minimum_delegation(feature_set)
{
return Err(InstructionError::InsufficientStakeDelegation);
}
Ok(ValidatedDelegatedInfo { stake_amount })
}

Expand All @@ -953,6 +981,7 @@ fn validate_split_amount(
lamports: u64,
source_meta: &Meta,
source_stake: Option<&Stake>,
additional_required_lamports: u64,
) -> Result<ValidatedSplitInfo, InstructionError> {
let source_account =
instruction_context.try_borrow_account(transaction_context, source_account_index)?;
Expand All @@ -979,10 +1008,9 @@ fn validate_split_amount(
// EITHER at least the minimum balance, OR zero (in this case the source
// account is transferring all lamports to new destination account, and the source
// account will be closed)
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
let source_minimum_balance = source_meta
.rent_exempt_reserve
.saturating_add(minimum_delegation);
.saturating_add(additional_required_lamports);
let source_remaining_balance = source_lamports.saturating_sub(lamports);
if source_remaining_balance == 0 {
// full amount is a withdrawal
Expand Down Expand Up @@ -1013,7 +1041,7 @@ fn validate_split_amount(
)
};
let destination_minimum_balance =
destination_rent_exempt_reserve.saturating_add(minimum_delegation);
destination_rent_exempt_reserve.saturating_add(additional_required_lamports);
let destination_balance_deficit =
destination_minimum_balance.saturating_sub(destination_lamports);
if lamports < destination_balance_deficit {
Expand All @@ -1030,7 +1058,7 @@ fn validate_split_amount(
// account, the split amount must be at least the minimum stake delegation. So if the minimum
// stake delegation was 10 lamports, then a split amount of 1 lamport would not meet the
// *delegation* requirements.
if source_stake.is_some() && lamports < minimum_delegation {
if source_stake.is_some() && lamports < additional_required_lamports {
return Err(InstructionError::InsufficientFunds);
}

Expand Down
4 changes: 4 additions & 0 deletions sdk/program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,10 @@ pub enum InstructionError {
/// Active vote account close
#[error("Cannot close vote account unless it stopped voting at least one full epoch ago")]
ActiveVoteAccountClose,

// Insufficient stake delegation
#[error("Stake amount is below the minimum delegation requirements")]
InsufficientStakeDelegation,
// Note: For any new error added here an equivalent ProgramError and its
// conversions must also be added
}
Expand Down
8 changes: 8 additions & 0 deletions sdk/program/src/program_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ pub enum ProgramError {
MaxAccountsDataSizeExceeded,
#[error("Cannot close vote account unless it stopped voting at least one full epoch ago")]
ActiveVoteAccountClose,
#[error("Stake amount is below the minimum delegation requirements")]
InsufficientStakeDelegation,
}

pub trait PrintProgramError {
Expand Down Expand Up @@ -95,6 +97,7 @@ impl PrintProgramError for ProgramError {
Self::IllegalOwner => msg!("Error: IllegalOwner"),
Self::MaxAccountsDataSizeExceeded => msg!("Error: MaxAccountsDataSizeExceeded"),
Self::ActiveVoteAccountClose => msg!("Error: ActiveVoteAccountClose"),
Self::InsufficientStakeDelegation => msg!("Error: InsufficientStakeDelegation"),
}
}
}
Expand Down Expand Up @@ -127,6 +130,7 @@ pub const UNSUPPORTED_SYSVAR: u64 = to_builtin!(17);
pub const ILLEGAL_OWNER: u64 = to_builtin!(18);
pub const MAX_ACCOUNTS_DATA_SIZE_EXCEEDED: u64 = to_builtin!(19);
pub const ACTIVE_VOTE_ACCOUNT_CLOSE: u64 = to_builtin!(20);
pub const INSUFFICIENT_STAKE_DELEGATION: u64 = to_builtin!(21);
// Warning: Any new program errors added here must also be:
// - Added to the below conversions
// - Added as an equivilent to InstructionError
Expand Down Expand Up @@ -155,6 +159,7 @@ impl From<ProgramError> for u64 {
ProgramError::IllegalOwner => ILLEGAL_OWNER,
ProgramError::MaxAccountsDataSizeExceeded => MAX_ACCOUNTS_DATA_SIZE_EXCEEDED,
ProgramError::ActiveVoteAccountClose => ACTIVE_VOTE_ACCOUNT_CLOSE,
ProgramError::InsufficientStakeDelegation => INSUFFICIENT_STAKE_DELEGATION,
ProgramError::Custom(error) => {
if error == 0 {
CUSTOM_ZERO
Expand Down Expand Up @@ -189,6 +194,7 @@ impl From<u64> for ProgramError {
ILLEGAL_OWNER => Self::IllegalOwner,
MAX_ACCOUNTS_DATA_SIZE_EXCEEDED => Self::MaxAccountsDataSizeExceeded,
ACTIVE_VOTE_ACCOUNT_CLOSE => Self::ActiveVoteAccountClose,
INSUFFICIENT_STAKE_DELEGATION => Self::InsufficientStakeDelegation,
_ => Self::Custom(error as u32),
}
}
Expand Down Expand Up @@ -219,6 +225,7 @@ impl TryFrom<InstructionError> for ProgramError {
Self::Error::IllegalOwner => Ok(Self::IllegalOwner),
Self::Error::MaxAccountsDataSizeExceeded => Ok(Self::MaxAccountsDataSizeExceeded),
Self::Error::ActiveVoteAccountClose => Ok(Self::ActiveVoteAccountClose),
Self::Error::InsufficientStakeDelegation => Ok(Self::InsufficientStakeDelegation),
_ => Err(error),
}
}
Expand Down Expand Up @@ -251,6 +258,7 @@ where
ILLEGAL_OWNER => Self::IllegalOwner,
MAX_ACCOUNTS_DATA_SIZE_EXCEEDED => Self::MaxAccountsDataSizeExceeded,
ACTIVE_VOTE_ACCOUNT_CLOSE => Self::ActiveVoteAccountClose,
INSUFFICIENT_STAKE_DELEGATION => Self::InsufficientStakeDelegation,
_ => {
// A valid custom error has no bits set in the upper 32
if error >> BUILTIN_BIT_SHIFT == 0 {
Expand Down
5 changes: 5 additions & 0 deletions sdk/src/feature_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,10 @@ pub mod update_rewards_from_cached_accounts {
solana_sdk::declare_id!("28s7i3htzhahXQKqmS2ExzbEoUypg9krwvtK2M9UWXh9");
}

pub mod stake_allow_zero_undelegated_amount {
solana_sdk::declare_id!("sTKz343FM8mqtyGvYWvbLpTThw3ixRM4Xk8QvZ985mw");
}

lazy_static! {
/// Map of feature identifiers to user-visible description
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
Expand Down Expand Up @@ -448,6 +452,7 @@ lazy_static! {
(executables_incur_cpi_data_cost::id(), "Executables incure CPI data costs"),
(fix_recent_blockhashes::id(), "stop adding hashes for skipped slots to recent blockhashes"),
(update_rewards_from_cached_accounts::id(), "update rewards from cached accounts"),
(stake_allow_zero_undelegated_amount::id(), "Allow zero-lamport undelegated amount for initialized stakes") // TODO JC
/*************** ADD NEW FEATURES HERE ***************/
]
.iter()
Expand Down
1 change: 1 addition & 0 deletions storage-proto/proto/transaction_by_addr.proto
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ enum InstructionErrorType {
ILLEGAL_OWNER = 49;
MAX_ACCOUNTS_DATA_SIZE_EXCEEDED = 50;
ACTIVE_VOTE_ACCOUNT_CLOSE = 51;
INSUFFICIENT_STAKE_DELEGATION = 52;
}

message UnixTimestamp {
Expand Down
4 changes: 4 additions & 0 deletions storage-proto/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,7 @@ impl TryFrom<tx_by_addr::TransactionError> for TransactionError {
49 => InstructionError::IllegalOwner,
50 => InstructionError::MaxAccountsDataSizeExceeded,
51 => InstructionError::ActiveVoteAccountClose,
52 => InstructionError::InsufficientStakeDelegation,
_ => return Err("Invalid InstructionError"),
};

Expand Down Expand Up @@ -1003,6 +1004,9 @@ impl From<TransactionError> for tx_by_addr::TransactionError {
InstructionError::ActiveVoteAccountClose => {
tx_by_addr::InstructionErrorType::ActiveVoteAccountClose
}
InstructionError::InsufficientStakeDelegation => {
tx_by_addr::InstructionErrorType::InsufficientStakeDelegation
}
} as i32,
custom: match instruction_error {
InstructionError::Custom(custom) => {
Expand Down

0 comments on commit 4eadbcc

Please sign in to comment.