From bae5a1ab66203ac81a9b8cbbc100bbb13b199afd Mon Sep 17 00:00:00 2001 From: Brooks Prumo Date: Thu, 20 Jan 2022 15:51:01 -0600 Subject: [PATCH] Add MINIMUM_STAKE_BALANCE constant --- programs/stake/src/stake_state.rs | 412 +++++++++++++++--- sdk/program/src/instruction.rs | 4 + sdk/program/src/program_error.rs | 8 + sdk/program/src/stake/state.rs | 5 + storage-proto/proto/transaction_by_addr.proto | 1 + storage-proto/src/convert.rs | 4 + 6 files changed, 366 insertions(+), 68 deletions(-) diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 27491d4fce32ab..8d0d30f50450fc 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -23,6 +23,7 @@ use { config::Config, instruction::{LockupArgs, StakeError}, program::id, + state::MINIMUM_STAKE_DELEGATION, }, stake_history::{StakeHistory, StakeHistoryEntry}, }, @@ -420,8 +421,9 @@ impl<'a> StakeAccount for KeyedAccount<'a> { } 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; - if rent_exempt_reserve < self.lamports()? { + if self.lamports()? >= minimum_balance { self.set_state(&StakeState::Initialized(Meta { rent_exempt_reserve, authorized: *authorized, @@ -520,8 +522,12 @@ impl<'a> StakeAccount for KeyedAccount<'a> { match self.state()? { StakeState::Initialized(meta) => { meta.authorized.check(signers, StakeAuthorize::Staker)?; + let stake_amount = self.lamports()?.saturating_sub(meta.rent_exempt_reserve); // can't stake the rent ;) + if stake_amount < MINIMUM_STAKE_DELEGATION { + return Err(InstructionError::StakeDelegationTooSmall); + } let stake = new_stake( - self.lamports()?.saturating_sub(meta.rent_exempt_reserve), // can't stake the rent ;) + stake_amount, vote_account.unsigned_key(), &State::::state(vote_account)?.convert_to_current(), clock.epoch, @@ -531,9 +537,13 @@ impl<'a> StakeAccount for KeyedAccount<'a> { } StakeState::Stake(meta, mut stake) => { meta.authorized.check(signers, StakeAuthorize::Staker)?; + let stake_amount = self.lamports()?.saturating_sub(meta.rent_exempt_reserve); // can't stake the rent ;) + if stake_amount < MINIMUM_STAKE_DELEGATION { + return Err(InstructionError::StakeDelegationTooSmall); + } redelegate( &mut stake, - self.lamports()?.saturating_sub(meta.rent_exempt_reserve), // can't stake the rent ;) + stake_amount, vote_account.unsigned_key(), &State::::state(vote_account)?.convert_to_current(), clock, @@ -588,82 +598,54 @@ impl<'a> StakeAccount for KeyedAccount<'a> { } if let StakeState::Uninitialized = split.state()? { - // verify enough account lamports - if lamports > self.lamports()? { - return Err(InstructionError::InsufficientFunds); - } + let validated_split_info = validate_split_amount(self, split, lamports)?; match self.state()? { StakeState::Stake(meta, mut stake) => { meta.authorized.check(signers, StakeAuthorize::Staker)?; - let split_rent_exempt_reserve = calculate_split_rent_exempt_reserve( - meta.rent_exempt_reserve, - self.data_len()? as u64, - split.data_len()? as u64, - ); + let validated_info = validated_split_info.unwrap(); - // verify enough lamports for rent and more than 0 stake in new split account - if lamports <= split_rent_exempt_reserve.saturating_sub(split.lamports()?) - // if not full withdrawal - || (lamports != self.lamports()? - // verify more than 0 stake left in previous stake - && checked_add(lamports, meta.rent_exempt_reserve)? >= self.lamports()?) - { - return Err(InstructionError::InsufficientFunds); - } // split the stake, subtract rent_exempt_balance unless // the destination account already has those lamports // in place. // this means that the new stake account will have a stake equivalent to // lamports minus rent_exempt_reserve if it starts out with a zero balance - let (remaining_stake_delta, split_stake_amount) = if lamports - == self.lamports()? - { - // If split amount equals the full source stake, the new split stake must - // equal the same amount, regardless of any current lamport balance in the - // split account. Since split accounts retain the state of their source - // account, this prevents any magic activation of stake by prefunding the - // split account. - // The new split stake also needs to ignore any positive delta between the - // original rent_exempt_reserve and the split_rent_exempt_reserve, in order - // to prevent magic activation of stake by splitting between accounts of - // different sizes. - let remaining_stake_delta = - lamports.saturating_sub(meta.rent_exempt_reserve); - (remaining_stake_delta, remaining_stake_delta) - } else { - // Otherwise, the new split stake should reflect the entire split - // requested, less any lamports needed to cover the split_rent_exempt_reserve - ( - lamports, - lamports - split_rent_exempt_reserve.saturating_sub(split.lamports()?), - ) - }; + let (remaining_stake_delta, split_stake_amount) = + if validated_info.source_remaining_balance == 0 { + // If split amount equals the full source stake, the new split stake must + // equal the same amount, regardless of any current lamport balance in the + // split account. Since split accounts retain the state of their source + // account, this prevents any magic activation of stake by prefunding the + // split account. + // The new split stake also needs to ignore any positive delta between the + // original rent_exempt_reserve and the split_rent_exempt_reserve, in order + // to prevent magic activation of stake by splitting between accounts of + // different sizes. + let remaining_stake_delta = + lamports.saturating_sub(meta.rent_exempt_reserve); + (remaining_stake_delta, remaining_stake_delta) + } else { + // Otherwise, the new split stake should reflect the entire split + // requested, less any lamports needed to cover the split_rent_exempt_reserve + ( + lamports, + lamports + - validated_info + .dest_rent_exempt_reserve + .saturating_sub(split.lamports()?), + ) + }; let split_stake = stake.split(remaining_stake_delta, split_stake_amount)?; let mut split_meta = meta; - split_meta.rent_exempt_reserve = split_rent_exempt_reserve; + split_meta.rent_exempt_reserve = validated_info.dest_rent_exempt_reserve; self.set_state(&StakeState::Stake(meta, stake))?; split.set_state(&StakeState::Stake(split_meta, split_stake))?; } StakeState::Initialized(meta) => { meta.authorized.check(signers, StakeAuthorize::Staker)?; - let split_rent_exempt_reserve = calculate_split_rent_exempt_reserve( - meta.rent_exempt_reserve, - self.data_len()? as u64, - split.data_len()? as u64, - ); - - // enough lamports for rent and more than 0 stake in new split account - if lamports <= split_rent_exempt_reserve.saturating_sub(split.lamports()?) - // if not full withdrawal - || (lamports != self.lamports()? - // verify more than 0 stake left in previous stake - && checked_add(lamports, meta.rent_exempt_reserve)? >= self.lamports()?) - { - return Err(InstructionError::InsufficientFunds); - } - + let split_rent_exempt_reserve = + validated_split_info.unwrap().dest_rent_exempt_reserve; let mut split_meta = meta; split_meta.rent_exempt_reserve = split_rent_exempt_reserve; split.set_state(&StakeState::Initialized(split_meta))?; @@ -774,7 +756,8 @@ impl<'a> StakeAccount for KeyedAccount<'a> { StakeState::Initialized(meta) => { meta.authorized .check(&signers, StakeAuthorize::Withdrawer)?; - let reserve = checked_add(meta.rent_exempt_reserve, 1)?; // stake accounts must have a balance > rent_exempt_reserve + // stake accounts must have a balance >= rent_exempt_reserve + minimum_stake_delegation + let reserve = checked_add(meta.rent_exempt_reserve, MINIMUM_STAKE_DELEGATION)?; (meta.lockup, reserve, false) } @@ -820,6 +803,69 @@ impl<'a> StakeAccount for KeyedAccount<'a> { } } +#[derive(Copy, Clone, Debug, Default)] +struct ValidatedSplitInfo { + source_remaining_balance: u64, + dest_rent_exempt_reserve: u64, +} + +fn validate_split_amount( + source: &KeyedAccount, + dest: &KeyedAccount, + lamports: u64, +) -> Result, InstructionError> { + let source_lamports = source.lamports()?; + + // verify enough account lamports + if lamports > source_lamports { + return Err(InstructionError::InsufficientFunds); + } + + match source.state()? { + StakeState::Stake(meta, _) | StakeState::Initialized(meta) => { + // verify that the source account still has enough lamports left + // 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 source_minimum_balance = meta + .rent_exempt_reserve + .saturating_add(MINIMUM_STAKE_DELEGATION); + let source_remaining_balance = source_lamports - lamports; + if source_remaining_balance == 0 { + // full amount is a withdrawal + // nothing to do here + } else if source_remaining_balance < source_minimum_balance { + // the remaining balance is too low to do the split + return Err(InstructionError::InsufficientFunds); + } else { + // all clear! + // nothing to do here + } + + // verify split amount is at least the rent exempt reserve PLUS the minimum stake delegation + let dest_rent_exempt_reserve = calculate_split_rent_exempt_reserve( + meta.rent_exempt_reserve, + source.data_len()? as u64, + dest.data_len()? as u64, + ); + let dest_minimum_balance = + dest_rent_exempt_reserve.saturating_add(MINIMUM_STAKE_DELEGATION); + if lamports < dest_minimum_balance { + return Err(InstructionError::InsufficientFunds); + } + + Ok(Some(ValidatedSplitInfo { + source_remaining_balance, + dest_rent_exempt_reserve, + })) + } + _ => { + // nothing to do here + Ok(None) + } + } +} + #[derive(Clone, Debug, PartialEq)] enum MergeKind { Inactive(Meta, u64), @@ -3104,7 +3150,7 @@ mod tests { let clock = Clock::default(); let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let stake = 42; + let stake = 7 * MINIMUM_STAKE_DELEGATION; let stake_account = AccountSharedData::new_ref_data_with_space( stake + rent_exempt_reserve, &StakeState::Initialized(Meta { @@ -3124,7 +3170,7 @@ mod tests { let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); assert_eq!( stake_keyed_account.withdraw( - stake - 1, + stake - MINIMUM_STAKE_DELEGATION, &to_keyed_account, &clock, &StakeHistory::default(), @@ -3137,7 +3183,7 @@ mod tests { // Withdrawing account down to only rent-exempt reserve should fail stake_account .borrow_mut() - .checked_add_lamports(stake - 1) + .checked_add_lamports(stake - MINIMUM_STAKE_DELEGATION) .unwrap(); // top up account let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); assert_eq!( @@ -3156,7 +3202,7 @@ mod tests { let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); assert_eq!( stake_keyed_account.withdraw( - stake + 1, + stake + MINIMUM_STAKE_DELEGATION, &to_keyed_account, &clock, &StakeHistory::default(), @@ -3168,7 +3214,7 @@ mod tests { let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); assert_eq!( stake_keyed_account.withdraw( - stake + 1, + stake + MINIMUM_STAKE_DELEGATION, &to_keyed_account, &clock, &StakeHistory::default(), @@ -4698,7 +4744,7 @@ 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 + 1; + let stake_lamports = rent_exempt_reserve + MINIMUM_STAKE_DELEGATION; let split_stake_pubkey = solana_sdk::pubkey::new_rand(); let signers = vec![stake_pubkey].into_iter().collect(); @@ -6409,6 +6455,236 @@ mod tests { assert_eq!(new_stake.delegation.stake, delegation * 2); } + /// Ensure that `initialize()` respects the MINIMUM_STAKE_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() { + for (stake_delegation, expected_result) in [ + (MINIMUM_STAKE_DELEGATION, Ok(())), + ( + MINIMUM_STAKE_DELEGATION - 1, + Err(InstructionError::InsufficientFunds), + ), + ] { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let stake_pubkey = Pubkey::new_unique(); + let stake_account = AccountSharedData::new_ref( + stake_delegation + rent_exempt_reserve, + std::mem::size_of::(), + &id(), + ); + let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); + + assert_eq!( + expected_result, + stake_keyed_account.initialize( + &Authorized::auto(&stake_pubkey), + &Lockup::default(), + &rent + ), + ); + } + } + + /// Ensure that `delegate()` respects the MINIMUM_STAKE_DELEGATION requirements + /// - Assert 1: delegating an amount equal-to the minimum delegates OK + /// - Assert 2: delegating an amount less-than the minimum do not delegate + /// Also test both asserts above over both StakeState::{Initialized and Stake}, since the logic + /// is slightly different for the variants. + #[test] + fn test_delegate_minimum_stake_delegation() { + for (stake_delegation, expected_result) in [ + (MINIMUM_STAKE_DELEGATION, Ok(())), + ( + MINIMUM_STAKE_DELEGATION - 1, + Err(InstructionError::StakeDelegationTooSmall), + ), + ] { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let stake_pubkey = Pubkey::new_unique(); + let signers = HashSet::from([stake_pubkey]); + let meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&stake_pubkey) + }; + + for stake_state in &[ + StakeState::Initialized(meta), + StakeState::Stake(meta, Stake::default()), + StakeState::Stake(meta, just_stake(stake_delegation)), + ] { + let stake_account = AccountSharedData::new_ref_data_with_space( + stake_delegation + rent_exempt_reserve, + stake_state, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); + + let vote_pubkey = Pubkey::new_unique(); + let vote_account = RefCell::new(vote_state::create_account( + &vote_pubkey, + &Pubkey::new_unique(), + 0, + 100, + )); + let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); + + assert_eq!( + expected_result, + stake_keyed_account.delegate( + &vote_keyed_account, + &Clock::default(), + &StakeHistory::default(), + &Config::default(), + &signers, + ), + ); + } + } + } + + /// Ensure that `withdraw()` respects the MINIMUM_STAKE_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 starting_stake_delegation = MINIMUM_STAKE_DELEGATION; + for (ending_stake_delegation, expected_result) in [ + (MINIMUM_STAKE_DELEGATION, Ok(())), + ( + MINIMUM_STAKE_DELEGATION - 1, + Err(InstructionError::InsufficientFunds), + ), + ] { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let stake_pubkey = Pubkey::new_unique(); + let meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&stake_pubkey) + }; + + for stake_state in &[ + StakeState::Initialized(meta), + StakeState::Stake(meta, just_stake(starting_stake_delegation)), + ] { + let rewards_balance = 123; + let stake_account = AccountSharedData::new_ref_data_with_space( + starting_stake_delegation + rent_exempt_reserve + rewards_balance, + stake_state, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); + + let to_pubkey = Pubkey::new_unique(); + let to_account = + AccountSharedData::new_ref(rent_exempt_reserve, 0, &system_program::id()); + let to_keyed_account = KeyedAccount::new(&to_pubkey, false, &to_account); + + let withdraw_amount = + (starting_stake_delegation + rewards_balance) - ending_stake_delegation; + assert_eq!( + expected_result, + stake_keyed_account.withdraw( + withdraw_amount, + &to_keyed_account, + &Clock::default(), + &StakeHistory::default(), + &stake_keyed_account, + None, + ), + ); + } + } + } + + /// Ensure that `split()` respects the MINIMUM_STAKE_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: + /// + /// source | dest | result + /// --------+------+-------- + /// EQ | EQ | Ok + /// EQ | LT | Err + /// LT | EQ | Err + /// LT | LT | Err + #[test] + fn test_split_minimum_stake_delegation() { + for (source_stake_delegation, dest_stake_delegation, expected_result) in [ + (MINIMUM_STAKE_DELEGATION, MINIMUM_STAKE_DELEGATION, Ok(())), + ( + MINIMUM_STAKE_DELEGATION, + MINIMUM_STAKE_DELEGATION - 1, + Err(InstructionError::InsufficientFunds), + ), + ( + MINIMUM_STAKE_DELEGATION - 1, + MINIMUM_STAKE_DELEGATION, + Err(InstructionError::InsufficientFunds), + ), + ( + MINIMUM_STAKE_DELEGATION - 1, + MINIMUM_STAKE_DELEGATION - 1, + Err(InstructionError::InsufficientFunds), + ), + ] { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let source_pubkey = Pubkey::new_unique(); + let source_meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&source_pubkey) + }; + // The source account's starting balance is equal to *both* the source and dest + // accounts' *final* balance + let source_starting_balance = + source_stake_delegation + dest_stake_delegation + rent_exempt_reserve * 2; + + for source_stake_state in &[ + StakeState::Initialized(source_meta), + StakeState::Stake( + source_meta, + just_stake(source_starting_balance - rent_exempt_reserve), + ), + ] { + let source_account = AccountSharedData::new_ref_data_with_space( + source_starting_balance, + source_stake_state, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let source_keyed_account = KeyedAccount::new(&source_pubkey, true, &source_account); + + let dest_pubkey = Pubkey::new_unique(); + let dest_account = AccountSharedData::new_ref_data_with_space( + 0, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let dest_keyed_account = KeyedAccount::new(&dest_pubkey, true, &dest_account); + + assert_eq!( + expected_result, + source_keyed_account.split( + dest_stake_delegation + rent_exempt_reserve, + &dest_keyed_account, + &HashSet::from([source_pubkey]), + ), + ); + } + } + } + prop_compose! { pub fn sum_within(max: u64)(total in 1..max) (intermediate in 1..total, total in Just(total)) diff --git a/sdk/program/src/instruction.rs b/sdk/program/src/instruction.rs index e1fbb1825a845a..2d76e68ac2e391 100644 --- a/sdk/program/src/instruction.rs +++ b/sdk/program/src/instruction.rs @@ -252,6 +252,10 @@ pub enum InstructionError { /// Accounts data budget exceeded #[error("Requested account data allocation exceeded the accounts data budget")] AccountsDataBudgetExceeded, + + /// bprumo TODO: doc + #[error("Stake delegation amount is too small")] + StakeDelegationTooSmall, // Note: For any new error added here an equivalent ProgramError and its // conversions must also be added } diff --git a/sdk/program/src/program_error.rs b/sdk/program/src/program_error.rs index c47638dddb29b9..de28e34142a47b 100644 --- a/sdk/program/src/program_error.rs +++ b/sdk/program/src/program_error.rs @@ -51,6 +51,8 @@ pub enum ProgramError { IllegalOwner, #[error("Requested account data allocation exceeded the accounts data budget")] AccountsDataBudgetExceeded, + #[error("Stake delegation amount is too small")] + StakeDelegationTooSmall, } pub trait PrintProgramError { @@ -90,6 +92,7 @@ impl PrintProgramError for ProgramError { Self::UnsupportedSysvar => msg!("Error: UnsupportedSysvar"), Self::IllegalOwner => msg!("Error: IllegalOwner"), Self::AccountsDataBudgetExceeded => msg!("Error: AccountsDataBudgetExceeded"), + Self::StakeDelegationTooSmall => msg!("Error: StakeDelegationTooSmall"), } } } @@ -121,6 +124,7 @@ pub const ACCOUNT_NOT_RENT_EXEMPT: u64 = to_builtin!(16); pub const UNSUPPORTED_SYSVAR: u64 = to_builtin!(17); pub const ILLEGAL_OWNER: u64 = to_builtin!(18); pub const ACCOUNTS_DATA_BUDGET_EXCEEDED: u64 = to_builtin!(19); +pub const STAKE_DELEGATION_TOO_SMALL: u64 = to_builtin!(20); // Warning: Any new program errors added here must also be: // - Added to the below conversions // - Added as an equivilent to InstructionError @@ -148,6 +152,7 @@ impl From for u64 { ProgramError::UnsupportedSysvar => UNSUPPORTED_SYSVAR, ProgramError::IllegalOwner => ILLEGAL_OWNER, ProgramError::AccountsDataBudgetExceeded => ACCOUNTS_DATA_BUDGET_EXCEEDED, + ProgramError::StakeDelegationTooSmall => STAKE_DELEGATION_TOO_SMALL, ProgramError::Custom(error) => { if error == 0 { CUSTOM_ZERO @@ -181,6 +186,7 @@ impl From for ProgramError { UNSUPPORTED_SYSVAR => Self::UnsupportedSysvar, ILLEGAL_OWNER => Self::IllegalOwner, ACCOUNTS_DATA_BUDGET_EXCEEDED => Self::AccountsDataBudgetExceeded, + STAKE_DELEGATION_TOO_SMALL => Self::StakeDelegationTooSmall, _ => Self::Custom(error as u32), } } @@ -210,6 +216,7 @@ impl TryFrom for ProgramError { Self::Error::UnsupportedSysvar => Ok(Self::UnsupportedSysvar), Self::Error::IllegalOwner => Ok(Self::IllegalOwner), Self::Error::AccountsDataBudgetExceeded => Ok(Self::AccountsDataBudgetExceeded), + Self::Error::StakeDelegationTooSmall => Ok(Self::StakeDelegationTooSmall), _ => Err(error), } } @@ -241,6 +248,7 @@ where UNSUPPORTED_SYSVAR => Self::UnsupportedSysvar, ILLEGAL_OWNER => Self::IllegalOwner, ACCOUNTS_DATA_BUDGET_EXCEEDED => Self::AccountsDataBudgetExceeded, + STAKE_DELEGATION_TOO_SMALL => Self::StakeDelegationTooSmall, _ => { // A valid custom error has no bits set in the upper 32 if error >> BUILTIN_BIT_SHIFT == 0 { diff --git a/sdk/program/src/stake/state.rs b/sdk/program/src/stake/state.rs index 5b346342718b05..8d0edb97c6e18a 100644 --- a/sdk/program/src/stake/state.rs +++ b/sdk/program/src/stake/state.rs @@ -15,6 +15,11 @@ use { std::collections::HashSet, }; +/// 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. +pub const MINIMUM_STAKE_DELEGATION: u64 = 1; + pub type StakeActivationStatus = StakeHistoryEntry; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)] diff --git a/storage-proto/proto/transaction_by_addr.proto b/storage-proto/proto/transaction_by_addr.proto index ee88455e66c65f..d9da65278f83d1 100644 --- a/storage-proto/proto/transaction_by_addr.proto +++ b/storage-proto/proto/transaction_by_addr.proto @@ -113,6 +113,7 @@ enum InstructionErrorType { UNSUPPORTED_SYSVAR = 48; ILLEGAL_OWNER = 49; ACCOUNTS_DATA_BUDGET_EXCEEDED = 50; + STAKE_DELEGATION_TOO_SMALL = 51; } message UnixTimestamp { diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index 02698cb6194ca6..dd5632a5be2b03 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -689,6 +689,7 @@ impl TryFrom for TransactionError { 48 => InstructionError::UnsupportedSysvar, 49 => InstructionError::IllegalOwner, 50 => InstructionError::AccountsDataBudgetExceeded, + 51 => InstructionError::StakeDelegationTooSmall, _ => return Err("Invalid InstructionError"), }; @@ -979,6 +980,9 @@ impl From for tx_by_addr::TransactionError { InstructionError::AccountsDataBudgetExceeded => { tx_by_addr::InstructionErrorType::AccountsDataBudgetExceeded } + InstructionError::StakeDelegationTooSmall => { + tx_by_addr::InstructionErrorType::StakeDelegationTooSmall + } } as i32, custom: match instruction_error { InstructionError::Custom(custom) => {