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(),
+ }),
+ })
+ }
}
}