Add StakeInstruction::Redelegate

This commit is contained in:
Michael Vines 2022-06-28 10:39:00 -07:00
parent 1404860be3
commit 48862c575a
5 changed files with 900 additions and 5 deletions

View File

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

View File

@ -94,7 +94,8 @@ pub fn meta_from(account: &AccountSharedData) -> Option<Meta> {
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<Pubkey>,
) -> 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::<VoteStateVersions>()?;
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,

View File

@ -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<E> DecodeError<E> 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<Instruction> {
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<Instruction> {
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};

View File

@ -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"),

View File

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