From f46f5ee9d1d26e949b7c786cc8074763ea6e018c Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Tue, 28 Jun 2022 10:39:00 -0700 Subject: [PATCH] Add StakeInstruction::Redelegate --- programs/stake/src/stake_instruction.rs | 45 +++++++++++- programs/stake/src/stake_state.rs | 95 ++++++++++++++++++++++++- sdk/program/src/stake/instruction.rs | 80 +++++++++++++++++++++ sdk/src/feature_set.rs | 5 ++ transaction-status/src/parse_stake.rs | 12 ++++ 5 files changed, 234 insertions(+), 3 deletions(-) diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index ab1cb34efaf588..be9d7f9e620da5 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -3,7 +3,7 @@ use { config, stake_state::{ authorize, authorize_with_seed, deactivate, deactivate_delinquent, delegate, - initialize, merge, set_lockup, split, withdraw, + initialize, merge, redelegate, set_lockup, split, withdraw, }, }, log::*, @@ -424,6 +424,49 @@ pub fn process_instruction( Err(InstructionError::InvalidInstructionData) } } + Ok(StakeInstruction::Redelegate) => { + if invoke_context + .feature_set + .is_active(&feature_set::stake_redelegate_instruction::id()) + { + let mut me = get_stake_account()?; + instruction_context.check_number_of_instruction_accounts(3)?; + let clock = invoke_context.get_sysvar_cache().get_clock()?; + let stake_history = get_sysvar_with_account_check::stake_history( + invoke_context, + instruction_context, + 3, + )?; + let config_account = + instruction_context.try_borrow_instruction_account(transaction_context, 4)?; + if !config::check_id(config_account.get_key()) { + return Err(InstructionError::InvalidArgument); + } + let config = + config::from(&config_account).ok_or(InstructionError::InvalidArgument)?; + drop(config_account); + + redelegate( + invoke_context, + transaction_context, + instruction_context, + &mut me, + 1, + 2, + &clock, + &stake_history, + &config, + &signers, + ) + } else { + if !invoke_context.feature_set.is_active( + &feature_set::add_get_minimum_delegation_instruction_to_stake_program::id(), + ) { + let _ = get_stake_account()?; + } + Err(InstructionError::InvalidInstructionData) + } + } Err(err) => { if !invoke_context.feature_set.is_active( &feature_set::add_get_minimum_delegation_instruction_to_stake_program::id(), diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 539dc83152417f..ef3c7451160082 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -94,7 +94,7 @@ pub fn meta_from(account: &AccountSharedData) -> Option { from(account).and_then(|state: StakeState| state.meta()) } -fn redelegate( +fn redelegate_stake( stake: &mut Stake, stake_lamports: u64, voter_pubkey: &Pubkey, @@ -596,7 +596,7 @@ pub fn delegate( meta.authorized.check(signers, StakeAuthorize::Staker)?; let ValidatedDelegatedInfo { stake_amount } = validate_delegated_amount(&stake_account, &meta, feature_set)?; - redelegate( + redelegate_stake( &mut stake, stake_amount, &vote_pubkey, @@ -856,6 +856,97 @@ pub fn merge( Ok(()) } +#[allow(clippy::too_many_arguments)] +pub fn redelegate( + invoke_context: &InvokeContext, + transaction_context: &TransactionContext, + instruction_context: &InstructionContext, + stake_account: &mut BorrowedAccount, + uninitialized_stake_account_index: usize, + vote_account_index: usize, + clock: &Clock, + stake_history: &StakeHistory, + config: &Config, + signers: &HashSet, +) -> Result<(), InstructionError> { + // ensure `uninitialized_stake_account_index` is in the uninitialized state + let mut uninitialized_stake_account = instruction_context + .try_borrow_instruction_account(transaction_context, uninitialized_stake_account_index)?; + if *uninitialized_stake_account.get_owner() != id() { + return Err(InstructionError::IncorrectProgramId); + } + if uninitialized_stake_account.get_data().len() != StakeState::size_of() { + return Err(InstructionError::InvalidAccountData); + } + if !matches!( + uninitialized_stake_account.get_state()?, + StakeState::Uninitialized + ) { + return Err(InstructionError::InvalidAccountData); + } + + // validate the provided vote account + let vote_account = instruction_context + .try_borrow_instruction_account(transaction_context, vote_account_index)?; + if *vote_account.get_owner() != solana_vote_program::id() { + return Err(InstructionError::IncorrectProgramId); + } + let vote_pubkey = *vote_account.get_key(); + let vote_state = vote_account.get_state::(); + + // ensure `stake_account` is active, check for presence of the stake authority, and + // extract its Meta + let meta = if let StakeState::Stake(meta, stake) = stake_account.get_state()? { + meta.authorized.check(signers, StakeAuthorize::Staker)?; + + let status = stake + .delegation + .stake_activating_and_deactivating(clock.epoch, Some(stake_history)); + if status.effective == 0 || status.activating != 0 || status.deactivating != 0 { + return Err(InstructionError::InvalidAccountData); + } + + // Deny redelegating to the same vote account. This is nonsensical and could be used to + // grief the global stake warm-up/cool-down rate + if stake.delegation.voter_pubkey == vote_pubkey { + return Err(InstructionError::InvalidArgument); + } + + meta + } else { + return Err(InstructionError::InvalidAccountData); + }; + + // deactivate `stake_account` + deactivate(stake_account, clock, signers)?; + + // transfer all but the `rent_exempt_reserve` balance to the uninitialized stake account + let redelegation_amount = stake_account + .get_lamports() + .checked_sub(meta.rent_exempt_reserve) + .ok_or(InstructionError::InsufficientFunds)?; + + stake_account.checked_sub_lamports(redelegation_amount)?; + uninitialized_stake_account.checked_add_lamports(redelegation_amount)?; + + // initialize and delegate `uninitialized_stake_account` + let ValidatedDelegatedInfo { stake_amount } = validate_delegated_amount( + &uninitialized_stake_account, + &meta, + &invoke_context.feature_set, + )?; + let stake = new_stake( + stake_amount, + &vote_pubkey, + &vote_state?.convert_to_current(), + clock.epoch, + config, + ); + uninitialized_stake_account.set_state(&StakeState::Stake(meta, stake))?; + + Ok(()) +} + #[allow(clippy::too_many_arguments)] pub fn withdraw( transaction_context: &TransactionContext, diff --git a/sdk/program/src/stake/instruction.rs b/sdk/program/src/stake/instruction.rs index 9a7601a640807f..d0e3346a0a238e 100644 --- a/sdk/program/src/stake/instruction.rs +++ b/sdk/program/src/stake/instruction.rs @@ -261,6 +261,25 @@ pub enum StakeInstruction { /// 2. `[]` Reference vote account that has voted at least once in the last /// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` epochs DeactivateDelinquent, + + /// Redelegate activated stake to another vote account. + /// + /// Upon success: + /// * the balance of the delegated stake account will be reduced to the minimum delegation + /// amount, and scheduled for deactivation. + /// * the provided uninitialized stake account will hold the entire balance minus the minimum + /// delegation amount, and scheduled for activation. + /// + /// # Account references + /// 0. `[WRITE]` Delegated stake account to be redelegated. The account must be fully + /// activated and carry a balance equal to twice the minimum delegation amount or greater + /// 1. `[WRITE]` Uninitialized stake account that will hold the redelegated stake + /// 2. `[]` Vote account to which this stake will be re-delegated + /// 3. `[]` Stake history sysvar that carries stake warmup/cooldown history + /// 4. `[]` Address of config account that carries stake config + /// 5. `[SIGNER]` Stake authority + /// + Redelegate, } #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] @@ -738,6 +757,67 @@ pub fn deactivate_delinquent_stake( Instruction::new_with_bincode(id(), &StakeInstruction::DeactivateDelinquent, account_metas) } +fn _redelegate( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + vote_pubkey: &Pubkey, + uninitialized_stake_pubkey: &Pubkey, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new(*uninitialized_stake_pubkey, false), + AccountMeta::new_readonly(*vote_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::stake_history::id(), false), + AccountMeta::new_readonly(config::id(), false), + AccountMeta::new_readonly(*authorized_pubkey, true), + ]; + Instruction::new_with_bincode(id(), &StakeInstruction::Redelegate, account_metas) +} + +pub fn redelegate( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + vote_pubkey: &Pubkey, + uninitialized_stake_pubkey: &Pubkey, +) -> Vec { + vec![ + system_instruction::allocate(uninitialized_stake_pubkey, StakeState::size_of() as u64), + system_instruction::assign(uninitialized_stake_pubkey, &id()), + _redelegate( + stake_pubkey, + uninitialized_stake_pubkey, + authorized_pubkey, + vote_pubkey, + ), + ] +} + +pub fn redelegate_with_seed( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + vote_pubkey: &Pubkey, + uninitialized_stake_pubkey: &Pubkey, // derived using create_with_seed() + base: &Pubkey, // base + seed: &str, // seed +) -> Vec { + vec![ + system_instruction::allocate_with_seed( + uninitialized_stake_pubkey, + base, + seed, + StakeState::size_of() as u64, + &id(), + ), + _redelegate( + stake_pubkey, + uninitialized_stake_pubkey, + authorized_pubkey, + vote_pubkey, + ), + ] +} + #[cfg(test)] mod tests { use {super::*, crate::instruction::InstructionError}; diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index fbb226c6cd2e7a..c22ac8ba466baf 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -283,6 +283,10 @@ pub mod stake_deactivate_delinquent_instruction { solana_sdk::declare_id!("437r62HoAdUb63amq3D7ENnBLDhHT2xY8eFkLJYVKK4x"); } +pub mod stake_redelegate_instruction { + solana_sdk::declare_id!("3EPmAX94PvVJCjMeFfRFvj4avqCPL8vv3TGsZQg7ydMx"); +} + pub mod vote_withdraw_authority_may_change_authorized_voter { solana_sdk::declare_id!("AVZS3ZsN4gi6Rkx2QUibYuSJG3S6QHib7xCYhG6vGJxU"); } @@ -549,6 +553,7 @@ lazy_static! { (nonce_must_be_advanceable::id(), "durable nonces must be advanceable"), (vote_authorize_with_seed::id(), "An instruction you can use to change a vote accounts authority when the current authority is a derived key #25860"), (cap_accounts_data_size_per_block::id(), "cap the accounts data size per block #25517"), + (stake_redelegate_instruction::id(), "enable the redelegate stake instruction #TBD"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/transaction-status/src/parse_stake.rs b/transaction-status/src/parse_stake.rs index f1f9ff86fa8438..7483f64fab52f9 100644 --- a/transaction-status/src/parse_stake.rs +++ b/transaction-status/src/parse_stake.rs @@ -284,6 +284,18 @@ pub fn parse_stake( }), }) } + StakeInstruction::Redelegate => { + check_num_stake_accounts(&instruction.accounts, 4)?; + Ok(ParsedInstructionEnum { + instruction_type: "redelegate".to_string(), + info: json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "newStakeAccount": account_keys[instruction.accounts[1] as usize].to_string(), + "voteAccount": account_keys[instruction.accounts[2] as usize].to_string(), + "stakeAuthority": account_keys[instruction.accounts[5] as usize].to_string(), + }), + }) + } } }