From e6c02f30dd3f9f2565f0d07a146f79d9d4e82ec0 Mon Sep 17 00:00:00 2001 From: Brooks Prumo Date: Sat, 7 May 2022 19:01:05 -0400 Subject: [PATCH] Raise minimum stake delegation to 1 SOL (#24603) --- programs/stake/src/lib.rs | 20 +- programs/stake/src/stake_instruction.rs | 916 +++++++++++++++++++----- programs/stake/src/stake_state.rs | 13 +- sdk/src/feature_set.rs | 6 + 4 files changed, 755 insertions(+), 200 deletions(-) diff --git a/programs/stake/src/lib.rs b/programs/stake/src/lib.rs index a5731078a..0f0d20853 100644 --- a/programs/stake/src/lib.rs +++ b/programs/stake/src/lib.rs @@ -5,7 +5,11 @@ note = "Please use `solana_sdk::stake::program::id` or `solana_program::stake::program::id` instead" )] pub use solana_sdk::stake::program::{check_id, id}; -use solana_sdk::{feature_set::FeatureSet, genesis_config::GenesisConfig}; +use solana_sdk::{ + feature_set::{self, FeatureSet}, + genesis_config::GenesisConfig, + native_token::LAMPORTS_PER_SOL, +}; pub mod config; pub mod stake_instruction; @@ -19,10 +23,12 @@ pub fn add_genesis_accounts(genesis_config: &mut GenesisConfig) -> u64 { /// NOTE: This is also used to calculate the minimum balance of a stake account, which is the /// rent exempt reserve _plus_ the minimum stake delegation. #[inline(always)] -pub fn get_minimum_delegation(_feature_set: &FeatureSet) -> u64 { - // If/when the minimum delegation amount is changed, the `feature_set` parameter will be used - // to chose the correct value. And since the MINIMUM_STAKE_DELEGATION constant cannot be - // removed, use it here as to not duplicate magic constants. - #[allow(deprecated)] - solana_sdk::stake::MINIMUM_STAKE_DELEGATION +pub fn get_minimum_delegation(feature_set: &FeatureSet) -> u64 { + if feature_set.is_active(&feature_set::stake_raise_minimum_delegation_to_1_sol::id()) { + const MINIMUM_DELEGATION_SOL: u64 = 1; + MINIMUM_DELEGATION_SOL * LAMPORTS_PER_SOL + } else { + #[allow(deprecated)] + solana_sdk::stake::MINIMUM_STAKE_DELEGATION + } } diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index bac5875bc..7eeb633ed 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -512,25 +512,28 @@ mod tests { } fn process_instruction( + feature_set: &FeatureSet, instruction_data: &[u8], transaction_accounts: Vec<(Pubkey, AccountSharedData)>, instruction_accounts: Vec, expected_result: Result<(), InstructionError>, ) -> Vec { - process_instruction_with_sysvar_cache( + process_instruction_with_overrides( instruction_data, transaction_accounts, instruction_accounts, None, + Some(Arc::new(feature_set.clone())), expected_result, ) } - fn process_instruction_with_sysvar_cache( + fn process_instruction_with_overrides( instruction_data: &[u8], transaction_accounts: Vec<(Pubkey, AccountSharedData)>, instruction_accounts: Vec, sysvar_cache_override: Option<&SysvarCache>, + feature_set_override: Option>, expected_result: Result<(), InstructionError>, ) -> Vec { mock_process_instruction( @@ -540,13 +543,14 @@ mod tests { transaction_accounts, instruction_accounts, sysvar_cache_override, - None, + feature_set_override, expected_result, super::process_instruction, ) } fn process_instruction_as_one_arg( + feature_set: &FeatureSet, instruction: &Instruction, expected_result: Result<(), InstructionError>, ) -> Vec { @@ -588,6 +592,7 @@ mod tests { }) .collect(); process_instruction( + feature_set, &instruction.data, transaction_accounts, instruction.accounts.clone(), @@ -608,9 +613,9 @@ mod tests { ) } - #[test] - fn test_stake_process_instruction() { + fn do_test_stake_process_instruction(feature_set: FeatureSet) { process_instruction_as_one_arg( + &feature_set, &instruction::initialize( &Pubkey::new_unique(), &Authorized::default(), @@ -619,6 +624,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + &feature_set, &instruction::authorize( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -629,6 +635,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + &feature_set, &instruction::split( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -638,6 +645,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + &feature_set, &instruction::merge( &Pubkey::new_unique(), &invalid_stake_state_pubkey(), @@ -646,6 +654,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + &feature_set, &instruction::split_with_seed( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -657,6 +666,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + &feature_set, &instruction::delegate_stake( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -665,6 +675,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + &feature_set, &instruction::withdraw( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -675,10 +686,12 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + &feature_set, &instruction::deactivate_stake(&Pubkey::new_unique(), &Pubkey::new_unique()), Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + &feature_set, &instruction::set_lockup( &Pubkey::new_unique(), &LockupArgs::default(), @@ -687,6 +700,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + &feature_set, &instruction::deactivate_delinquent_stake( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -695,6 +709,7 @@ mod tests { Err(InstructionError::IncorrectProgramId), ); process_instruction_as_one_arg( + &feature_set, &instruction::deactivate_delinquent_stake( &Pubkey::new_unique(), &invalid_vote_state_pubkey(), @@ -703,6 +718,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + &feature_set, &instruction::deactivate_delinquent_stake( &Pubkey::new_unique(), &invalid_vote_state_pubkey(), @@ -712,9 +728,9 @@ mod tests { ); } - #[test] - fn test_spoofed_stake_accounts() { + fn do_test_spoofed_stake_accounts(feature_set: FeatureSet) { process_instruction_as_one_arg( + &feature_set, &instruction::initialize( &spoofed_stake_state_pubkey(), &Authorized::default(), @@ -723,6 +739,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + &feature_set, &instruction::authorize( &spoofed_stake_state_pubkey(), &Pubkey::new_unique(), @@ -733,6 +750,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + &feature_set, &instruction::split( &spoofed_stake_state_pubkey(), &Pubkey::new_unique(), @@ -742,6 +760,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + &feature_set, &instruction::split( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -751,6 +770,7 @@ mod tests { Err(InstructionError::IncorrectProgramId), ); process_instruction_as_one_arg( + &feature_set, &instruction::merge( &spoofed_stake_state_pubkey(), &Pubkey::new_unique(), @@ -759,6 +779,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + &feature_set, &instruction::merge( &Pubkey::new_unique(), &spoofed_stake_state_pubkey(), @@ -767,6 +788,7 @@ mod tests { Err(InstructionError::IncorrectProgramId), ); process_instruction_as_one_arg( + &feature_set, &instruction::split_with_seed( &spoofed_stake_state_pubkey(), &Pubkey::new_unique(), @@ -778,6 +800,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + &feature_set, &instruction::delegate_stake( &spoofed_stake_state_pubkey(), &Pubkey::new_unique(), @@ -786,6 +809,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + &feature_set, &instruction::withdraw( &spoofed_stake_state_pubkey(), &Pubkey::new_unique(), @@ -796,10 +820,12 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + &feature_set, &instruction::deactivate_stake(&spoofed_stake_state_pubkey(), &Pubkey::new_unique()), Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + &feature_set, &instruction::set_lockup( &spoofed_stake_state_pubkey(), &LockupArgs::default(), @@ -808,6 +834,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + &feature_set, &instruction::deactivate_delinquent_stake( &spoofed_stake_state_pubkey(), &Pubkey::new_unique(), @@ -817,8 +844,7 @@ mod tests { ); } - #[test] - fn test_stake_process_instruction_decode_bail() { + fn do_test_stake_process_instruction_decode_bail(feature_set: FeatureSet) { // these will not call stake_state, have bogus contents let stake_address = Pubkey::new_unique(); let stake_account = create_default_stake_account(); @@ -839,11 +865,12 @@ mod tests { let config_address = stake_config::id(); let config_account = config::create_account(0, &stake_config::Config::default()); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); let withdrawal_amount = rent_exempt_reserve + minimum_delegation; // gets the "is_empty()" check process_instruction( + &feature_set, &serialize(&StakeInstruction::Initialize( Authorized::default(), Lockup::default(), @@ -856,6 +883,7 @@ mod tests { // no account for rent process_instruction( + &feature_set, &serialize(&StakeInstruction::Initialize( Authorized::default(), Lockup::default(), @@ -872,6 +900,7 @@ mod tests { // fails to deserialize stake state process_instruction( + &feature_set, &serialize(&StakeInstruction::Initialize( Authorized::default(), Lockup::default(), @@ -898,6 +927,7 @@ mod tests { // gets the first check in delegate, wrong number of accounts process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), vec![(stake_address, stake_account.clone())], vec![AccountMeta { @@ -910,6 +940,7 @@ mod tests { // gets the sub-check for number of args process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), vec![(stake_address, stake_account.clone())], vec![AccountMeta { @@ -922,6 +953,7 @@ mod tests { // gets the check non-deserialize-able account in delegate_stake process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), vec![ (stake_address, stake_account.clone()), @@ -962,6 +994,7 @@ mod tests { // Tests 3rd keyed account is of correct type (Clock instead of rewards) in withdraw process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(withdrawal_amount)).unwrap(), vec![ (stake_address, stake_account.clone()), @@ -996,6 +1029,7 @@ mod tests { // Tests correct number of accounts are provided in withdraw process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(withdrawal_amount)).unwrap(), vec![(stake_address, stake_account.clone())], vec![AccountMeta { @@ -1008,6 +1042,7 @@ mod tests { // Tests 2nd keyed account is of correct type (Clock instead of rewards) in deactivate process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), vec![ (stake_address, stake_account.clone()), @@ -1030,6 +1065,7 @@ mod tests { // Tests correct number of accounts are provided in deactivate process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), Vec::new(), Vec::new(), @@ -1038,12 +1074,14 @@ mod tests { // Tests correct number of accounts are provided in deactivate_delinquent process_instruction( + &feature_set, &serialize(&StakeInstruction::DeactivateDelinquent).unwrap(), Vec::new(), Vec::new(), Err(InstructionError::NotEnoughAccountKeys), ); process_instruction( + &feature_set, &serialize(&StakeInstruction::DeactivateDelinquent).unwrap(), vec![(stake_address, stake_account.clone())], vec![AccountMeta { @@ -1054,6 +1092,7 @@ mod tests { Err(InstructionError::NotEnoughAccountKeys), ); process_instruction( + &feature_set, &serialize(&StakeInstruction::DeactivateDelinquent).unwrap(), vec![(stake_address, stake_account), (vote_address, vote_account)], vec![ @@ -1072,8 +1111,7 @@ mod tests { ); } - #[test] - fn test_stake_checked_instructions() { + fn do_test_stake_checked_instructions(feature_set: FeatureSet) { let stake_address = Pubkey::new_unique(); let staker = Pubkey::new_unique(); let staker_account = create_default_account(); @@ -1090,13 +1128,14 @@ mod tests { let rent_address = sysvar::rent::id(); let rent_account = account::create_account_shared_data_for_test(&rent); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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( + &feature_set, &instruction, Err(InstructionError::MissingRequiredSignature), ); @@ -1104,10 +1143,11 @@ mod tests { // Test InitializeChecked with withdrawer signer let stake_account = AccountSharedData::new( rent_exempt_reserve + minimum_delegation, - std::mem::size_of::(), + StakeState::size_of(), &id(), ); process_instruction( + &feature_set, &serialize(&StakeInstruction::InitializeChecked).unwrap(), vec![ (stake_address, stake_account), @@ -1150,6 +1190,7 @@ mod tests { ); instruction.accounts[3] = AccountMeta::new_readonly(staker, false); process_instruction_as_one_arg( + &feature_set, &instruction, Err(InstructionError::MissingRequiredSignature), ); @@ -1163,6 +1204,7 @@ mod tests { ); instruction.accounts[3] = AccountMeta::new_readonly(withdrawer, false); process_instruction_as_one_arg( + &feature_set, &instruction, Err(InstructionError::MissingRequiredSignature), ); @@ -1176,6 +1218,7 @@ mod tests { ) .unwrap(); process_instruction( + &feature_set, &serialize(&StakeInstruction::AuthorizeChecked(StakeAuthorize::Staker)).unwrap(), vec![ (stake_address, stake_account.clone()), @@ -1209,6 +1252,7 @@ mod tests { ); process_instruction( + &feature_set, &serialize(&StakeInstruction::AuthorizeChecked( StakeAuthorize::Withdrawer, )) @@ -1260,6 +1304,7 @@ mod tests { ); instruction.accounts[3] = AccountMeta::new_readonly(staker, false); process_instruction_as_one_arg( + &feature_set, &instruction, Err(InstructionError::MissingRequiredSignature), ); @@ -1275,6 +1320,7 @@ mod tests { ); instruction.accounts[3] = AccountMeta::new_readonly(staker, false); process_instruction_as_one_arg( + &feature_set, &instruction, Err(InstructionError::MissingRequiredSignature), ); @@ -1288,6 +1334,7 @@ mod tests { ) .unwrap(); process_instruction( + &feature_set, &serialize(&StakeInstruction::AuthorizeCheckedWithSeed( AuthorizeCheckedWithSeedArgs { stake_authorize: StakeAuthorize::Staker, @@ -1328,6 +1375,7 @@ mod tests { ); process_instruction( + &feature_set, &serialize(&StakeInstruction::AuthorizeCheckedWithSeed( AuthorizeCheckedWithSeedArgs { stake_authorize: StakeAuthorize::Withdrawer, @@ -1379,6 +1427,7 @@ mod tests { ); instruction.accounts[2] = AccountMeta::new_readonly(custodian, false); process_instruction_as_one_arg( + &feature_set, &instruction, Err(InstructionError::MissingRequiredSignature), ); @@ -1393,6 +1442,7 @@ mod tests { .unwrap(); process_instruction( + &feature_set, &instruction.data, vec![ (clock_address, clock_account), @@ -1421,12 +1471,10 @@ mod tests { ); } - #[test] - fn test_stake_initialize() { + fn do_test_stake_initialize(feature_set: FeatureSet) { let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); - let stake_lamports = rent_exempt_reserve + minimum_delegation; + let stake_lamports = rent_exempt_reserve; let stake_address = solana_sdk::pubkey::new_rand(); let stake_account = AccountSharedData::new(stake_lamports, StakeState::size_of(), &id()); let custodian_address = solana_sdk::pubkey::new_rand(); @@ -1462,6 +1510,7 @@ mod tests { // should pass let accounts = process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1480,6 +1529,7 @@ mod tests { // 2nd time fails, can't move it from anything other than uninit->init transaction_accounts[0] = (stake_address, accounts[0].clone()); process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1487,7 +1537,7 @@ mod tests { ); transaction_accounts[0] = (stake_address, stake_account); - // not enough balance for rent and minimum delegation... + // not enough balance for rent transaction_accounts[1] = ( sysvar::rent::id(), account::create_account_shared_data_for_test(&Rent { @@ -1496,6 +1546,7 @@ mod tests { }), ); process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1507,6 +1558,7 @@ mod tests { AccountSharedData::new(stake_lamports, StakeState::size_of() + 1, &id()); transaction_accounts[0] = (stake_address, stake_account); process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1517,6 +1569,7 @@ mod tests { AccountSharedData::new(stake_lamports, StakeState::size_of() - 1, &id()); transaction_accounts[0] = (stake_address, stake_account); process_instruction( + &feature_set, &instruction_data, transaction_accounts, instruction_accounts, @@ -1524,8 +1577,7 @@ mod tests { ); } - #[test] - fn test_authorize() { + fn do_test_authorize(feature_set: 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(); @@ -1572,6 +1624,7 @@ mod tests { // should fail, uninit process_instruction( + &feature_set, &serialize(&StakeInstruction::Authorize( authority_address, StakeAuthorize::Staker, @@ -1592,6 +1645,7 @@ mod tests { .unwrap(); transaction_accounts[0] = (stake_address, stake_account); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Authorize( authority_address, StakeAuthorize::Staker, @@ -1603,6 +1657,7 @@ mod tests { ); transaction_accounts[0] = (stake_address, accounts[0].clone()); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Authorize( authority_address, StakeAuthorize::Withdrawer, @@ -1622,6 +1677,7 @@ mod tests { // A second authorization signed by the stake account should fail process_instruction( + &feature_set, &serialize(&StakeInstruction::Authorize( authority_address_2, StakeAuthorize::Staker, @@ -1636,6 +1692,7 @@ mod tests { instruction_accounts[0].is_signer = false; instruction_accounts[2].is_signer = true; let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Authorize( authority_address_2, StakeAuthorize::Staker, @@ -1680,6 +1737,7 @@ mod tests { }, ]; let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1690,6 +1748,7 @@ mod tests { // Test that withdrawal to account fails without authorized withdrawer instruction_accounts[4].is_signer = false; process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), transaction_accounts, instruction_accounts, @@ -1697,8 +1756,7 @@ mod tests { ); } - #[test] - fn test_authorize_override() { + fn do_test_authorize_override(feature_set: 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(); @@ -1738,6 +1796,7 @@ mod tests { // Authorize a staker pubkey and move the withdrawer key into cold storage. let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Authorize( authority_address, StakeAuthorize::Staker, @@ -1753,6 +1812,7 @@ mod tests { instruction_accounts[0].is_signer = false; instruction_accounts[2].is_signer = true; let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Authorize( mallory_address, StakeAuthorize::Staker, @@ -1766,6 +1826,7 @@ mod tests { // Verify the original staker no longer has access. process_instruction( + &feature_set, &serialize(&StakeInstruction::Authorize( authority_address, StakeAuthorize::Staker, @@ -1780,6 +1841,7 @@ mod tests { instruction_accounts[0].is_signer = true; instruction_accounts[2].is_signer = false; let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Authorize( authority_address, StakeAuthorize::Withdrawer, @@ -1799,6 +1861,7 @@ mod tests { is_writable: false, }; process_instruction( + &feature_set, &serialize(&StakeInstruction::Authorize( authority_address, StakeAuthorize::Withdrawer, @@ -1810,8 +1873,7 @@ mod tests { ); } - #[test] - fn test_authorize_with_seed() { + fn do_test_authorize_with_seed(feature_set: FeatureSet) { let authority_base_address = solana_sdk::pubkey::new_rand(); let authority_address = solana_sdk::pubkey::new_rand(); let seed = "42"; @@ -1852,6 +1914,7 @@ mod tests { // Wrong seed process_instruction( + &feature_set, &serialize(&StakeInstruction::AuthorizeWithSeed( AuthorizeWithSeedArgs { new_authorized_pubkey: authority_address, @@ -1878,6 +1941,7 @@ mod tests { )) .unwrap(); process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1887,6 +1951,7 @@ mod tests { // Set stake authority let accounts = process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1905,6 +1970,7 @@ mod tests { )) .unwrap(); let accounts = process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1914,6 +1980,7 @@ mod tests { // No longer withdraw authority process_instruction( + &feature_set, &instruction_data, transaction_accounts, instruction_accounts, @@ -1921,11 +1988,10 @@ mod tests { ); } - #[test] - fn test_authorize_delegated_stake() { + fn do_test_authorize_delegated_stake(feature_set: FeatureSet) { let authority_address = solana_sdk::pubkey::new_rand(); let stake_address = solana_sdk::pubkey::new_rand(); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = minimum_delegation; let stake_account = AccountSharedData::new_data_with_space( stake_lamports, @@ -1992,6 +2058,7 @@ mod tests { // delegate stake let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2001,6 +2068,7 @@ mod tests { // deactivate, so we can re-delegate let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), transaction_accounts.clone(), vec![ @@ -2021,6 +2089,7 @@ mod tests { // authorize let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Authorize( authority_address, StakeAuthorize::Staker, @@ -2056,6 +2125,7 @@ mod tests { instruction_accounts[0].is_signer = false; instruction_accounts[1].pubkey = vote_address_2; process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2069,6 +2139,7 @@ mod tests { is_writable: false, }); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), instruction_accounts, @@ -2082,6 +2153,7 @@ mod tests { // Test another staking action process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), transaction_accounts, vec![ @@ -2105,8 +2177,7 @@ mod tests { ); } - #[test] - fn test_stake_delegate() { + fn do_test_stake_delegate(feature_set: FeatureSet) { let mut vote_state = VoteState::default(); for i in 0..1000 { vote_state.process_slot_vote_unchecked(i); @@ -2124,7 +2195,7 @@ mod tests { vote_account_2 .set_state(&VoteStateVersions::new_current(vote_state)) .unwrap(); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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( @@ -2192,6 +2263,7 @@ mod tests { // should fail, unsigned stake account instruction_accounts[0].is_signer = false; process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2201,6 +2273,7 @@ mod tests { // should pass let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2229,6 +2302,7 @@ mod tests { account::create_account_shared_data_for_test(&clock), ); process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2237,6 +2311,7 @@ mod tests { // deactivate let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), transaction_accounts.clone(), vec![ @@ -2259,6 +2334,7 @@ mod tests { transaction_accounts[0] = (stake_address, accounts[0].clone()); instruction_accounts[1].pubkey = vote_address_2; process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2269,6 +2345,7 @@ mod tests { // verify that delegate succeeds to same vote account // when stake is deactivating let accounts_2 = process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2283,6 +2360,7 @@ mod tests { transaction_accounts[0] = (stake_address, accounts_2[0].clone()); instruction_accounts[1].pubkey = vote_address_2; process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2299,6 +2377,7 @@ mod tests { // 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( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2326,6 +2405,7 @@ mod tests { .1 .set_owner(solana_sdk::pubkey::new_rand()); process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2337,6 +2417,7 @@ mod tests { stake_account.set_state(&stake_state).unwrap(); transaction_accounts[0] = (stake_address, stake_account); process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts, instruction_accounts, @@ -2344,8 +2425,7 @@ mod tests { ); } - #[test] - fn test_redelegate_consider_balance_changes() { + fn do_test_redelegate_consider_balance_changes(feature_set: FeatureSet) { let mut clock = Clock::default(); let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); @@ -2439,6 +2519,7 @@ mod tests { ]; let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), delegate_instruction_accounts.clone(), @@ -2452,6 +2533,7 @@ mod tests { account::create_account_shared_data_for_test(&clock), ); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), transaction_accounts.clone(), deactivate_instruction_accounts.clone(), @@ -2467,6 +2549,7 @@ mod tests { ); let withdraw_lamports = initial_lamports / 2; let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(withdraw_lamports)).unwrap(), transaction_accounts.clone(), vec![ @@ -2508,6 +2591,7 @@ mod tests { account::create_account_shared_data_for_test(&clock), ); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), delegate_instruction_accounts.clone(), @@ -2525,6 +2609,7 @@ mod tests { account::create_account_shared_data_for_test(&clock), ); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), transaction_accounts.clone(), deactivate_instruction_accounts, @@ -2544,6 +2629,7 @@ mod tests { account::create_account_shared_data_for_test(&clock), ); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts, delegate_instruction_accounts, @@ -2555,10 +2641,9 @@ mod tests { ); } - #[test] - fn test_split() { + fn do_test_split(feature_set: FeatureSet) { let stake_address = solana_sdk::pubkey::new_rand(); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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( @@ -2592,6 +2677,7 @@ mod tests { lamports_per_byte_year: 0, ..Rent::default() }); + let feature_set = Arc::new(feature_set); for state in [ StakeState::Initialized(Meta::auto(&stake_address)), @@ -2607,20 +2693,22 @@ mod tests { transaction_accounts[0] = (stake_address, stake_account); // should fail, split more than available - process_instruction_with_sysvar_cache( + process_instruction_with_overrides( &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Some(&sysvar_cache_override), + Some(Arc::clone(&feature_set)), Err(InstructionError::InsufficientFunds), ); // should pass - let accounts = process_instruction_with_sysvar_cache( + let accounts = process_instruction_with_overrides( &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Some(&sysvar_cache_override), + Some(Arc::clone(&feature_set)), Ok(()), ); // no lamport leakage @@ -2651,21 +2739,22 @@ mod tests { ) .unwrap(); transaction_accounts[1] = (split_to_address, split_to_account); - process_instruction( + process_instruction_with_overrides( &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), transaction_accounts, instruction_accounts, + Some(&sysvar_cache_override), + Some(Arc::clone(&feature_set)), Err(InstructionError::IncorrectProgramId), ); } - #[test] - fn test_withdraw_stake() { + fn do_test_withdraw_stake(feature_set: 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(&FeatureSet::all_enabled()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = minimum_delegation; let stake_account = AccountSharedData::new_data_with_space( stake_lamports, @@ -2737,6 +2826,7 @@ mod tests { // should fail, no signer instruction_accounts[4].is_signer = false; process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2746,6 +2836,7 @@ mod tests { // should pass, signed keyed account and uninitialized let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2761,6 +2852,7 @@ mod tests { custodian: custodian_address, }; let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Initialize( Authorized::auto(&stake_address), lockup, @@ -2785,6 +2877,7 @@ mod tests { // should fail, signed keyed account and locked up, more than available process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(stake_lamports + 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2793,6 +2886,7 @@ mod tests { // Stake some lamports (available lamports for withdrawals will reduce to zero) let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), vec![ @@ -2831,6 +2925,7 @@ mod tests { // withdrawal before deactivate works for rewards amount process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(10)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2839,6 +2934,7 @@ mod tests { // withdrawal of rewards fails if not in excess of stake process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(11)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2847,6 +2943,7 @@ mod tests { // deactivate the stake before withdrawal let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), transaction_accounts.clone(), vec![ @@ -2877,6 +2974,7 @@ mod tests { // Try to withdraw more than what's available process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(stake_lamports + 11)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2885,6 +2983,7 @@ mod tests { // Try to withdraw all lamports let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(stake_lamports + 10)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2914,6 +3013,7 @@ mod tests { transaction_accounts[2] = (recipient_address, stake_account); instruction_accounts[4].pubkey = authority_address; process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(u64::MAX - 10)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -2930,6 +3030,7 @@ mod tests { .unwrap(); transaction_accounts[0] = (stake_address, stake_account); process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), transaction_accounts, instruction_accounts, @@ -2937,11 +3038,10 @@ mod tests { ); } - #[test] - fn test_withdraw_stake_before_warmup() { + fn do_test_withdraw_stake_before_warmup(feature_set: FeatureSet) { let recipient_address = solana_sdk::pubkey::new_rand(); let stake_address = solana_sdk::pubkey::new_rand(); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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( @@ -3008,6 +3108,7 @@ mod tests { // Stake some lamports (available lamports for withdrawals will reduce to zero) let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), vec![ @@ -3057,6 +3158,7 @@ mod tests { account::create_account_shared_data_for_test(&clock), ); process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw( total_lamports - stake_lamports + 1, )) @@ -3067,8 +3169,7 @@ mod tests { ); } - #[test] - fn test_withdraw_lockup() { + fn do_test_withdraw_lockup(feature_set: 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(); @@ -3132,6 +3233,7 @@ mod tests { // should fail, lockup is still in force process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -3145,6 +3247,7 @@ mod tests { is_writable: false, }); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -3164,6 +3267,7 @@ mod tests { .unwrap(); transaction_accounts[0] = (stake_address, stake_account_self_as_custodian); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -3180,6 +3284,7 @@ mod tests { account::create_account_shared_data_for_test(&clock), ); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(), transaction_accounts, instruction_accounts, @@ -3188,14 +3293,13 @@ mod tests { assert_eq!(from(&accounts[0]).unwrap(), StakeState::Uninitialized); } - #[test] - fn test_withdraw_rent_exempt() { + fn do_test_withdraw_rent_exempt(feature_set: 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(StakeState::size_of()); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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, @@ -3250,6 +3354,7 @@ mod tests { // should pass, withdrawing initialized account down to minimum balance process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -3258,6 +3363,7 @@ mod tests { // should fail, withdrawal that would leave less than rent-exempt reserve process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(stake_lamports + 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -3266,6 +3372,7 @@ mod tests { // should pass, withdrawal of complete account process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw( stake_lamports + rent_exempt_reserve, )) @@ -3276,10 +3383,9 @@ mod tests { ); } - #[test] - fn test_deactivate() { + fn do_test_deactivate(feature_set: FeatureSet) { let stake_address = solana_sdk::pubkey::new_rand(); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = minimum_delegation; let stake_account = AccountSharedData::new_data_with_space( stake_lamports, @@ -3326,6 +3432,7 @@ mod tests { // should fail, not signed instruction_accounts[0].is_signer = false; process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -3335,6 +3442,7 @@ mod tests { // should fail, not staked yet process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -3343,6 +3451,7 @@ mod tests { // Staking let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), vec![ @@ -3378,6 +3487,7 @@ mod tests { // should pass let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -3387,6 +3497,7 @@ mod tests { // should fail, only works once process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), transaction_accounts, instruction_accounts, @@ -3394,12 +3505,11 @@ mod tests { ); } - #[test] - fn test_set_lockup() { + fn do_test_set_lockup(feature_set: 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(&FeatureSet::all_enabled()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = minimum_delegation; let stake_account = AccountSharedData::new_data_with_space( stake_lamports, @@ -3462,6 +3572,7 @@ mod tests { // should fail, wrong state process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -3475,6 +3586,7 @@ mod tests { custodian: custodian_address, }; let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Initialize( Authorized::auto(&stake_address), lockup, @@ -3500,6 +3612,7 @@ mod tests { // should fail, not signed instruction_accounts[2].is_signer = false; process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -3509,6 +3622,7 @@ mod tests { // should pass process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -3517,6 +3631,7 @@ mod tests { // Staking let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), vec![ @@ -3553,6 +3668,7 @@ mod tests { // should fail, not signed instruction_accounts[2].is_signer = false; process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -3562,6 +3678,7 @@ mod tests { // should pass process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -3580,6 +3697,7 @@ mod tests { instruction_accounts[0].is_signer = true; instruction_accounts[2].is_signer = false; process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -3590,6 +3708,7 @@ mod tests { // should pass, custodian can change it process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -3609,6 +3728,7 @@ mod tests { // should fail, custodian cannot change it process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -3619,6 +3739,7 @@ mod tests { instruction_accounts[0].is_signer = true; instruction_accounts[2].is_signer = false; process_instruction( + &feature_set, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -3627,6 +3748,7 @@ mod tests { // Change authorized withdrawer let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Authorize( authorized_address, StakeAuthorize::Withdrawer, @@ -3656,6 +3778,7 @@ mod tests { // should fail, previous authorized withdrawer cannot change the lockup anymore process_instruction( + &feature_set, &instruction_data, transaction_accounts, instruction_accounts, @@ -3666,8 +3789,7 @@ mod tests { /// 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] - fn test_initialize_minimum_balance() { + fn do_test_initialize_minimum_balance(feature_set: FeatureSet) { let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); let stake_address = solana_sdk::pubkey::new_rand(); @@ -3697,6 +3819,7 @@ mod tests { ] { let stake_account = AccountSharedData::new(lamports, StakeState::size_of(), &id()); process_instruction( + &feature_set, &instruction_data, vec![ (stake_address, stake_account), @@ -3712,19 +3835,17 @@ mod tests { } /// Ensure that `delegate()` respects the minimum delegation requirements - /// - Assert 1: delegating an amount equal-to the minimum delegates OK + /// - 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 StakeState::{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 current behavior allows + /// 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] - fn test_delegate_minimum_stake_delegation() { - let feature_set = FeatureSet::all_enabled(); + fn do_test_delegate_minimum_stake_delegation(feature_set: FeatureSet) { let minimum_delegation = crate::get_minimum_delegation(&feature_set); let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); @@ -3763,13 +3884,13 @@ mod tests { is_writable: false, }, ]; - for (stake_delegation, expected_result) in [ + for (stake_delegation, expected_result) in &[ (minimum_delegation, Ok(())), (minimum_delegation - 1, Err(StakeError::InsufficientStake)), ] { for stake_state in &[ StakeState::Initialized(meta), - just_stake(meta, stake_delegation), + just_stake(meta, *stake_delegation), ] { let stake_account = AccountSharedData::new_data_with_space( stake_delegation + rent_exempt_reserve, @@ -3779,6 +3900,7 @@ mod tests { ) .unwrap(); process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), vec![ (stake_address, stake_account), @@ -3813,9 +3935,7 @@ mod tests { /// EQ | LT | Err /// LT | EQ | Err /// LT | LT | Err - #[test] - fn test_split_minimum_stake_delegation() { - let feature_set = FeatureSet::all_enabled(); + fn do_test_split_minimum_stake_delegation(feature_set: FeatureSet) { let minimum_delegation = crate::get_minimum_delegation(&feature_set); let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); @@ -3884,6 +4004,7 @@ mod tests { ) .unwrap(); process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(dest_reserve + delegation)).unwrap(), vec![ (source_address, source_account), @@ -3907,9 +4028,7 @@ mod tests { /// delegation is OK /// - Assert 2: splitting the full amount from an account that has less than the minimum /// delegation is not OK - #[test] - fn test_split_full_amount_minimum_stake_delegation() { - let feature_set = FeatureSet::all_enabled(); + fn do_test_split_full_amount_minimum_stake_delegation(feature_set: FeatureSet) { let minimum_delegation = crate::get_minimum_delegation(&feature_set); let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); @@ -3960,6 +4079,7 @@ mod tests { ) .unwrap(); process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(source_account.lamports())).unwrap(), vec![ (source_address, source_account), @@ -3979,16 +4099,11 @@ mod tests { /// 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] - fn test_initialized_split_destination_minimum_balance() { + fn do_test_initialized_split_destination_minimum_balance(feature_set: FeatureSet) { let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); let source_address = Pubkey::new_unique(); - let source_meta = Meta { - rent_exempt_reserve, - ..Meta::auto(&source_address) - }; - let dest_address = Pubkey::new_unique(); + let destination_address = Pubkey::new_unique(); let instruction_accounts = vec![ AccountMeta { pubkey: source_address, @@ -3996,13 +4111,11 @@ mod tests { is_writable: false, }, AccountMeta { - pubkey: dest_address, + pubkey: destination_address, is_signer: false, is_writable: false, }, ]; - let source_balance = u64::MAX; - let source_stake_state = StakeState::Initialized(source_meta); for (destination_starting_balance, split_amount, expected_result) in [ // split amount must be non zero ( @@ -4014,7 +4127,7 @@ mod tests { (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, so 1 isn't enough (non-zero split amount) + // destination short by 2 lamports, then 1 isn't enough (non-zero split amount) ( rent_exempt_reserve - 2, 1, @@ -4030,27 +4143,30 @@ mod tests { rent_exempt_reserve - 2, Err(InstructionError::InsufficientFunds), ), - // destination has zero lamports, so split must be at least rent exempt reserve plus - // minimum delegation + // 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 - // plus minimum delegation ( 0, rent_exempt_reserve - 1, Err(InstructionError::InsufficientFunds), ), ] { - // Set the source's starting balance and stake delegation amount to something large - // to ensure its post-split balance meets all the requirements + // 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, - &source_stake_state, + &StakeState::Initialized(source_meta), StakeState::size_of(), &id(), ) .unwrap(); - let dest_account = AccountSharedData::new_data_with_space( + let destination_account = AccountSharedData::new_data_with_space( destination_starting_balance, &StakeState::Uninitialized, StakeState::size_of(), @@ -4059,10 +4175,11 @@ mod tests { .unwrap(); process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(split_amount)).unwrap(), vec![ (source_address, source_account), - (dest_address, dest_account), + (destination_address, destination_account), ( sysvar::rent::id(), account::create_account_shared_data_for_test(&rent), @@ -4074,20 +4191,18 @@ mod tests { } } - /// Ensure that `split()` correctly handles prefunded destination accounts from - /// delegated stakes. When a destination account already has funds, ensure - /// the minimum split amount reduces accordingly. - #[test] - fn test_stake_split_destination_minimum_delegation() { - let feature_set = FeatureSet::all_enabled(); + /// 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. + fn do_test_staked_split_destination_minimum_balance( + feature_set: 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(StakeState::size_of()); let source_address = Pubkey::new_unique(); - let source_meta = Meta { - rent_exempt_reserve, - ..Meta::auto(&source_address) - }; - let dest_address = Pubkey::new_unique(); + let destination_address = Pubkey::new_unique(); let instruction_accounts = vec![ AccountMeta { pubkey: source_address, @@ -4095,17 +4210,11 @@ mod tests { is_writable: false, }, AccountMeta { - pubkey: dest_address, + pubkey: destination_address, is_signer: false, is_writable: false, }, ]; - // Set the source's starting balance and stake delegation amount to - // something large to ensure its post-split balance meets all the requirements - let source_balance = u64::MAX; - let source_stake_delegation = source_balance - rent_exempt_reserve; - let minimum_delegation = crate::get_minimum_delegation(&feature_set); - let source_stake_state = just_stake(source_meta, source_stake_delegation); for (destination_starting_balance, split_amount, expected_result) in [ // split amount must be non zero ( @@ -4113,10 +4222,22 @@ mod tests { 0, Err(InstructionError::InsufficientFunds), ), - // any split amount is OK when destination account is already fully funded - (rent_exempt_reserve + minimum_delegation, 1, Ok(())), - // if destination is only short by 1 lamport, then split amount can be 1 lamport - (rent_exempt_reserve + minimum_delegation - 1, 1, Ok(())), + // 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, @@ -4160,26 +4281,34 @@ mod tests { 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, - &source_stake_state, + &just_stake(source_meta, source_stake_delegation), StakeState::size_of(), &id(), ) .unwrap(); - let dest_account = AccountSharedData::new_data_with_space( + let destination_account = AccountSharedData::new_data_with_space( destination_starting_balance, &StakeState::Uninitialized, StakeState::size_of(), &id(), ) .unwrap(); - let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(split_amount)).unwrap(), vec![ - (source_address, source_account), - (dest_address, dest_account), + (source_address, source_account.clone()), + (destination_address, destination_account), ( sysvar::rent::id(), account::create_account_shared_data_for_test(&rent), @@ -4216,9 +4345,7 @@ mod tests { /// 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] - fn test_withdraw_minimum_stake_delegation() { - let feature_set = FeatureSet::all_enabled(); + fn do_test_withdraw_minimum_stake_delegation(feature_set: FeatureSet) { let minimum_delegation = crate::get_minimum_delegation(&feature_set); let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); @@ -4278,6 +4405,7 @@ mod tests { let withdraw_amount = (starting_stake_delegation + rewards_balance) - ending_stake_delegation; process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(withdraw_amount)).unwrap(), vec![ (stake_address, stake_account), @@ -4309,9 +4437,10 @@ mod tests { } } - /// The stake program currently allows delegations below the minimum stake delegation (see also - /// `test_delegate_minimum_stake_delegation()`). This is not the ultimate desired behavior, - /// but this test ensures the existing behavior is not changed inadvertently. + /// 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) @@ -4319,9 +4448,10 @@ mod tests { /// 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 = FeatureSet::all_enabled(); + fn do_test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation( + feature_set: FeatureSet, + expected_result: Result<(), InstructionError>, + ) { let minimum_delegation = crate::get_minimum_delegation(&feature_set); let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); @@ -4389,6 +4519,7 @@ mod tests { ]; let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Initialize( Authorized::auto(&stake_address), Lockup::default(), @@ -4412,6 +4543,7 @@ mod tests { transaction_accounts[0] = (stake_address, accounts[0].clone()); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -4426,6 +4558,7 @@ mod tests { account::create_account_shared_data_for_test(&clock), ); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Deactivate).unwrap(), transaction_accounts.clone(), vec![ @@ -4451,7 +4584,8 @@ mod tests { ); let withdraw_amount = accounts[0].lamports() - (rent_exempt_reserve + minimum_delegation - 1); - process_instruction( + let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Withdraw(withdraw_amount)).unwrap(), transaction_accounts.clone(), vec![ @@ -4486,18 +4620,18 @@ mod tests { transaction_accounts[0] = (stake_address, accounts[0].clone()); process_instruction( + &feature_set, &serialize(&StakeInstruction::DelegateStake).unwrap(), transaction_accounts, instruction_accounts, - Ok(()), + expected_result, ); } - #[test] - fn test_split_source_uninitialized() { + fn do_test_split_source_uninitialized(feature_set: FeatureSet) { let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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( @@ -4541,24 +4675,28 @@ mod tests { // // and splitting should fail when the split amount is greater than the balance process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), ); process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(0)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), ); process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), ); process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -4569,6 +4707,7 @@ mod tests { // this should work instruction_accounts[1].pubkey = split_to_address; let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -4579,6 +4718,7 @@ mod tests { // no signers should fail instruction_accounts[0].is_signer = false; process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), transaction_accounts, instruction_accounts, @@ -4586,8 +4726,7 @@ mod tests { ); } - #[test] - fn test_split_split_not_uninitialized() { + fn do_test_split_split_not_uninitialized(feature_set: FeatureSet) { let stake_lamports = 42; let stake_address = solana_sdk::pubkey::new_rand(); let stake_account = AccountSharedData::new_data_with_space( @@ -4624,6 +4763,7 @@ mod tests { ) .unwrap(); process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), vec![ (stake_address, stake_account.clone()), @@ -4635,11 +4775,10 @@ mod tests { } } - #[test] - fn test_split_more_than_staked() { + fn do_test_split_more_than_staked(feature_set: FeatureSet) { let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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( @@ -4685,6 +4824,7 @@ mod tests { ]; process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), transaction_accounts, instruction_accounts, @@ -4692,11 +4832,10 @@ mod tests { ); } - #[test] - fn test_split_with_rent() { + fn do_test_split_with_rent(feature_set: FeatureSet) { let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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( @@ -4751,6 +4890,7 @@ mod tests { // not enough to make a non-zero stake account process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(minimum_balance - 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -4759,6 +4899,7 @@ mod tests { // doesn't leave enough for initial stake to be non-zero process_instruction( + &feature_set, &serialize(&StakeInstruction::Split( stake_lamports - minimum_balance + 1, )) @@ -4771,6 +4912,7 @@ mod tests { // split account already has way enough lamports transaction_accounts[1].1.set_lamports(*minimum_balance); let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports - minimum_balance)).unwrap(), transaction_accounts, instruction_accounts.clone(), @@ -4798,11 +4940,10 @@ mod tests { } } - #[test] - fn test_split_to_account_with_rent_exempt_reserve() { + fn do_test_split_to_account_with_rent_exempt_reserve(feature_set: FeatureSet) { let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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 { @@ -4861,6 +5002,7 @@ mod tests { // split more than available fails process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -4869,6 +5011,7 @@ mod tests { // should work let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), transaction_accounts, instruction_accounts.clone(), @@ -4920,12 +5063,11 @@ mod tests { } } - #[test] - fn test_split_from_larger_sized_account() { + fn do_test_split_from_larger_sized_account(feature_set: FeatureSet) { let rent = Rent::default(); let source_larger_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of() + 100); let split_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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 { @@ -4984,6 +5126,7 @@ mod tests { // split more than available fails process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -4992,6 +5135,7 @@ mod tests { // should work let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -5048,8 +5192,7 @@ mod tests { } } - #[test] - fn test_split_from_smaller_sized_account() { + fn do_test_split_from_smaller_sized_account(feature_set: FeatureSet) { let rent = Rent::default(); let source_smaller_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); let split_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of() + 100); @@ -5108,6 +5251,7 @@ mod tests { // should always return error when splitting to larger account process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(split_amount)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -5116,6 +5260,7 @@ mod tests { // Splitting 100% of source should not make a difference process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), transaction_accounts, instruction_accounts.clone(), @@ -5124,11 +5269,10 @@ mod tests { } } - #[test] - fn test_split_100_percent_of_source() { + fn do_test_split_100_percent_of_source(feature_set: FeatureSet) { let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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 { @@ -5180,6 +5324,7 @@ mod tests { // split 100% over to dest let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), transaction_accounts, instruction_accounts.clone(), @@ -5218,11 +5363,10 @@ mod tests { } } - #[test] - fn test_split_100_percent_of_source_to_account_with_lamports() { + fn do_test_split_100_percent_of_source_to_account_with_lamports(feature_set: FeatureSet) { let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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 { @@ -5281,6 +5425,7 @@ mod tests { // split 100% over to dest let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), transaction_accounts, instruction_accounts.clone(), @@ -5312,12 +5457,11 @@ mod tests { } } - #[test] - fn test_split_rent_exemptness() { + fn do_test_split_rent_exemptness(feature_set: FeatureSet) { let rent = Rent::default(); let source_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of() + 100); let split_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); - let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + 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 { @@ -5367,6 +5511,7 @@ mod tests { ), ]; process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), transaction_accounts, instruction_accounts.clone(), @@ -5398,6 +5543,7 @@ mod tests { ), ]; let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), transaction_accounts, instruction_accounts.clone(), @@ -5447,8 +5593,7 @@ mod tests { } } - #[test] - fn test_merge() { + fn do_test_merge(feature_set: 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(); @@ -5521,6 +5666,7 @@ mod tests { // Authorized staker signature required... instruction_accounts[4].is_signer = false; process_instruction( + &feature_set, &serialize(&StakeInstruction::Merge).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -5529,6 +5675,7 @@ mod tests { instruction_accounts[4].is_signer = true; let accounts = process_instruction( + &feature_set, &serialize(&StakeInstruction::Merge).unwrap(), transaction_accounts, instruction_accounts.clone(), @@ -5574,8 +5721,7 @@ mod tests { } } - #[test] - fn test_merge_self_fails() { + fn do_test_merge_self_fails(feature_set: FeatureSet) { let stake_address = solana_sdk::pubkey::new_rand(); let authorized_address = solana_sdk::pubkey::new_rand(); let rent = Rent::default(); @@ -5642,6 +5788,7 @@ mod tests { ]; process_instruction( + &feature_set, &serialize(&StakeInstruction::Merge).unwrap(), transaction_accounts, instruction_accounts, @@ -5649,8 +5796,7 @@ mod tests { ); } - #[test] - fn test_merge_incorrect_authorized_staker() { + fn do_test_merge_incorrect_authorized_staker(feature_set: 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(); @@ -5723,6 +5869,7 @@ mod tests { instruction_accounts[4].pubkey = wrong_authorized_address; process_instruction( + &feature_set, &serialize(&StakeInstruction::Merge).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -5731,6 +5878,7 @@ mod tests { instruction_accounts[4].pubkey = authorized_address; process_instruction( + &feature_set, &serialize(&StakeInstruction::Merge).unwrap(), transaction_accounts, instruction_accounts.clone(), @@ -5740,8 +5888,7 @@ mod tests { } } - #[test] - fn test_merge_invalid_account_data() { + fn do_test_merge_invalid_account_data(feature_set: 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(); @@ -5810,6 +5957,7 @@ mod tests { ]; process_instruction( + &feature_set, &serialize(&StakeInstruction::Merge).unwrap(), transaction_accounts, instruction_accounts.clone(), @@ -5819,8 +5967,7 @@ mod tests { } } - #[test] - fn test_merge_fake_stake_source() { + fn do_test_merge_fake_stake_source(feature_set: 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(); @@ -5881,6 +6028,7 @@ mod tests { ]; process_instruction( + &feature_set, &serialize(&StakeInstruction::Merge).unwrap(), transaction_accounts, instruction_accounts, @@ -5888,8 +6036,7 @@ mod tests { ); } - #[test] - fn test_merge_active_stake() { + fn do_test_merge_active_stake(feature_set: 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(); @@ -5990,6 +6137,7 @@ mod tests { ]; fn try_merge( + feature_set: &FeatureSet, transaction_accounts: Vec<(Pubkey, AccountSharedData)>, mut instruction_accounts: Vec, expected_result: Result<(), InstructionError>, @@ -5998,11 +6146,11 @@ mod tests { if iteration == 1 { instruction_accounts.swap(0, 1); } - let accounts = process_instruction_with_sysvar_cache( + let accounts = process_instruction( + feature_set, &serialize(&StakeInstruction::Merge).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), - None, expected_result.clone(), ); if expected_result.is_ok() { @@ -6016,6 +6164,7 @@ mod tests { // stake activation epoch, source initialized succeeds try_merge( + &feature_set, transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), @@ -6053,6 +6202,7 @@ mod tests { break; } try_merge( + &feature_set, transaction_accounts.clone(), instruction_accounts.clone(), Err(InstructionError::from(StakeError::MergeTransientStake)), @@ -6061,6 +6211,7 @@ mod tests { // Both fully activated works try_merge( + &feature_set, transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), @@ -6127,6 +6278,7 @@ mod tests { break; } try_merge( + &feature_set, transaction_accounts.clone(), instruction_accounts.clone(), Err(InstructionError::from(StakeError::MergeTransientStake)), @@ -6134,11 +6286,15 @@ mod tests { } // Both fully deactivated works - try_merge(transaction_accounts, instruction_accounts, Ok(())); + try_merge( + &feature_set, + transaction_accounts, + instruction_accounts, + Ok(()), + ); } - #[test] - fn test_stake_get_minimum_delegation() { + fn do_test_stake_get_minimum_delegation(feature_set: FeatureSet) { let stake_address = Pubkey::new_unique(); let stake_account = create_default_stake_account(); let instruction_data = serialize(&StakeInstruction::GetMinimumDelegation).unwrap(); @@ -6156,7 +6312,7 @@ mod tests { transaction_accounts, instruction_accounts, None, - None, + Some(Arc::new(feature_set)), Ok(()), |first_instruction_account, invoke_context| { super::process_instruction(first_instruction_account, invoke_context)?; @@ -6191,18 +6347,13 @@ mod tests { // disabled | bad | some || Err InvalidInstructionData // disabled | good | none || Err NotEnoughAccountKeys // disabled | bad | none || Err NotEnoughAccountKeys - #[test] - fn test_stake_process_instruction_error_ordering() { + fn do_test_stake_process_instruction_error_ordering(feature_set: FeatureSet) { let rent = Rent::default(); let rent_address = sysvar::rent::id(); let rent_account = account::create_account_shared_data_for_test(&rent); let good_stake_address = Pubkey::new_unique(); - let good_stake_account = AccountSharedData::new( - u64::MAX, - std::mem::size_of::(), - &id(), - ); + let good_stake_account = AccountSharedData::new(u64::MAX, StakeState::size_of(), &id()); let good_instruction = instruction::initialize( &good_stake_address, &Authorized::auto(&good_stake_address), @@ -6279,7 +6430,7 @@ mod tests { Err(InstructionError::NotEnoughAccountKeys), ), ] { - let mut feature_set = FeatureSet::all_enabled(); + let mut feature_set = feature_set.clone(); if !is_feature_enabled { feature_set.deactivate( &feature_set::add_get_minimum_delegation_instruction_to_stake_program::id(), @@ -6300,8 +6451,8 @@ mod tests { } } - #[test] - fn test_deactivate_delinquent() { + fn do_test_deactivate_delinquent(feature_set: FeatureSet) { + let feature_set = Arc::new(feature_set); let mut sysvar_cache_override = SysvarCache::default(); let reference_vote_address = Pubkey::new_unique(); @@ -6356,7 +6507,7 @@ mod tests { vote_account: &AccountSharedData, reference_vote_account: &AccountSharedData, expected_result| { - process_instruction_with_sysvar_cache( + process_instruction_with_overrides( &serialize(&StakeInstruction::DeactivateDelinquent).unwrap(), vec![ (*stake_address, stake_account.clone()), @@ -6381,6 +6532,7 @@ mod tests { }, ], Some(&sysvar_cache_override), + Some(Arc::clone(&feature_set)), expected_result, ) }; @@ -6563,4 +6715,398 @@ mod tests { Err(StakeError::MinimumDelinquentEpochsForDeactivationNotMet.into()), ); } + + mod old_behavior { + use super::*; + + fn new_feature_set() -> FeatureSet { + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&feature_set::stake_raise_minimum_delegation_to_1_sol::id()); + feature_set + } + + #[test] + fn test_stake_process_instruction() { + do_test_stake_process_instruction(new_feature_set()); + } + #[test] + fn test_stake_process_instruction_decode_bail() { + do_test_stake_process_instruction_decode_bail(new_feature_set()); + } + #[test] + fn test_stake_checked_instructions() { + do_test_stake_checked_instructions(new_feature_set()); + } + #[test] + fn test_stake_initialize() { + do_test_stake_initialize(new_feature_set()); + } + #[test] + fn test_authorize() { + do_test_authorize(new_feature_set()); + } + #[test] + fn test_authorize_override() { + do_test_authorize_override(new_feature_set()); + } + #[test] + fn test_authorize_with_seed() { + do_test_authorize_with_seed(new_feature_set()); + } + #[test] + fn test_authorize_delegated_stake() { + do_test_authorize_delegated_stake(new_feature_set()); + } + #[test] + fn test_stake_delegate() { + do_test_stake_delegate(new_feature_set()); + } + #[test] + fn test_redelegate_consider_balance_changes() { + do_test_redelegate_consider_balance_changes(new_feature_set()); + } + #[test] + fn test_split() { + do_test_split(new_feature_set()); + } + #[test] + fn test_withdraw_stake() { + do_test_withdraw_stake(new_feature_set()); + } + #[test] + fn test_withdraw_stake_before_warmup() { + do_test_withdraw_stake_before_warmup(new_feature_set()); + } + #[test] + fn test_withdraw_lockup() { + do_test_withdraw_lockup(new_feature_set()); + } + #[test] + fn test_withdraw_rent_exempt() { + do_test_withdraw_rent_exempt(new_feature_set()); + } + #[test] + fn test_deactivate() { + do_test_deactivate(new_feature_set()); + } + #[test] + fn test_set_lockup() { + do_test_set_lockup(new_feature_set()); + } + #[test] + fn test_initialize_minimum_balance() { + do_test_initialize_minimum_balance(new_feature_set()); + } + #[test] + fn test_delegate_minimum_stake_delegation() { + do_test_delegate_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_split_minimum_stake_delegation() { + do_test_split_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_split_full_amount_minimum_stake_delegation() { + do_test_split_full_amount_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_initialized_split_destination_minimum_balance() { + do_test_initialized_split_destination_minimum_balance(new_feature_set()); + } + #[test] + fn test_staked_split_destination_minimum_balance() { + do_test_staked_split_destination_minimum_balance(new_feature_set(), &[Ok(()), Ok(())]); + } + #[test] + fn test_withdraw_minimum_stake_delegation() { + do_test_withdraw_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation() { + let mut feature_set = new_feature_set(); + // The "old" behavior relies on `validate_delegated_amount()` *not* checking if the + // stake amount meets the minimum delegation. Once the + // `stake_allow_zero_undelegated_amount` feature is activated, `the expected_result` + // parameter can be removed and consolidated. + feature_set.deactivate(&feature_set::stake_allow_zero_undelegated_amount::id()); + do_test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation( + feature_set, + Ok(()), + ); + } + #[test] + fn test_split_source_uninitialized() { + do_test_split_source_uninitialized(new_feature_set()); + } + #[test] + fn test_split_split_not_uninitialized() { + do_test_split_split_not_uninitialized(new_feature_set()); + } + #[test] + fn test_split_more_than_staked() { + do_test_split_more_than_staked(new_feature_set()); + } + #[test] + fn test_split_with_rent() { + do_test_split_with_rent(new_feature_set()); + } + #[test] + fn test_split_to_account_with_rent_exempt_reserve() { + do_test_split_to_account_with_rent_exempt_reserve(new_feature_set()); + } + #[test] + fn test_split_from_larger_sized_account() { + do_test_split_from_larger_sized_account(new_feature_set()); + } + #[test] + fn test_split_from_smaller_sized_account() { + do_test_split_from_smaller_sized_account(new_feature_set()); + } + #[test] + fn test_split_100_percent_of_source() { + do_test_split_100_percent_of_source(new_feature_set()); + } + #[test] + fn test_split_100_percent_of_source_to_account_with_lamports() { + do_test_split_100_percent_of_source_to_account_with_lamports(new_feature_set()); + } + #[test] + fn test_split_rent_exemptness() { + do_test_split_rent_exemptness(new_feature_set()); + } + #[test] + fn test_merge() { + do_test_merge(new_feature_set()); + } + #[test] + fn test_merge_self_fails() { + do_test_merge_self_fails(new_feature_set()); + } + #[test] + fn test_merge_incorrect_authorized_staker() { + do_test_merge_incorrect_authorized_staker(new_feature_set()); + } + #[test] + fn test_merge_invalid_account_data() { + do_test_merge_invalid_account_data(new_feature_set()); + } + #[test] + fn test_merge_fake_stake_source() { + do_test_merge_fake_stake_source(new_feature_set()); + } + #[test] + fn test_merge_active_stake() { + do_test_merge_active_stake(new_feature_set()); + } + #[test] + fn test_stake_get_minimum_delegation() { + do_test_stake_get_minimum_delegation(new_feature_set()); + } + #[test] + fn test_stake_process_instruction_error_ordering() { + do_test_stake_process_instruction_error_ordering(new_feature_set()); + } + #[test] + fn test_deactivate_delinquent() { + do_test_deactivate_delinquent(new_feature_set()); + } + } + + mod new_behavior { + use super::*; + + fn new_feature_set() -> FeatureSet { + FeatureSet::all_enabled() + } + + #[test] + fn test_stake_process_instruction() { + do_test_stake_process_instruction(new_feature_set()); + } + #[test] + fn test_spoofed_stake_accounts() { + do_test_spoofed_stake_accounts(new_feature_set()); + } + #[test] + fn test_stake_process_instruction_decode_bail() { + do_test_stake_process_instruction_decode_bail(new_feature_set()); + } + #[test] + fn test_stake_checked_instructions() { + do_test_stake_checked_instructions(new_feature_set()); + } + #[test] + fn test_stake_initialize() { + do_test_stake_initialize(new_feature_set()); + } + #[test] + fn test_authorize() { + do_test_authorize(new_feature_set()); + } + #[test] + fn test_authorize_override() { + do_test_authorize_override(new_feature_set()); + } + #[test] + fn test_authorize_with_seed() { + do_test_authorize_with_seed(new_feature_set()); + } + #[test] + fn test_authorize_delegated_stake() { + do_test_authorize_delegated_stake(new_feature_set()); + } + #[test] + fn test_stake_delegate() { + do_test_stake_delegate(new_feature_set()); + } + #[test] + fn test_redelegate_consider_balance_changes() { + do_test_redelegate_consider_balance_changes(new_feature_set()); + } + #[test] + fn test_split() { + do_test_split(new_feature_set()); + } + #[test] + fn test_withdraw_stake() { + do_test_withdraw_stake(new_feature_set()); + } + #[test] + fn test_withdraw_stake_before_warmup() { + do_test_withdraw_stake_before_warmup(new_feature_set()); + } + #[test] + fn test_withdraw_lockup() { + do_test_withdraw_lockup(new_feature_set()); + } + #[test] + fn test_withdraw_rent_exempt() { + do_test_withdraw_rent_exempt(new_feature_set()); + } + #[test] + fn test_deactivate() { + do_test_deactivate(new_feature_set()); + } + #[test] + fn test_set_lockup() { + do_test_set_lockup(new_feature_set()); + } + #[test] + fn test_initialize_minimum_balance() { + do_test_initialize_minimum_balance(new_feature_set()); + } + #[test] + fn test_delegate_minimum_stake_delegation() { + do_test_delegate_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_split_minimum_stake_delegation() { + do_test_split_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_split_full_amount_minimum_stake_delegation() { + do_test_split_full_amount_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_initialized_split_destination_minimum_balance() { + do_test_initialized_split_destination_minimum_balance(new_feature_set()); + } + #[test] + fn test_staked_split_destination_minimum_balance() { + do_test_staked_split_destination_minimum_balance( + new_feature_set(), + &[ + Err(InstructionError::InsufficientFunds), + Err(InstructionError::InsufficientFunds), + ], + ); + } + #[test] + fn test_withdraw_minimum_stake_delegation() { + do_test_withdraw_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation() { + do_test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation( + new_feature_set(), + Err(StakeError::InsufficientStake.into()), + ); + } + #[test] + fn test_split_source_uninitialized() { + do_test_split_source_uninitialized(new_feature_set()); + } + #[test] + fn test_split_split_not_uninitialized() { + do_test_split_split_not_uninitialized(new_feature_set()); + } + #[test] + fn test_split_more_than_staked() { + do_test_split_more_than_staked(new_feature_set()); + } + #[test] + fn test_split_with_rent() { + do_test_split_with_rent(new_feature_set()); + } + #[test] + fn test_split_to_account_with_rent_exempt_reserve() { + do_test_split_to_account_with_rent_exempt_reserve(new_feature_set()); + } + #[test] + fn test_split_from_larger_sized_account() { + do_test_split_from_larger_sized_account(new_feature_set()); + } + #[test] + fn test_split_from_smaller_sized_account() { + do_test_split_from_smaller_sized_account(new_feature_set()); + } + #[test] + fn test_split_100_percent_of_source() { + do_test_split_100_percent_of_source(new_feature_set()); + } + #[test] + fn test_split_100_percent_of_source_to_account_with_lamports() { + do_test_split_100_percent_of_source_to_account_with_lamports(new_feature_set()); + } + #[test] + fn test_split_rent_exemptness() { + do_test_split_rent_exemptness(new_feature_set()); + } + #[test] + fn test_merge() { + do_test_merge(new_feature_set()); + } + #[test] + fn test_merge_self_fails() { + do_test_merge_self_fails(new_feature_set()); + } + #[test] + fn test_merge_incorrect_authorized_staker() { + do_test_merge_incorrect_authorized_staker(new_feature_set()); + } + #[test] + fn test_merge_invalid_account_data() { + do_test_merge_invalid_account_data(new_feature_set()); + } + #[test] + fn test_merge_fake_stake_source() { + do_test_merge_fake_stake_source(new_feature_set()); + } + #[test] + fn test_merge_active_stake() { + do_test_merge_active_stake(new_feature_set()); + } + #[test] + fn test_stake_get_minimum_delegation() { + do_test_stake_get_minimum_delegation(new_feature_set()); + } + #[test] + fn test_stake_process_instruction_error_ordering() { + do_test_stake_process_instruction_error_ordering(new_feature_set()); + } + #[test] + fn test_deactivate_delinquent() { + do_test_deactivate_delinquent(new_feature_set()); + } + } } diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 936110ff4..f1cdfa8c7 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -15,7 +15,7 @@ use { account_utils::StateMut, clock::{Clock, Epoch}, feature_set::{ - stake_allow_zero_undelegated_amount, stake_merge_with_unmatched_credits_observed, + self, stake_allow_zero_undelegated_amount, stake_merge_with_unmatched_credits_observed, stake_split_uses_rent_sysvar, FeatureSet, }, instruction::{checked_add, InstructionError}, @@ -1028,12 +1028,10 @@ fn validate_delegated_amount( .get_lamports() .saturating_sub(meta.rent_exempt_reserve); // can't stake the rent - // Previously, `initialize` checked that the stake account balance met - // the minimum delegation amount. - // With the `stake_allow_zero_undelegated_amount` feature, stake accounts - // may be initialized with a lower balance, so check the minimum in this - // function, on delegation. - if feature_set.is_active(&stake_allow_zero_undelegated_amount::id()) + // Stake accounts may be initialized with a stake amount below the minimum delegation so check + // that the minimum is met before delegation. + if (feature_set.is_active(&stake_allow_zero_undelegated_amount::id()) + || feature_set.is_active(&feature_set::stake_raise_minimum_delegation_to_1_sol::id())) && stake_amount < crate::get_minimum_delegation(feature_set) { return Err(StakeError::InsufficientStake.into()); @@ -2166,7 +2164,6 @@ mod tests { #[test] fn test_stop_activating_after_deactivation() { - solana_logger::setup(); let stake = Delegation { stake: 1_000, activation_epoch: 0, diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 20af010c1..3f20b8105 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -392,6 +392,11 @@ pub mod require_static_program_ids_in_transaction { solana_sdk::declare_id!("8FdwgyHFEjhAdjWfV2vfqk7wA1g9X3fQpKH7SBpEv3kC"); } +pub mod stake_raise_minimum_delegation_to_1_sol { + // This is a feature-proposal *feature id*. The feature keypair address is `3YHAo6wWw5rDbQxb59BmJkQ3XwVhX3m8tdBVbtxnJmma`. + solana_sdk::declare_id!("4xmyBuR2VCXzy9H6qYpH9ckfgnTuMDQFPFBfTs4eBCY1"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -483,6 +488,7 @@ lazy_static! { (default_units_per_instruction::id(), "Default max tx-wide compute units calculated per instruction"), (stake_allow_zero_undelegated_amount::id(), "Allow zero-lamport undelegated amount for initialized stakes #24670"), (require_static_program_ids_in_transaction::id(), "require static program ids in versioned transactions"), + (stake_raise_minimum_delegation_to_1_sol::id(), "Raise minimum stake delegation to 1.0 SOL #24357"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter()