Add StakeInstruction::Redelegate
This commit is contained in:
parent
1404860be3
commit
48862c575a
|
@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue