Skip to content

Commit

Permalink
Add StakeInstruction::Redelegate
Browse files Browse the repository at this point in the history
  • Loading branch information
mvines committed Jun 28, 2022
1 parent 2436a2b commit f46f5ee
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 3 deletions.
45 changes: 44 additions & 1 deletion programs/stake/src/stake_instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*,
Expand Down Expand Up @@ -424,6 +424,49 @@ pub fn process_instruction(
Err(InstructionError::InvalidInstructionData)
}
}
Ok(StakeInstruction::Redelegate) => {
if invoke_context
.feature_set
.is_active(&feature_set::stake_redelegate_instruction::id())
{
let mut me = get_stake_account()?;
instruction_context.check_number_of_instruction_accounts(3)?;
let clock = invoke_context.get_sysvar_cache().get_clock()?;
let stake_history = get_sysvar_with_account_check::stake_history(
invoke_context,
instruction_context,
3,
)?;
let config_account =
instruction_context.try_borrow_instruction_account(transaction_context, 4)?;
if !config::check_id(config_account.get_key()) {
return Err(InstructionError::InvalidArgument);
}
let config =
config::from(&config_account).ok_or(InstructionError::InvalidArgument)?;
drop(config_account);

redelegate(
invoke_context,
transaction_context,
instruction_context,
&mut me,
1,
2,
&clock,
&stake_history,
&config,
&signers,
)
} else {
if !invoke_context.feature_set.is_active(
&feature_set::add_get_minimum_delegation_instruction_to_stake_program::id(),
) {
let _ = get_stake_account()?;
}
Err(InstructionError::InvalidInstructionData)
}
}
Err(err) => {
if !invoke_context.feature_set.is_active(
&feature_set::add_get_minimum_delegation_instruction_to_stake_program::id(),
Expand Down
95 changes: 93 additions & 2 deletions programs/stake/src/stake_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pub fn meta_from(account: &AccountSharedData) -> Option<Meta> {
from(account).and_then(|state: StakeState| state.meta())
}

fn redelegate(
fn redelegate_stake(
stake: &mut Stake,
stake_lamports: u64,
voter_pubkey: &Pubkey,
Expand Down Expand Up @@ -596,7 +596,7 @@ pub fn delegate(
meta.authorized.check(signers, StakeAuthorize::Staker)?;
let ValidatedDelegatedInfo { stake_amount } =
validate_delegated_amount(&stake_account, &meta, feature_set)?;
redelegate(
redelegate_stake(
&mut stake,
stake_amount,
&vote_pubkey,
Expand Down Expand Up @@ -856,6 +856,97 @@ pub fn merge(
Ok(())
}

#[allow(clippy::too_many_arguments)]
pub fn redelegate(
invoke_context: &InvokeContext,
transaction_context: &TransactionContext,
instruction_context: &InstructionContext,
stake_account: &mut BorrowedAccount,
uninitialized_stake_account_index: usize,
vote_account_index: usize,
clock: &Clock,
stake_history: &StakeHistory,
config: &Config,
signers: &HashSet<Pubkey>,
) -> Result<(), InstructionError> {
// ensure `uninitialized_stake_account_index` is in the uninitialized state
let mut uninitialized_stake_account = instruction_context
.try_borrow_instruction_account(transaction_context, uninitialized_stake_account_index)?;
if *uninitialized_stake_account.get_owner() != id() {
return Err(InstructionError::IncorrectProgramId);
}
if uninitialized_stake_account.get_data().len() != StakeState::size_of() {
return Err(InstructionError::InvalidAccountData);
}
if !matches!(
uninitialized_stake_account.get_state()?,
StakeState::Uninitialized
) {
return Err(InstructionError::InvalidAccountData);
}

// validate the provided vote account
let vote_account = instruction_context
.try_borrow_instruction_account(transaction_context, vote_account_index)?;
if *vote_account.get_owner() != solana_vote_program::id() {
return Err(InstructionError::IncorrectProgramId);
}
let vote_pubkey = *vote_account.get_key();
let vote_state = vote_account.get_state::<VoteStateVersions>();

// ensure `stake_account` is active, check for presence of the stake authority, and
// extract its Meta
let meta = if let StakeState::Stake(meta, stake) = stake_account.get_state()? {
meta.authorized.check(signers, StakeAuthorize::Staker)?;

let status = stake
.delegation
.stake_activating_and_deactivating(clock.epoch, Some(stake_history));
if status.effective == 0 || status.activating != 0 || status.deactivating != 0 {
return Err(InstructionError::InvalidAccountData);
}

// Deny redelegating to the same vote account. This is nonsensical and could be used to
// grief the global stake warm-up/cool-down rate
if stake.delegation.voter_pubkey == vote_pubkey {
return Err(InstructionError::InvalidArgument);
}

meta
} else {
return Err(InstructionError::InvalidAccountData);
};

// deactivate `stake_account`
deactivate(stake_account, clock, signers)?;

// transfer all but the `rent_exempt_reserve` balance to the uninitialized stake account
let redelegation_amount = stake_account
.get_lamports()
.checked_sub(meta.rent_exempt_reserve)
.ok_or(InstructionError::InsufficientFunds)?;

stake_account.checked_sub_lamports(redelegation_amount)?;
uninitialized_stake_account.checked_add_lamports(redelegation_amount)?;

// initialize and delegate `uninitialized_stake_account`
let ValidatedDelegatedInfo { stake_amount } = validate_delegated_amount(
&uninitialized_stake_account,
&meta,
&invoke_context.feature_set,
)?;
let stake = new_stake(
stake_amount,
&vote_pubkey,
&vote_state?.convert_to_current(),
clock.epoch,
config,
);
uninitialized_stake_account.set_state(&StakeState::Stake(meta, stake))?;

Ok(())
}

#[allow(clippy::too_many_arguments)]
pub fn withdraw(
transaction_context: &TransactionContext,
Expand Down
80 changes: 80 additions & 0 deletions sdk/program/src/stake/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,25 @@ pub enum StakeInstruction {
/// 2. `[]` Reference vote account that has voted at least once in the last
/// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` epochs
DeactivateDelinquent,

/// Redelegate activated stake to another vote account.
///
/// Upon success:
/// * the balance of the delegated stake account will be reduced to the minimum delegation
/// amount, and scheduled for deactivation.
/// * the provided uninitialized stake account will hold the entire balance minus the minimum
/// delegation amount, and scheduled for activation.
///
/// # Account references
/// 0. `[WRITE]` Delegated stake account to be redelegated. The account must be fully
/// activated and carry a balance equal to twice the minimum delegation amount or greater
/// 1. `[WRITE]` Uninitialized stake account that will hold the redelegated stake
/// 2. `[]` Vote account to which this stake will be re-delegated
/// 3. `[]` Stake history sysvar that carries stake warmup/cooldown history
/// 4. `[]` Address of config account that carries stake config
/// 5. `[SIGNER]` Stake authority
///
Redelegate,
}

#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
Expand Down Expand Up @@ -738,6 +757,67 @@ pub fn deactivate_delinquent_stake(
Instruction::new_with_bincode(id(), &StakeInstruction::DeactivateDelinquent, account_metas)
}

fn _redelegate(
stake_pubkey: &Pubkey,
authorized_pubkey: &Pubkey,
vote_pubkey: &Pubkey,
uninitialized_stake_pubkey: &Pubkey,
) -> Instruction {
let account_metas = vec![
AccountMeta::new(*stake_pubkey, false),
AccountMeta::new(*uninitialized_stake_pubkey, false),
AccountMeta::new_readonly(*vote_pubkey, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
AccountMeta::new_readonly(config::id(), false),
AccountMeta::new_readonly(*authorized_pubkey, true),
];
Instruction::new_with_bincode(id(), &StakeInstruction::Redelegate, account_metas)
}

pub fn redelegate(
stake_pubkey: &Pubkey,
authorized_pubkey: &Pubkey,
vote_pubkey: &Pubkey,
uninitialized_stake_pubkey: &Pubkey,
) -> Vec<Instruction> {
vec![
system_instruction::allocate(uninitialized_stake_pubkey, StakeState::size_of() as u64),
system_instruction::assign(uninitialized_stake_pubkey, &id()),
_redelegate(
stake_pubkey,
uninitialized_stake_pubkey,
authorized_pubkey,
vote_pubkey,
),
]
}

pub fn redelegate_with_seed(
stake_pubkey: &Pubkey,
authorized_pubkey: &Pubkey,
vote_pubkey: &Pubkey,
uninitialized_stake_pubkey: &Pubkey, // derived using create_with_seed()
base: &Pubkey, // base
seed: &str, // seed
) -> Vec<Instruction> {
vec![
system_instruction::allocate_with_seed(
uninitialized_stake_pubkey,
base,
seed,
StakeState::size_of() as u64,
&id(),
),
_redelegate(
stake_pubkey,
uninitialized_stake_pubkey,
authorized_pubkey,
vote_pubkey,
),
]
}

#[cfg(test)]
mod tests {
use {super::*, crate::instruction::InstructionError};
Expand Down
5 changes: 5 additions & 0 deletions sdk/src/feature_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,10 @@ pub mod stake_deactivate_delinquent_instruction {
solana_sdk::declare_id!("437r62HoAdUb63amq3D7ENnBLDhHT2xY8eFkLJYVKK4x");
}

pub mod stake_redelegate_instruction {
solana_sdk::declare_id!("3EPmAX94PvVJCjMeFfRFvj4avqCPL8vv3TGsZQg7ydMx");
}

pub mod vote_withdraw_authority_may_change_authorized_voter {
solana_sdk::declare_id!("AVZS3ZsN4gi6Rkx2QUibYuSJG3S6QHib7xCYhG6vGJxU");
}
Expand Down Expand Up @@ -549,6 +553,7 @@ lazy_static! {
(nonce_must_be_advanceable::id(), "durable nonces must be advanceable"),
(vote_authorize_with_seed::id(), "An instruction you can use to change a vote accounts authority when the current authority is a derived key #25860"),
(cap_accounts_data_size_per_block::id(), "cap the accounts data size per block #25517"),
(stake_redelegate_instruction::id(), "enable the redelegate stake instruction #TBD"),
/*************** ADD NEW FEATURES HERE ***************/
]
.iter()
Expand Down
12 changes: 12 additions & 0 deletions transaction-status/src/parse_stake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,18 @@ pub fn parse_stake(
}),
})
}
StakeInstruction::Redelegate => {
check_num_stake_accounts(&instruction.accounts, 4)?;
Ok(ParsedInstructionEnum {
instruction_type: "redelegate".to_string(),
info: json!({
"stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(),
"newStakeAccount": account_keys[instruction.accounts[1] as usize].to_string(),
"voteAccount": account_keys[instruction.accounts[2] as usize].to_string(),
"stakeAuthority": account_keys[instruction.accounts[5] as usize].to_string(),
}),
})
}
}
}

Expand Down

0 comments on commit f46f5ee

Please sign in to comment.