From 48862c575a1f62f2d010d7568cbe0279b2e51f62 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 | 657 +++++++++++++++++++++++- programs/stake/src/stake_state.rs | 143 +++++- sdk/program/src/stake/instruction.rs | 87 ++++ sdk/src/feature_set.rs | 5 + transaction-status/src/parse_stake.rs | 13 + 5 files changed, 900 insertions(+), 5 deletions(-) diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 3d10506212..a0de27299a 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::*, @@ -177,6 +177,7 @@ pub fn process_instruction( let config = config::from(&config_account).ok_or(InstructionError::InvalidArgument)?; drop(config_account); delegate( + invoke_context, transaction_context, instruction_context, 0, @@ -424,6 +425,36 @@ pub fn process_instruction( Err(InstructionError::InvalidInstructionData) } } + Ok(StakeInstruction::Redelegate) => { + let mut me = get_stake_account()?; + if invoke_context + .feature_set + .is_active(&feature_set::stake_redelegate_instruction::id()) + { + instruction_context.check_number_of_instruction_accounts(3)?; + let config_account = + instruction_context.try_borrow_instruction_account(transaction_context, 3)?; + 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, + &config, + &signers, + ) + } else { + Err(InstructionError::InvalidInstructionData) + } + } Err(err) => { if !invoke_context.feature_set.is_active( &feature_set::add_get_minimum_delegation_instruction_to_stake_program::id(), @@ -463,14 +494,19 @@ mod tests { set_lockup_checked, AuthorizeCheckedWithSeedArgs, AuthorizeWithSeedArgs, LockupArgs, StakeError, }, - state::{Authorized, Lockup, StakeAuthorize}, + state::{Authorized, Lockup, StakeActivationStatus, StakeAuthorize}, MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION, }, stake_history::{StakeHistory, StakeHistoryEntry}, system_program, sysvar, }, solana_vote_program::vote_state::{self, VoteState, VoteStateVersions}, - std::{borrow::BorrowMut, collections::HashSet, str::FromStr, sync::Arc}, + std::{ + borrow::{Borrow, BorrowMut}, + collections::HashSet, + str::FromStr, + sync::Arc, + }, test_case::test_case, }; @@ -853,6 +889,16 @@ mod tests { ), Err(InstructionError::InvalidAccountOwner), ); + process_instruction_as_one_arg( + &feature_set, + &instruction::redelegate( + &spoofed_stake_state_pubkey(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + )[2], + Err(InstructionError::InvalidAccountOwner), + ); } #[test_case(feature_set_old_behavior(); "old_behavior")] @@ -6823,4 +6869,609 @@ mod tests { Err(StakeError::MinimumDelinquentEpochsForDeactivationNotMet.into()), ); } + + #[test_case(feature_set_old_behavior(); "old_behavior")] + #[test_case(feature_set_new_behavior(); "new_behavior")] + fn test_redelegate(feature_set: FeatureSet) { + let feature_set = Arc::new(feature_set); + + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; + + let mut sysvar_cache_override = SysvarCache::default(); + sysvar_cache_override.set_stake_history(stake_history.clone()); + sysvar_cache_override.set_rent(rent); + sysvar_cache_override.set_clock(Clock { + epoch: current_epoch, + ..Clock::default() + }); + + let authorized_staker = Pubkey::new_unique(); + let vote_address = Pubkey::new_unique(); + let new_vote_address = Pubkey::new_unique(); + let stake_address = Pubkey::new_unique(); + let uninitialized_stake_address = Pubkey::new_unique(); + + let prepare_stake_account = |activation_epoch, expected_stake_activation_status| { + let initial_stake_delegation = minimum_delegation + rent_exempt_reserve; + let initial_stake_state = StakeState::Stake( + Meta { + authorized: Authorized { + staker: authorized_staker, + withdrawer: Pubkey::new_unique(), + }, + rent_exempt_reserve, + ..Meta::default() + }, + new_stake( + initial_stake_delegation, + &vote_address, + &VoteState::default(), + activation_epoch, + &stake_config::Config::default(), + ), + ); + + if let Some(expected_stake_activation_status) = expected_stake_activation_status { + assert_eq!( + expected_stake_activation_status, + initial_stake_state + .delegation() + .unwrap() + .stake_activating_and_deactivating(current_epoch, Some(&stake_history)) + ); + } + + AccountSharedData::new_data_with_space( + rent_exempt_reserve + initial_stake_delegation, /* lamports */ + &initial_stake_state, + StakeState::size_of(), + &id(), + ) + .unwrap() + }; + + let new_vote_account = AccountSharedData::new_data_with_space( + 1, /* lamports */ + &VoteStateVersions::new_current(VoteState::default()), + VoteState::size_of(), + &solana_vote_program::id(), + ) + .unwrap(); + + let process_instruction_redelegate = + |stake_address: &Pubkey, + stake_account: &AccountSharedData, + authorized_staker: &Pubkey, + vote_address: &Pubkey, + vote_account: &AccountSharedData, + uninitialized_stake_address: &Pubkey, + uninitialized_stake_account: &AccountSharedData, + expected_result| { + process_instruction_with_overrides( + &serialize(&StakeInstruction::Redelegate).unwrap(), + vec![ + (*stake_address, stake_account.clone()), + ( + *uninitialized_stake_address, + uninitialized_stake_account.clone(), + ), + (*vote_address, vote_account.clone()), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + (*authorized_staker, AccountSharedData::default()), + ], + vec![ + AccountMeta { + pubkey: *stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: *uninitialized_stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: *vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: *authorized_staker, + is_signer: true, + is_writable: false, + }, + ], + Some(&sysvar_cache_override), + Some(Arc::clone(&feature_set)), + expected_result, + ) + }; + + // + // Failure: incorrect authorized staker + // + let stake_account = prepare_stake_account(0 /*activation_epoch*/, None); + let uninitialized_stake_account = + AccountSharedData::new(0 /* lamports */, StakeState::size_of(), &id()); + + let _ = process_instruction_redelegate( + &stake_address, + &stake_account, + &Pubkey::new_unique(), // <-- Incorrect authorized staker + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &uninitialized_stake_account, + Err(InstructionError::MissingRequiredSignature), + ); + + // + // Success: normal case + // + let output_accounts = process_instruction_redelegate( + &stake_address, + &stake_account, + &authorized_staker, + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &uninitialized_stake_account, + Ok(()), + ); + + assert_eq!(output_accounts[0].lamports(), rent_exempt_reserve); + if let StakeState::Stake(meta, stake) = + output_accounts[0].borrow().deserialize_data().unwrap() + { + assert_eq!(meta.rent_exempt_reserve, rent_exempt_reserve); + assert_eq!( + stake.delegation.stake, + minimum_delegation + rent_exempt_reserve + ); + assert_eq!(stake.delegation.activation_epoch, 0); + assert_eq!(stake.delegation.deactivation_epoch, current_epoch); + } else { + panic!("Invalid output_accounts[0] data"); + } + assert_eq!( + output_accounts[1].lamports(), + minimum_delegation + rent_exempt_reserve + ); + if let StakeState::Stake(meta, stake) = + output_accounts[1].borrow().deserialize_data().unwrap() + { + assert_eq!(meta.rent_exempt_reserve, rent_exempt_reserve); + assert_eq!(stake.delegation.stake, minimum_delegation); + assert_eq!(stake.delegation.activation_epoch, current_epoch); + assert_eq!(stake.delegation.deactivation_epoch, u64::MAX); + } else { + panic!("Invalid output_accounts[1] data"); + } + + // + // Variations of rescinding the deactivation of `stake_account` + // + let deactivated_stake_accounts = [ + ( + // Failure: insufficient stake in `stake_account` to even delegate normally + { + let mut deactivated_stake_account = output_accounts[0].clone(); + deactivated_stake_account + .checked_add_lamports(minimum_delegation - 1) + .unwrap(); + deactivated_stake_account + }, + Err(StakeError::InsufficientDelegation.into()), + ), + ( + // Failure: `stake_account` holds the "virtual stake" that's cooling now, with the + // real stake now warming up in `uninitialized_stake_account` + { + let mut deactivated_stake_account = output_accounts[0].clone(); + deactivated_stake_account + .checked_add_lamports(minimum_delegation) + .unwrap(); + deactivated_stake_account + }, + Err(StakeError::TooSoonToRedelegate.into()), + ), + ( + // Success: `stake_account` has been replenished with additional lamports to + // fully realize its "virtual stake" + { + let mut deactivated_stake_account = output_accounts[0].clone(); + deactivated_stake_account + .checked_add_lamports(minimum_delegation + rent_exempt_reserve) + .unwrap(); + deactivated_stake_account + }, + Ok(()), + ), + ( + // Failure: `stake_account` has been replenished with 1 lamport less than what's + // necessary to fully realize its "virtual stake" + { + let mut deactivated_stake_account = output_accounts[0].clone(); + deactivated_stake_account + .checked_add_lamports(minimum_delegation + rent_exempt_reserve - 1) + .unwrap(); + deactivated_stake_account + }, + Err(StakeError::TooSoonToRedelegate.into()), + ), + ]; + for (deactivated_stake_account, expected_result) in deactivated_stake_accounts { + let _ = process_instruction_with_overrides( + &serialize(&StakeInstruction::DelegateStake).unwrap(), + vec![ + (stake_address, deactivated_stake_account), + (vote_address, new_vote_account.clone()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + (authorized_staker, AccountSharedData::default()), + ], + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_staker, + is_signer: true, + is_writable: false, + }, + ], + Some(&sysvar_cache_override), + Some(Arc::clone(&feature_set)), + expected_result, + ); + } + + // + // Success: `uninitialized_stake_account` starts with 42 extra lamports + // + let uninitialized_stake_account_with_extra_lamports = + AccountSharedData::new(42 /* lamports */, StakeState::size_of(), &id()); + let output_accounts = process_instruction_redelegate( + &stake_address, + &stake_account, + &authorized_staker, + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &uninitialized_stake_account_with_extra_lamports, + Ok(()), + ); + + assert_eq!(output_accounts[0].lamports(), rent_exempt_reserve); + assert_eq!( + output_accounts[1].lamports(), + minimum_delegation + rent_exempt_reserve + 42 + ); + if let StakeState::Stake(meta, stake) = + output_accounts[1].borrow().deserialize_data().unwrap() + { + assert_eq!(meta.rent_exempt_reserve, rent_exempt_reserve); + assert_eq!(stake.delegation.stake, minimum_delegation + 42); + assert_eq!(stake.delegation.activation_epoch, current_epoch); + assert_eq!(stake.delegation.deactivation_epoch, u64::MAX); + } else { + panic!("Invalid output_accounts[1] data"); + } + + // + // Success: `stake_account` is over-allocated and holds a greater than required `rent_exempt_reserve` + // + let mut stake_account_over_allocated = + prepare_stake_account(0 /*activation_epoch:*/, None); + if let StakeState::Stake(mut meta, stake) = stake_account_over_allocated + .borrow_mut() + .deserialize_data() + .unwrap() + { + meta.rent_exempt_reserve += 42; + stake_account_over_allocated + .set_state(&StakeState::Stake(meta, stake)) + .unwrap(); + } + stake_account_over_allocated + .checked_add_lamports(42) + .unwrap(); + assert_eq!( + stake_account_over_allocated.lamports(), + (minimum_delegation + rent_exempt_reserve) + (rent_exempt_reserve + 42), + ); + assert_eq!(uninitialized_stake_account.lamports(), 0); + let output_accounts = process_instruction_redelegate( + &stake_address, + &stake_account_over_allocated, + &authorized_staker, + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &uninitialized_stake_account, + Ok(()), + ); + + assert_eq!(output_accounts[0].lamports(), rent_exempt_reserve + 42); + if let StakeState::Stake(meta, _stake) = + output_accounts[0].borrow().deserialize_data().unwrap() + { + assert_eq!(meta.rent_exempt_reserve, rent_exempt_reserve + 42); + } else { + panic!("Invalid output_accounts[0] data"); + } + assert_eq!( + output_accounts[1].lamports(), + minimum_delegation + rent_exempt_reserve, + ); + if let StakeState::Stake(meta, stake) = + output_accounts[1].borrow().deserialize_data().unwrap() + { + assert_eq!(meta.rent_exempt_reserve, rent_exempt_reserve); + assert_eq!(stake.delegation.stake, minimum_delegation); + } else { + panic!("Invalid output_accounts[1] data"); + } + + // + // Failure: `uninitialized_stake_account` with invalid program id + // + let _ = process_instruction_redelegate( + &stake_address, + &stake_account, + &authorized_staker, + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &AccountSharedData::new( + 0, /* lamports */ + StakeState::size_of(), + &Pubkey::new_unique(), // <-- Invalid program id + ), + Err(InstructionError::IncorrectProgramId), + ); + + // + // Failure: `uninitialized_stake_account` with size too small + // + let _ = process_instruction_redelegate( + &stake_address, + &stake_account, + &authorized_staker, + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &AccountSharedData::new(0 /* lamports */, StakeState::size_of() - 1, &id()), // <-- size too small + Err(InstructionError::InvalidAccountData), + ); + + // + // Failure: `uninitialized_stake_account` with size too large + // + let _ = process_instruction_redelegate( + &stake_address, + &stake_account, + &authorized_staker, + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &AccountSharedData::new(0 /* lamports */, StakeState::size_of() + 1, &id()), // <-- size too large + Err(InstructionError::InvalidAccountData), + ); + + // + // Failure: `uninitialized_stake_account` with initialized stake account + // + let _ = process_instruction_redelegate( + &stake_address, + &stake_account, + &authorized_staker, + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &stake_account.clone(), // <-- Initialized stake account + Err(InstructionError::AccountAlreadyInitialized), + ); + + // + // Failure: invalid `new_vote_account` + // + let _ = process_instruction_redelegate( + &stake_address, + &stake_account, + &authorized_staker, + &new_vote_address, + &uninitialized_stake_account.clone(), // <-- Invalid vote account + &uninitialized_stake_address, + &uninitialized_stake_account, + Err(InstructionError::IncorrectProgramId), + ); + + // + // Failure: invalid `stake_account` + // + let _ = process_instruction_redelegate( + &stake_address, + &uninitialized_stake_account.clone(), // <-- Uninitialized stake account + &authorized_staker, + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &uninitialized_stake_account, + Err(InstructionError::InvalidAccountData), + ); + + // + // Failure: stake is inactive, activating or deactivating + // + let inactive_stake_account = prepare_stake_account( + current_epoch + 1, /*activation_epoch*/ + Some(StakeActivationStatus { + effective: 0, + activating: 0, + deactivating: 0, + }), + ); + let _ = process_instruction_redelegate( + &stake_address, + &inactive_stake_account, + &authorized_staker, + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &uninitialized_stake_account, + Err(StakeError::RedelegateTransientOrInactiveStake.into()), + ); + + let activating_stake_account = prepare_stake_account( + current_epoch, /*activation_epoch*/ + Some(StakeActivationStatus { + effective: 0, + activating: minimum_delegation + rent_exempt_reserve, + deactivating: 0, + }), + ); + let _ = process_instruction_redelegate( + &stake_address, + &activating_stake_account, + &authorized_staker, + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &uninitialized_stake_account, + Err(StakeError::RedelegateTransientOrInactiveStake.into()), + ); + + let mut deactivating_stake_account = + prepare_stake_account(0 /*activation_epoch:*/, None); + if let StakeState::Stake(meta, mut stake) = deactivating_stake_account + .borrow_mut() + .deserialize_data() + .unwrap() + { + stake.deactivate(current_epoch).unwrap(); + assert_eq!( + StakeActivationStatus { + effective: minimum_delegation + rent_exempt_reserve, + activating: 0, + deactivating: minimum_delegation + rent_exempt_reserve, + }, + stake + .delegation + .stake_activating_and_deactivating(current_epoch, Some(&stake_history)) + ); + + deactivating_stake_account + .set_state(&StakeState::Stake(meta, stake)) + .unwrap(); + } + let _ = process_instruction_redelegate( + &stake_address, + &deactivating_stake_account, + &authorized_staker, + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &uninitialized_stake_account, + Err(StakeError::RedelegateTransientOrInactiveStake.into()), + ); + + // + // Failure: `stake_account` has insufficient stake + // (less than `minimum_delegation + rent_exempt_reserve`) + // + let mut stake_account_too_few_lamports = stake_account.clone(); + if let StakeState::Stake(meta, mut stake) = stake_account_too_few_lamports + .borrow_mut() + .deserialize_data() + .unwrap() + { + stake.delegation.stake -= 1; + assert_eq!( + stake.delegation.stake, + minimum_delegation + rent_exempt_reserve - 1 + ); + stake_account_too_few_lamports + .set_state(&StakeState::Stake(meta, stake)) + .unwrap(); + } else { + panic!("Invalid stake_account"); + } + stake_account_too_few_lamports + .checked_sub_lamports(1) + .unwrap(); + assert_eq!( + stake_account_too_few_lamports.lamports(), + minimum_delegation + 2 * rent_exempt_reserve - 1 + ); + + let _ = process_instruction_redelegate( + &stake_address, + &stake_account_too_few_lamports, + &authorized_staker, + &new_vote_address, + &new_vote_account, + &uninitialized_stake_address, + &uninitialized_stake_account, + Err(StakeError::InsufficientDelegation.into()), + ); + + // + // Failure: redelegate to same vote addresss + // + let _ = process_instruction_redelegate( + &stake_address, + &stake_account, + &authorized_staker, + &vote_address, // <-- Same vote address + &new_vote_account, + &uninitialized_stake_address, + &uninitialized_stake_account, + Err(StakeError::RedelegateToSameVoteAccount.into()), + ); + } } diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index ddd87cd3cd..a3d55e3d10 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -94,7 +94,8 @@ pub fn meta_from(account: &AccountSharedData) -> Option { from(account).and_then(|state: StakeState| state.meta()) } -fn redelegate( +fn redelegate_stake( + invoke_context: &InvokeContext, stake: &mut Stake, stake_lamports: u64, voter_pubkey: &Pubkey, @@ -105,11 +106,25 @@ fn redelegate( ) -> Result<(), StakeError> { // If stake is currently active: if stake.stake(clock.epoch, Some(stake_history)) != 0 { + let stake_lamports_ok = if invoke_context + .feature_set + .is_active(&feature_set::stake_redelegate_instruction::id()) + { + // When a stake account is redelegated, the delegated lamports from the source stake + // account are transferred to a new stake account. Do not permit the deactivation of + // the source stake account to be rescinded, by more generally requiring the delegation + // be configured with the expected amount of stake lamports before rescinding. + stake_lamports >= stake.delegation.stake + } else { + true + }; + // If pubkey of new voter is the same as current, // and we are scheduled to start deactivating this epoch, // we rescind deactivation if stake.delegation.voter_pubkey == *voter_pubkey && clock.epoch == stake.delegation.deactivation_epoch + && stake_lamports_ok { stake.delegation.deactivation_epoch = std::u64::MAX; return Ok(()); @@ -556,7 +571,9 @@ pub fn authorize_with_seed( ) } +#[allow(clippy::too_many_arguments)] pub fn delegate( + invoke_context: &InvokeContext, transaction_context: &TransactionContext, instruction_context: &InstructionContext, stake_account_index: usize, @@ -596,7 +613,8 @@ pub fn delegate( meta.authorized.check(signers, StakeAuthorize::Staker)?; let ValidatedDelegatedInfo { stake_amount } = validate_delegated_amount(&stake_account, &meta, feature_set)?; - redelegate( + redelegate_stake( + invoke_context, &mut stake, stake_amount, &vote_pubkey, @@ -856,6 +874,127 @@ pub fn merge( Ok(()) } +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, + config: &Config, + signers: &HashSet, +) -> Result<(), InstructionError> { + let clock = invoke_context.get_sysvar_cache().get_clock()?; + + // 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() { + ic_msg!( + invoke_context, + "expected uninitialized stake account owner to be {}, not {}", + id(), + *uninitialized_stake_account.get_owner() + ); + return Err(InstructionError::IncorrectProgramId); + } + if uninitialized_stake_account.get_data().len() != StakeState::size_of() { + ic_msg!( + invoke_context, + "expected uninitialized stake account data len to be {}, not {}", + StakeState::size_of(), + uninitialized_stake_account.get_data().len() + ); + return Err(InstructionError::InvalidAccountData); + } + if !matches!( + uninitialized_stake_account.get_state()?, + StakeState::Uninitialized + ) { + ic_msg!( + invoke_context, + "expected uninitialized stake account to be uninitialized", + ); + return Err(InstructionError::AccountAlreadyInitialized); + } + + // 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() { + ic_msg!( + invoke_context, + "expected vote account owner to be {}, not {}", + solana_vote_program::id(), + *vote_account.get_owner() + ); + return Err(InstructionError::IncorrectProgramId); + } + let vote_pubkey = *vote_account.get_key(); + let vote_state = vote_account.get_state::()?; + + let (stake_meta, effective_stake) = + if let StakeState::Stake(meta, stake) = stake_account.get_state()? { + let stake_history = invoke_context.get_sysvar_cache().get_stake_history()?; + let status = stake + .delegation + .stake_activating_and_deactivating(clock.epoch, Some(&stake_history)); + if status.effective == 0 || status.activating != 0 || status.deactivating != 0 { + ic_msg!(invoke_context, "stake is not active"); + return Err(StakeError::RedelegateTransientOrInactiveStake.into()); + } + + // 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 { + ic_msg!( + invoke_context, + "redelegating to the same vote account not permitted" + ); + return Err(StakeError::RedelegateToSameVoteAccount.into()); + } + + (meta, status.effective) + } else { + ic_msg!(invoke_context, "invalid stake account data",); + return Err(InstructionError::InvalidAccountData); + }; + + // deactivate `stake_account` + // + // Note: This function also ensures `signers` contains the `StakeAuthorize::Staker` + deactivate(stake_account, &clock, signers)?; + + // transfer the effective stake to the uninitialized stake account + stake_account.checked_sub_lamports(effective_stake)?; + uninitialized_stake_account.checked_add_lamports(effective_stake)?; + + // initialize and schedule `uninitialized_stake_account` for activation + let sysvar_cache = invoke_context.get_sysvar_cache(); + let rent = sysvar_cache.get_rent()?; + let mut uninitialized_stake_meta = stake_meta; + uninitialized_stake_meta.rent_exempt_reserve = + rent.minimum_balance(uninitialized_stake_account.get_data().len()); + + let ValidatedDelegatedInfo { stake_amount } = validate_delegated_amount( + &uninitialized_stake_account, + &uninitialized_stake_meta, + &invoke_context.feature_set, + )?; + uninitialized_stake_account.set_state(&StakeState::Stake( + uninitialized_stake_meta, + new_stake( + stake_amount, + &vote_pubkey, + &vote_state.convert_to_current(), + clock.epoch, + config, + ), + ))?; + + 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 9a7601a640..67d784c38d 100644 --- a/sdk/program/src/stake/instruction.rs +++ b/sdk/program/src/stake/instruction.rs @@ -60,6 +60,12 @@ pub enum StakeError { #[error("delegation amount is less than the minimum")] InsufficientDelegation, + + #[error("stake account with transient or inactive stake cannot be redelegated")] + RedelegateTransientOrInactiveStake, + + #[error("stake redelegation to the same vote account is not permitted")] + RedelegateToSameVoteAccount, } impl DecodeError for StakeError { @@ -261,6 +267,28 @@ 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 undelegated amount in + /// the account (rent exempt minimum and any additional lamports not part of the delegation), + /// and scheduled for deactivation. + /// * the provided uninitialized stake account will receive the original balance of the + /// delegated stake account, minus the rent exempt minimum, and scheduled for activation to + /// the provided vote account. Any existing lamports in the uninitialized stake account + /// will also be included in the re-delegation. + /// + /// # Account references + /// 0. `[WRITE]` Delegated stake account to be redelegated. The account must be fully + /// activated and carry a balance greater than or equal to the minimum delegation amount + /// plus rent exempt minimum + /// 1. `[WRITE]` Uninitialized stake account that will hold the redelegated stake + /// 2. `[]` Vote account to which this stake will be re-delegated + /// 3. `[]` Address of config account that carries stake config + /// 4. `[SIGNER]` Stake authority + /// + Redelegate, } #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] @@ -738,6 +766,65 @@ 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(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, + authorized_pubkey, + vote_pubkey, + uninitialized_stake_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, + authorized_pubkey, + vote_pubkey, + uninitialized_stake_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 6708fcf417..899120dada 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -287,6 +287,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"); } @@ -586,6 +590,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 #26294"), (preserve_rent_epoch_for_rent_exempt_accounts::id(), "preserve rent epoch for rent exempt accounts #26479"), (enable_bpf_loader_extend_program_data_ix::id(), "enable bpf upgradeable loader ExtendProgramData instruction #25234"), (enable_early_verification_of_account_modifications::id(), "enable early verification of account modifications #25899"), diff --git a/transaction-status/src/parse_stake.rs b/transaction-status/src/parse_stake.rs index f1f9ff86fa..a973643ed2 100644 --- a/transaction-status/src/parse_stake.rs +++ b/transaction-status/src/parse_stake.rs @@ -284,6 +284,19 @@ 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(), + "stakeConfigAccount": account_keys[instruction.accounts[3] as usize].to_string(), + "stakeAuthority": account_keys[instruction.accounts[4] as usize].to_string(), + }), + }) + } } }