diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index d9499eab1d..f7b436b3ff 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -1,10 +1,11 @@ -#[deprecated( - since = "1.8.0", - note = "Please use `solana_sdk::stake::instruction` or `solana_program::stake::instruction` instead" -)] -pub use solana_sdk::stake::instruction::*; use { - crate::{config, stake_state::StakeAccount}, + crate::{ + config, + stake_state::{ + authorize, authorize_with_seed, deactivate, delegate, initialize, merge, set_lockup, + split, withdraw, + }, + }, log::*, solana_program_runtime::{ invoke_context::InvokeContext, sysvar_cache::get_sysvar_with_account_check, @@ -15,7 +16,7 @@ use { keyed_account::keyed_account_at_index, program_utils::limited_deserialize, stake::{ - instruction::StakeInstruction, + instruction::{LockupArgs, StakeInstruction}, program::id, state::{Authorized, Lockup}, }, @@ -44,7 +45,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, &invoke_context.feature_set) + initialize(me, &authorized, &lockup, &rent, &invoke_context.feature_set) } StakeInstruction::Authorize(authorized_pubkey, stake_authorize) => { instruction_context.check_number_of_instruction_accounts(3)?; @@ -62,7 +63,8 @@ pub fn process_instruction( .ok() .map(|ka| ka.unsigned_key()); - me.authorize( + authorize( + me, &signers, &authorized_pubkey, stake_authorize, @@ -71,7 +73,8 @@ pub fn process_instruction( custodian, ) } else { - me.authorize( + authorize( + me, &signers, &authorized_pubkey, stake_authorize, @@ -97,7 +100,8 @@ pub fn process_instruction( .ok() .map(|ka| ka.unsigned_key()); - me.authorize_with_seed( + authorize_with_seed( + me, authority_base, &args.authority_seed, &args.authority_owner, @@ -108,7 +112,8 @@ pub fn process_instruction( custodian, ) } else { - me.authorize_with_seed( + authorize_with_seed( + me, authority_base, &args.authority_seed, &args.authority_owner, @@ -138,13 +143,13 @@ pub fn process_instruction( } let config = config::from(&*config_account.try_account_ref()?) .ok_or(InstructionError::InvalidArgument)?; - me.delegate(vote, &clock, &stake_history, &config, &signers) + delegate(me, vote, &clock, &stake_history, &config, &signers) } StakeInstruction::Split(lamports) => { instruction_context.check_number_of_instruction_accounts(2)?; let split_stake = &keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?; - me.split(invoke_context, lamports, split_stake, &signers) + split(me, invoke_context, lamports, split_stake, &signers) } StakeInstruction::Merge => { instruction_context.check_number_of_instruction_accounts(2)?; @@ -157,7 +162,8 @@ pub fn process_instruction( instruction_context, 3, )?; - me.merge( + merge( + me, invoke_context, source_stake, &clock, @@ -176,7 +182,8 @@ pub fn process_instruction( 3, )?; instruction_context.check_number_of_instruction_accounts(5)?; - me.withdraw( + withdraw( + me, lamports, to, &clock, @@ -189,11 +196,11 @@ pub fn process_instruction( StakeInstruction::Deactivate => { let clock = get_sysvar_with_account_check::clock(invoke_context, instruction_context, 1)?; - me.deactivate(&clock, &signers) + deactivate(me, &clock, &signers) } StakeInstruction::SetLockup(lockup) => { let clock = invoke_context.get_sysvar_cache().get_clock()?; - me.set_lockup(&lockup, &signers, &clock) + set_lockup(me, &lockup, &signers, &clock) } StakeInstruction::InitializeChecked => { if invoke_context @@ -214,7 +221,8 @@ pub fn process_instruction( let rent = get_sysvar_with_account_check::rent(invoke_context, instruction_context, 1)?; - me.initialize( + initialize( + me, &authorized, &Lockup::default(), &rent, @@ -243,7 +251,8 @@ pub fn process_instruction( .ok() .map(|ka| ka.unsigned_key()); - me.authorize( + authorize( + me, &signers, authorized_pubkey, stake_authorize, @@ -275,7 +284,8 @@ pub fn process_instruction( .ok() .map(|ka| ka.unsigned_key()); - me.authorize_with_seed( + authorize_with_seed( + me, authority_base, &args.authority_seed, &args.authority_owner, @@ -312,7 +322,7 @@ pub fn process_instruction( custodian, }; let clock = invoke_context.get_sysvar_cache().get_clock()?; - me.set_lockup(&lockup, &signers, &clock) + set_lockup(me, &lockup, &signers, &clock) } else { Err(InstructionError::InvalidInstructionData) } @@ -356,7 +366,11 @@ mod tests { rent::Rent, stake::{ config as stake_config, - instruction::{self, LockupArgs}, + instruction::{ + self, authorize_checked, authorize_checked_with_seed, initialize_checked, + set_lockup_checked, AuthorizeCheckedWithSeedArgs, AuthorizeWithSeedArgs, + LockupArgs, StakeError, + }, state::{Authorized, Lockup, StakeAuthorize}, }, stake_history::{StakeHistory, StakeHistoryEntry}, diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index b8f868a17a..6e96fc699f 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -365,484 +365,422 @@ fn calculate_stake_rewards( Some((staker_rewards, voter_rewards, credits_observed)) } -pub trait StakeAccount { - fn initialize( - &self, - authorized: &Authorized, - lockup: &Lockup, - rent: &Rent, - feature_set: &FeatureSet, - ) -> Result<(), InstructionError>; - fn authorize( - &self, - signers: &HashSet, - new_authority: &Pubkey, - stake_authorize: StakeAuthorize, - require_custodian_for_locked_stake_authorize: bool, - clock: &Clock, - custodian: Option<&Pubkey>, - ) -> Result<(), InstructionError>; - fn authorize_with_seed( - &self, - authority_base: &KeyedAccount, - authority_seed: &str, - authority_owner: &Pubkey, - new_authority: &Pubkey, - stake_authorize: StakeAuthorize, - require_custodian_for_locked_stake_authorize: bool, - clock: &Clock, - custodian: Option<&Pubkey>, - ) -> Result<(), InstructionError>; - fn delegate( - &self, - vote_account: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - config: &Config, - signers: &HashSet, - ) -> Result<(), InstructionError>; - fn deactivate(&self, clock: &Clock, signers: &HashSet) -> Result<(), InstructionError>; - fn set_lockup( - &self, - lockup: &LockupArgs, - signers: &HashSet, - clock: &Clock, - ) -> Result<(), InstructionError>; - fn split( - &self, - invoke_context: &InvokeContext, - lamports: u64, - split_stake: &KeyedAccount, - signers: &HashSet, - ) -> Result<(), InstructionError>; - fn merge( - &self, - invoke_context: &InvokeContext, - source_stake: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - signers: &HashSet, - ) -> Result<(), InstructionError>; - fn withdraw( - &self, - lamports: u64, - to: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - withdraw_authority: &KeyedAccount, - custodian: Option<&KeyedAccount>, - feature_set: &FeatureSet, - ) -> Result<(), InstructionError>; +pub fn initialize( + stake_account: &KeyedAccount, + authorized: &Authorized, + lockup: &Lockup, + rent: &Rent, + feature_set: &FeatureSet, +) -> Result<(), InstructionError> { + if stake_account.data_len()? != std::mem::size_of::() { + return Err(InstructionError::InvalidAccountData); + } + if let StakeState::Uninitialized = stake_account.state()? { + let rent_exempt_reserve = rent.minimum_balance(stake_account.data_len()?); + let minimum_delegation = crate::get_minimum_delegation(feature_set); + let minimum_balance = rent_exempt_reserve + minimum_delegation; + + if stake_account.lamports()? >= minimum_balance { + stake_account.set_state(&StakeState::Initialized(Meta { + rent_exempt_reserve, + authorized: *authorized, + lockup: *lockup, + })) + } else { + Err(InstructionError::InsufficientFunds) + } + } else { + Err(InstructionError::InvalidAccountData) + } } -impl<'a> StakeAccount for KeyedAccount<'a> { - fn initialize( - &self, - 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_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 { - rent_exempt_reserve, - authorized: *authorized, - lockup: *lockup, - })) - } else { - Err(InstructionError::InsufficientFunds) - } - } else { - Err(InstructionError::InvalidAccountData) - } - } - - /// Authorize the given pubkey to manage stake (deactivate, withdraw). This may be called - /// multiple times, but will implicitly withdraw authorization from the previously authorized - /// staker. The default staker is the owner of the stake account's pubkey. - fn authorize( - &self, - signers: &HashSet, - new_authority: &Pubkey, - stake_authorize: StakeAuthorize, - require_custodian_for_locked_stake_authorize: bool, - clock: &Clock, - custodian: Option<&Pubkey>, - ) -> Result<(), InstructionError> { - match self.state()? { - StakeState::Stake(mut meta, stake) => { - meta.authorized.authorize( - signers, - new_authority, - stake_authorize, - if require_custodian_for_locked_stake_authorize { - Some((&meta.lockup, clock, custodian)) - } else { - None - }, - )?; - self.set_state(&StakeState::Stake(meta, stake)) - } - StakeState::Initialized(mut meta) => { - meta.authorized.authorize( - signers, - new_authority, - stake_authorize, - if require_custodian_for_locked_stake_authorize { - Some((&meta.lockup, clock, custodian)) - } else { - None - }, - )?; - self.set_state(&StakeState::Initialized(meta)) - } - _ => Err(InstructionError::InvalidAccountData), - } - } - fn authorize_with_seed( - &self, - authority_base: &KeyedAccount, - authority_seed: &str, - authority_owner: &Pubkey, - new_authority: &Pubkey, - stake_authorize: StakeAuthorize, - require_custodian_for_locked_stake_authorize: bool, - clock: &Clock, - custodian: Option<&Pubkey>, - ) -> Result<(), InstructionError> { - let mut signers = HashSet::default(); - if let Some(base_pubkey) = authority_base.signer_key() { - signers.insert(Pubkey::create_with_seed( - base_pubkey, - authority_seed, - authority_owner, - )?); - } - self.authorize( - &signers, - new_authority, - stake_authorize, - require_custodian_for_locked_stake_authorize, - clock, - custodian, - ) - } - fn delegate( - &self, - vote_account: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - config: &Config, - signers: &HashSet, - ) -> Result<(), InstructionError> { - if vote_account.owner()? != solana_vote_program::id() { - return Err(InstructionError::IncorrectProgramId); - } - - match self.state()? { - StakeState::Initialized(meta) => { - meta.authorized.check(signers, StakeAuthorize::Staker)?; - let ValidatedDelegatedInfo { stake_amount } = - validate_delegated_amount(self, &meta)?; - let stake = new_stake( - stake_amount, - vote_account.unsigned_key(), - &State::::state(vote_account)?.convert_to_current(), - clock.epoch, - config, - ); - self.set_state(&StakeState::Stake(meta, stake)) - } - StakeState::Stake(meta, mut stake) => { - meta.authorized.check(signers, StakeAuthorize::Staker)?; - let ValidatedDelegatedInfo { stake_amount } = - validate_delegated_amount(self, &meta)?; - redelegate( - &mut stake, - stake_amount, - vote_account.unsigned_key(), - &State::::state(vote_account)?.convert_to_current(), - clock, - stake_history, - config, - )?; - self.set_state(&StakeState::Stake(meta, stake)) - } - _ => Err(InstructionError::InvalidAccountData), - } - } - fn deactivate(&self, clock: &Clock, signers: &HashSet) -> Result<(), InstructionError> { - if let StakeState::Stake(meta, mut stake) = self.state()? { - meta.authorized.check(signers, StakeAuthorize::Staker)?; - stake.deactivate(clock.epoch)?; - - self.set_state(&StakeState::Stake(meta, stake)) - } else { - Err(InstructionError::InvalidAccountData) - } - } - fn set_lockup( - &self, - lockup: &LockupArgs, - signers: &HashSet, - clock: &Clock, - ) -> Result<(), InstructionError> { - match self.state()? { - StakeState::Initialized(mut meta) => { - meta.set_lockup(lockup, signers, clock)?; - self.set_state(&StakeState::Initialized(meta)) - } - StakeState::Stake(mut meta, stake) => { - meta.set_lockup(lockup, signers, clock)?; - self.set_state(&StakeState::Stake(meta, stake)) - } - _ => Err(InstructionError::InvalidAccountData), - } - } - - fn split( - &self, - invoke_context: &InvokeContext, - lamports: u64, - split: &KeyedAccount, - signers: &HashSet, - ) -> Result<(), InstructionError> { - if split.owner()? != id() { - return Err(InstructionError::IncorrectProgramId); - } - if split.data_len()? != std::mem::size_of::() { - return Err(InstructionError::InvalidAccountData); - } - if !matches!(split.state()?, StakeState::Uninitialized) { - return Err(InstructionError::InvalidAccountData); - } - if lamports > self.lamports()? { - return Err(InstructionError::InsufficientFunds); - } - - match self.state()? { - StakeState::Stake(meta, mut stake) => { - meta.authorized.check(signers, StakeAuthorize::Staker)?; - let validated_split_info = validate_split_amount( - invoke_context, - self, - split, - lamports, - &meta, - Some(&stake), - )?; - - // 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 validated_split_info.source_remaining_balance == 0 { - // If split amount equals the full source stake (as implied by 0 - // source_remaining_balance), 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.saturating_sub( - validated_split_info - .destination_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 = - validated_split_info.destination_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 validated_split_info = - validate_split_amount(invoke_context, self, split, lamports, &meta, None)?; - let mut split_meta = meta; - split_meta.rent_exempt_reserve = - validated_split_info.destination_rent_exempt_reserve; - split.set_state(&StakeState::Initialized(split_meta))?; - } - StakeState::Uninitialized => { - if !signers.contains(self.unsigned_key()) { - return Err(InstructionError::MissingRequiredSignature); - } - } - _ => return Err(InstructionError::InvalidAccountData), - } - - // Deinitialize state upon zero balance - if lamports == self.lamports()? { - self.set_state(&StakeState::Uninitialized)?; - } - - split - .try_account_ref_mut()? - .checked_add_lamports(lamports)?; - self.try_account_ref_mut()?.checked_sub_lamports(lamports)?; - Ok(()) - } - - fn merge( - &self, - invoke_context: &InvokeContext, - source_account: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - signers: &HashSet, - ) -> Result<(), InstructionError> { - // Ensure source isn't spoofed - if source_account.owner()? != id() { - return Err(InstructionError::IncorrectProgramId); - } - // Close the self-reference loophole - if source_account.unsigned_key() == self.unsigned_key() { - return Err(InstructionError::InvalidArgument); - } - - ic_msg!(invoke_context, "Checking if destination stake is mergeable"); - let stake_merge_kind = - MergeKind::get_if_mergeable(invoke_context, self, clock, stake_history)?; - let meta = stake_merge_kind.meta(); - - // Authorized staker is allowed to split/merge accounts - meta.authorized.check(signers, StakeAuthorize::Staker)?; - - ic_msg!(invoke_context, "Checking if source stake is mergeable"); - let source_merge_kind = - MergeKind::get_if_mergeable(invoke_context, source_account, clock, stake_history)?; - - ic_msg!(invoke_context, "Merging stake accounts"); - if let Some(merged_state) = - stake_merge_kind.merge(invoke_context, source_merge_kind, clock)? - { - self.set_state(&merged_state)?; - } - - // Source is about to be drained, deinitialize its state - source_account.set_state(&StakeState::Uninitialized)?; - - // Drain the source stake account - let lamports = source_account.lamports()?; - source_account - .try_account_ref_mut()? - .checked_sub_lamports(lamports)?; - self.try_account_ref_mut()?.checked_add_lamports(lamports)?; - Ok(()) - } - - fn withdraw( - &self, - lamports: u64, - to: &KeyedAccount, - clock: &Clock, - 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 - .signer_key() - .ok_or(InstructionError::MissingRequiredSignature)?; - signers.insert(*withdraw_authority_pubkey); - - let (lockup, reserve, is_staked) = match self.state()? { - StakeState::Stake(meta, stake) => { - meta.authorized - .check(&signers, StakeAuthorize::Withdrawer)?; - // if we have a deactivation epoch and we're in cooldown - let staked = if clock.epoch >= stake.delegation.deactivation_epoch { - stake.delegation.stake(clock.epoch, Some(stake_history)) +/// Authorize the given pubkey to manage stake (deactivate, withdraw). This may be called +/// multiple times, but will implicitly withdraw authorization from the previously authorized +/// staker. The default staker is the owner of the stake account's pubkey. +pub fn authorize( + stake_account: &KeyedAccount, + signers: &HashSet, + new_authority: &Pubkey, + stake_authorize: StakeAuthorize, + require_custodian_for_locked_stake_authorize: bool, + clock: &Clock, + custodian: Option<&Pubkey>, +) -> Result<(), InstructionError> { + match stake_account.state()? { + StakeState::Stake(mut meta, stake) => { + meta.authorized.authorize( + signers, + new_authority, + stake_authorize, + if require_custodian_for_locked_stake_authorize { + Some((&meta.lockup, clock, custodian)) } else { - // Assume full stake if the stake account hasn't been - // de-activated, because in the future the exposed stake - // might be higher than stake.stake() due to warmup - stake.delegation.stake - }; - - let staked_and_reserve = checked_add(staked, meta.rent_exempt_reserve)?; - (meta.lockup, staked_and_reserve, staked != 0) - } - 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), - )?; - - (meta.lockup, reserve, false) - } - StakeState::Uninitialized => { - if !signers.contains(self.unsigned_key()) { - return Err(InstructionError::MissingRequiredSignature); - } - (Lockup::default(), 0, false) // no lockup, no restrictions - } - _ => return Err(InstructionError::InvalidAccountData), - }; - - // verify that lockup has expired or that the withdrawal is signed by - // the custodian, both epoch and unix_timestamp must have passed - let custodian_pubkey = custodian.and_then(|keyed_account| keyed_account.signer_key()); - if lockup.is_in_force(clock, custodian_pubkey) { - return Err(StakeError::LockupInForce.into()); + None + }, + )?; + stake_account.set_state(&StakeState::Stake(meta, stake)) } - - let lamports_and_reserve = checked_add(lamports, reserve)?; - // if the stake is active, we mustn't allow the account to go away - if is_staked // line coverage for branch coverage - && lamports_and_reserve > self.lamports()? - { - return Err(InstructionError::InsufficientFunds); + StakeState::Initialized(mut meta) => { + meta.authorized.authorize( + signers, + new_authority, + stake_authorize, + if require_custodian_for_locked_stake_authorize { + Some((&meta.lockup, clock, custodian)) + } else { + None + }, + )?; + stake_account.set_state(&StakeState::Initialized(meta)) } - - if lamports != self.lamports()? // not a full withdrawal - && lamports_and_reserve > self.lamports()? - { - assert!(!is_staked); - return Err(InstructionError::InsufficientFunds); - } - - // Deinitialize state upon zero balance - if lamports == self.lamports()? { - self.set_state(&StakeState::Uninitialized)?; - } - - self.try_account_ref_mut()?.checked_sub_lamports(lamports)?; - to.try_account_ref_mut()?.checked_add_lamports(lamports)?; - Ok(()) + _ => Err(InstructionError::InvalidAccountData), } } +pub fn authorize_with_seed( + stake_account: &KeyedAccount, + authority_base: &KeyedAccount, + authority_seed: &str, + authority_owner: &Pubkey, + new_authority: &Pubkey, + stake_authorize: StakeAuthorize, + require_custodian_for_locked_stake_authorize: bool, + clock: &Clock, + custodian: Option<&Pubkey>, +) -> Result<(), InstructionError> { + let mut signers = HashSet::default(); + if let Some(base_pubkey) = authority_base.signer_key() { + signers.insert(Pubkey::create_with_seed( + base_pubkey, + authority_seed, + authority_owner, + )?); + } + authorize( + stake_account, + &signers, + new_authority, + stake_authorize, + require_custodian_for_locked_stake_authorize, + clock, + custodian, + ) +} + +pub fn delegate( + stake_account: &KeyedAccount, + vote_account: &KeyedAccount, + clock: &Clock, + stake_history: &StakeHistory, + config: &Config, + signers: &HashSet, +) -> Result<(), InstructionError> { + if vote_account.owner()? != solana_vote_program::id() { + return Err(InstructionError::IncorrectProgramId); + } + + match stake_account.state()? { + StakeState::Initialized(meta) => { + meta.authorized.check(signers, StakeAuthorize::Staker)?; + let ValidatedDelegatedInfo { stake_amount } = + validate_delegated_amount(stake_account, &meta)?; + let stake = new_stake( + stake_amount, + vote_account.unsigned_key(), + &State::::state(vote_account)?.convert_to_current(), + clock.epoch, + config, + ); + stake_account.set_state(&StakeState::Stake(meta, stake)) + } + StakeState::Stake(meta, mut stake) => { + meta.authorized.check(signers, StakeAuthorize::Staker)?; + let ValidatedDelegatedInfo { stake_amount } = + validate_delegated_amount(stake_account, &meta)?; + redelegate( + &mut stake, + stake_amount, + vote_account.unsigned_key(), + &State::::state(vote_account)?.convert_to_current(), + clock, + stake_history, + config, + )?; + stake_account.set_state(&StakeState::Stake(meta, stake)) + } + _ => Err(InstructionError::InvalidAccountData), + } +} + +pub fn deactivate( + stake_account: &KeyedAccount, + clock: &Clock, + signers: &HashSet, +) -> Result<(), InstructionError> { + if let StakeState::Stake(meta, mut stake) = stake_account.state()? { + meta.authorized.check(signers, StakeAuthorize::Staker)?; + stake.deactivate(clock.epoch)?; + + stake_account.set_state(&StakeState::Stake(meta, stake)) + } else { + Err(InstructionError::InvalidAccountData) + } +} + +pub fn set_lockup( + stake_account: &KeyedAccount, + lockup: &LockupArgs, + signers: &HashSet, + clock: &Clock, +) -> Result<(), InstructionError> { + match stake_account.state()? { + StakeState::Initialized(mut meta) => { + meta.set_lockup(lockup, signers, clock)?; + stake_account.set_state(&StakeState::Initialized(meta)) + } + StakeState::Stake(mut meta, stake) => { + meta.set_lockup(lockup, signers, clock)?; + stake_account.set_state(&StakeState::Stake(meta, stake)) + } + _ => Err(InstructionError::InvalidAccountData), + } +} + +pub fn split( + stake_account: &KeyedAccount, + invoke_context: &InvokeContext, + lamports: u64, + split: &KeyedAccount, + signers: &HashSet, +) -> Result<(), InstructionError> { + if split.owner()? != id() { + return Err(InstructionError::IncorrectProgramId); + } + if split.data_len()? != std::mem::size_of::() { + return Err(InstructionError::InvalidAccountData); + } + if !matches!(split.state()?, StakeState::Uninitialized) { + return Err(InstructionError::InvalidAccountData); + } + if lamports > stake_account.lamports()? { + return Err(InstructionError::InsufficientFunds); + } + + match stake_account.state()? { + StakeState::Stake(meta, mut stake) => { + meta.authorized.check(signers, StakeAuthorize::Staker)?; + let validated_split_info = validate_split_amount( + invoke_context, + stake_account, + split, + lamports, + &meta, + Some(&stake), + )?; + + // 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 validated_split_info.source_remaining_balance == 0 { + // If split amount equals the full source stake (as implied by 0 + // source_remaining_balance), 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.saturating_sub( + validated_split_info + .destination_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 = validated_split_info.destination_rent_exempt_reserve; + + stake_account.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 validated_split_info = + validate_split_amount(invoke_context, stake_account, split, lamports, &meta, None)?; + let mut split_meta = meta; + split_meta.rent_exempt_reserve = validated_split_info.destination_rent_exempt_reserve; + split.set_state(&StakeState::Initialized(split_meta))?; + } + StakeState::Uninitialized => { + if !signers.contains(stake_account.unsigned_key()) { + return Err(InstructionError::MissingRequiredSignature); + } + } + _ => return Err(InstructionError::InvalidAccountData), + } + + // Deinitialize state upon zero balance + if lamports == stake_account.lamports()? { + stake_account.set_state(&StakeState::Uninitialized)?; + } + + split + .try_account_ref_mut()? + .checked_add_lamports(lamports)?; + stake_account + .try_account_ref_mut()? + .checked_sub_lamports(lamports)?; + Ok(()) +} + +pub fn merge( + stake_account: &KeyedAccount, + invoke_context: &InvokeContext, + source_account: &KeyedAccount, + clock: &Clock, + stake_history: &StakeHistory, + signers: &HashSet, +) -> Result<(), InstructionError> { + // Ensure source isn't spoofed + if source_account.owner()? != id() { + return Err(InstructionError::IncorrectProgramId); + } + // Close the stake_account-reference loophole + if source_account.unsigned_key() == stake_account.unsigned_key() { + return Err(InstructionError::InvalidArgument); + } + + ic_msg!(invoke_context, "Checking if destination stake is mergeable"); + let stake_merge_kind = + MergeKind::get_if_mergeable(invoke_context, stake_account, clock, stake_history)?; + let meta = stake_merge_kind.meta(); + + // Authorized staker is allowed to split/merge accounts + meta.authorized.check(signers, StakeAuthorize::Staker)?; + + ic_msg!(invoke_context, "Checking if source stake is mergeable"); + let source_merge_kind = + MergeKind::get_if_mergeable(invoke_context, source_account, clock, stake_history)?; + + ic_msg!(invoke_context, "Merging stake accounts"); + if let Some(merged_state) = stake_merge_kind.merge(invoke_context, source_merge_kind, clock)? { + stake_account.set_state(&merged_state)?; + } + + // Source is about to be drained, deinitialize its state + source_account.set_state(&StakeState::Uninitialized)?; + + // Drain the source stake account + let lamports = source_account.lamports()?; + source_account + .try_account_ref_mut()? + .checked_sub_lamports(lamports)?; + stake_account + .try_account_ref_mut()? + .checked_add_lamports(lamports)?; + Ok(()) +} + +pub fn withdraw( + stake_account: &KeyedAccount, + lamports: u64, + to: &KeyedAccount, + clock: &Clock, + 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 + .signer_key() + .ok_or(InstructionError::MissingRequiredSignature)?; + signers.insert(*withdraw_authority_pubkey); + + let (lockup, reserve, is_staked) = match stake_account.state()? { + StakeState::Stake(meta, stake) => { + meta.authorized + .check(&signers, StakeAuthorize::Withdrawer)?; + // if we have a deactivation epoch and we're in cooldown + let staked = if clock.epoch >= stake.delegation.deactivation_epoch { + stake.delegation.stake(clock.epoch, Some(stake_history)) + } else { + // Assume full stake if the stake account hasn't been + // de-activated, because in the future the exposed stake + // might be higher than stake.stake() due to warmup + stake.delegation.stake + }; + + let staked_and_reserve = checked_add(staked, meta.rent_exempt_reserve)?; + (meta.lockup, staked_and_reserve, staked != 0) + } + 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), + )?; + + (meta.lockup, reserve, false) + } + StakeState::Uninitialized => { + if !signers.contains(stake_account.unsigned_key()) { + return Err(InstructionError::MissingRequiredSignature); + } + (Lockup::default(), 0, false) // no lockup, no restrictions + } + _ => return Err(InstructionError::InvalidAccountData), + }; + + // verify that lockup has expired or that the withdrawal is signed by + // the custodian, both epoch and unix_timestamp must have passed + let custodian_pubkey = custodian.and_then(|keyed_account| keyed_account.signer_key()); + if lockup.is_in_force(clock, custodian_pubkey) { + return Err(StakeError::LockupInForce.into()); + } + + let lamports_and_reserve = checked_add(lamports, reserve)?; + // if the stake is active, we mustn't allow the account to go away + if is_staked // line coverage for branch coverage + && lamports_and_reserve > stake_account.lamports()? + { + return Err(InstructionError::InsufficientFunds); + } + + if lamports != stake_account.lamports()? // not a full withdrawal + && lamports_and_reserve > stake_account.lamports()? + { + assert!(!is_staked); + return Err(InstructionError::InsufficientFunds); + } + + // Deinitialize state upon zero balance + if lamports == stake_account.lamports()? { + stake_account.set_state(&StakeState::Uninitialized)?; + } + + stake_account + .try_account_ref_mut()? + .checked_sub_lamports(lamports)?; + to.try_account_ref_mut()?.checked_add_lamports(lamports)?; + Ok(()) +} + /// After calling `validate_delegated_amount()`, this struct contains calculated values that are used /// by the caller. struct ValidatedDelegatedInfo {