Skip to content

Commit

Permalink
implement both instructions
Browse files Browse the repository at this point in the history
  • Loading branch information
2501babe committed Jun 5, 2024
1 parent 9ac6b0d commit 6105c77
Show file tree
Hide file tree
Showing 4 changed files with 402 additions and 1 deletion.
51 changes: 50 additions & 1 deletion programs/stake/src/stake_instruction.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use {
crate::stake_state::{
authorize, authorize_with_seed, deactivate, deactivate_delinquent, delegate, initialize,
merge, new_warmup_cooldown_rate_epoch, redelegate, set_lockup, split, withdraw,
merge, move_lamports, move_stake, new_warmup_cooldown_rate_epoch, redelegate, set_lockup,
split, withdraw,
},
log::*,
solana_program_runtime::{
Expand Down Expand Up @@ -352,6 +353,54 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context|
Err(InstructionError::InvalidInstructionData)
}
}
StakeInstruction::MoveStake(lamports) => {
let me = get_stake_account()?;
instruction_context.check_number_of_instruction_accounts(2)?;
let clock =
get_sysvar_with_account_check::clock(invoke_context, instruction_context, 2)?;
let stake_history = get_sysvar_with_account_check::stake_history(
invoke_context,
instruction_context,
3,
)?;
instruction_context.check_number_of_instruction_accounts(5)?;
drop(me);
move_stake(
invoke_context,
transaction_context,
instruction_context,
0,
lamports,
1,
&clock,
&stake_history,
4,
)
}
StakeInstruction::MoveLamports(lamports) => {
let me = get_stake_account()?;
instruction_context.check_number_of_instruction_accounts(2)?;
let clock =
get_sysvar_with_account_check::clock(invoke_context, instruction_context, 2)?;
let stake_history = get_sysvar_with_account_check::stake_history(
invoke_context,
instruction_context,
3,
)?;
instruction_context.check_number_of_instruction_accounts(5)?;
drop(me);
move_lamports(
invoke_context,
transaction_context,
instruction_context,
0,
lamports,
1,
&clock,
&stake_history,
4,
)
}
}
});

Expand Down
263 changes: 263 additions & 0 deletions programs/stake/src/stake_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,85 @@ fn redelegate_stake(
Ok(())
}

fn move_stake_or_lamports_shared_checks(
invoke_context: &InvokeContext,
transaction_context: &TransactionContext,
instruction_context: &InstructionContext,
source_account: &BorrowedAccount,
lamports: u64,
destination_account: &BorrowedAccount,
clock: &Clock,
stake_history: &StakeHistory,
stake_authority_index: IndexOfAccount,
) -> Result<(MergeKind, MergeKind), InstructionError> {
// authority must sign
let stake_authority_pubkey = transaction_context.get_key_of_account_at_index(
instruction_context
.get_index_of_instruction_account_in_transaction(stake_authority_index)?,
)?;
if !instruction_context.is_instruction_account_signer(stake_authority_index)? {
return Err(InstructionError::MissingRequiredSignature);
}

let mut signers = HashSet::new();
signers.insert(*stake_authority_pubkey);

// check owners
if *source_account.get_owner() != id() || *destination_account.get_owner() != id() {
return Err(InstructionError::IncorrectProgramId);
}

// confirm not the same account
if *source_account.get_key() == *destination_account.get_key() {
return Err(InstructionError::InvalidInstructionData);
}

// source and destination must be writable
if !source_account.is_writable() || !destination_account.is_writable() {
return Err(InstructionError::InvalidInstructionData);
}

// must move something
if lamports == 0 {
return Err(InstructionError::InvalidArgument);
}

// get_if_mergeable ensures accounts are not partly activated or in any form of deactivating
// we still need to exclude activating state ourselves
let source_merge_kind = MergeKind::get_if_mergeable(
invoke_context,
&source_account.get_state()?,
source_account.get_lamports(),
clock,
stake_history,
)?;

// Authorized staker is allowed to move stake
source_merge_kind
.meta()
.authorized
.check(&signers, StakeAuthorize::Staker)?;

// same transient assurance as with source
let destination_merge_kind = MergeKind::get_if_mergeable(
invoke_context,
&destination_account.get_state()?,
destination_account.get_lamports(),
clock,
stake_history,
)?;

// ensure all authorities match and lockups match if lockup is in force
MergeKind::metas_can_merge(
invoke_context,
source_merge_kind.meta(),
destination_merge_kind.meta(),
clock,
)?;

Ok((source_merge_kind, destination_merge_kind))
}

pub(crate) fn new_stake(
stake: u64,
voter_pubkey: &Pubkey,
Expand Down Expand Up @@ -705,6 +784,190 @@ pub fn redelegate(
Ok(())
}

pub fn move_stake(
invoke_context: &InvokeContext,
transaction_context: &TransactionContext,
instruction_context: &InstructionContext,
source_account_index: IndexOfAccount,
lamports: u64,
destination_account_index: IndexOfAccount,
clock: &Clock,
stake_history: &StakeHistory,
stake_authority_index: IndexOfAccount,
) -> Result<(), InstructionError> {
let mut source_account = instruction_context
.try_borrow_instruction_account(transaction_context, source_account_index)?;

let mut destination_account = instruction_context
.try_borrow_instruction_account(transaction_context, destination_account_index)?;

let (source_merge_kind, destination_merge_kind) = move_stake_or_lamports_shared_checks(
invoke_context,
transaction_context,
instruction_context,
&source_account,
lamports,
&destination_account,
clock,
stake_history,
stake_authority_index,
)?;

// source and destination will have their data reassigned
if source_account.get_data().len() != StakeStateV2::size_of()
|| destination_account.get_data().len() != StakeStateV2::size_of()
{
return Err(InstructionError::InvalidAccountData);
}

// source must be fully active
// destination must not be activating
// if active, destination must be delegated to the same vote account as source
// minimum delegations must be respected for any accounts that become/remain active
match source_merge_kind {
MergeKind::FullyActive(source_meta, mut source_stake) => {
let minimum_delegation =
crate::get_minimum_delegation(invoke_context.get_feature_set());

let source_effective_stake = source_stake.delegation.stake;
let source_final_stake = source_effective_stake
.checked_sub(lamports)
.ok_or(InstructionError::InvalidArgument)?;

if source_final_stake != 0 && source_final_stake < minimum_delegation {
return Err(InstructionError::InvalidArgument);
}

let destination_meta = match destination_merge_kind {
MergeKind::FullyActive(destination_meta, mut destination_stake) => {
if source_stake.delegation.voter_pubkey
!= destination_stake.delegation.voter_pubkey
{
return Err(StakeError::VoteAddressMismatch.into());
}

let destination_effective_stake = destination_stake.delegation.stake;
let destination_final_stake = destination_effective_stake
.checked_add(lamports)
.ok_or(InstructionError::ArithmeticOverflow)?;

if destination_final_stake < minimum_delegation {
return Err(InstructionError::InvalidArgument);
}

merge_delegation_stake_and_credits_observed(
&mut destination_stake,
lamports,
source_stake.credits_observed,
)?;

destination_account.set_state(&StakeStateV2::Stake(
destination_meta,
destination_stake,
StakeFlags::empty(),
))?;

destination_meta
}
MergeKind::Inactive(destination_meta, _, _) => {
if lamports < minimum_delegation {
return Err(InstructionError::InvalidArgument);
}

let mut destination_stake = source_stake;
destination_stake.delegation.stake = lamports;
destination_account.set_state(&StakeStateV2::Stake(
destination_meta,
destination_stake,
StakeFlags::empty(),
))?;

destination_meta
}
_ => return Err(InstructionError::InvalidAccountData),
};

if source_final_stake == 0 {
source_account.set_state(&StakeStateV2::Initialized(source_meta))?;
} else {
source_stake.delegation.stake = source_final_stake;
source_account.set_state(&StakeStateV2::Stake(
source_meta,
source_stake,
StakeFlags::empty(),
))?;
}

source_account.checked_sub_lamports(lamports)?;
destination_account.checked_add_lamports(lamports)?;

// this should be impossible, but because we do all our math with delegations, best to guard it
if source_account.get_lamports() < source_meta.rent_exempt_reserve
|| destination_account.get_lamports() < destination_meta.rent_exempt_reserve
{
ic_msg!(
invoke_context,
"Delegation calculations violated lamport balance assumptions"
);
return Err(InstructionError::InvalidArgument);
}
}
_ => return Err(InstructionError::InvalidAccountData),
}

Ok(())
}

pub fn move_lamports(
invoke_context: &InvokeContext,
transaction_context: &TransactionContext,
instruction_context: &InstructionContext,
source_account_index: IndexOfAccount,
lamports: u64,
destination_account_index: IndexOfAccount,
clock: &Clock,
stake_history: &StakeHistory,
stake_authority_index: IndexOfAccount,
) -> Result<(), InstructionError> {
let mut source_account = instruction_context
.try_borrow_instruction_account(transaction_context, source_account_index)?;

let mut destination_account = instruction_context
.try_borrow_instruction_account(transaction_context, destination_account_index)?;

let (source_merge_kind, _) = move_stake_or_lamports_shared_checks(
invoke_context,
transaction_context,
instruction_context,
&source_account,
lamports,
&destination_account,
clock,
stake_history,
stake_authority_index,
)?;

let source_free_lamports = match source_merge_kind {
MergeKind::FullyActive(source_meta, source_stake) => source_account
.get_lamports()
.saturating_sub(source_stake.delegation.stake)
.saturating_sub(source_meta.rent_exempt_reserve),
MergeKind::Inactive(source_meta, source_lamports, _) => {
source_lamports.saturating_sub(source_meta.rent_exempt_reserve)
}
_ => return Err(InstructionError::InvalidAccountData),
};

if lamports > source_free_lamports {
return Err(InstructionError::InvalidArgument);
}

source_account.checked_sub_lamports(lamports)?;
destination_account.checked_add_lamports(lamports)?;

Ok(())
}

#[allow(clippy::too_many_arguments)]
pub fn withdraw(
transaction_context: &TransactionContext,
Expand Down
Loading

0 comments on commit 6105c77

Please sign in to comment.