solana/programs/stake/src/stake_instruction.rs

8004 lines
304 KiB
Rust

use {
crate::{
config,
stake_state::{
authorize, authorize_with_seed, deactivate, deactivate_delinquent, delegate,
initialize, merge, new_warmup_cooldown_rate_epoch, redelegate, set_lockup, split,
withdraw,
},
},
log::*,
solana_program_runtime::{
declare_process_instruction, sysvar_cache::get_sysvar_with_account_check,
},
solana_sdk::{
clock::Clock,
feature_set,
instruction::InstructionError,
program_utils::limited_deserialize,
pubkey::Pubkey,
stake::{
instruction::{LockupArgs, StakeInstruction},
program::id,
state::{Authorized, Lockup},
},
transaction_context::{IndexOfAccount, InstructionContext, TransactionContext},
},
};
fn get_optional_pubkey<'a>(
transaction_context: &'a TransactionContext,
instruction_context: &'a InstructionContext,
instruction_account_index: IndexOfAccount,
should_be_signer: bool,
) -> Result<Option<&'a Pubkey>, InstructionError> {
Ok(
if instruction_account_index < instruction_context.get_number_of_instruction_accounts() {
if should_be_signer
&& !instruction_context.is_instruction_account_signer(instruction_account_index)?
{
return Err(InstructionError::MissingRequiredSignature);
}
Some(
transaction_context.get_key_of_account_at_index(
instruction_context.get_index_of_instruction_account_in_transaction(
instruction_account_index,
)?,
)?,
)
} else {
None
},
)
}
pub const DEFAULT_COMPUTE_UNITS: u64 = 750;
declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| {
let transaction_context = &invoke_context.transaction_context;
let instruction_context = transaction_context.get_current_instruction_context()?;
let data = instruction_context.get_instruction_data();
trace!("process_instruction: {:?}", data);
let get_stake_account = || {
let me = instruction_context.try_borrow_instruction_account(transaction_context, 0)?;
if *me.get_owner() != id() {
return Err(InstructionError::InvalidAccountOwner);
}
Ok(me)
};
let signers = instruction_context.get_signers(transaction_context)?;
match limited_deserialize(data) {
Ok(StakeInstruction::Initialize(authorized, lockup)) => {
let mut me = get_stake_account()?;
let rent = get_sysvar_with_account_check::rent(invoke_context, instruction_context, 1)?;
initialize(&mut me, &authorized, &lockup, &rent)
}
Ok(StakeInstruction::Authorize(authorized_pubkey, stake_authorize)) => {
let mut me = get_stake_account()?;
let require_custodian_for_locked_stake_authorize = invoke_context
.feature_set
.is_active(&feature_set::require_custodian_for_locked_stake_authorize::id());
if require_custodian_for_locked_stake_authorize {
let clock =
get_sysvar_with_account_check::clock(invoke_context, instruction_context, 1)?;
instruction_context.check_number_of_instruction_accounts(3)?;
let custodian_pubkey =
get_optional_pubkey(transaction_context, instruction_context, 3, false)?;
authorize(
&mut me,
&signers,
&authorized_pubkey,
stake_authorize,
require_custodian_for_locked_stake_authorize,
&clock,
custodian_pubkey,
)
} else {
authorize(
&mut me,
&signers,
&authorized_pubkey,
stake_authorize,
require_custodian_for_locked_stake_authorize,
&Clock::default(),
None,
)
}
}
Ok(StakeInstruction::AuthorizeWithSeed(args)) => {
let mut me = get_stake_account()?;
instruction_context.check_number_of_instruction_accounts(2)?;
let require_custodian_for_locked_stake_authorize = invoke_context
.feature_set
.is_active(&feature_set::require_custodian_for_locked_stake_authorize::id());
if require_custodian_for_locked_stake_authorize {
let clock =
get_sysvar_with_account_check::clock(invoke_context, instruction_context, 2)?;
let custodian_pubkey =
get_optional_pubkey(transaction_context, instruction_context, 3, false)?;
authorize_with_seed(
transaction_context,
instruction_context,
&mut me,
1,
&args.authority_seed,
&args.authority_owner,
&args.new_authorized_pubkey,
args.stake_authorize,
require_custodian_for_locked_stake_authorize,
&clock,
custodian_pubkey,
)
} else {
authorize_with_seed(
transaction_context,
instruction_context,
&mut me,
1,
&args.authority_seed,
&args.authority_owner,
&args.new_authorized_pubkey,
args.stake_authorize,
require_custodian_for_locked_stake_authorize,
&Clock::default(),
None,
)
}
}
Ok(StakeInstruction::DelegateStake) => {
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);
if !invoke_context
.feature_set
.is_active(&feature_set::reduce_stake_warmup_cooldown::id())
{
// Post feature activation, remove both the feature gate code and the config completely in the interface
let config_account =
instruction_context.try_borrow_instruction_account(transaction_context, 4)?;
#[allow(deprecated)]
if !config::check_id(config_account.get_key()) {
return Err(InstructionError::InvalidArgument);
}
config::from(&config_account).ok_or(InstructionError::InvalidArgument)?;
}
delegate(
invoke_context,
transaction_context,
instruction_context,
0,
1,
&clock,
&stake_history,
&signers,
&invoke_context.feature_set,
)
}
Ok(StakeInstruction::Split(lamports)) => {
let me = get_stake_account()?;
instruction_context.check_number_of_instruction_accounts(2)?;
drop(me);
split(
invoke_context,
transaction_context,
instruction_context,
0,
lamports,
1,
&signers,
)
}
Ok(StakeInstruction::Merge) => {
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,
)?;
drop(me);
merge(
invoke_context,
transaction_context,
instruction_context,
0,
1,
&clock,
&stake_history,
&signers,
)
}
Ok(StakeInstruction::Withdraw(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);
withdraw(
transaction_context,
instruction_context,
0,
lamports,
1,
&clock,
&stake_history,
4,
if instruction_context.get_number_of_instruction_accounts() >= 6 {
Some(5)
} else {
None
},
new_warmup_cooldown_rate_epoch(invoke_context),
)
}
Ok(StakeInstruction::Deactivate) => {
let mut me = get_stake_account()?;
let clock =
get_sysvar_with_account_check::clock(invoke_context, instruction_context, 1)?;
deactivate(invoke_context, &mut me, &clock, &signers)
}
Ok(StakeInstruction::SetLockup(lockup)) => {
let mut me = get_stake_account()?;
let clock = invoke_context.get_sysvar_cache().get_clock()?;
set_lockup(&mut me, &lockup, &signers, &clock)
}
Ok(StakeInstruction::InitializeChecked) => {
let mut me = get_stake_account()?;
if invoke_context
.feature_set
.is_active(&feature_set::vote_stake_checked_instructions::id())
{
instruction_context.check_number_of_instruction_accounts(4)?;
let staker_pubkey = transaction_context.get_key_of_account_at_index(
instruction_context.get_index_of_instruction_account_in_transaction(2)?,
)?;
let withdrawer_pubkey = transaction_context.get_key_of_account_at_index(
instruction_context.get_index_of_instruction_account_in_transaction(3)?,
)?;
if !instruction_context.is_instruction_account_signer(3)? {
return Err(InstructionError::MissingRequiredSignature);
}
let authorized = Authorized {
staker: *staker_pubkey,
withdrawer: *withdrawer_pubkey,
};
let rent =
get_sysvar_with_account_check::rent(invoke_context, instruction_context, 1)?;
initialize(&mut me, &authorized, &Lockup::default(), &rent)
} else {
Err(InstructionError::InvalidInstructionData)
}
}
Ok(StakeInstruction::AuthorizeChecked(stake_authorize)) => {
let mut me = get_stake_account()?;
if invoke_context
.feature_set
.is_active(&feature_set::vote_stake_checked_instructions::id())
{
let clock =
get_sysvar_with_account_check::clock(invoke_context, instruction_context, 1)?;
instruction_context.check_number_of_instruction_accounts(4)?;
let authorized_pubkey = transaction_context.get_key_of_account_at_index(
instruction_context.get_index_of_instruction_account_in_transaction(3)?,
)?;
if !instruction_context.is_instruction_account_signer(3)? {
return Err(InstructionError::MissingRequiredSignature);
}
let custodian_pubkey =
get_optional_pubkey(transaction_context, instruction_context, 4, false)?;
authorize(
&mut me,
&signers,
authorized_pubkey,
stake_authorize,
true,
&clock,
custodian_pubkey,
)
} else {
Err(InstructionError::InvalidInstructionData)
}
}
Ok(StakeInstruction::AuthorizeCheckedWithSeed(args)) => {
let mut me = get_stake_account()?;
if invoke_context
.feature_set
.is_active(&feature_set::vote_stake_checked_instructions::id())
{
instruction_context.check_number_of_instruction_accounts(2)?;
let clock =
get_sysvar_with_account_check::clock(invoke_context, instruction_context, 2)?;
instruction_context.check_number_of_instruction_accounts(4)?;
let authorized_pubkey = transaction_context.get_key_of_account_at_index(
instruction_context.get_index_of_instruction_account_in_transaction(3)?,
)?;
if !instruction_context.is_instruction_account_signer(3)? {
return Err(InstructionError::MissingRequiredSignature);
}
let custodian_pubkey =
get_optional_pubkey(transaction_context, instruction_context, 4, false)?;
authorize_with_seed(
transaction_context,
instruction_context,
&mut me,
1,
&args.authority_seed,
&args.authority_owner,
authorized_pubkey,
args.stake_authorize,
true,
&clock,
custodian_pubkey,
)
} else {
Err(InstructionError::InvalidInstructionData)
}
}
Ok(StakeInstruction::SetLockupChecked(lockup_checked)) => {
let mut me = get_stake_account()?;
if invoke_context
.feature_set
.is_active(&feature_set::vote_stake_checked_instructions::id())
{
let custodian_pubkey =
get_optional_pubkey(transaction_context, instruction_context, 2, true)?;
let lockup = LockupArgs {
unix_timestamp: lockup_checked.unix_timestamp,
epoch: lockup_checked.epoch,
custodian: custodian_pubkey.cloned(),
};
let clock = invoke_context.get_sysvar_cache().get_clock()?;
set_lockup(&mut me, &lockup, &signers, &clock)
} else {
Err(InstructionError::InvalidInstructionData)
}
}
Ok(StakeInstruction::GetMinimumDelegation) => {
let feature_set = invoke_context.feature_set.as_ref();
let minimum_delegation = crate::get_minimum_delegation(feature_set);
let minimum_delegation = Vec::from(minimum_delegation.to_le_bytes());
invoke_context
.transaction_context
.set_return_data(id(), minimum_delegation)
}
Ok(StakeInstruction::DeactivateDelinquent) => {
let mut me = get_stake_account()?;
instruction_context.check_number_of_instruction_accounts(3)?;
let clock = invoke_context.get_sysvar_cache().get_clock()?;
deactivate_delinquent(
invoke_context,
transaction_context,
instruction_context,
&mut me,
1,
2,
clock.epoch,
)
}
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)?;
if !invoke_context
.feature_set
.is_active(&feature_set::reduce_stake_warmup_cooldown::id())
{
// Post feature activation, remove both the feature gate code and the config completely in the interface
let config_account = instruction_context
.try_borrow_instruction_account(transaction_context, 3)?;
#[allow(deprecated)]
if !config::check_id(config_account.get_key()) {
return Err(InstructionError::InvalidArgument);
}
config::from(&config_account).ok_or(InstructionError::InvalidArgument)?;
}
redelegate(
invoke_context,
transaction_context,
instruction_context,
&mut me,
1,
2,
&signers,
)
} else {
Err(InstructionError::InvalidInstructionData)
}
}
Err(err) => Err(err),
}
});
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
config,
stake_state::{
authorized_from, create_stake_history_from_delegations, from, new_stake,
stake_from, Delegation, Meta, Stake, StakeStateV2,
},
},
assert_matches::assert_matches,
bincode::serialize,
solana_program_runtime::invoke_context::mock_process_instruction,
solana_sdk::{
account::{
create_account_shared_data_for_test, AccountSharedData, ReadableAccount,
WritableAccount,
},
account_utils::StateMut,
clock::{Epoch, UnixTimestamp},
epoch_schedule::EpochSchedule,
feature_set::FeatureSet,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
rent::Rent,
stake::{
config as stake_config,
instruction::{
self, authorize_checked, authorize_checked_with_seed, initialize_checked,
set_lockup_checked, AuthorizeCheckedWithSeedArgs, AuthorizeWithSeedArgs,
LockupArgs, StakeError,
},
stake_flags::StakeFlags,
state::{
warmup_cooldown_rate, Authorized, Lockup, StakeActivationStatus, StakeAuthorize,
},
MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION,
},
stake_history::{StakeHistory, StakeHistoryEntry},
system_program,
sysvar::{clock, epoch_schedule, rent, rewards, stake_history},
},
solana_vote_program::vote_state::{self, VoteState, VoteStateVersions},
std::{collections::HashSet, str::FromStr, sync::Arc},
test_case::test_case,
};
fn feature_set_all_enabled() -> Arc<FeatureSet> {
Arc::new(FeatureSet::all_enabled())
}
/// With stake minimum delegation but 25% warmup/cooldown
fn feature_set_old_warmup_cooldown() -> Arc<FeatureSet> {
let mut feature_set = FeatureSet::all_enabled();
feature_set.deactivate(&feature_set::reduce_stake_warmup_cooldown::id());
Arc::new(feature_set)
}
/// No stake minimum delegation and 25% warmup/cooldown
fn feature_set_old_warmup_cooldown_no_minimum_delegation() -> Arc<FeatureSet> {
let mut feature_set = feature_set_old_warmup_cooldown();
Arc::get_mut(&mut feature_set)
.unwrap()
.deactivate(&feature_set::stake_raise_minimum_delegation_to_1_sol::id());
feature_set
}
fn feature_set_without_require_rent_exempt_split_destination() -> Arc<FeatureSet> {
let mut feature_set = FeatureSet::all_enabled();
feature_set.deactivate(&feature_set::require_rent_exempt_split_destination::id());
Arc::new(feature_set)
}
fn create_default_account() -> AccountSharedData {
AccountSharedData::new(0, 0, &Pubkey::new_unique())
}
fn create_default_stake_account() -> AccountSharedData {
AccountSharedData::new(0, 0, &id())
}
fn invalid_stake_state_pubkey() -> Pubkey {
Pubkey::from_str("BadStake11111111111111111111111111111111111").unwrap()
}
fn invalid_vote_state_pubkey() -> Pubkey {
Pubkey::from_str("BadVote111111111111111111111111111111111111").unwrap()
}
fn spoofed_stake_state_pubkey() -> Pubkey {
Pubkey::from_str("SpoofedStake1111111111111111111111111111111").unwrap()
}
fn spoofed_stake_program_id() -> Pubkey {
Pubkey::from_str("Spoofed111111111111111111111111111111111111").unwrap()
}
fn process_instruction(
feature_set: Arc<FeatureSet>,
instruction_data: &[u8],
transaction_accounts: Vec<(Pubkey, AccountSharedData)>,
instruction_accounts: Vec<AccountMeta>,
expected_result: Result<(), InstructionError>,
) -> Vec<AccountSharedData> {
mock_process_instruction(
&id(),
Vec::new(),
instruction_data,
transaction_accounts,
instruction_accounts,
expected_result,
Entrypoint::vm,
|invoke_context| {
invoke_context.feature_set = Arc::clone(&feature_set);
},
|_invoke_context| {},
)
}
fn process_instruction_as_one_arg(
feature_set: Arc<FeatureSet>,
instruction: &Instruction,
expected_result: Result<(), InstructionError>,
) -> Vec<AccountSharedData> {
let mut pubkeys: HashSet<Pubkey> = instruction
.accounts
.iter()
.map(|meta| meta.pubkey)
.collect();
pubkeys.insert(clock::id());
pubkeys.insert(epoch_schedule::id());
#[allow(deprecated)]
let transaction_accounts = pubkeys
.iter()
.map(|pubkey| {
(
*pubkey,
if clock::check_id(pubkey) {
create_account_shared_data_for_test(&clock::Clock::default())
} else if rewards::check_id(pubkey) {
create_account_shared_data_for_test(&rewards::Rewards::new(0.0))
} else if stake_history::check_id(pubkey) {
create_account_shared_data_for_test(&StakeHistory::default())
} else if stake_config::check_id(pubkey) {
config::create_account(0, &stake_config::Config::default())
} else if epoch_schedule::check_id(pubkey) {
create_account_shared_data_for_test(&EpochSchedule::default())
} else if rent::check_id(pubkey) {
create_account_shared_data_for_test(&Rent::default())
} else if *pubkey == invalid_stake_state_pubkey() {
AccountSharedData::new(0, 0, &id())
} else if *pubkey == invalid_vote_state_pubkey() {
AccountSharedData::new(0, 0, &solana_vote_program::id())
} else if *pubkey == spoofed_stake_state_pubkey() {
AccountSharedData::new(0, 0, &spoofed_stake_program_id())
} else {
AccountSharedData::new(0, 0, &id())
},
)
})
.collect();
process_instruction(
Arc::clone(&feature_set),
&instruction.data,
transaction_accounts,
instruction.accounts.clone(),
expected_result,
)
}
fn just_stake(meta: Meta, stake: u64) -> StakeStateV2 {
StakeStateV2::Stake(
meta,
Stake {
delegation: Delegation {
stake,
..Delegation::default()
},
..Stake::default()
},
StakeFlags::empty(),
)
}
fn get_active_stake_for_tests(
stake_accounts: &[AccountSharedData],
clock: &Clock,
stake_history: &StakeHistory,
) -> u64 {
let mut active_stake = 0;
for account in stake_accounts {
if let StakeStateV2::Stake(_meta, stake, _stake_flags) = account.state().unwrap() {
let stake_status = stake.delegation.stake_activating_and_deactivating(
clock.epoch,
Some(stake_history),
None,
);
active_stake += stake_status.effective;
}
}
active_stake
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_stake_process_instruction(feature_set: Arc<FeatureSet>) {
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::initialize(
&Pubkey::new_unique(),
&Authorized::default(),
&Lockup::default(),
),
Err(InstructionError::InvalidAccountData),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::authorize(
&Pubkey::new_unique(),
&Pubkey::new_unique(),
&Pubkey::new_unique(),
StakeAuthorize::Staker,
None,
),
Err(InstructionError::InvalidAccountData),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::split(
&Pubkey::new_unique(),
&Pubkey::new_unique(),
100,
&invalid_stake_state_pubkey(),
)[2],
Err(InstructionError::InvalidAccountData),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::merge(
&Pubkey::new_unique(),
&invalid_stake_state_pubkey(),
&Pubkey::new_unique(),
)[0],
Err(InstructionError::InvalidAccountData),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::split_with_seed(
&Pubkey::new_unique(),
&Pubkey::new_unique(),
100,
&invalid_stake_state_pubkey(),
&Pubkey::new_unique(),
"seed",
)[1],
Err(InstructionError::InvalidAccountData),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::delegate_stake(
&Pubkey::new_unique(),
&Pubkey::new_unique(),
&invalid_vote_state_pubkey(),
),
Err(InstructionError::InvalidAccountData),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::withdraw(
&Pubkey::new_unique(),
&Pubkey::new_unique(),
&Pubkey::new_unique(),
100,
None,
),
Err(InstructionError::InvalidAccountData),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::deactivate_stake(&Pubkey::new_unique(), &Pubkey::new_unique()),
Err(InstructionError::InvalidAccountData),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::set_lockup(
&Pubkey::new_unique(),
&LockupArgs::default(),
&Pubkey::new_unique(),
),
Err(InstructionError::InvalidAccountData),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::deactivate_delinquent_stake(
&Pubkey::new_unique(),
&Pubkey::new_unique(),
&invalid_vote_state_pubkey(),
),
Err(InstructionError::IncorrectProgramId),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::deactivate_delinquent_stake(
&Pubkey::new_unique(),
&invalid_vote_state_pubkey(),
&Pubkey::new_unique(),
),
Err(InstructionError::InvalidAccountData),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::deactivate_delinquent_stake(
&Pubkey::new_unique(),
&invalid_vote_state_pubkey(),
&invalid_vote_state_pubkey(),
),
Err(InstructionError::InvalidAccountData),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_spoofed_stake_accounts(feature_set: Arc<FeatureSet>) {
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::initialize(
&spoofed_stake_state_pubkey(),
&Authorized::default(),
&Lockup::default(),
),
Err(InstructionError::InvalidAccountOwner),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::authorize(
&spoofed_stake_state_pubkey(),
&Pubkey::new_unique(),
&Pubkey::new_unique(),
StakeAuthorize::Staker,
None,
),
Err(InstructionError::InvalidAccountOwner),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::split(
&spoofed_stake_state_pubkey(),
&Pubkey::new_unique(),
100,
&Pubkey::new_unique(),
)[2],
Err(InstructionError::InvalidAccountOwner),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::split(
&Pubkey::new_unique(),
&Pubkey::new_unique(),
100,
&spoofed_stake_state_pubkey(),
)[2],
Err(InstructionError::IncorrectProgramId),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::merge(
&spoofed_stake_state_pubkey(),
&Pubkey::new_unique(),
&Pubkey::new_unique(),
)[0],
Err(InstructionError::InvalidAccountOwner),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::merge(
&Pubkey::new_unique(),
&spoofed_stake_state_pubkey(),
&Pubkey::new_unique(),
)[0],
Err(InstructionError::IncorrectProgramId),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::split_with_seed(
&spoofed_stake_state_pubkey(),
&Pubkey::new_unique(),
100,
&Pubkey::new_unique(),
&Pubkey::new_unique(),
"seed",
)[1],
Err(InstructionError::InvalidAccountOwner),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::delegate_stake(
&spoofed_stake_state_pubkey(),
&Pubkey::new_unique(),
&Pubkey::new_unique(),
),
Err(InstructionError::InvalidAccountOwner),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::withdraw(
&spoofed_stake_state_pubkey(),
&Pubkey::new_unique(),
&Pubkey::new_unique(),
100,
None,
),
Err(InstructionError::InvalidAccountOwner),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::deactivate_stake(&spoofed_stake_state_pubkey(), &Pubkey::new_unique()),
Err(InstructionError::InvalidAccountOwner),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::set_lockup(
&spoofed_stake_state_pubkey(),
&LockupArgs::default(),
&Pubkey::new_unique(),
),
Err(InstructionError::InvalidAccountOwner),
);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction::deactivate_delinquent_stake(
&spoofed_stake_state_pubkey(),
&Pubkey::new_unique(),
&Pubkey::new_unique(),
),
Err(InstructionError::InvalidAccountOwner),
);
process_instruction_as_one_arg(
Arc::clone(&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_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_stake_process_instruction_decode_bail(feature_set: Arc<FeatureSet>) {
// these will not call stake_state, have bogus contents
let stake_address = Pubkey::new_unique();
let stake_account = create_default_stake_account();
let rent_address = rent::id();
let rent = Rent::default();
let rent_account = create_account_shared_data_for_test(&rent);
let rewards_address = rewards::id();
let rewards_account = create_account_shared_data_for_test(&rewards::Rewards::new(0.0));
let stake_history_address = stake_history::id();
let stake_history_account = create_account_shared_data_for_test(&StakeHistory::default());
let vote_address = Pubkey::new_unique();
let vote_account = AccountSharedData::new(0, 0, &solana_vote_program::id());
let clock_address = clock::id();
let clock_account = create_account_shared_data_for_test(&clock::Clock::default());
#[allow(deprecated)]
let config_address = stake_config::id();
#[allow(deprecated)]
let config_account = config::create_account(0, &stake_config::Config::default());
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let withdrawal_amount = rent_exempt_reserve + minimum_delegation;
// gets the "is_empty()" check
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Initialize(
Authorized::default(),
Lockup::default(),
))
.unwrap(),
Vec::new(),
Vec::new(),
Err(InstructionError::NotEnoughAccountKeys),
);
// no account for rent
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Initialize(
Authorized::default(),
Lockup::default(),
))
.unwrap(),
vec![(stake_address, stake_account.clone())],
vec![AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
}],
Err(InstructionError::NotEnoughAccountKeys),
);
// fails to deserialize stake state
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Initialize(
Authorized::default(),
Lockup::default(),
))
.unwrap(),
vec![
(stake_address, stake_account.clone()),
(rent_address, rent_account),
],
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: rent_address,
is_signer: false,
is_writable: false,
},
],
Err(InstructionError::InvalidAccountData),
);
// gets the first check in delegate, wrong number of accounts
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
vec![(stake_address, stake_account.clone())],
vec![AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
}],
Err(InstructionError::NotEnoughAccountKeys),
);
// gets the sub-check for number of args
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
vec![(stake_address, stake_account.clone())],
vec![AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
}],
Err(InstructionError::NotEnoughAccountKeys),
);
// gets the check non-deserialize-able account in delegate_stake
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
vec![
(stake_address, stake_account.clone()),
(vote_address, vote_account.clone()),
(clock_address, clock_account),
(stake_history_address, stake_history_account.clone()),
(config_address, config_account),
],
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: clock_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: config_address,
is_signer: false,
is_writable: false,
},
],
Err(InstructionError::InvalidAccountData),
);
// Tests 3rd keyed account is of correct type (Clock instead of rewards) in withdraw
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(withdrawal_amount)).unwrap(),
vec![
(stake_address, stake_account.clone()),
(vote_address, vote_account.clone()),
(rewards_address, rewards_account.clone()),
(stake_history_address, stake_history_account),
],
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: rewards_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history_address,
is_signer: false,
is_writable: false,
},
],
Err(InstructionError::InvalidArgument),
);
// Tests correct number of accounts are provided in withdraw
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(withdrawal_amount)).unwrap(),
vec![(stake_address, stake_account.clone())],
vec![AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
}],
Err(InstructionError::NotEnoughAccountKeys),
);
// Tests 2nd keyed account is of correct type (Clock instead of rewards) in deactivate
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
vec![
(stake_address, stake_account.clone()),
(rewards_address, rewards_account),
],
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: rewards_address,
is_signer: false,
is_writable: false,
},
],
Err(InstructionError::InvalidArgument),
);
// Tests correct number of accounts are provided in deactivate
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
Vec::new(),
Vec::new(),
Err(InstructionError::NotEnoughAccountKeys),
);
// Tests correct number of accounts are provided in deactivate_delinquent
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DeactivateDelinquent).unwrap(),
Vec::new(),
Vec::new(),
Err(InstructionError::NotEnoughAccountKeys),
);
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DeactivateDelinquent).unwrap(),
vec![(stake_address, stake_account.clone())],
vec![AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
}],
Err(InstructionError::NotEnoughAccountKeys),
);
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DeactivateDelinquent).unwrap(),
vec![(stake_address, stake_account), (vote_address, vote_account)],
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
],
Err(InstructionError::NotEnoughAccountKeys),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_stake_checked_instructions(feature_set: Arc<FeatureSet>) {
let stake_address = Pubkey::new_unique();
let staker = Pubkey::new_unique();
let staker_account = create_default_account();
let withdrawer = Pubkey::new_unique();
let withdrawer_account = create_default_account();
let authorized_address = Pubkey::new_unique();
let authorized_account = create_default_account();
let new_authorized_account = create_default_account();
let clock_address = clock::id();
let clock_account = create_account_shared_data_for_test(&Clock::default());
let custodian = Pubkey::new_unique();
let custodian_account = create_default_account();
let rent = Rent::default();
let rent_address = rent::id();
let rent_account = create_account_shared_data_for_test(&rent);
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
// Test InitializeChecked with non-signing withdrawer
let mut instruction =
initialize_checked(&stake_address, &Authorized { staker, withdrawer });
instruction.accounts[3] = AccountMeta::new_readonly(withdrawer, false);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction,
Err(InstructionError::MissingRequiredSignature),
);
// Test InitializeChecked with withdrawer signer
let stake_account = AccountSharedData::new(
rent_exempt_reserve + minimum_delegation,
StakeStateV2::size_of(),
&id(),
);
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::InitializeChecked).unwrap(),
vec![
(stake_address, stake_account),
(rent_address, rent_account),
(staker, staker_account),
(withdrawer, withdrawer_account.clone()),
],
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: rent_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: staker,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: withdrawer,
is_signer: true,
is_writable: false,
},
],
Ok(()),
);
// Test AuthorizeChecked with non-signing authority
let mut instruction = authorize_checked(
&stake_address,
&authorized_address,
&staker,
StakeAuthorize::Staker,
None,
);
instruction.accounts[3] = AccountMeta::new_readonly(staker, false);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction,
Err(InstructionError::MissingRequiredSignature),
);
let mut instruction = authorize_checked(
&stake_address,
&authorized_address,
&withdrawer,
StakeAuthorize::Withdrawer,
None,
);
instruction.accounts[3] = AccountMeta::new_readonly(withdrawer, false);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction,
Err(InstructionError::MissingRequiredSignature),
);
// Test AuthorizeChecked with authority signer
let stake_account = AccountSharedData::new_data_with_space(
42,
&StakeStateV2::Initialized(Meta::auto(&authorized_address)),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::AuthorizeChecked(StakeAuthorize::Staker)).unwrap(),
vec![
(stake_address, stake_account.clone()),
(clock_address, clock_account.clone()),
(authorized_address, authorized_account.clone()),
(staker, new_authorized_account.clone()),
],
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authorized_address,
is_signer: true,
is_writable: false,
},
AccountMeta {
pubkey: staker,
is_signer: true,
is_writable: false,
},
],
Ok(()),
);
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::AuthorizeChecked(
StakeAuthorize::Withdrawer,
))
.unwrap(),
vec![
(stake_address, stake_account),
(clock_address, clock_account.clone()),
(authorized_address, authorized_account.clone()),
(withdrawer, new_authorized_account.clone()),
],
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authorized_address,
is_signer: true,
is_writable: false,
},
AccountMeta {
pubkey: withdrawer,
is_signer: true,
is_writable: false,
},
],
Ok(()),
);
// Test AuthorizeCheckedWithSeed with non-signing authority
let authorized_owner = Pubkey::new_unique();
let seed = "test seed";
let address_with_seed =
Pubkey::create_with_seed(&authorized_owner, seed, &authorized_owner).unwrap();
let mut instruction = authorize_checked_with_seed(
&stake_address,
&authorized_owner,
seed.to_string(),
&authorized_owner,
&staker,
StakeAuthorize::Staker,
None,
);
instruction.accounts[3] = AccountMeta::new_readonly(staker, false);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction,
Err(InstructionError::MissingRequiredSignature),
);
let mut instruction = authorize_checked_with_seed(
&stake_address,
&authorized_owner,
seed.to_string(),
&authorized_owner,
&staker,
StakeAuthorize::Withdrawer,
None,
);
instruction.accounts[3] = AccountMeta::new_readonly(staker, false);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction,
Err(InstructionError::MissingRequiredSignature),
);
// Test AuthorizeCheckedWithSeed with authority signer
let stake_account = AccountSharedData::new_data_with_space(
42,
&StakeStateV2::Initialized(Meta::auto(&address_with_seed)),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::AuthorizeCheckedWithSeed(
AuthorizeCheckedWithSeedArgs {
stake_authorize: StakeAuthorize::Staker,
authority_seed: seed.to_string(),
authority_owner: authorized_owner,
},
))
.unwrap(),
vec![
(address_with_seed, stake_account.clone()),
(authorized_owner, authorized_account.clone()),
(clock_address, clock_account.clone()),
(staker, new_authorized_account.clone()),
],
vec![
AccountMeta {
pubkey: address_with_seed,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: authorized_owner,
is_signer: true,
is_writable: false,
},
AccountMeta {
pubkey: clock_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: staker,
is_signer: true,
is_writable: false,
},
],
Ok(()),
);
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::AuthorizeCheckedWithSeed(
AuthorizeCheckedWithSeedArgs {
stake_authorize: StakeAuthorize::Withdrawer,
authority_seed: seed.to_string(),
authority_owner: authorized_owner,
},
))
.unwrap(),
vec![
(address_with_seed, stake_account),
(authorized_owner, authorized_account),
(clock_address, clock_account.clone()),
(withdrawer, new_authorized_account),
],
vec![
AccountMeta {
pubkey: address_with_seed,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: authorized_owner,
is_signer: true,
is_writable: false,
},
AccountMeta {
pubkey: clock_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: withdrawer,
is_signer: true,
is_writable: false,
},
],
Ok(()),
);
// Test SetLockupChecked with non-signing lockup custodian
let mut instruction = set_lockup_checked(
&stake_address,
&LockupArgs {
unix_timestamp: None,
epoch: Some(1),
custodian: Some(custodian),
},
&withdrawer,
);
instruction.accounts[2] = AccountMeta::new_readonly(custodian, false);
process_instruction_as_one_arg(
Arc::clone(&feature_set),
&instruction,
Err(InstructionError::MissingRequiredSignature),
);
// Test SetLockupChecked with lockup custodian signer
let stake_account = AccountSharedData::new_data_with_space(
42,
&StakeStateV2::Initialized(Meta::auto(&withdrawer)),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
process_instruction(
Arc::clone(&feature_set),
&instruction.data,
vec![
(clock_address, clock_account),
(stake_address, stake_account),
(withdrawer, withdrawer_account),
(custodian, custodian_account),
],
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: withdrawer,
is_signer: true,
is_writable: false,
},
AccountMeta {
pubkey: custodian,
is_signer: true,
is_writable: false,
},
],
Ok(()),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_stake_initialize(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_lamports = rent_exempt_reserve;
let stake_address = solana_sdk::pubkey::new_rand();
let stake_account = AccountSharedData::new(stake_lamports, StakeStateV2::size_of(), &id());
let custodian_address = solana_sdk::pubkey::new_rand();
let lockup = Lockup {
epoch: 1,
unix_timestamp: 0,
custodian: custodian_address,
};
let instruction_data = serialize(&StakeInstruction::Initialize(
Authorized::auto(&stake_address),
lockup,
))
.unwrap();
let mut transaction_accounts = vec![
(stake_address, stake_account.clone()),
(rent::id(), create_account_shared_data_for_test(&rent)),
];
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: rent::id(),
is_signer: false,
is_writable: false,
},
];
// should pass
let accounts = process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
// check that we see what we expect
assert_eq!(
from(&accounts[0]).unwrap(),
StakeStateV2::Initialized(Meta {
authorized: Authorized::auto(&stake_address),
rent_exempt_reserve,
lockup,
}),
);
// 2nd time fails, can't move it from anything other than uninit->init
transaction_accounts[0] = (stake_address, accounts[0].clone());
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InvalidAccountData),
);
transaction_accounts[0] = (stake_address, stake_account);
// not enough balance for rent
transaction_accounts[1] = (
rent::id(),
create_account_shared_data_for_test(&Rent {
lamports_per_byte_year: rent.lamports_per_byte_year + 1,
..rent
}),
);
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
// incorrect account sizes
let stake_account =
AccountSharedData::new(stake_lamports, StakeStateV2::size_of() + 1, &id());
transaction_accounts[0] = (stake_address, stake_account);
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InvalidAccountData),
);
let stake_account =
AccountSharedData::new(stake_lamports, StakeStateV2::size_of() - 1, &id());
transaction_accounts[0] = (stake_address, stake_account);
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts,
instruction_accounts,
Err(InstructionError::InvalidAccountData),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_authorize(feature_set: Arc<FeatureSet>) {
let authority_address = solana_sdk::pubkey::new_rand();
let authority_address_2 = solana_sdk::pubkey::new_rand();
let stake_address = solana_sdk::pubkey::new_rand();
let stake_lamports = 42;
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::default(),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let to_address = solana_sdk::pubkey::new_rand();
let to_account = AccountSharedData::new(1, 0, &system_program::id());
let mut transaction_accounts = vec![
(stake_address, stake_account),
(to_address, to_account),
(authority_address, AccountSharedData::default()),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authority_address,
is_signer: false,
is_writable: false,
},
];
// should fail, uninit
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Authorize(
authority_address,
StakeAuthorize::Staker,
))
.unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InvalidAccountData),
);
// should pass
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::Initialized(Meta::auto(&stake_address)),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
transaction_accounts[0] = (stake_address, stake_account);
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Authorize(
authority_address,
StakeAuthorize::Staker,
))
.unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Authorize(
authority_address,
StakeAuthorize::Withdrawer,
))
.unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
if let StakeStateV2::Initialized(Meta { authorized, .. }) = from(&accounts[0]).unwrap() {
assert_eq!(authorized.staker, authority_address);
assert_eq!(authorized.withdrawer, authority_address);
} else {
panic!();
}
// A second authorization signed by the stake account should fail
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Authorize(
authority_address_2,
StakeAuthorize::Staker,
))
.unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
// Test a second authorization by the new authority_address
instruction_accounts[0].is_signer = false;
instruction_accounts[2].is_signer = true;
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Authorize(
authority_address_2,
StakeAuthorize::Staker,
))
.unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
if let StakeStateV2::Initialized(Meta { authorized, .. }) = from(&accounts[0]).unwrap() {
assert_eq!(authorized.staker, authority_address_2);
} else {
panic!();
}
// Test a successful action by the currently authorized withdrawer
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: to_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authority_address,
is_signer: true,
is_writable: false,
},
];
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
assert_eq!(from(&accounts[0]).unwrap(), StakeStateV2::Uninitialized);
// Test that withdrawal to account fails without authorized withdrawer
instruction_accounts[4].is_signer = false;
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(),
transaction_accounts,
instruction_accounts,
Err(InstructionError::MissingRequiredSignature),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_authorize_override(feature_set: Arc<FeatureSet>) {
let authority_address = solana_sdk::pubkey::new_rand();
let mallory_address = solana_sdk::pubkey::new_rand();
let stake_address = solana_sdk::pubkey::new_rand();
let stake_lamports = 42;
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::Initialized(Meta::auto(&stake_address)),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let mut transaction_accounts = vec![
(stake_address, stake_account),
(authority_address, AccountSharedData::default()),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
];
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authority_address,
is_signer: false,
is_writable: false,
},
];
// Authorize a staker pubkey and move the withdrawer key into cold storage.
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Authorize(
authority_address,
StakeAuthorize::Staker,
))
.unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// Attack! The stake key (a hot key) is stolen and used to authorize a new staker.
instruction_accounts[0].is_signer = false;
instruction_accounts[2].is_signer = true;
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Authorize(
mallory_address,
StakeAuthorize::Staker,
))
.unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// Verify the original staker no longer has access.
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Authorize(
authority_address,
StakeAuthorize::Staker,
))
.unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
// Verify the withdrawer (pulled from cold storage) can save the day.
instruction_accounts[0].is_signer = true;
instruction_accounts[2].is_signer = false;
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Authorize(
authority_address,
StakeAuthorize::Withdrawer,
))
.unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// Attack! Verify the staker cannot be used to authorize a withdraw.
instruction_accounts[0].is_signer = false;
instruction_accounts[2] = AccountMeta {
pubkey: mallory_address,
is_signer: true,
is_writable: false,
};
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Authorize(
authority_address,
StakeAuthorize::Withdrawer,
))
.unwrap(),
transaction_accounts,
instruction_accounts,
Err(InstructionError::MissingRequiredSignature),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_authorize_with_seed(feature_set: Arc<FeatureSet>) {
let authority_base_address = solana_sdk::pubkey::new_rand();
let authority_address = solana_sdk::pubkey::new_rand();
let seed = "42";
let stake_address = Pubkey::create_with_seed(&authority_base_address, seed, &id()).unwrap();
let stake_lamports = 42;
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::Initialized(Meta::auto(&stake_address)),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let mut transaction_accounts = vec![
(stake_address, stake_account),
(authority_base_address, AccountSharedData::default()),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
];
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: authority_base_address,
is_signer: true,
is_writable: false,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
];
// Wrong seed
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::AuthorizeWithSeed(
AuthorizeWithSeedArgs {
new_authorized_pubkey: authority_address,
stake_authorize: StakeAuthorize::Staker,
authority_seed: "".to_string(),
authority_owner: id(),
},
))
.unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
// Wrong base
instruction_accounts[1].pubkey = authority_address;
let instruction_data = serialize(&StakeInstruction::AuthorizeWithSeed(
AuthorizeWithSeedArgs {
new_authorized_pubkey: authority_address,
stake_authorize: StakeAuthorize::Staker,
authority_seed: seed.to_string(),
authority_owner: id(),
},
))
.unwrap();
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
instruction_accounts[1].pubkey = authority_base_address;
// Set stake authority
let accounts = process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// Set withdraw authority
let instruction_data = serialize(&StakeInstruction::AuthorizeWithSeed(
AuthorizeWithSeedArgs {
new_authorized_pubkey: authority_address,
stake_authorize: StakeAuthorize::Withdrawer,
authority_seed: seed.to_string(),
authority_owner: id(),
},
))
.unwrap();
let accounts = process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// No longer withdraw authority
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts,
instruction_accounts,
Err(InstructionError::MissingRequiredSignature),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_authorize_delegated_stake(feature_set: Arc<FeatureSet>) {
let authority_address = solana_sdk::pubkey::new_rand();
let stake_address = solana_sdk::pubkey::new_rand();
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = minimum_delegation;
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::Initialized(Meta::auto(&stake_address)),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let vote_address = solana_sdk::pubkey::new_rand();
let vote_account =
vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100);
let vote_address_2 = solana_sdk::pubkey::new_rand();
let mut vote_account_2 =
vote_state::create_account(&vote_address_2, &solana_sdk::pubkey::new_rand(), 0, 100);
vote_account_2.set_state(&VoteState::default()).unwrap();
#[allow(deprecated)]
let mut transaction_accounts = vec![
(stake_address, stake_account),
(vote_address, vote_account),
(vote_address_2, vote_account_2),
(
authority_address,
AccountSharedData::new(42, 0, &system_program::id()),
),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
stake_config::id(),
config::create_account(0, &stake_config::Config::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
#[allow(deprecated)]
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_config::id(),
is_signer: false,
is_writable: false,
},
];
// delegate stake
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// deactivate, so we can re-delegate
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// authorize
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Authorize(
authority_address,
StakeAuthorize::Staker,
))
.unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authority_address,
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
assert_eq!(
authorized_from(&accounts[0]).unwrap().staker,
authority_address
);
// Random other account should fail
instruction_accounts[0].is_signer = false;
instruction_accounts[1].pubkey = vote_address_2;
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
// Authorized staker should succeed
instruction_accounts.push(AccountMeta {
pubkey: authority_address,
is_signer: true,
is_writable: false,
});
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
instruction_accounts,
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
assert_eq!(
stake_from(&accounts[0]).unwrap().delegation.voter_pubkey,
vote_address_2,
);
// Test another staking action
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
transaction_accounts,
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authority_address,
is_signer: true,
is_writable: false,
},
],
Ok(()),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_stake_delegate(feature_set: Arc<FeatureSet>) {
let mut vote_state = VoteState::default();
for i in 0..1000 {
vote_state::process_slot_vote_unchecked(&mut vote_state, i);
}
let vote_state_credits = vote_state.credits();
let vote_address = solana_sdk::pubkey::new_rand();
let vote_address_2 = solana_sdk::pubkey::new_rand();
let mut vote_account =
vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100);
let mut vote_account_2 =
vote_state::create_account(&vote_address_2, &solana_sdk::pubkey::new_rand(), 0, 100);
vote_account
.set_state(&VoteStateVersions::new_current(vote_state.clone()))
.unwrap();
vote_account_2
.set_state(&VoteStateVersions::new_current(vote_state))
.unwrap();
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = minimum_delegation;
let stake_address = solana_sdk::pubkey::new_rand();
let mut stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::Initialized(Meta {
authorized: Authorized {
staker: stake_address,
withdrawer: stake_address,
},
..Meta::default()
}),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let mut clock = Clock {
epoch: 1,
..Clock::default()
};
#[allow(deprecated)]
let mut transaction_accounts = vec![
(stake_address, stake_account.clone()),
(vote_address, vote_account),
(vote_address_2, vote_account_2.clone()),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
stake_config::id(),
config::create_account(0, &stake_config::Config::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
#[allow(deprecated)]
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_config::id(),
is_signer: false,
is_writable: false,
},
];
// should fail, unsigned stake account
instruction_accounts[0].is_signer = false;
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
instruction_accounts[0].is_signer = true;
// should pass
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
// verify that delegate() looks right, compare against hand-rolled
assert_eq!(
stake_from(&accounts[0]).unwrap(),
Stake {
delegation: Delegation {
voter_pubkey: vote_address,
stake: stake_lamports,
activation_epoch: clock.epoch,
deactivation_epoch: std::u64::MAX,
..Delegation::default()
},
credits_observed: vote_state_credits,
}
);
// verify that delegate fails as stake is active and not deactivating
clock.epoch += 1;
transaction_accounts[0] = (stake_address, accounts[0].clone());
transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock));
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(StakeError::TooSoonToRedelegate.into()),
);
// deactivate
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
// verify that delegate to a different vote account fails
// during deactivation
transaction_accounts[0] = (stake_address, accounts[0].clone());
instruction_accounts[1].pubkey = vote_address_2;
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(StakeError::TooSoonToRedelegate.into()),
);
instruction_accounts[1].pubkey = vote_address;
// verify that delegate succeeds to same vote account
// when stake is deactivating
let accounts_2 = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
// verify that deactivation has been cleared
let stake = stake_from(&accounts_2[0]).unwrap();
assert_eq!(stake.delegation.deactivation_epoch, std::u64::MAX);
// verify that delegate to a different vote account fails
// if stake is still active
transaction_accounts[0] = (stake_address, accounts_2[0].clone());
instruction_accounts[1].pubkey = vote_address_2;
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(StakeError::TooSoonToRedelegate.into()),
);
// without stake history, cool down is instantaneous
clock.epoch += 1;
transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock));
// verify that delegate can be called to new vote account, 2nd is redelegate
transaction_accounts[0] = (stake_address, accounts[0].clone());
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
instruction_accounts[1].pubkey = vote_address;
// verify that delegate() looks right, compare against hand-rolled
assert_eq!(
stake_from(&accounts[0]).unwrap(),
Stake {
delegation: Delegation {
voter_pubkey: vote_address_2,
stake: stake_lamports,
activation_epoch: clock.epoch,
deactivation_epoch: std::u64::MAX,
..Delegation::default()
},
credits_observed: vote_state_credits,
}
);
// signed but faked vote account
transaction_accounts[1] = (vote_address_2, vote_account_2);
transaction_accounts[1]
.1
.set_owner(solana_sdk::pubkey::new_rand());
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(solana_sdk::instruction::InstructionError::IncorrectProgramId),
);
// verify that non-stakes fail delegate()
let stake_state = StakeStateV2::RewardsPool;
stake_account.set_state(&stake_state).unwrap();
transaction_accounts[0] = (stake_address, stake_account);
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts,
instruction_accounts,
Err(solana_sdk::instruction::InstructionError::IncorrectProgramId),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_redelegate_consider_balance_changes(feature_set: Arc<FeatureSet>) {
let mut clock = Clock::default();
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let initial_lamports = 4242424242;
let stake_lamports = rent_exempt_reserve + initial_lamports;
let recipient_address = solana_sdk::pubkey::new_rand();
let authority_address = solana_sdk::pubkey::new_rand();
let vote_address = solana_sdk::pubkey::new_rand();
let vote_account =
vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100);
let stake_address = solana_sdk::pubkey::new_rand();
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::Initialized(Meta {
rent_exempt_reserve,
..Meta::auto(&authority_address)
}),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
#[allow(deprecated)]
let mut transaction_accounts = vec![
(stake_address, stake_account),
(vote_address, vote_account),
(
recipient_address,
AccountSharedData::new(1, 0, &system_program::id()),
),
(authority_address, AccountSharedData::default()),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
stake_config::id(),
config::create_account(0, &stake_config::Config::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
#[allow(deprecated)]
let delegate_instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_config::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authority_address,
is_signer: true,
is_writable: false,
},
];
let deactivate_instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authority_address,
is_signer: true,
is_writable: false,
},
];
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
delegate_instruction_accounts.clone(),
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
clock.epoch += 1;
transaction_accounts[2] = (clock::id(), create_account_shared_data_for_test(&clock));
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
transaction_accounts.clone(),
deactivate_instruction_accounts.clone(),
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// Once deactivated, we withdraw stake to new account
clock.epoch += 1;
transaction_accounts[2] = (clock::id(), create_account_shared_data_for_test(&clock));
let withdraw_lamports = initial_lamports / 2;
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(withdraw_lamports)).unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: recipient_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authority_address,
is_signer: true,
is_writable: false,
},
],
Ok(()),
);
let expected_balance = rent_exempt_reserve + initial_lamports - withdraw_lamports;
assert_eq!(accounts[0].lamports(), expected_balance);
transaction_accounts[0] = (stake_address, accounts[0].clone());
clock.epoch += 1;
transaction_accounts[2] = (clock::id(), create_account_shared_data_for_test(&clock));
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
delegate_instruction_accounts.clone(),
Ok(()),
);
assert_eq!(
stake_from(&accounts[0]).unwrap().delegation.stake,
accounts[0].lamports() - rent_exempt_reserve,
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
clock.epoch += 1;
transaction_accounts[2] = (clock::id(), create_account_shared_data_for_test(&clock));
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
transaction_accounts.clone(),
deactivate_instruction_accounts,
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// Out of band deposit
transaction_accounts[0]
.1
.checked_add_lamports(withdraw_lamports)
.unwrap();
clock.epoch += 1;
transaction_accounts[2] = (clock::id(), create_account_shared_data_for_test(&clock));
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts,
delegate_instruction_accounts,
Ok(()),
);
assert_eq!(
stake_from(&accounts[0]).unwrap().delegation.stake,
accounts[0].lamports() - rent_exempt_reserve,
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split(feature_set: Arc<FeatureSet>) {
let stake_history = StakeHistory::default();
let current_epoch = 100;
let clock = Clock {
epoch: current_epoch,
..Clock::default()
};
let stake_address = solana_sdk::pubkey::new_rand();
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = minimum_delegation * 2;
let split_to_address = solana_sdk::pubkey::new_rand();
let split_to_account = AccountSharedData::new_data_with_space(
0,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let mut transaction_accounts = vec![
(stake_address, AccountSharedData::default()),
(split_to_address, split_to_account.clone()),
(
rent::id(),
create_account_shared_data_for_test(&Rent {
lamports_per_byte_year: 0,
..Rent::default()
}),
),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: split_to_address,
is_signer: false,
is_writable: true,
},
];
let feature_set = Arc::new(feature_set);
for state in [
StakeStateV2::Initialized(Meta::auto(&stake_address)),
just_stake(Meta::auto(&stake_address), stake_lamports),
] {
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let expected_active_stake = get_active_stake_for_tests(
&[stake_account.clone(), split_to_account.clone()],
&clock,
&stake_history,
);
transaction_accounts[0] = (stake_address, stake_account);
// should fail, split more than available
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
// should pass
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
// no lamport leakage
assert_eq!(
accounts[0].lamports() + accounts[1].lamports(),
stake_lamports
);
// no deactivated stake
assert_eq!(
expected_active_stake,
get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history)
);
assert_eq!(from(&accounts[0]).unwrap(), from(&accounts[1]).unwrap());
match state {
StakeStateV2::Initialized(_meta) => {
assert_eq!(from(&accounts[0]).unwrap(), state);
}
StakeStateV2::Stake(_meta, _stake, _) => {
let stake_0 = from(&accounts[0]).unwrap().stake();
assert_eq!(stake_0.unwrap().delegation.stake, stake_lamports / 2);
}
_ => unreachable!(),
}
}
// should fail, fake owner of destination
let split_to_account = AccountSharedData::new_data_with_space(
0,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&solana_sdk::pubkey::new_rand(),
)
.unwrap();
transaction_accounts[1] = (split_to_address, split_to_account);
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
transaction_accounts,
instruction_accounts,
Err(InstructionError::IncorrectProgramId),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_withdraw_stake(feature_set: Arc<FeatureSet>) {
let recipient_address = solana_sdk::pubkey::new_rand();
let authority_address = solana_sdk::pubkey::new_rand();
let custodian_address = solana_sdk::pubkey::new_rand();
let stake_address = solana_sdk::pubkey::new_rand();
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = minimum_delegation;
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let vote_address = solana_sdk::pubkey::new_rand();
let mut vote_account =
vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100);
vote_account
.set_state(&VoteStateVersions::new_current(VoteState::default()))
.unwrap();
#[allow(deprecated)]
let mut transaction_accounts = vec![
(stake_address, stake_account),
(vote_address, vote_account),
(recipient_address, AccountSharedData::default()),
(
authority_address,
AccountSharedData::new(42, 0, &system_program::id()),
),
(custodian_address, AccountSharedData::default()),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
rent::id(),
create_account_shared_data_for_test(&Rent::free()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
stake_config::id(),
config::create_account(0, &stake_config::Config::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: recipient_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: false,
},
];
// should fail, no signer
instruction_accounts[4].is_signer = false;
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
instruction_accounts[4].is_signer = true;
// should pass, signed keyed account and uninitialized
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
assert_eq!(accounts[0].lamports(), 0);
assert_eq!(from(&accounts[0]).unwrap(), StakeStateV2::Uninitialized);
// initialize stake
let lockup = Lockup {
unix_timestamp: 0,
epoch: 0,
custodian: custodian_address,
};
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Initialize(
Authorized::auto(&stake_address),
lockup,
))
.unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: rent::id(),
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// should fail, signed keyed account and locked up, more than available
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(stake_lamports + 1)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
// Stake some lamports (available lamports for withdrawals will reduce to zero)
#[allow(deprecated)]
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_config::id(),
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// simulate rewards
transaction_accounts[0].1.checked_add_lamports(10).unwrap();
// withdrawal before deactivate works for rewards amount
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(10)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
// withdrawal of rewards fails if not in excess of stake
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(11)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
// deactivate the stake before withdrawal
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// simulate time passing
let clock = Clock {
epoch: 100,
..Clock::default()
};
transaction_accounts[5] = (clock::id(), create_account_shared_data_for_test(&clock));
// Try to withdraw more than what's available
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(stake_lamports + 11)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
// Try to withdraw all lamports
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(stake_lamports + 10)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
assert_eq!(accounts[0].lamports(), 0);
assert_eq!(from(&accounts[0]).unwrap(), StakeStateV2::Uninitialized);
// overflow
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_account = AccountSharedData::new_data_with_space(
1_000_000_000,
&StakeStateV2::Initialized(Meta {
rent_exempt_reserve,
authorized: Authorized {
staker: authority_address,
withdrawer: authority_address,
},
lockup: Lockup::default(),
}),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
transaction_accounts[0] = (stake_address, stake_account.clone());
transaction_accounts[2] = (recipient_address, stake_account);
instruction_accounts[4].pubkey = authority_address;
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(u64::MAX - 10)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
// should fail, invalid state
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::RewardsPool,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
transaction_accounts[0] = (stake_address, stake_account);
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(),
transaction_accounts,
instruction_accounts,
Err(InstructionError::InvalidAccountData),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_withdraw_stake_before_warmup(feature_set: Arc<FeatureSet>) {
let recipient_address = solana_sdk::pubkey::new_rand();
let stake_address = solana_sdk::pubkey::new_rand();
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = minimum_delegation;
let total_lamports = stake_lamports + 33;
let stake_account = AccountSharedData::new_data_with_space(
total_lamports,
&StakeStateV2::Initialized(Meta::auto(&stake_address)),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let vote_address = solana_sdk::pubkey::new_rand();
let mut vote_account =
vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100);
vote_account
.set_state(&VoteStateVersions::new_current(VoteState::default()))
.unwrap();
let mut clock = Clock {
epoch: 16,
..Clock::default()
};
#[allow(deprecated)]
let mut transaction_accounts = vec![
(stake_address, stake_account),
(vote_address, vote_account),
(recipient_address, AccountSharedData::default()),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
stake_config::id(),
config::create_account(0, &stake_config::Config::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: recipient_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: false,
},
];
// Stake some lamports (available lamports for withdrawals will reduce to zero)
#[allow(deprecated)]
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_config::id(),
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// Try to withdraw stake
let stake_history = create_stake_history_from_delegations(
None,
0..clock.epoch,
&[stake_from(&accounts[0]).unwrap().delegation],
None,
);
transaction_accounts[4] = (
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
);
clock.epoch = 0;
transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock));
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(
total_lamports - stake_lamports + 1,
))
.unwrap(),
transaction_accounts,
instruction_accounts,
Err(InstructionError::InsufficientFunds),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_withdraw_lockup(feature_set: Arc<FeatureSet>) {
let recipient_address = solana_sdk::pubkey::new_rand();
let custodian_address = solana_sdk::pubkey::new_rand();
let stake_address = solana_sdk::pubkey::new_rand();
let total_lamports = 100;
let mut meta = Meta {
lockup: Lockup {
unix_timestamp: 0,
epoch: 1,
custodian: custodian_address,
},
..Meta::auto(&stake_address)
};
let stake_account = AccountSharedData::new_data_with_space(
total_lamports,
&StakeStateV2::Initialized(meta),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let mut clock = Clock::default();
let mut transaction_accounts = vec![
(stake_address, stake_account.clone()),
(recipient_address, AccountSharedData::default()),
(custodian_address, AccountSharedData::default()),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: recipient_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: false,
},
];
// should fail, lockup is still in force
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(StakeError::LockupInForce.into()),
);
// should pass
instruction_accounts.push(AccountMeta {
pubkey: custodian_address,
is_signer: true,
is_writable: false,
});
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
assert_eq!(from(&accounts[0]).unwrap(), StakeStateV2::Uninitialized);
// should pass, custodian is the same as the withdraw authority
instruction_accounts[5].pubkey = stake_address;
meta.lockup.custodian = stake_address;
let stake_account_self_as_custodian = AccountSharedData::new_data_with_space(
total_lamports,
&StakeStateV2::Initialized(meta),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
transaction_accounts[0] = (stake_address, stake_account_self_as_custodian);
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
assert_eq!(from(&accounts[0]).unwrap(), StakeStateV2::Uninitialized);
transaction_accounts[0] = (stake_address, stake_account);
// should pass, lockup has expired
instruction_accounts.pop();
clock.epoch += 1;
transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock));
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(),
transaction_accounts,
instruction_accounts,
Ok(()),
);
assert_eq!(from(&accounts[0]).unwrap(), StakeStateV2::Uninitialized);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_withdraw_rent_exempt(feature_set: Arc<FeatureSet>) {
let recipient_address = solana_sdk::pubkey::new_rand();
let custodian_address = solana_sdk::pubkey::new_rand();
let stake_address = solana_sdk::pubkey::new_rand();
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = 7 * minimum_delegation;
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports + rent_exempt_reserve,
&StakeStateV2::Initialized(Meta {
rent_exempt_reserve,
..Meta::auto(&stake_address)
}),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let transaction_accounts = vec![
(stake_address, stake_account),
(recipient_address, AccountSharedData::default()),
(custodian_address, AccountSharedData::default()),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: recipient_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: false,
},
];
// should pass, withdrawing initialized account down to minimum balance
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
// should fail, withdrawal that would leave less than rent-exempt reserve
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(stake_lamports + 1)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
// should pass, withdrawal of complete account
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(
stake_lamports + rent_exempt_reserve,
))
.unwrap(),
transaction_accounts,
instruction_accounts,
Ok(()),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_deactivate(feature_set: Arc<FeatureSet>) {
let stake_address = solana_sdk::pubkey::new_rand();
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = minimum_delegation;
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::Initialized(Meta::auto(&stake_address)),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let vote_address = solana_sdk::pubkey::new_rand();
let mut vote_account =
vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100);
vote_account
.set_state(&VoteStateVersions::new_current(VoteState::default()))
.unwrap();
#[allow(deprecated)]
let mut transaction_accounts = vec![
(stake_address, stake_account),
(vote_address, vote_account),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
stake_config::id(),
config::create_account(0, &stake_config::Config::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
];
// should fail, not signed
instruction_accounts[0].is_signer = false;
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InvalidAccountData),
);
instruction_accounts[0].is_signer = true;
// should fail, not staked yet
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InvalidAccountData),
);
// Staking
#[allow(deprecated)]
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_config::id(),
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// should pass
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// should fail, only works once
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
transaction_accounts,
instruction_accounts,
Err(StakeError::AlreadyDeactivated.into()),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_set_lockup(feature_set: Arc<FeatureSet>) {
let custodian_address = solana_sdk::pubkey::new_rand();
let authorized_address = solana_sdk::pubkey::new_rand();
let stake_address = solana_sdk::pubkey::new_rand();
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = minimum_delegation;
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let vote_address = solana_sdk::pubkey::new_rand();
let mut vote_account =
vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100);
vote_account
.set_state(&VoteStateVersions::new_current(VoteState::default()))
.unwrap();
let instruction_data = serialize(&StakeInstruction::SetLockup(LockupArgs {
unix_timestamp: Some(1),
epoch: Some(1),
custodian: Some(custodian_address),
}))
.unwrap();
#[allow(deprecated)]
let mut transaction_accounts = vec![
(stake_address, stake_account),
(vote_address, vote_account),
(authorized_address, AccountSharedData::default()),
(custodian_address, AccountSharedData::default()),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
rent::id(),
create_account_shared_data_for_test(&Rent::free()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
stake_config::id(),
config::create_account(0, &stake_config::Config::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: custodian_address,
is_signer: true,
is_writable: false,
},
];
// should fail, wrong state
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InvalidAccountData),
);
// initialize stake
let lockup = Lockup {
unix_timestamp: 1,
epoch: 1,
custodian: custodian_address,
};
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Initialize(
Authorized::auto(&stake_address),
lockup,
))
.unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: rent::id(),
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// should fail, not signed
instruction_accounts[2].is_signer = false;
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
instruction_accounts[2].is_signer = true;
// should pass
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
// Staking
#[allow(deprecated)]
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_config::id(),
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// should fail, not signed
instruction_accounts[2].is_signer = false;
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
instruction_accounts[2].is_signer = true;
// should pass
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
// Lockup in force
let instruction_data = serialize(&StakeInstruction::SetLockup(LockupArgs {
unix_timestamp: Some(2),
epoch: None,
custodian: None,
}))
.unwrap();
// should fail, authorized withdrawer cannot change it
instruction_accounts[0].is_signer = true;
instruction_accounts[2].is_signer = false;
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
instruction_accounts[0].is_signer = false;
instruction_accounts[2].is_signer = true;
// should pass, custodian can change it
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
// Lockup expired
let clock = Clock {
unix_timestamp: UnixTimestamp::MAX,
epoch: Epoch::MAX,
..Clock::default()
};
transaction_accounts[4] = (clock::id(), create_account_shared_data_for_test(&clock));
// should fail, custodian cannot change it
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
// should pass, authorized withdrawer can change it
instruction_accounts[0].is_signer = true;
instruction_accounts[2].is_signer = false;
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
// Change authorized withdrawer
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Authorize(
authorized_address,
StakeAuthorize::Withdrawer,
))
.unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authorized_address,
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
// should fail, previous authorized withdrawer cannot change the lockup anymore
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
transaction_accounts,
instruction_accounts,
Err(InstructionError::MissingRequiredSignature),
);
}
/// Ensure that `initialize()` respects the minimum balance requirements
/// - Assert 1: accounts with a balance equal-to the rent exemption initialize OK
/// - Assert 2: accounts with a balance less-than the rent exemption do not initialize
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_initialize_minimum_balance(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_address = solana_sdk::pubkey::new_rand();
let instruction_data = serialize(&StakeInstruction::Initialize(
Authorized::auto(&stake_address),
Lockup::default(),
))
.unwrap();
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: rent::id(),
is_signer: false,
is_writable: false,
},
];
for (lamports, expected_result) in [
(rent_exempt_reserve, Ok(())),
(
rent_exempt_reserve - 1,
Err(InstructionError::InsufficientFunds),
),
] {
let stake_account = AccountSharedData::new(lamports, StakeStateV2::size_of(), &id());
process_instruction(
Arc::clone(&feature_set),
&instruction_data,
vec![
(stake_address, stake_account),
(rent::id(), create_account_shared_data_for_test(&rent)),
],
instruction_accounts.clone(),
expected_result,
);
}
}
/// Ensure that `delegate()` respects the minimum delegation requirements
/// - Assert 1: delegating an amount equal-to the minimum succeeds
/// - Assert 2: delegating an amount less-than the minimum fails
/// Also test both asserts above over both StakeStateV2::{Initialized and Stake}, since the logic
/// is slightly different for the variants.
///
/// NOTE: Even though new stake accounts must have a minimum balance that is at least
/// the minimum delegation (plus rent exempt reserve), the old behavior allowed
/// withdrawing below the minimum delegation, then re-delegating successfully (see
/// `test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation()` for
/// more information.)
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_delegate_minimum_stake_delegation(feature_set: Arc<FeatureSet>) {
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_address = solana_sdk::pubkey::new_rand();
let meta = Meta {
rent_exempt_reserve,
..Meta::auto(&stake_address)
};
let vote_address = solana_sdk::pubkey::new_rand();
let vote_account =
vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100);
#[allow(deprecated)]
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_config::id(),
is_signer: false,
is_writable: false,
},
];
for (stake_delegation, expected_result) in &[
(minimum_delegation, Ok(())),
(
minimum_delegation - 1,
Err(StakeError::InsufficientDelegation),
),
] {
for stake_state in &[
StakeStateV2::Initialized(meta),
just_stake(meta, *stake_delegation),
] {
let stake_account = AccountSharedData::new_data_with_space(
stake_delegation + rent_exempt_reserve,
stake_state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
#[allow(deprecated)]
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
vec![
(stake_address, stake_account),
(vote_address, vote_account.clone()),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
stake_config::id(),
config::create_account(0, &stake_config::Config::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
],
instruction_accounts.clone(),
expected_result.clone().map_err(|e| e.into()),
);
}
}
}
/// Ensure that `split()` respects the minimum delegation requirements. This applies to
/// both the source and destination acounts. Thus, we have four permutations possible based on
/// if each account's post-split delegation is equal-to (EQ) or less-than (LT) the minimum:
///
/// source | dest | result
/// --------+------+--------
/// EQ | EQ | Ok
/// EQ | LT | Err
/// LT | EQ | Err
/// LT | LT | Err
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split_minimum_stake_delegation(feature_set: Arc<FeatureSet>) {
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_history = StakeHistory::default();
let current_epoch = 100;
let clock = Clock {
epoch: current_epoch,
..Clock::default()
};
let source_address = Pubkey::new_unique();
let source_meta = Meta {
rent_exempt_reserve,
..Meta::auto(&source_address)
};
let dest_address = Pubkey::new_unique();
let dest_account = AccountSharedData::new_data_with_space(
rent_exempt_reserve,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let instruction_accounts = vec![
AccountMeta {
pubkey: source_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: dest_address,
is_signer: false,
is_writable: true,
},
];
for (source_delegation, split_amount, expected_result) in [
(minimum_delegation * 2, minimum_delegation, Ok(())),
(
minimum_delegation * 2,
minimum_delegation - 1,
Err(InstructionError::InsufficientFunds),
),
(
(minimum_delegation * 2) - 1,
minimum_delegation,
Err(InstructionError::InsufficientFunds),
),
(
(minimum_delegation - 1) * 2,
minimum_delegation - 1,
Err(InstructionError::InsufficientFunds),
),
] {
let source_account = AccountSharedData::new_data_with_space(
source_delegation + rent_exempt_reserve,
&just_stake(source_meta, source_delegation),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let expected_active_stake = get_active_stake_for_tests(
&[source_account.clone(), dest_account.clone()],
&clock,
&stake_history,
);
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(split_amount)).unwrap(),
vec![
(source_address, source_account),
(dest_address, dest_account.clone()),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
],
instruction_accounts.clone(),
expected_result.clone(),
);
assert_eq!(
expected_active_stake,
get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history)
);
}
}
/// Ensure that splitting the full amount from an account respects the minimum delegation
/// requirements. This ensures that we are future-proofing/testing any raises to the minimum
/// delegation.
/// - Assert 1: splitting the full amount from an account that has at least the minimum
/// delegation is OK
/// - Assert 2: splitting the full amount from an account that has less than the minimum
/// delegation is not OK
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split_full_amount_minimum_stake_delegation(feature_set: Arc<FeatureSet>) {
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_history = StakeHistory::default();
let current_epoch = 100;
let clock = Clock {
epoch: current_epoch,
..Clock::default()
};
let source_address = Pubkey::new_unique();
let source_meta = Meta {
rent_exempt_reserve,
..Meta::auto(&source_address)
};
let dest_address = Pubkey::new_unique();
let dest_account = AccountSharedData::new_data_with_space(
0,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let instruction_accounts = vec![
AccountMeta {
pubkey: source_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: dest_address,
is_signer: false,
is_writable: true,
},
];
for (reserve, expected_result) in [
(rent_exempt_reserve, Ok(())),
(
rent_exempt_reserve - 1,
Err(InstructionError::InsufficientFunds),
),
] {
for (stake_delegation, source_stake_state) in &[
(0, StakeStateV2::Initialized(source_meta)),
(
minimum_delegation,
just_stake(source_meta, minimum_delegation),
),
] {
let source_account = AccountSharedData::new_data_with_space(
stake_delegation + reserve,
source_stake_state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let expected_active_stake = get_active_stake_for_tests(
&[source_account.clone(), dest_account.clone()],
&clock,
&stake_history,
);
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(source_account.lamports())).unwrap(),
vec![
(source_address, source_account),
(dest_address, dest_account.clone()),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
],
instruction_accounts.clone(),
expected_result.clone(),
);
assert_eq!(
expected_active_stake,
get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history)
);
}
}
}
/// Ensure that `split()` correctly handles prefunded destination accounts from
/// initialized stakes. When a destination account already has funds, ensure
/// the minimum split amount reduces accordingly.
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_initialized_split_destination_minimum_balance(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let source_address = Pubkey::new_unique();
let destination_address = Pubkey::new_unique();
let instruction_accounts = vec![
AccountMeta {
pubkey: source_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: destination_address,
is_signer: false,
is_writable: true,
},
];
for (destination_starting_balance, split_amount, expected_result) in [
// split amount must be non zero
(
rent_exempt_reserve,
0,
Err(InstructionError::InsufficientFunds),
),
// any split amount is OK when destination account is already fully funded
(rent_exempt_reserve, 1, Ok(())),
// if destination is only short by 1 lamport, then split amount can be 1 lamport
(rent_exempt_reserve - 1, 1, Ok(())),
// destination short by 2 lamports, then 1 isn't enough (non-zero split amount)
(
rent_exempt_reserve - 2,
1,
Err(InstructionError::InsufficientFunds),
),
// destination has smallest non-zero balance, so can split the minimum balance
// requirements minus what destination already has
(1, rent_exempt_reserve - 1, Ok(())),
// destination has smallest non-zero balance, but cannot split less than the minimum
// balance requirements minus what destination already has
(
1,
rent_exempt_reserve - 2,
Err(InstructionError::InsufficientFunds),
),
// destination has zero lamports, so split must be at least rent exempt reserve
(0, rent_exempt_reserve, Ok(())),
// destination has zero lamports, but split amount is less than rent exempt reserve
(
0,
rent_exempt_reserve - 1,
Err(InstructionError::InsufficientFunds),
),
] {
// Set the source's starting balance to something large to ensure its post-split
// balance meets all the requirements
let source_balance = u64::MAX;
let source_meta = Meta {
rent_exempt_reserve,
..Meta::auto(&source_address)
};
let source_account = AccountSharedData::new_data_with_space(
source_balance,
&StakeStateV2::Initialized(source_meta),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let destination_account = AccountSharedData::new_data_with_space(
destination_starting_balance,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(split_amount)).unwrap(),
vec![
(source_address, source_account),
(destination_address, destination_account),
(rent::id(), create_account_shared_data_for_test(&rent)),
],
instruction_accounts.clone(),
expected_result.clone(),
);
}
}
/// Ensure that `split()` correctly handles prefunded destination accounts from staked stakes.
/// When a destination account already has funds, ensure the minimum split amount reduces
/// accordingly.
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(), &[Ok(()), Ok(())]; "old_behavior")]
#[test_case(feature_set_old_warmup_cooldown(), &[ Err(StakeError::InsufficientDelegation.into()), Err(StakeError::InsufficientDelegation.into()) ] ; "new_behavior")]
#[test_case(feature_set_all_enabled(), &[Err(StakeError::InsufficientDelegation.into()), Err(StakeError::InsufficientDelegation.into())]; "all_enabled")]
fn test_staked_split_destination_minimum_balance(
feature_set: Arc<FeatureSet>,
expected_results: &[Result<(), InstructionError>],
) {
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_history = StakeHistory::default();
let current_epoch = 100;
let clock = Clock {
epoch: current_epoch,
..Clock::default()
};
let source_address = Pubkey::new_unique();
let destination_address = Pubkey::new_unique();
let instruction_accounts = vec![
AccountMeta {
pubkey: source_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: destination_address,
is_signer: false,
is_writable: true,
},
];
for (destination_starting_balance, split_amount, expected_result) in [
// split amount must be non zero
(
rent_exempt_reserve + minimum_delegation,
0,
Err(InstructionError::InsufficientFunds),
),
// destination is fully funded:
// - old behavior: any split amount is OK
// - new behavior: split amount must be at least the minimum delegation
(
rent_exempt_reserve + minimum_delegation,
1,
expected_results[0].clone(),
),
// if destination is only short by 1 lamport, then...
// - old behavior: split amount can be 1 lamport
// - new behavior: split amount must be at least the minimum delegation
(
rent_exempt_reserve + minimum_delegation - 1,
1,
expected_results[1].clone(),
),
// destination short by 2 lamports, so 1 isn't enough (non-zero split amount)
(
rent_exempt_reserve + minimum_delegation - 2,
1,
Err(InstructionError::InsufficientFunds),
),
// destination is rent exempt, so split enough for minimum delegation
(rent_exempt_reserve, minimum_delegation, Ok(())),
// destination is rent exempt, but split amount less than minimum delegation
(
rent_exempt_reserve,
minimum_delegation.saturating_sub(1), // when minimum is 0, this blows up!
Err(InstructionError::InsufficientFunds),
),
// destination is not rent exempt, so any split amount fails, including enough for rent
// and minimum delegation
(
rent_exempt_reserve - 1,
minimum_delegation + 1,
Err(InstructionError::InsufficientFunds),
),
// destination is not rent exempt, but split amount only for minimum delegation
(
rent_exempt_reserve - 1,
minimum_delegation,
Err(InstructionError::InsufficientFunds),
),
// destination is not rent exempt, so any split amount fails, including case where
// destination has smallest non-zero balance
(
1,
rent_exempt_reserve + minimum_delegation - 1,
Err(InstructionError::InsufficientFunds),
),
// destination has smallest non-zero balance, but cannot split less than the minimum
// balance requirements minus what destination already has
(
1,
rent_exempt_reserve + minimum_delegation - 2,
Err(InstructionError::InsufficientFunds),
),
// destination has zero lamports, so any split amount fails, including at least rent
// exempt reserve plus minimum delegation
(
0,
rent_exempt_reserve + minimum_delegation,
Err(InstructionError::InsufficientFunds),
),
// destination has zero lamports, but split amount is less than rent exempt reserve
// plus minimum delegation
(
0,
rent_exempt_reserve + minimum_delegation - 1,
Err(InstructionError::InsufficientFunds),
),
] {
// Set the source's starting balance to something large to ensure its post-split
// balance meets all the requirements
let source_balance = u64::MAX;
let source_meta = Meta {
rent_exempt_reserve,
..Meta::auto(&source_address)
};
let source_stake_delegation = source_balance - rent_exempt_reserve;
let source_account = AccountSharedData::new_data_with_space(
source_balance,
&just_stake(source_meta, source_stake_delegation),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let destination_account = AccountSharedData::new_data_with_space(
destination_starting_balance,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let expected_active_stake = get_active_stake_for_tests(
&[source_account.clone(), destination_account.clone()],
&clock,
&stake_history,
);
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(split_amount)).unwrap(),
vec![
(source_address, source_account.clone()),
(destination_address, destination_account),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
],
instruction_accounts.clone(),
expected_result.clone(),
);
assert_eq!(
expected_active_stake,
get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history)
);
// For the expected OK cases, when the source's StakeStateV2 is Stake, then the
// destination's StakeStateV2 *must* also end up as Stake as well. Additionally,
// check to ensure the destination's delegation amount is correct. If the
// destination is already rent exempt, then the destination's stake delegation
// *must* equal the split amount. Otherwise, the split amount must first be used to
// make the destination rent exempt, and then the leftover lamports are delegated.
if expected_result.is_ok() {
assert_matches!(accounts[0].state().unwrap(), StakeStateV2::Stake(_, _, _));
if let StakeStateV2::Stake(_, destination_stake, _) = accounts[1].state().unwrap() {
let destination_initial_rent_deficit =
rent_exempt_reserve.saturating_sub(destination_starting_balance);
let expected_destination_stake_delegation =
split_amount - destination_initial_rent_deficit;
assert_eq!(
expected_destination_stake_delegation,
destination_stake.delegation.stake
);
assert!(destination_stake.delegation.stake >= minimum_delegation,);
} else {
panic!("destination state must be StakeStake::Stake after successful split when source is also StakeStateV2::Stake!");
}
}
}
}
/// Ensure that `withdraw()` respects the minimum delegation requirements
/// - Assert 1: withdrawing so remaining stake is equal-to the minimum is OK
/// - Assert 2: withdrawing so remaining stake is less-than the minimum is not OK
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_withdraw_minimum_stake_delegation(feature_set: Arc<FeatureSet>) {
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_address = solana_sdk::pubkey::new_rand();
let meta = Meta {
rent_exempt_reserve,
..Meta::auto(&stake_address)
};
let recipient_address = solana_sdk::pubkey::new_rand();
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: recipient_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: false,
},
];
let starting_stake_delegation = minimum_delegation;
for (ending_stake_delegation, expected_result) in [
(minimum_delegation, Ok(())),
(
minimum_delegation - 1,
Err(InstructionError::InsufficientFunds),
),
] {
for (stake_delegation, stake_state) in &[
(0, StakeStateV2::Initialized(meta)),
(minimum_delegation, just_stake(meta, minimum_delegation)),
] {
let rewards_balance = 123;
let stake_account = AccountSharedData::new_data_with_space(
stake_delegation + rent_exempt_reserve + rewards_balance,
stake_state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let withdraw_amount =
(starting_stake_delegation + rewards_balance) - ending_stake_delegation;
#[allow(deprecated)]
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(withdraw_amount)).unwrap(),
vec![
(stake_address, stake_account),
(
recipient_address,
AccountSharedData::new(rent_exempt_reserve, 0, &system_program::id()),
),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
rent::id(),
create_account_shared_data_for_test(&Rent::free()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
stake_config::id(),
config::create_account(0, &stake_config::Config::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
],
instruction_accounts.clone(),
expected_result.clone(),
);
}
}
}
/// The stake program's old behavior allowed delegations below the minimum stake delegation
/// (see also `test_delegate_minimum_stake_delegation()`). This was not the desired behavior,
/// and has been fixed in the new behavior. This test ensures the behavior is not changed
/// inadvertently.
///
/// This test:
/// 1. Initialises a stake account (with sufficient balance for both rent and minimum delegation)
/// 2. Delegates the minimum amount
/// 3. Deactives the delegation
/// 4. Withdraws from the account such that the ending balance is *below* rent + minimum delegation
/// 5. Re-delegates, now with less than the minimum delegation, but it still succeeds
#[test]
fn test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation() {
let feature_set = feature_set_all_enabled();
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_address = solana_sdk::pubkey::new_rand();
let stake_account = AccountSharedData::new(
rent_exempt_reserve + minimum_delegation,
StakeStateV2::size_of(),
&id(),
);
let vote_address = solana_sdk::pubkey::new_rand();
let vote_account =
vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100);
let recipient_address = solana_sdk::pubkey::new_rand();
let mut clock = Clock::default();
#[allow(deprecated)]
let mut transaction_accounts = vec![
(stake_address, stake_account),
(vote_address, vote_account),
(
recipient_address,
AccountSharedData::new(rent_exempt_reserve, 0, &system_program::id()),
),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
stake_config::id(),
config::create_account(0, &stake_config::Config::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
(rent::id(), create_account_shared_data_for_test(&rent)),
];
#[allow(deprecated)]
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_config::id(),
is_signer: false,
is_writable: false,
},
];
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Initialize(
Authorized::auto(&stake_address),
Lockup::default(),
))
.unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: rent::id(),
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
transaction_accounts[1] = (vote_address, accounts[1].clone());
clock.epoch += 1;
transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock));
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Deactivate).unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
clock.epoch += 1;
transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock));
let withdraw_amount =
accounts[0].lamports() - (rent_exempt_reserve + minimum_delegation - 1);
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Withdraw(withdraw_amount)).unwrap(),
transaction_accounts.clone(),
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: recipient_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: false,
},
],
Ok(()),
);
transaction_accounts[0] = (stake_address, accounts[0].clone());
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
transaction_accounts,
instruction_accounts,
Err(StakeError::InsufficientDelegation.into()),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split_source_uninitialized(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2;
let stake_address = solana_sdk::pubkey::new_rand();
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let split_to_address = solana_sdk::pubkey::new_rand();
let split_to_account = AccountSharedData::new_data_with_space(
0,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let transaction_accounts = vec![
(stake_address, stake_account),
(split_to_address, split_to_account),
];
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
];
// splitting an uninitialized account where the destination is the same as the source
{
// splitting should work when...
// - when split amount is the full balance
// - when split amount is zero
// - when split amount is non-zero and less than the full balance
//
// and splitting should fail when the split amount is greater than the balance
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(0)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
}
// this should work
instruction_accounts[1].pubkey = split_to_address;
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
assert_eq!(accounts[0].lamports(), accounts[1].lamports());
// no signers should fail
instruction_accounts[0].is_signer = false;
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
transaction_accounts,
instruction_accounts,
Err(InstructionError::MissingRequiredSignature),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split_split_not_uninitialized(feature_set: Arc<FeatureSet>) {
let stake_lamports = 42;
let stake_address = solana_sdk::pubkey::new_rand();
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&just_stake(Meta::auto(&stake_address), stake_lamports),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let split_to_address = solana_sdk::pubkey::new_rand();
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
];
for split_to_state in &[
StakeStateV2::Initialized(Meta::default()),
StakeStateV2::Stake(Meta::default(), Stake::default(), StakeFlags::default()),
StakeStateV2::RewardsPool,
] {
let split_to_account = AccountSharedData::new_data_with_space(
0,
split_to_state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
vec![
(stake_address, stake_account.clone()),
(split_to_address, split_to_account),
],
instruction_accounts.clone(),
Err(InstructionError::InvalidAccountData),
);
}
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split_more_than_staked(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_history = StakeHistory::default();
let current_epoch = 100;
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2;
let stake_address = solana_sdk::pubkey::new_rand();
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&just_stake(
Meta {
rent_exempt_reserve,
..Meta::auto(&stake_address)
},
stake_lamports / 2 - 1,
),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let split_to_address = solana_sdk::pubkey::new_rand();
let split_to_account = AccountSharedData::new_data_with_space(
rent_exempt_reserve,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let transaction_accounts = vec![
(stake_address, stake_account),
(split_to_address, split_to_account),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(
clock::id(),
create_account_shared_data_for_test(&Clock {
epoch: current_epoch,
..Clock::default()
}),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: split_to_address,
is_signer: false,
is_writable: true,
},
];
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
transaction_accounts,
instruction_accounts,
Err(StakeError::InsufficientDelegation.into()),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split_with_rent(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_history = StakeHistory::default();
let current_epoch = 100;
let clock = Clock {
epoch: current_epoch,
..Clock::default()
};
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_address = solana_sdk::pubkey::new_rand();
let split_to_address = solana_sdk::pubkey::new_rand();
let split_to_account = AccountSharedData::new_data_with_space(
0,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: split_to_address,
is_signer: false,
is_writable: true,
},
];
let meta = Meta {
authorized: Authorized::auto(&stake_address),
rent_exempt_reserve,
..Meta::default()
};
// test splitting both an Initialized stake and a Staked stake
for (minimum_balance, state) in &[
(rent_exempt_reserve, StakeStateV2::Initialized(meta)),
(
rent_exempt_reserve + minimum_delegation,
just_stake(meta, minimum_delegation * 2 + rent_exempt_reserve),
),
] {
let stake_lamports = minimum_balance * 2;
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let expected_active_stake = get_active_stake_for_tests(
&[stake_account.clone(), split_to_account.clone()],
&clock,
&stake_history,
);
let mut transaction_accounts = vec![
(stake_address, stake_account),
(split_to_address, split_to_account.clone()),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
// not enough to make a non-zero stake account
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(minimum_balance - 1)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
// doesn't leave enough for initial stake to be non-zero
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(
stake_lamports - minimum_balance + 1,
))
.unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
// split account already has enough lamports
transaction_accounts[1].1.set_lamports(*minimum_balance);
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports - minimum_balance)).unwrap(),
transaction_accounts,
instruction_accounts.clone(),
Ok(()),
);
assert_eq!(
expected_active_stake,
get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history)
);
// verify no stake leakage in the case of a stake
if let StakeStateV2::Stake(meta, stake, stake_flags) = state {
assert_eq!(
accounts[1].state(),
Ok(StakeStateV2::Stake(
*meta,
Stake {
delegation: Delegation {
stake: stake_lamports - minimum_balance,
..stake.delegation
},
..*stake
},
*stake_flags,
))
);
assert_eq!(accounts[0].lamports(), *minimum_balance,);
assert_eq!(accounts[1].lamports(), stake_lamports,);
}
}
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split_to_account_with_rent_exempt_reserve(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_history = StakeHistory::default();
let current_epoch = 100;
let clock = Clock {
epoch: current_epoch,
..Clock::default()
};
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2;
let stake_address = solana_sdk::pubkey::new_rand();
let meta = Meta {
authorized: Authorized::auto(&stake_address),
rent_exempt_reserve,
..Meta::default()
};
let state = just_stake(meta, stake_lamports - rent_exempt_reserve);
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let split_to_address = solana_sdk::pubkey::new_rand();
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: split_to_address,
is_signer: false,
is_writable: true,
},
];
let transaction_accounts = |initial_balance: u64| -> Vec<(Pubkey, AccountSharedData)> {
let split_to_account = AccountSharedData::new_data_with_space(
initial_balance,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
vec![
(stake_address, stake_account.clone()),
(split_to_address, split_to_account),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
]
};
// Test insufficient account prefunding, including empty and less than rent_exempt_reserve.
// The empty case is not covered in test_split, since that test uses a Meta with
// rent_exempt_reserve = 0
let split_lamport_balances = vec![0, rent_exempt_reserve - 1];
for initial_balance in split_lamport_balances {
let transaction_accounts = transaction_accounts(initial_balance);
// split more than available fails
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
// split to insufficiently funded dest fails
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
transaction_accounts,
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
}
// Test various account prefunding, including exactly rent_exempt_reserve, and more than
// rent_exempt_reserve
let split_lamport_balances = vec![
rent_exempt_reserve,
rent_exempt_reserve + minimum_delegation - 1,
rent_exempt_reserve + minimum_delegation,
];
for initial_balance in split_lamport_balances {
let transaction_accounts = transaction_accounts(initial_balance);
let expected_active_stake = get_active_stake_for_tests(
&[
transaction_accounts[0].1.clone(),
transaction_accounts[1].1.clone(),
],
&clock,
&stake_history,
);
// split more than available fails
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
// should work
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
transaction_accounts,
instruction_accounts.clone(),
Ok(()),
);
// no lamport leakage
assert_eq!(
accounts[0].lamports() + accounts[1].lamports(),
stake_lamports + initial_balance,
);
// no deactivated stake
assert_eq!(
expected_active_stake,
get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history)
);
if let StakeStateV2::Stake(meta, stake, stake_flags) = state {
let expected_stake =
stake_lamports / 2 - (rent_exempt_reserve.saturating_sub(initial_balance));
assert_eq!(
Ok(StakeStateV2::Stake(
meta,
Stake {
delegation: Delegation {
stake: stake_lamports / 2
- (rent_exempt_reserve.saturating_sub(initial_balance)),
..stake.delegation
},
..stake
},
stake_flags
)),
accounts[1].state(),
);
assert_eq!(
accounts[1].lamports(),
expected_stake
+ rent_exempt_reserve
+ initial_balance.saturating_sub(rent_exempt_reserve),
);
assert_eq!(
Ok(StakeStateV2::Stake(
meta,
Stake {
delegation: Delegation {
stake: stake_lamports / 2 - rent_exempt_reserve,
..stake.delegation
},
..stake
},
stake_flags,
)),
accounts[0].state(),
);
}
}
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split_from_larger_sized_account(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let source_larger_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of() + 100);
let split_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_history = StakeHistory::default();
let current_epoch = 100;
let clock = Clock {
epoch: current_epoch,
..Clock::default()
};
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = (source_larger_rent_exempt_reserve + minimum_delegation) * 2;
let stake_address = solana_sdk::pubkey::new_rand();
let meta = Meta {
authorized: Authorized::auto(&stake_address),
rent_exempt_reserve: source_larger_rent_exempt_reserve,
..Meta::default()
};
let state = just_stake(meta, stake_lamports - source_larger_rent_exempt_reserve);
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&state,
StakeStateV2::size_of() + 100,
&id(),
)
.unwrap();
let split_to_address = solana_sdk::pubkey::new_rand();
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: split_to_address,
is_signer: false,
is_writable: true,
},
];
let transaction_accounts = |initial_balance: u64| -> Vec<(Pubkey, AccountSharedData)> {
let split_to_account = AccountSharedData::new_data_with_space(
initial_balance,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
vec![
(stake_address, stake_account.clone()),
(split_to_address, split_to_account),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
]
};
// Test insufficient account prefunding, including empty and less than rent_exempt_reserve
let split_lamport_balances = vec![0, split_rent_exempt_reserve - 1];
for initial_balance in split_lamport_balances {
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
transaction_accounts(initial_balance),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
}
// Test various account prefunding, including exactly rent_exempt_reserve, and more than
// rent_exempt_reserve. The empty case is not covered in test_split, since that test uses a
// Meta with rent_exempt_reserve = 0
let split_lamport_balances = vec![
split_rent_exempt_reserve,
split_rent_exempt_reserve + minimum_delegation - 1,
split_rent_exempt_reserve + minimum_delegation,
];
for initial_balance in split_lamport_balances {
let transaction_accounts = transaction_accounts(initial_balance);
let expected_active_stake = get_active_stake_for_tests(
&[
transaction_accounts[0].1.clone(),
transaction_accounts[1].1.clone(),
],
&clock,
&stake_history,
);
// split more than available fails
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InsufficientFunds),
);
// should work
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
// no lamport leakage
assert_eq!(
accounts[0].lamports() + accounts[1].lamports(),
stake_lamports + initial_balance
);
// no deactivated stake
assert_eq!(
expected_active_stake,
get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history)
);
if let StakeStateV2::Stake(meta, stake, stake_flags) = state {
let expected_split_meta = Meta {
authorized: Authorized::auto(&stake_address),
rent_exempt_reserve: split_rent_exempt_reserve,
..Meta::default()
};
let expected_stake = stake_lamports / 2
- (split_rent_exempt_reserve.saturating_sub(initial_balance));
assert_eq!(
Ok(StakeStateV2::Stake(
expected_split_meta,
Stake {
delegation: Delegation {
stake: expected_stake,
..stake.delegation
},
..stake
},
stake_flags,
)),
accounts[1].state()
);
assert_eq!(
accounts[1].lamports(),
expected_stake
+ split_rent_exempt_reserve
+ initial_balance.saturating_sub(split_rent_exempt_reserve)
);
assert_eq!(
Ok(StakeStateV2::Stake(
meta,
Stake {
delegation: Delegation {
stake: stake_lamports / 2 - source_larger_rent_exempt_reserve,
..stake.delegation
},
..stake
},
stake_flags,
)),
accounts[0].state()
);
}
}
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split_from_smaller_sized_account(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let source_smaller_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let split_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of() + 100);
let stake_history = StakeHistory::default();
let current_epoch = 100;
let stake_lamports = split_rent_exempt_reserve + 1;
let stake_address = solana_sdk::pubkey::new_rand();
let meta = Meta {
authorized: Authorized::auto(&stake_address),
rent_exempt_reserve: source_smaller_rent_exempt_reserve,
..Meta::default()
};
let state = just_stake(meta, stake_lamports - source_smaller_rent_exempt_reserve);
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let split_to_address = solana_sdk::pubkey::new_rand();
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: split_to_address,
is_signer: false,
is_writable: true,
},
];
let split_amount = stake_lamports - (source_smaller_rent_exempt_reserve + 1); // Enough so that split stake is > 0
let split_lamport_balances = vec![
0,
1,
split_rent_exempt_reserve,
split_rent_exempt_reserve + 1,
];
for initial_balance in split_lamport_balances {
let split_to_account = AccountSharedData::new_data_with_space(
initial_balance,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of() + 100,
&id(),
)
.unwrap();
let transaction_accounts = vec![
(stake_address, stake_account.clone()),
(split_to_address, split_to_account),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(
clock::id(),
create_account_shared_data_for_test(&Clock {
epoch: current_epoch,
..Clock::default()
}),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
// should always return error when splitting to larger account
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(split_amount)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::InvalidAccountData),
);
// Splitting 100% of source should not make a difference
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports)).unwrap(),
transaction_accounts,
instruction_accounts.clone(),
Err(InstructionError::InvalidAccountData),
);
}
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split_100_percent_of_source(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_history = StakeHistory::default();
let current_epoch = 100;
let clock = Clock {
epoch: current_epoch,
..Clock::default()
};
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = rent_exempt_reserve + minimum_delegation;
let stake_address = solana_sdk::pubkey::new_rand();
let meta = Meta {
authorized: Authorized::auto(&stake_address),
rent_exempt_reserve,
..Meta::default()
};
let split_to_address = solana_sdk::pubkey::new_rand();
let split_to_account = AccountSharedData::new_data_with_space(
0,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: split_to_address,
is_signer: false,
is_writable: true,
},
];
// test splitting both an Initialized stake and a Staked stake
for state in &[
StakeStateV2::Initialized(meta),
just_stake(meta, stake_lamports - rent_exempt_reserve),
] {
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let expected_active_stake = get_active_stake_for_tests(
&[stake_account.clone(), split_to_account.clone()],
&clock,
&stake_history,
);
let transaction_accounts = vec![
(stake_address, stake_account),
(split_to_address, split_to_account.clone()),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
// split 100% over to dest
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports)).unwrap(),
transaction_accounts,
instruction_accounts.clone(),
Ok(()),
);
// no lamport leakage
assert_eq!(
accounts[0].lamports() + accounts[1].lamports(),
stake_lamports
);
// no deactivated stake
assert_eq!(
expected_active_stake,
get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history)
);
match state {
StakeStateV2::Initialized(_) => {
assert_eq!(Ok(*state), accounts[1].state());
assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state());
}
StakeStateV2::Stake(meta, stake, stake_flags) => {
assert_eq!(
Ok(StakeStateV2::Stake(
*meta,
Stake {
delegation: Delegation {
stake: stake_lamports - rent_exempt_reserve,
..stake.delegation
},
..*stake
},
*stake_flags
)),
accounts[1].state()
);
assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state());
}
_ => unreachable!(),
}
}
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split_100_percent_of_source_to_account_with_lamports(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_history = StakeHistory::default();
let current_epoch = 100;
let clock = Clock {
epoch: current_epoch,
..Clock::default()
};
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = rent_exempt_reserve + minimum_delegation;
let stake_address = solana_sdk::pubkey::new_rand();
let meta = Meta {
authorized: Authorized::auto(&stake_address),
rent_exempt_reserve,
..Meta::default()
};
let state = just_stake(meta, stake_lamports - rent_exempt_reserve);
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let split_to_address = solana_sdk::pubkey::new_rand();
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: split_to_address,
is_signer: false,
is_writable: true,
},
];
// Test various account prefunding, including empty, less than rent_exempt_reserve, exactly
// rent_exempt_reserve, and more than rent_exempt_reserve. Technically, the empty case is
// covered in test_split_100_percent_of_source, but included here as well for readability
let split_lamport_balances = vec![
0,
rent_exempt_reserve - 1,
rent_exempt_reserve,
rent_exempt_reserve + minimum_delegation - 1,
rent_exempt_reserve + minimum_delegation,
];
for initial_balance in split_lamport_balances {
let split_to_account = AccountSharedData::new_data_with_space(
initial_balance,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let expected_active_stake = get_active_stake_for_tests(
&[stake_account.clone(), split_to_account.clone()],
&clock,
&stake_history,
);
let transaction_accounts = vec![
(stake_address, stake_account.clone()),
(split_to_address, split_to_account),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
// split 100% over to dest
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports)).unwrap(),
transaction_accounts,
instruction_accounts.clone(),
Ok(()),
);
// no lamport leakage
assert_eq!(
accounts[0].lamports() + accounts[1].lamports(),
stake_lamports + initial_balance
);
// no deactivated stake
assert_eq!(
expected_active_stake,
get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history)
);
if let StakeStateV2::Stake(meta, stake, stake_flags) = state {
assert_eq!(
Ok(StakeStateV2::Stake(
meta,
Stake {
delegation: Delegation {
stake: stake_lamports - rent_exempt_reserve,
..stake.delegation
},
..stake
},
stake_flags,
)),
accounts[1].state()
);
assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state());
}
}
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_split_rent_exemptness(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let source_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of() + 100);
let split_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_history = StakeHistory::default();
let current_epoch = 100;
let clock = Clock {
epoch: current_epoch,
..Clock::default()
};
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let stake_lamports = source_rent_exempt_reserve + minimum_delegation;
let stake_address = solana_sdk::pubkey::new_rand();
let meta = Meta {
authorized: Authorized::auto(&stake_address),
rent_exempt_reserve: source_rent_exempt_reserve,
..Meta::default()
};
let split_to_address = solana_sdk::pubkey::new_rand();
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: split_to_address,
is_signer: false,
is_writable: true,
},
];
for state in &[
StakeStateV2::Initialized(meta),
just_stake(meta, stake_lamports - source_rent_exempt_reserve),
] {
// Test that splitting to a larger account fails
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let split_to_account = AccountSharedData::new_data_with_space(
0,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of() + 10000,
&id(),
)
.unwrap();
let transaction_accounts = vec![
(stake_address, stake_account),
(split_to_address, split_to_account),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports)).unwrap(),
transaction_accounts,
instruction_accounts.clone(),
Err(InstructionError::InvalidAccountData),
);
// Test that splitting from a larger account to a smaller one works.
// Split amount should not matter, assuming other fund criteria are met
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&state,
StakeStateV2::size_of() + 100,
&id(),
)
.unwrap();
let split_to_account = AccountSharedData::new_data_with_space(
0,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let expected_active_stake = get_active_stake_for_tests(
&[stake_account.clone(), split_to_account.clone()],
&clock,
&stake_history,
);
let transaction_accounts = vec![
(stake_address, stake_account),
(split_to_address, split_to_account),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(
clock::id(),
create_account_shared_data_for_test(&Clock {
epoch: current_epoch,
..Clock::default()
}),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(stake_lamports)).unwrap(),
transaction_accounts,
instruction_accounts.clone(),
Ok(()),
);
assert_eq!(accounts[1].lamports(), stake_lamports);
assert_eq!(
expected_active_stake,
get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history)
);
let expected_split_meta = Meta {
authorized: Authorized::auto(&stake_address),
rent_exempt_reserve: split_rent_exempt_reserve,
..Meta::default()
};
match state {
StakeStateV2::Initialized(_) => {
assert_eq!(
Ok(StakeStateV2::Initialized(expected_split_meta)),
accounts[1].state()
);
assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state());
}
StakeStateV2::Stake(_meta, stake, stake_flags) => {
// Expected stake should reflect original stake amount so that extra lamports
// from the rent_exempt_reserve inequality do not magically activate
let expected_stake = stake_lamports - source_rent_exempt_reserve;
assert_eq!(
Ok(StakeStateV2::Stake(
expected_split_meta,
Stake {
delegation: Delegation {
stake: expected_stake,
..stake.delegation
},
..*stake
},
*stake_flags,
)),
accounts[1].state()
);
assert_eq!(
accounts[1].lamports(),
expected_stake + source_rent_exempt_reserve,
);
assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state());
}
_ => unreachable!(),
}
}
}
#[test_case(feature_set_without_require_rent_exempt_split_destination(), Ok(()); "without_require_rent_exempt_split_destination")]
#[test_case(feature_set_all_enabled(), Err(InstructionError::InsufficientFunds); "all_enabled")]
fn test_split_require_rent_exempt_destination(
feature_set: Arc<FeatureSet>,
expected_result: Result<(), InstructionError>,
) {
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_history = StakeHistory::default();
let current_epoch = 100;
let clock = Clock {
epoch: current_epoch,
..Clock::default()
};
let minimum_delegation = crate::get_minimum_delegation(&feature_set);
let delegation_amount = 3 * minimum_delegation;
let source_lamports = rent_exempt_reserve + delegation_amount;
let source_address = Pubkey::new_unique();
let destination_address = Pubkey::new_unique();
let meta = Meta {
authorized: Authorized::auto(&source_address),
rent_exempt_reserve,
..Meta::default()
};
let instruction_accounts = vec![
AccountMeta {
pubkey: source_address,
is_signer: true,
is_writable: true,
},
AccountMeta {
pubkey: destination_address,
is_signer: false,
is_writable: true,
},
];
for (split_amount, expected_result) in [
(2 * minimum_delegation, expected_result),
(source_lamports, Ok(())),
] {
for (state, expected_result) in &[
(StakeStateV2::Initialized(meta), Ok(())),
(just_stake(meta, delegation_amount), expected_result),
] {
let source_account = AccountSharedData::new_data_with_space(
source_lamports,
&state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let transaction_accounts =
|initial_balance: u64| -> Vec<(Pubkey, AccountSharedData)> {
let destination_account = AccountSharedData::new_data_with_space(
initial_balance,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
vec![
(source_address, source_account.clone()),
(destination_address, destination_account),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
]
};
// Test insufficient recipient prefunding; should error once feature is activated
let split_lamport_balances = vec![0, rent_exempt_reserve - 1];
for initial_balance in split_lamport_balances {
let transaction_accounts = transaction_accounts(initial_balance);
let expected_active_stake = get_active_stake_for_tests(
&[source_account.clone(), transaction_accounts[1].1.clone()],
&clock,
&stake_history,
);
let result_accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(split_amount)).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
expected_result.clone(),
);
let result_active_stake =
get_active_stake_for_tests(&result_accounts[0..2], &clock, &stake_history);
if expected_active_stake > 0 // starting stake was delegated
// partial split
&& result_accounts[0].lamports() > 0
// successful split to deficient recipient
&& expected_result.is_ok()
{
assert_ne!(expected_active_stake, result_active_stake);
} else {
assert_eq!(expected_active_stake, result_active_stake);
}
}
// Test recipient prefunding, including exactly rent_exempt_reserve, and more than
// rent_exempt_reserve.
let split_lamport_balances = vec![rent_exempt_reserve, rent_exempt_reserve + 1];
for initial_balance in split_lamport_balances {
let transaction_accounts = transaction_accounts(initial_balance);
let expected_active_stake = get_active_stake_for_tests(
&[source_account.clone(), transaction_accounts[1].1.clone()],
&clock,
&stake_history,
);
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Split(split_amount)).unwrap(),
transaction_accounts,
instruction_accounts.clone(),
Ok(()),
);
// no lamport leakage
assert_eq!(
accounts[0].lamports() + accounts[1].lamports(),
source_lamports + initial_balance
);
// no deactivated stake
assert_eq!(
expected_active_stake,
get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history)
);
if let StakeStateV2::Stake(meta, stake, stake_flags) = state {
// split entire source account, including rent-exempt reserve
if accounts[0].lamports() == 0 {
assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state());
assert_eq!(
Ok(StakeStateV2::Stake(
*meta,
Stake {
delegation: Delegation {
// delegated amount should not include source
// rent-exempt reserve
stake: delegation_amount,
..stake.delegation
},
..*stake
},
*stake_flags,
)),
accounts[1].state()
);
} else {
assert_eq!(
Ok(StakeStateV2::Stake(
*meta,
Stake {
delegation: Delegation {
stake: minimum_delegation,
..stake.delegation
},
..*stake
},
*stake_flags,
)),
accounts[0].state()
);
assert_eq!(
Ok(StakeStateV2::Stake(
*meta,
Stake {
delegation: Delegation {
stake: split_amount,
..stake.delegation
},
..*stake
},
*stake_flags,
)),
accounts[1].state()
);
}
}
}
}
}
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_merge(feature_set: Arc<FeatureSet>) {
let stake_address = solana_sdk::pubkey::new_rand();
let merge_from_address = solana_sdk::pubkey::new_rand();
let authorized_address = solana_sdk::pubkey::new_rand();
let meta = Meta::auto(&authorized_address);
let stake_lamports = 42;
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: merge_from_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authorized_address,
is_signer: true,
is_writable: false,
},
];
for state in &[
StakeStateV2::Initialized(meta),
just_stake(meta, stake_lamports),
] {
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
for merge_from_state in &[
StakeStateV2::Initialized(meta),
just_stake(meta, stake_lamports),
] {
let merge_from_account = AccountSharedData::new_data_with_space(
stake_lamports,
merge_from_state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let transaction_accounts = vec![
(stake_address, stake_account.clone()),
(merge_from_address, merge_from_account),
(authorized_address, AccountSharedData::default()),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
// Authorized staker signature required...
instruction_accounts[4].is_signer = false;
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Merge).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
instruction_accounts[4].is_signer = true;
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Merge).unwrap(),
transaction_accounts,
instruction_accounts.clone(),
Ok(()),
);
// check lamports
assert_eq!(accounts[0].lamports(), stake_lamports * 2);
assert_eq!(accounts[1].lamports(), 0);
// check state
match state {
StakeStateV2::Initialized(meta) => {
assert_eq!(accounts[0].state(), Ok(StakeStateV2::Initialized(*meta)),);
}
StakeStateV2::Stake(meta, stake, stake_flags) => {
let expected_stake = stake.delegation.stake
+ merge_from_state
.stake()
.map(|stake| stake.delegation.stake)
.unwrap_or_else(|| {
stake_lamports
- merge_from_state.meta().unwrap().rent_exempt_reserve
});
assert_eq!(
accounts[0].state(),
Ok(StakeStateV2::Stake(
*meta,
Stake {
delegation: Delegation {
stake: expected_stake,
..stake.delegation
},
..*stake
},
*stake_flags,
)),
);
}
_ => unreachable!(),
}
assert_eq!(accounts[1].state(), Ok(StakeStateV2::Uninitialized));
}
}
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_merge_self_fails(feature_set: Arc<FeatureSet>) {
let stake_address = solana_sdk::pubkey::new_rand();
let authorized_address = solana_sdk::pubkey::new_rand();
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_amount = 4242424242;
let stake_lamports = rent_exempt_reserve + stake_amount;
let meta = Meta {
rent_exempt_reserve,
..Meta::auto(&authorized_address)
};
let stake = Stake {
delegation: Delegation {
stake: stake_amount,
activation_epoch: 0,
..Delegation::default()
},
..Stake::default()
};
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::Stake(meta, stake, StakeFlags::empty()),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let transaction_accounts = vec![
(stake_address, stake_account),
(authorized_address, AccountSharedData::default()),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
];
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authorized_address,
is_signer: true,
is_writable: false,
},
];
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Merge).unwrap(),
transaction_accounts,
instruction_accounts,
Err(InstructionError::InvalidArgument),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_merge_incorrect_authorized_staker(feature_set: Arc<FeatureSet>) {
let stake_address = solana_sdk::pubkey::new_rand();
let merge_from_address = solana_sdk::pubkey::new_rand();
let authorized_address = solana_sdk::pubkey::new_rand();
let wrong_authorized_address = solana_sdk::pubkey::new_rand();
let stake_lamports = 42;
let mut instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: merge_from_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authorized_address,
is_signer: true,
is_writable: false,
},
];
for state in &[
StakeStateV2::Initialized(Meta::auto(&authorized_address)),
just_stake(Meta::auto(&authorized_address), stake_lamports),
] {
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
for merge_from_state in &[
StakeStateV2::Initialized(Meta::auto(&wrong_authorized_address)),
just_stake(Meta::auto(&wrong_authorized_address), stake_lamports),
] {
let merge_from_account = AccountSharedData::new_data_with_space(
stake_lamports,
merge_from_state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let transaction_accounts = vec![
(stake_address, stake_account.clone()),
(merge_from_address, merge_from_account),
(authorized_address, AccountSharedData::default()),
(wrong_authorized_address, AccountSharedData::default()),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
instruction_accounts[4].pubkey = wrong_authorized_address;
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Merge).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::MissingRequiredSignature),
);
instruction_accounts[4].pubkey = authorized_address;
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Merge).unwrap(),
transaction_accounts,
instruction_accounts.clone(),
Err(StakeError::MergeMismatch.into()),
);
}
}
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_merge_invalid_account_data(feature_set: Arc<FeatureSet>) {
let stake_address = solana_sdk::pubkey::new_rand();
let merge_from_address = solana_sdk::pubkey::new_rand();
let authorized_address = solana_sdk::pubkey::new_rand();
let stake_lamports = 42;
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: merge_from_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authorized_address,
is_signer: true,
is_writable: false,
},
];
for state in &[
StakeStateV2::Uninitialized,
StakeStateV2::RewardsPool,
StakeStateV2::Initialized(Meta::auto(&authorized_address)),
just_stake(Meta::auto(&authorized_address), stake_lamports),
] {
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
for merge_from_state in &[StakeStateV2::Uninitialized, StakeStateV2::RewardsPool] {
let merge_from_account = AccountSharedData::new_data_with_space(
stake_lamports,
merge_from_state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let transaction_accounts = vec![
(stake_address, stake_account.clone()),
(merge_from_address, merge_from_account),
(authorized_address, AccountSharedData::default()),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Merge).unwrap(),
transaction_accounts,
instruction_accounts.clone(),
Err(InstructionError::InvalidAccountData),
);
}
}
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_merge_fake_stake_source(feature_set: Arc<FeatureSet>) {
let stake_address = solana_sdk::pubkey::new_rand();
let merge_from_address = solana_sdk::pubkey::new_rand();
let authorized_address = solana_sdk::pubkey::new_rand();
let stake_lamports = 42;
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&just_stake(Meta::auto(&authorized_address), stake_lamports),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let merge_from_account = AccountSharedData::new_data_with_space(
stake_lamports,
&just_stake(Meta::auto(&authorized_address), stake_lamports),
StakeStateV2::size_of(),
&solana_sdk::pubkey::new_rand(),
)
.unwrap();
let transaction_accounts = vec![
(stake_address, stake_account),
(merge_from_address, merge_from_account),
(authorized_address, AccountSharedData::default()),
(
clock::id(),
create_account_shared_data_for_test(&Clock::default()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
];
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: merge_from_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authorized_address,
is_signer: true,
is_writable: false,
},
];
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Merge).unwrap(),
transaction_accounts,
instruction_accounts,
Err(InstructionError::IncorrectProgramId),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_merge_active_stake(feature_set: Arc<FeatureSet>) {
let stake_address = solana_sdk::pubkey::new_rand();
let merge_from_address = solana_sdk::pubkey::new_rand();
let authorized_address = solana_sdk::pubkey::new_rand();
let base_lamports = 4242424242;
let rent = Rent::default();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());
let stake_amount = base_lamports;
let stake_lamports = rent_exempt_reserve + stake_amount;
let merge_from_amount = base_lamports;
let merge_from_lamports = rent_exempt_reserve + merge_from_amount;
let meta = Meta {
rent_exempt_reserve,
..Meta::auto(&authorized_address)
};
let mut stake = Stake {
delegation: Delegation {
stake: stake_amount,
activation_epoch: 0,
..Delegation::default()
},
..Stake::default()
};
let stake_account = AccountSharedData::new_data_with_space(
stake_lamports,
&StakeStateV2::Stake(meta, stake, StakeFlags::empty()),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let merge_from_activation_epoch = 2;
let mut merge_from_stake = Stake {
delegation: Delegation {
stake: merge_from_amount,
activation_epoch: merge_from_activation_epoch,
..stake.delegation
},
..stake
};
let merge_from_account = AccountSharedData::new_data_with_space(
merge_from_lamports,
&StakeStateV2::Stake(meta, merge_from_stake, StakeFlags::empty()),
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let mut clock = Clock::default();
let mut stake_history = StakeHistory::default();
let mut effective = base_lamports;
let mut activating = stake_amount;
let mut deactivating = 0;
stake_history.add(
clock.epoch,
StakeHistoryEntry {
effective,
activating,
deactivating,
},
);
let mut transaction_accounts = vec![
(stake_address, stake_account),
(merge_from_address, merge_from_account),
(authorized_address, AccountSharedData::default()),
(clock::id(), create_account_shared_data_for_test(&clock)),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
];
let instruction_accounts = vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: merge_from_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: stake_history::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: authorized_address,
is_signer: true,
is_writable: false,
},
];
fn try_merge(
feature_set: Arc<FeatureSet>,
transaction_accounts: Vec<(Pubkey, AccountSharedData)>,
mut instruction_accounts: Vec<AccountMeta>,
expected_result: Result<(), InstructionError>,
) {
for iteration in 0..2 {
if iteration == 1 {
instruction_accounts.swap(0, 1);
}
let accounts = process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Merge).unwrap(),
transaction_accounts.clone(),
instruction_accounts.clone(),
expected_result.clone(),
);
if expected_result.is_ok() {
assert_eq!(
accounts[1 - iteration].state(),
Ok(StakeStateV2::Uninitialized)
);
}
}
}
// stake activation epoch, source initialized succeeds
try_merge(
Arc::clone(&feature_set),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
let new_warmup_cooldown_rate_epoch =
feature_set.new_warmup_cooldown_rate_epoch(&EpochSchedule::default());
// both activating fails
loop {
clock.epoch += 1;
if clock.epoch == merge_from_activation_epoch {
activating += merge_from_amount;
}
let delta = activating.min(
(effective as f64
* warmup_cooldown_rate(clock.epoch, new_warmup_cooldown_rate_epoch))
as u64,
);
effective += delta;
activating -= delta;
stake_history.add(
clock.epoch,
StakeHistoryEntry {
effective,
activating,
deactivating,
},
);
transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock));
transaction_accounts[4] = (
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
);
if stake_amount
== stake.stake(
clock.epoch,
Some(&stake_history),
new_warmup_cooldown_rate_epoch,
)
&& merge_from_amount
== merge_from_stake.stake(
clock.epoch,
Some(&stake_history),
new_warmup_cooldown_rate_epoch,
)
{
break;
}
try_merge(
Arc::clone(&feature_set),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::from(StakeError::MergeTransientStake)),
);
}
// Both fully activated works
try_merge(
Arc::clone(&feature_set),
transaction_accounts.clone(),
instruction_accounts.clone(),
Ok(()),
);
// deactivate setup for deactivation
let merge_from_deactivation_epoch = clock.epoch + 1;
let stake_deactivation_epoch = clock.epoch + 2;
// active/deactivating and deactivating/inactive mismatches fail
loop {
clock.epoch += 1;
let delta = deactivating.min(
(effective as f64
* warmup_cooldown_rate(clock.epoch, new_warmup_cooldown_rate_epoch))
as u64,
);
effective -= delta;
deactivating -= delta;
if clock.epoch == stake_deactivation_epoch {
deactivating += stake_amount;
stake = Stake {
delegation: Delegation {
deactivation_epoch: stake_deactivation_epoch,
..stake.delegation
},
..stake
};
transaction_accounts[0]
.1
.set_state(&StakeStateV2::Stake(meta, stake, StakeFlags::empty()))
.unwrap();
}
if clock.epoch == merge_from_deactivation_epoch {
deactivating += merge_from_amount;
merge_from_stake = Stake {
delegation: Delegation {
deactivation_epoch: merge_from_deactivation_epoch,
..merge_from_stake.delegation
},
..merge_from_stake
};
transaction_accounts[1]
.1
.set_state(&StakeStateV2::Stake(
meta,
merge_from_stake,
StakeFlags::empty(),
))
.unwrap();
}
stake_history.add(
clock.epoch,
StakeHistoryEntry {
effective,
activating,
deactivating,
},
);
transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock));
transaction_accounts[4] = (
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
);
if 0 == stake.stake(
clock.epoch,
Some(&stake_history),
new_warmup_cooldown_rate_epoch,
) && 0
== merge_from_stake.stake(
clock.epoch,
Some(&stake_history),
new_warmup_cooldown_rate_epoch,
)
{
break;
}
try_merge(
Arc::clone(&feature_set),
transaction_accounts.clone(),
instruction_accounts.clone(),
Err(InstructionError::from(StakeError::MergeTransientStake)),
);
}
// Both fully deactivated works
try_merge(
Arc::clone(&feature_set),
transaction_accounts,
instruction_accounts,
Ok(()),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_stake_get_minimum_delegation(feature_set: Arc<FeatureSet>) {
let stake_address = Pubkey::new_unique();
let stake_account = create_default_stake_account();
let instruction_data = serialize(&StakeInstruction::GetMinimumDelegation).unwrap();
let transaction_accounts = vec![(stake_address, stake_account)];
let instruction_accounts = vec![AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
}];
mock_process_instruction(
&id(),
Vec::new(),
&instruction_data,
transaction_accounts,
instruction_accounts,
Ok(()),
Entrypoint::vm,
|invoke_context| {
invoke_context.feature_set = Arc::clone(&feature_set);
},
|invoke_context| {
let expected_minimum_delegation =
crate::get_minimum_delegation(&invoke_context.feature_set).to_le_bytes();
let actual_minimum_delegation =
invoke_context.transaction_context.get_return_data().1;
assert_eq!(expected_minimum_delegation, actual_minimum_delegation);
},
);
}
// Ensure that the correct errors are returned when processing instructions
//
// The GetMinimumDelegation instruction does not take any accounts; so when it was added,
// `process_instruction()` needed to be updated to *not* need a stake account passed in, which
// changes the error *ordering* conditions.
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_stake_process_instruction_error_ordering(feature_set: Arc<FeatureSet>) {
let rent = Rent::default();
let rent_address = rent::id();
let rent_account = create_account_shared_data_for_test(&rent);
let good_stake_address = Pubkey::new_unique();
let good_stake_account = AccountSharedData::new(u64::MAX, StakeStateV2::size_of(), &id());
let good_instruction = instruction::initialize(
&good_stake_address,
&Authorized::auto(&good_stake_address),
&Lockup::default(),
);
let good_transaction_accounts = vec![
(good_stake_address, good_stake_account),
(rent_address, rent_account),
];
let good_instruction_accounts = vec![
AccountMeta {
pubkey: good_stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: rent_address,
is_signer: false,
is_writable: false,
},
];
let good_accounts = (good_transaction_accounts, good_instruction_accounts);
// The instruction data needs to deserialize to a bogus StakeInstruction. We likely never
// will have `usize::MAX`-number of instructions, so this should be a safe constant to
// always map to an invalid stake instruction.
let bad_instruction = Instruction::new_with_bincode(id(), &usize::MAX, Vec::default());
let bad_transaction_accounts = Vec::default();
let bad_instruction_accounts = Vec::default();
let bad_accounts = (bad_transaction_accounts, bad_instruction_accounts);
for (instruction, (transaction_accounts, instruction_accounts), expected_result) in [
(&good_instruction, &good_accounts, Ok(())),
(
&bad_instruction,
&good_accounts,
Err(InstructionError::InvalidInstructionData),
),
(
&good_instruction,
&bad_accounts,
Err(InstructionError::NotEnoughAccountKeys),
),
(
&bad_instruction,
&bad_accounts,
Err(InstructionError::InvalidInstructionData),
),
] {
process_instruction(
feature_set.clone(),
&instruction.data,
transaction_accounts.clone(),
instruction_accounts.clone(),
expected_result,
);
}
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_deactivate_delinquent(feature_set: Arc<FeatureSet>) {
let reference_vote_address = Pubkey::new_unique();
let vote_address = Pubkey::new_unique();
let stake_address = Pubkey::new_unique();
let initial_stake_state = StakeStateV2::Stake(
Meta::default(),
new_stake(
1, /* stake */
&vote_address,
&VoteState::default(),
1, /* activation_epoch */
),
StakeFlags::empty(),
);
let stake_account = AccountSharedData::new_data_with_space(
1, /* lamports */
&initial_stake_state,
StakeStateV2::size_of(),
&id(),
)
.unwrap();
let mut vote_account = AccountSharedData::new_data_with_space(
1, /* lamports */
&VoteStateVersions::new_current(VoteState::default()),
VoteState::size_of(),
&solana_vote_program::id(),
)
.unwrap();
let mut reference_vote_account = AccountSharedData::new_data_with_space(
1, /* lamports */
&VoteStateVersions::new_current(VoteState::default()),
VoteState::size_of(),
&solana_vote_program::id(),
)
.unwrap();
let current_epoch = 20;
let process_instruction_deactivate_delinquent =
|stake_address: &Pubkey,
stake_account: &AccountSharedData,
vote_account: &AccountSharedData,
reference_vote_account: &AccountSharedData,
expected_result| {
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DeactivateDelinquent).unwrap(),
vec![
(*stake_address, stake_account.clone()),
(vote_address, vote_account.clone()),
(reference_vote_address, reference_vote_account.clone()),
(
clock::id(),
create_account_shared_data_for_test(&Clock {
epoch: current_epoch,
..Clock::default()
}),
),
(
stake_history::id(),
create_account_shared_data_for_test(&StakeHistory::default()),
),
],
vec![
AccountMeta {
pubkey: *stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: reference_vote_address,
is_signer: false,
is_writable: false,
},
],
expected_result,
)
};
// `reference_vote_account` has not voted. Instruction will fail
process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Err(StakeError::InsufficientReferenceVotes.into()),
);
// `reference_vote_account` has not consistently voted for at least
// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`.
// Instruction will fail
let mut reference_vote_state = VoteState::default();
for epoch in 0..MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION / 2 {
reference_vote_state.increment_credits(epoch as Epoch, 1);
}
reference_vote_account
.serialize_data(&VoteStateVersions::new_current(reference_vote_state))
.unwrap();
process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Err(StakeError::InsufficientReferenceVotes.into()),
);
// `reference_vote_account` has not consistently voted for the last
// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`.
// Instruction will fail
let mut reference_vote_state = VoteState::default();
for epoch in 0..=current_epoch {
reference_vote_state.increment_credits(epoch, 1);
}
assert_eq!(
reference_vote_state.epoch_credits[current_epoch as usize - 2].0,
current_epoch - 2
);
reference_vote_state
.epoch_credits
.remove(current_epoch as usize - 2);
assert_eq!(
reference_vote_state.epoch_credits[current_epoch as usize - 2].0,
current_epoch - 1
);
reference_vote_account
.serialize_data(&VoteStateVersions::new_current(reference_vote_state))
.unwrap();
process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Err(StakeError::InsufficientReferenceVotes.into()),
);
// `reference_vote_account` has consistently voted and `vote_account` has never voted.
// Instruction will succeed
let mut reference_vote_state = VoteState::default();
for epoch in 0..=current_epoch {
reference_vote_state.increment_credits(epoch, 1);
}
reference_vote_account
.serialize_data(&VoteStateVersions::new_current(reference_vote_state))
.unwrap();
let post_stake_account = &process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Ok(()),
)[0];
assert_eq!(
stake_from(post_stake_account)
.unwrap()
.delegation
.deactivation_epoch,
current_epoch
);
// `reference_vote_account` has consistently voted and `vote_account` has not voted for the
// last `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`.
// Instruction will succeed
let mut vote_state = VoteState::default();
for epoch in 0..MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION / 2 {
vote_state.increment_credits(epoch as Epoch, 1);
}
vote_account
.serialize_data(&VoteStateVersions::new_current(vote_state))
.unwrap();
let post_stake_account = &process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Ok(()),
)[0];
assert_eq!(
stake_from(post_stake_account)
.unwrap()
.delegation
.deactivation_epoch,
current_epoch
);
// `reference_vote_account` has consistently voted and `vote_account` has not voted for the
// last `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`. Try to deactivate an unrelated stake
// account. Instruction will fail
let unrelated_vote_address = Pubkey::new_unique();
let unrelated_stake_address = Pubkey::new_unique();
let mut unrelated_stake_account = stake_account.clone();
assert_ne!(unrelated_vote_address, vote_address);
unrelated_stake_account
.serialize_data(&StakeStateV2::Stake(
Meta::default(),
new_stake(
1, /* stake */
&unrelated_vote_address,
&VoteState::default(),
1, /* activation_epoch */
),
StakeFlags::empty(),
))
.unwrap();
process_instruction_deactivate_delinquent(
&unrelated_stake_address,
&unrelated_stake_account,
&vote_account,
&reference_vote_account,
Err(StakeError::VoteAddressMismatch.into()),
);
// `reference_vote_account` has consistently voted and `vote_account` voted once
// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` ago.
// Instruction will succeed
let mut vote_state = VoteState::default();
vote_state.increment_credits(
current_epoch - MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch,
1,
);
vote_account
.serialize_data(&VoteStateVersions::new_current(vote_state))
.unwrap();
process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Ok(()),
);
// `reference_vote_account` has consistently voted and `vote_account` voted once
// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` - 1 epochs ago
// Instruction will fail
let mut vote_state = VoteState::default();
vote_state.increment_credits(
current_epoch - (MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION - 1) as Epoch,
1,
);
vote_account
.serialize_data(&VoteStateVersions::new_current(vote_state))
.unwrap();
process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Err(StakeError::MinimumDelinquentEpochsForDeactivationNotMet.into()),
);
}
#[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")]
#[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")]
#[test_case(feature_set_all_enabled(); "all_enabled")]
fn test_redelegate(feature_set: Arc<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(StakeStateV2::size_of());
let stake_history = StakeHistory::default();
let current_epoch = 100;
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 = StakeStateV2::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,
),
StakeFlags::empty(),
);
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),
None
)
);
}
AccountSharedData::new_data_with_space(
rent_exempt_reserve + initial_stake_delegation, /* lamports */
&initial_stake_state,
StakeStateV2::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| {
#[allow(deprecated)]
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::Redelegate).unwrap(),
vec![
(*stake_address, stake_account.clone()),
(
*uninitialized_stake_address,
uninitialized_stake_account.clone(),
),
(*vote_address, vote_account.clone()),
(*authorized_staker, AccountSharedData::default()),
(
stake_config::id(),
config::create_account(0, &stake_config::Config::default()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
clock::id(),
create_account_shared_data_for_test(&Clock {
epoch: current_epoch,
..Clock::default()
}),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::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,
},
],
expected_result,
)
};
//
// Failure: incorrect authorized staker
//
let stake_account = prepare_stake_account(0 /*activation_epoch*/, None);
let uninitialized_stake_account =
AccountSharedData::new(0 /* lamports */, StakeStateV2::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 StakeStateV2::Stake(meta, stake, _) = output_accounts[0].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 StakeStateV2::Stake(meta, stake, _) = output_accounts[1].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 {
#[allow(deprecated)]
process_instruction(
Arc::clone(&feature_set),
&serialize(&StakeInstruction::DelegateStake).unwrap(),
vec![
(stake_address, deactivated_stake_account),
(vote_address, new_vote_account.clone()),
(authorized_staker, AccountSharedData::default()),
(
stake_config::id(),
config::create_account(0, &stake_config::Config::default()),
),
(
stake_history::id(),
create_account_shared_data_for_test(&stake_history),
),
(rent::id(), create_account_shared_data_for_test(&rent)),
(
clock::id(),
create_account_shared_data_for_test(&Clock {
epoch: current_epoch,
..Clock::default()
}),
),
(
epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
),
],
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: clock::id(),
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: 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,
},
],
expected_result,
);
}
//
// Success: `uninitialized_stake_account` starts with 42 extra lamports
//
let uninitialized_stake_account_with_extra_lamports =
AccountSharedData::new(42 /* lamports */, StakeStateV2::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 StakeStateV2::Stake(meta, stake, _) = output_accounts[1].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 StakeStateV2::Stake(mut meta, stake, stake_flags) =
stake_account_over_allocated.deserialize_data().unwrap()
{
meta.rent_exempt_reserve += 42;
stake_account_over_allocated
.set_state(&StakeStateV2::Stake(meta, stake, stake_flags))
.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 StakeStateV2::Stake(meta, _stake, _) = output_accounts[0].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 StakeStateV2::Stake(meta, stake, _) = output_accounts[1].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 */
StakeStateV2::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 */, StakeStateV2::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 */, StakeStateV2::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, // <-- 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, // <-- 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 StakeStateV2::Stake(meta, mut stake, _stake_flags) =
deactivating_stake_account.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),
None
)
);
deactivating_stake_account
.set_state(&StakeStateV2::Stake(meta, stake, StakeFlags::empty()))
.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 StakeStateV2::Stake(meta, mut stake, stake_flags) =
stake_account_too_few_lamports.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(&StakeStateV2::Stake(meta, stake, stake_flags))
.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 address
//
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()),
);
}
}