diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 7add8540df..5e3b450181 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -491,11 +491,16 @@ pub fn process_instruction( mod tests { use { super::*, - crate::stake_state::{Meta, StakeState}, + crate::stake_state::{ + authorized_from, create_stake_history_from_delegations, from, stake_from, Delegation, + Meta, Stake, StakeState, + }, bincode::serialize, solana_program_runtime::invoke_context::mock_process_instruction, solana_sdk::{ - account::{self, AccountSharedData}, + account::{self, AccountSharedData, ReadableAccount, WritableAccount}, + account_utils::StateMut, + clock::{Epoch, UnixTimestamp}, instruction::{AccountMeta, Instruction}, pubkey::Pubkey, rent::Rent, @@ -503,9 +508,12 @@ mod tests { config as stake_config, instruction::{self, LockupArgs}, state::{Authorized, Lockup, StakeAuthorize}, + MINIMUM_STAKE_DELEGATION, }, + system_program, sysvar::{self, stake_history::StakeHistory}, }, + solana_vote_program::vote_state::{self, VoteState, VoteStateVersions}, std::{collections::HashSet, str::FromStr}, }; @@ -599,6 +607,19 @@ mod tests { ) } + fn just_stake(meta: Meta, stake: u64) -> StakeState { + StakeState::Stake( + meta, + Stake { + delegation: Delegation { + stake, + ..Delegation::default() + }, + ..Stake::default() + }, + ) + } + #[test] fn test_stake_process_instruction() { process_instruction_as_one_arg( @@ -1337,4 +1358,2230 @@ mod tests { Ok(()), ); } + + #[test] + fn test_stake_initialize() { + let stake_lamports = 42; + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = + AccountSharedData::new(stake_lamports, std::mem::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()), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&Rent::free()), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::rent::id(), + is_signer: false, + is_writable: false, + }, + ]; + + // should pass + let accounts = process_instruction( + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + // check that we see what we expect + assert_eq!( + from(&accounts[0]).unwrap(), + StakeState::Initialized(Meta { + lockup, + ..Meta { + authorized: Authorized::auto(&stake_address), + ..Meta::default() + } + }), + ); + + // 2nd time fails, can't move it from anything other than uninit->init + transaction_accounts[0] = (stake_address, accounts[0].clone()); + process_instruction( + &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] = ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&Rent { + lamports_per_byte_year: 42, + ..Rent::free() + }), + ); + process_instruction( + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // incorrect account sizes + let stake_account = + AccountSharedData::new(stake_lamports, std::mem::size_of::() + 1, &id()); + transaction_accounts[0] = (stake_address, stake_account); + process_instruction( + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InvalidAccountData), + ); + + let stake_account = + AccountSharedData::new(stake_lamports, std::mem::size_of::() - 1, &id()); + transaction_accounts[0] = (stake_address, stake_account); + process_instruction( + &instruction_data, + transaction_accounts, + instruction_accounts, + Err(InstructionError::InvalidAccountData), + ); + } + + #[test] + fn test_authorize() { + 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, + &StakeState::default(), + std::mem::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()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + ]; + + // should fail, uninit + process_instruction( + &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, + &StakeState::Initialized(Meta::auto(&stake_address)), + std::mem::size_of::(), + &id(), + ) + .unwrap(); + transaction_accounts[0] = (stake_address, stake_account); + let accounts = process_instruction( + &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( + &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 StakeState::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( + &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.push(AccountMeta { + pubkey: authority_address, + is_signer: true, + is_writable: false, + }); + let accounts = process_instruction( + &serialize(&StakeInstruction::Authorize( + authority_address_2, + StakeAuthorize::Staker, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + if let StakeState::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: false, + }, + AccountMeta { + pubkey: to_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authority_address, + is_signer: true, + is_writable: false, + }, + ]; + let accounts = process_instruction( + &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(from(&accounts[0]).unwrap(), StakeState::Uninitialized); + + // Test that withdrawal to account fails without authorized withdrawer + instruction_accounts[4].is_signer = false; + process_instruction( + &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::MissingRequiredSignature), + ); + } + + #[test] + fn test_authorize_override() { + 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, + &StakeState::Initialized(Meta::auto(&stake_address)), + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (authority_address, AccountSharedData::default()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + ]; + + // Authorize a staker pubkey and move the withdrawer key into cold storage. + let accounts = process_instruction( + &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.push(AccountMeta { + pubkey: authority_address, + is_signer: true, + is_writable: false, + }); + let accounts = process_instruction( + &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( + &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.pop(); + let accounts = process_instruction( + &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.push(AccountMeta { + pubkey: mallory_address, + is_signer: true, + is_writable: false, + }); + process_instruction( + &serialize(&StakeInstruction::Authorize( + authority_address, + StakeAuthorize::Withdrawer, + )) + .unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::MissingRequiredSignature), + ); + } + + #[test] + fn test_authorize_with_seed() { + 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, + &StakeState::Initialized(Meta::auto(&stake_address)), + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (authority_base_address, AccountSharedData::default()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: authority_base_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + ]; + + // Wrong seed + process_instruction( + &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( + &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( + &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( + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // No longer withdraw authority + process_instruction( + &instruction_data, + transaction_accounts, + instruction_accounts, + Err(InstructionError::MissingRequiredSignature), + ); + } + + #[test] + fn test_authorize_delegated_stake() { + let authority_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, + &StakeState::Initialized(Meta::auto(&stake_address)), + std::mem::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(); + 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()), + ), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ]; + + // delegate stake + let accounts = process_instruction( + &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( + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // authorize + let accounts = process_instruction( + &serialize(&StakeInstruction::Authorize( + authority_address, + StakeAuthorize::Staker, + )) + .unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + 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( + &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( + &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( + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts, + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authority_address, + is_signer: true, + is_writable: false, + }, + ], + Ok(()), + ); + } + + #[test] + fn test_stake_delegate() { + let mut vote_state = VoteState::default(); + for i in 0..1000 { + vote_state.process_slot_vote_unchecked(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 stake_lamports = 42; + let stake_address = solana_sdk::pubkey::new_rand(); + let mut stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeState::Initialized(Meta { + authorized: Authorized { + staker: stake_address, + withdrawer: stake_address, + }, + ..Meta::default() + }), + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let mut clock = Clock { + epoch: 1, + ..Clock::default() + }; + let mut transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (vote_address, vote_account), + (vote_address_2, vote_account_2.clone()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ]; + + // should fail, unsigned stake account + instruction_accounts[0].is_signer = false; + process_instruction( + &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( + &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] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + process_instruction( + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(StakeError::TooSoonToRedelegate.into()), + ); + + // deactivate + let accounts = process_instruction( + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::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( + &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( + &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( + &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] = ( + sysvar::clock::id(), + account::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( + &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( + &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 = StakeState::RewardsPool; + stake_account.set_state(&stake_state).unwrap(); + transaction_accounts[0] = (stake_address, stake_account); + process_instruction( + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts, + instruction_accounts, + Err(solana_sdk::instruction::InstructionError::IncorrectProgramId), + ); + } + + #[test] + fn test_redelegate_consider_balance_changes() { + let mut clock = Clock::default(); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::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, + &StakeState::Initialized(Meta { + rent_exempt_reserve, + ..Meta::auto(&authority_address) + }), + std::mem::size_of::(), + &id(), + ) + .unwrap(); + 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()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ]; + let delegate_instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authority_address, + is_signer: true, + is_writable: false, + }, + ]; + let deactivate_instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authority_address, + is_signer: true, + is_writable: false, + }, + ]; + + let accounts = process_instruction( + &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] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + let accounts = process_instruction( + &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] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + let withdraw_lamports = initial_lamports / 2; + let accounts = process_instruction( + &serialize(&StakeInstruction::Withdraw(withdraw_lamports)).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: 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] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + let accounts = process_instruction( + &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] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + let accounts = process_instruction( + &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] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + let accounts = process_instruction( + &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] + fn test_split() { + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_lamports = 42; + let split_to_address = solana_sdk::pubkey::new_rand(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let mut transaction_accounts = vec![ + (stake_address, AccountSharedData::default()), + (split_to_address, split_to_account), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + for state in [ + StakeState::Initialized(Meta::auto(&stake_address)), + just_stake(Meta::auto(&stake_address), stake_lamports), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + transaction_accounts[0] = (stake_address, stake_account); + + // should fail, split more than available + process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // should pass + let accounts = process_instruction( + &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 + ); + + assert_eq!(from(&accounts[0]).unwrap(), from(&accounts[1]).unwrap()); + match state { + StakeState::Initialized(_meta) => { + assert_eq!(from(&accounts[0]).unwrap(), state); + } + StakeState::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, + &StakeState::Uninitialized, + std::mem::size_of::(), + &solana_sdk::pubkey::new_rand(), + ) + .unwrap(); + transaction_accounts[1] = (split_to_address, split_to_account); + process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::IncorrectProgramId), + ); + } + + #[test] + fn test_withdraw_stake() { + 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 stake_lamports = 42; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeState::Uninitialized, + std::mem::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 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()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&Rent::free()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ]; + + // should fail, no signer + instruction_accounts[4].is_signer = false; + process_instruction( + &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( + &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(), StakeState::Uninitialized); + + // initialize stake + let lockup = Lockup { + unix_timestamp: 0, + epoch: 0, + custodian: custodian_address, + }; + let accounts = process_instruction( + &serialize(&StakeInstruction::Initialize( + Authorized::auto(&stake_address), + lockup, + )) + .unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::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( + &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) + let accounts = process_instruction( + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ], + 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( + &serialize(&StakeInstruction::Withdraw(10)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // withdrawal of rewards fails if not in excess of stake + process_instruction( + &serialize(&StakeInstruction::Withdraw(11)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // deactivate the stake before withdrawal + let accounts = process_instruction( + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::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] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + + // Try to withdraw more than what's available + process_instruction( + &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( + &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(), StakeState::Uninitialized); + + // overflow + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let stake_account = AccountSharedData::new_data_with_space( + 1_000_000_000, + &StakeState::Initialized(Meta { + rent_exempt_reserve, + authorized: Authorized { + staker: authority_address, + withdrawer: authority_address, + }, + lockup: Lockup::default(), + }), + std::mem::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( + &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, + &StakeState::RewardsPool, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + transaction_accounts[0] = (stake_address, stake_account); + process_instruction( + &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::InvalidAccountData), + ); + } + + #[test] + fn test_withdraw_stake_before_warmup() { + let recipient_address = solana_sdk::pubkey::new_rand(); + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_lamports = 42; + let total_lamports = 100; + let stake_account = AccountSharedData::new_data_with_space( + total_lamports, + &StakeState::Initialized(Meta::auto(&stake_address)), + std::mem::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() + }; + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (vote_address, vote_account), + (recipient_address, AccountSharedData::default()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ]; + + // Stake some lamports (available lamports for withdrawals will reduce to zero) + let accounts = process_instruction( + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ], + 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], + ); + transaction_accounts[4] = ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&stake_history), + ); + clock.epoch = 0; + transaction_accounts[3] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + process_instruction( + &serialize(&StakeInstruction::Withdraw( + total_lamports - stake_lamports + 1, + )) + .unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::InsufficientFunds), + ); + } + + #[test] + fn test_withdraw_lockup() { + 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, + &StakeState::Initialized(meta), + std::mem::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()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ]; + + // should fail, lockup is still in force + process_instruction( + &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( + &serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(from(&accounts[0]).unwrap(), StakeState::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, + &StakeState::Initialized(meta), + std::mem::size_of::(), + &id(), + ) + .unwrap(); + transaction_accounts[0] = (stake_address, stake_account_self_as_custodian); + let accounts = process_instruction( + &serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(from(&accounts[0]).unwrap(), StakeState::Uninitialized); + transaction_accounts[0] = (stake_address, stake_account); + + // should pass, lockup has expired + instruction_accounts.pop(); + clock.epoch += 1; + transaction_accounts[3] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + let accounts = process_instruction( + &serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(), + transaction_accounts, + instruction_accounts, + Ok(()), + ); + assert_eq!(from(&accounts[0]).unwrap(), StakeState::Uninitialized); + } + + #[test] + fn test_withdraw_rent_exempt() { + 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(std::mem::size_of::()); + let stake_lamports = 7 * MINIMUM_STAKE_DELEGATION; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports + rent_exempt_reserve, + &StakeState::Initialized(Meta { + rent_exempt_reserve, + ..Meta::auto(&stake_address) + }), + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (recipient_address, AccountSharedData::default()), + (custodian_address, AccountSharedData::default()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ]; + + // should pass, withdrawing account down to minimum balance + process_instruction( + &serialize(&StakeInstruction::Withdraw( + stake_lamports - MINIMUM_STAKE_DELEGATION, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // should fail, withdrawing account down to only rent-exempt reserve + process_instruction( + &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // should fail, withdrawal that would leave less than rent-exempt reserve + process_instruction( + &serialize(&StakeInstruction::Withdraw( + stake_lamports + MINIMUM_STAKE_DELEGATION, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // should pass, withdrawal of complete account + process_instruction( + &serialize(&StakeInstruction::Withdraw( + stake_lamports + rent_exempt_reserve, + )) + .unwrap(), + transaction_accounts, + instruction_accounts, + Ok(()), + ); + } + + #[test] + fn test_deactivate() { + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_lamports = 42; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeState::Initialized(Meta::auto(&stake_address)), + std::mem::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 transaction_accounts = vec![ + (stake_address, stake_account), + (vote_address, vote_account), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + ]; + + // should fail, not signed + instruction_accounts[0].is_signer = false; + process_instruction( + &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( + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InvalidAccountData), + ); + + // Staking + let accounts = process_instruction( + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // should pass + let accounts = process_instruction( + &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( + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts, + instruction_accounts, + Err(StakeError::AlreadyDeactivated.into()), + ); + } + + #[test] + fn test_set_lockup() { + 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 stake_lamports = 42; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeState::Uninitialized, + std::mem::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(); + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (vote_address, vote_account), + (authorized_address, AccountSharedData::default()), + (custodian_address, AccountSharedData::default()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&Rent::free()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: custodian_address, + is_signer: true, + is_writable: false, + }, + ]; + + // should fail, wrong state + process_instruction( + &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( + &serialize(&StakeInstruction::Initialize( + Authorized::auto(&stake_address), + lockup, + )) + .unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::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( + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::MissingRequiredSignature), + ); + instruction_accounts[2].is_signer = true; + + // should pass + process_instruction( + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // Staking + let accounts = process_instruction( + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // should fail, not signed + instruction_accounts[2].is_signer = false; + process_instruction( + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::MissingRequiredSignature), + ); + instruction_accounts[2].is_signer = true; + + // should pass + process_instruction( + &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( + &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( + &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[3] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + + // should fail, custodian cannot change it + process_instruction( + &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( + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // Change authorized withdrawer + let accounts = process_instruction( + &serialize(&StakeInstruction::Authorize( + authorized_address, + StakeAuthorize::Withdrawer, + )) + .unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + 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( + &instruction_data, + transaction_accounts, + instruction_accounts, + Err(InstructionError::MissingRequiredSignature), + ); + } } diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 6e6dc08b0e..e514f9f7a0 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -1191,6 +1191,36 @@ where }) } +// utility function, used by tests +pub fn create_stake_history_from_delegations( + bootstrap: Option, + epochs: std::ops::Range, + delegations: &[Delegation], +) -> StakeHistory { + let mut stake_history = StakeHistory::default(); + + let bootstrap_delegation = if let Some(bootstrap) = bootstrap { + vec![Delegation { + activation_epoch: std::u64::MAX, + stake: bootstrap, + ..Delegation::default() + }] + } else { + vec![] + }; + + for epoch in epochs { + let entry = new_stake_history_entry( + epoch, + delegations.iter().chain(bootstrap_delegation.iter()), + Some(&stake_history), + ); + stake_history.add(epoch, entry); + } + + stake_history +} + // genesis investor accounts pub fn create_lockup_stake_account( authorized: &Authorized, @@ -1300,7 +1330,6 @@ mod tests { solana_program_runtime::invoke_context::InvokeContext, solana_sdk::{ account::{AccountSharedData, WritableAccount}, - clock::UnixTimestamp, native_token, pubkey::Pubkey, stake::MINIMUM_STAKE_DELEGATION, @@ -1474,263 +1503,6 @@ mod tests { .is_bootstrap()); } - #[test] - fn test_stake_delegate() { - let mut clock = Clock { - epoch: 1, - ..Clock::default() - }; - - let vote_pubkey = solana_sdk::pubkey::new_rand(); - let vote_pubkey_2 = solana_sdk::pubkey::new_rand(); - - let mut vote_state = VoteState::default(); - for i in 0..1000 { - vote_state.process_slot_vote_unchecked(i); - } - let mut vote_state_2 = VoteState::default(); - for i in 0..1000 { - vote_state_2.process_slot_vote_unchecked(i); - } - - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_account_2 = RefCell::new(vote_state::create_account( - &vote_pubkey_2, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - let vote_keyed_account_2 = KeyedAccount::new(&vote_pubkey_2, false, &vote_account_2); - - let vote_state_credits = vote_state.credits(); - vote_keyed_account - .set_state(&VoteStateVersions::new_current(vote_state)) - .unwrap(); - let vote_state_credits_2 = vote_state_2.credits(); - vote_keyed_account_2 - .set_state(&VoteStateVersions::new_current(vote_state_2)) - .unwrap(); - - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(Meta { - authorized: Authorized { - staker: stake_pubkey, - withdrawer: stake_pubkey, - }, - ..Meta::default() - }), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - // unsigned keyed account - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - - let mut signers = HashSet::default(); - assert_eq!( - stake_keyed_account.delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - // signed keyed account - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - signers.insert(stake_pubkey); - assert!(stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ) - .is_ok()); - - // verify that delegate() looks right, compare against hand-rolled - let stake = stake_from(&stake_keyed_account.account.borrow()).unwrap(); - assert_eq!( - stake, - Stake { - delegation: Delegation { - voter_pubkey: vote_pubkey, - stake: stake_lamports, - activation_epoch: clock.epoch, - deactivation_epoch: std::u64::MAX, - ..Delegation::default() - }, - credits_observed: vote_state_credits, - } - ); - - clock.epoch += 1; - - // verify that delegate fails as stake is active and not deactivating - assert_eq!( - stake_keyed_account.delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ), - Err(StakeError::TooSoonToRedelegate.into()) - ); - - // deactivate - stake_keyed_account.deactivate(&clock, &signers).unwrap(); - - // verify that delegate to a different vote account fails - // during deactivation - assert_eq!( - stake_keyed_account.delegate( - &vote_keyed_account_2, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ), - Err(StakeError::TooSoonToRedelegate.into()) - ); - - // verify that delegate succeeds to same vote account - // when stake is deactivating - stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ) - .unwrap(); - - // verify that deactivation has been cleared - let stake = stake_from(&stake_keyed_account.account.borrow()).unwrap(); - assert_eq!(stake.delegation.deactivation_epoch, std::u64::MAX); - - // verify that delegate to a different vote account fails - // if stake is still active - assert_eq!( - stake_keyed_account.delegate( - &vote_keyed_account_2, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ), - Err(StakeError::TooSoonToRedelegate.into()) - ); - - // deactivate, so we can re-delegate - stake_keyed_account.deactivate(&clock, &signers).unwrap(); - - // without stake history, cool down is instantaneous - clock.epoch += 1; - - // verify that delegate can be called to new vote account, 2nd is redelegate - assert!(stake_keyed_account - .delegate( - &vote_keyed_account_2, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ) - .is_ok()); - - // signed but faked vote account - let faked_vote_account = vote_account_2.clone(); - faked_vote_account - .borrow_mut() - .set_owner(solana_sdk::pubkey::new_rand()); - let faked_vote_keyed_account = - KeyedAccount::new(&vote_pubkey_2, false, &faked_vote_account); - assert_eq!( - stake_keyed_account.delegate( - &faked_vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ), - Err(solana_sdk::instruction::InstructionError::IncorrectProgramId) - ); - - // verify that delegate() looks right, compare against hand-rolled - let stake = stake_from(&stake_keyed_account.account.borrow()).unwrap(); - assert_eq!( - stake, - Stake { - delegation: Delegation { - voter_pubkey: vote_pubkey_2, - stake: stake_lamports, - activation_epoch: clock.epoch, - deactivation_epoch: std::u64::MAX, - ..Delegation::default() - }, - credits_observed: vote_state_credits_2, - } - ); - - // verify that non-stakes fail delegate() - let stake_state = StakeState::RewardsPool; - - stake_keyed_account.set_state(&stake_state).unwrap(); - assert!(stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ) - .is_err()); - } - - fn create_stake_history_from_delegations( - bootstrap: Option, - epochs: std::ops::Range, - delegations: &[Delegation], - ) -> StakeHistory { - let mut stake_history = StakeHistory::default(); - - let bootstrap_delegation = if let Some(bootstrap) = bootstrap { - vec![Delegation { - activation_epoch: std::u64::MAX, - stake: bootstrap, - ..Delegation::default() - }] - } else { - vec![] - }; - - for epoch in epochs { - let entry = new_stake_history_entry( - epoch, - delegations.iter().chain(bootstrap_delegation.iter()), - Some(&stake_history), - ); - stake_history.add(epoch, entry); - } - - stake_history - } - #[test] fn test_stake_activating_and_deactivating() { let stake = Delegation { @@ -2271,951 +2043,6 @@ mod tests { } } - #[test] - fn test_stake_initialize() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = - AccountSharedData::new_ref(stake_lamports, std::mem::size_of::(), &id()); - - // unsigned keyed account - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - let custodian = solana_sdk::pubkey::new_rand(); - - // not enough balance for rent... - assert_eq!( - stake_keyed_account.initialize( - &Authorized::default(), - &Lockup::default(), - &Rent { - lamports_per_byte_year: 42, - ..Rent::free() - }, - ), - Err(InstructionError::InsufficientFunds) - ); - - // this one works, as is uninit - assert_eq!( - stake_keyed_account.initialize( - &Authorized::auto(&stake_pubkey), - &Lockup { - epoch: 1, - unix_timestamp: 0, - custodian - }, - &Rent::free(), - ), - Ok(()) - ); - // check that we see what we expect - assert_eq!( - from(&stake_keyed_account.account.borrow()).unwrap(), - StakeState::Initialized(Meta { - lockup: Lockup { - unix_timestamp: 0, - epoch: 1, - custodian - }, - ..Meta { - authorized: Authorized::auto(&stake_pubkey), - ..Meta::default() - } - }) - ); - - // 2nd time fails, can't move it from anything other than uninit->init - assert_eq!( - stake_keyed_account.initialize( - &Authorized::default(), - &Lockup::default(), - &Rent::free() - ), - Err(InstructionError::InvalidAccountData) - ); - } - - #[test] - fn test_initialize_incorrect_account_sizes() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = AccountSharedData::new_ref( - stake_lamports, - std::mem::size_of::() + 1, - &id(), - ); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - - assert_eq!( - stake_keyed_account.initialize( - &Authorized::default(), - &Lockup::default(), - &Rent { - lamports_per_byte_year: 42, - ..Rent::free() - }, - ), - Err(InstructionError::InvalidAccountData) - ); - - let stake_account = AccountSharedData::new_ref( - stake_lamports, - std::mem::size_of::() - 1, - &id(), - ); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - - assert_eq!( - stake_keyed_account.initialize( - &Authorized::default(), - &Lockup::default(), - &Rent { - lamports_per_byte_year: 42, - ..Rent::free() - }, - ), - Err(InstructionError::InvalidAccountData) - ); - } - - #[test] - fn test_deactivate() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(Meta::auto(&stake_pubkey)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let clock = Clock { - epoch: 1, - ..Clock::default() - }; - - // signed keyed account but not staked yet - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let signers = vec![stake_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.deactivate(&clock, &signers), - Err(InstructionError::InvalidAccountData) - ); - - // Staking - let vote_pubkey = solana_sdk::pubkey::new_rand(); - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - vote_keyed_account - .set_state(&VoteStateVersions::new_current(VoteState::default())) - .unwrap(); - assert_eq!( - stake_keyed_account.delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ), - Ok(()) - ); - - // no signers fails - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - assert_eq!( - stake_keyed_account.deactivate(&clock, &HashSet::default()), - Err(InstructionError::MissingRequiredSignature) - ); - - // Deactivate after staking - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - assert_eq!(stake_keyed_account.deactivate(&clock, &signers), Ok(())); - - // verify that deactivate() only works once - assert_eq!( - stake_keyed_account.deactivate(&clock, &signers), - Err(StakeError::AlreadyDeactivated.into()) - ); - } - - #[test] - fn test_set_lockup() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let clock = Clock::default(); - - // wrong state, should fail - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - assert_eq!( - stake_keyed_account.set_lockup(&LockupArgs::default(), &HashSet::default(), &clock), - Err(InstructionError::InvalidAccountData) - ); - - // initialize the stake - let custodian = solana_sdk::pubkey::new_rand(); - stake_keyed_account - .initialize( - &Authorized::auto(&stake_pubkey), - &Lockup { - unix_timestamp: 1, - epoch: 1, - custodian, - }, - &Rent::free(), - ) - .unwrap(); - - assert_eq!( - stake_keyed_account.set_lockup(&LockupArgs::default(), &HashSet::default(), &clock), - Err(InstructionError::MissingRequiredSignature) - ); - - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs { - unix_timestamp: Some(1), - epoch: Some(1), - custodian: Some(custodian), - }, - &vec![custodian].into_iter().collect(), - &clock - ), - Ok(()) - ); - - // delegate stake - let vote_pubkey = solana_sdk::pubkey::new_rand(); - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - vote_keyed_account - .set_state(&VoteStateVersions::new_current(VoteState::default())) - .unwrap(); - - stake_keyed_account - .delegate( - &vote_keyed_account, - &Clock::default(), - &StakeHistory::default(), - &Config::default(), - &vec![stake_pubkey].into_iter().collect(), - ) - .unwrap(); - - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs { - unix_timestamp: Some(1), - epoch: Some(1), - custodian: Some(custodian), - }, - &HashSet::default(), - &clock - ), - Err(InstructionError::MissingRequiredSignature) - ); - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs { - unix_timestamp: Some(1), - epoch: Some(1), - custodian: Some(custodian), - }, - &vec![custodian].into_iter().collect(), - &clock - ), - Ok(()) - ); - } - - #[test] - fn test_optional_lockup_for_stake_program() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - - let custodian = solana_sdk::pubkey::new_rand(); - stake_keyed_account - .initialize( - &Authorized::auto(&stake_pubkey), - &Lockup { - unix_timestamp: 1, - epoch: 1, - custodian, - }, - &Rent::free(), - ) - .unwrap(); - - // Lockup in force: authorized withdrawer cannot change it - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs { - unix_timestamp: Some(2), - epoch: None, - custodian: None - }, - &vec![stake_pubkey].into_iter().collect(), - &Clock::default() - ), - Err(InstructionError::MissingRequiredSignature) - ); - - // Lockup in force: custodian can change it - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs { - unix_timestamp: Some(2), - epoch: None, - custodian: None - }, - &vec![custodian].into_iter().collect(), - &Clock::default() - ), - Ok(()) - ); - - // Lockup expired: custodian cannot change it - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs::default(), - &vec![custodian].into_iter().collect(), - &Clock { - unix_timestamp: UnixTimestamp::MAX, - epoch: Epoch::MAX, - ..Clock::default() - } - ), - Err(InstructionError::MissingRequiredSignature) - ); - - // Lockup expired: authorized withdrawer can change it - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs::default(), - &vec![stake_pubkey].into_iter().collect(), - &Clock { - unix_timestamp: UnixTimestamp::MAX, - epoch: Epoch::MAX, - ..Clock::default() - } - ), - Ok(()) - ); - - // Change authorized withdrawer - let new_withdraw_authority = solana_sdk::pubkey::new_rand(); - assert_eq!( - stake_keyed_account.authorize( - &vec![stake_pubkey].into_iter().collect(), - &new_withdraw_authority, - StakeAuthorize::Withdrawer, - false, - &Clock::default(), - None - ), - Ok(()) - ); - - // Previous authorized withdrawer cannot change the lockup anymore - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs::default(), - &vec![stake_pubkey].into_iter().collect(), - &Clock { - unix_timestamp: UnixTimestamp::MAX, - epoch: Epoch::MAX, - ..Clock::default() - } - ), - Err(InstructionError::MissingRequiredSignature) - ); - } - - #[test] - fn test_withdraw_stake() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let mut clock = Clock::default(); - - let to = solana_sdk::pubkey::new_rand(); - let to_account = AccountSharedData::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - - // no signers, should fail - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &to_keyed_account, // unsigned account as withdraw authority - None, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - // signed keyed account and uninitialized should work - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Ok(()) - ); - assert_eq!(stake_account.borrow().lamports(), 0); - assert_eq!(stake_keyed_account.state(), Ok(StakeState::Uninitialized)); - - // reset balance - stake_account.borrow_mut().set_lamports(stake_lamports); - - // lockup - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let custodian = solana_sdk::pubkey::new_rand(); - stake_keyed_account - .initialize( - &Authorized::auto(&stake_pubkey), - &Lockup { - unix_timestamp: 0, - epoch: 0, - custodian, - }, - &Rent::free(), - ) - .unwrap(); - - // signed keyed account and locked up, more than available should fail - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports + 1, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(InstructionError::InsufficientFunds) - ); - - // Stake some lamports (available lamports for withdrawals will reduce to zero) - let vote_pubkey = solana_sdk::pubkey::new_rand(); - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - vote_keyed_account - .set_state(&VoteStateVersions::new_current(VoteState::default())) - .unwrap(); - let signers = vec![stake_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ), - Ok(()) - ); - - // simulate rewards - stake_account.borrow_mut().checked_add_lamports(10).unwrap(); - // withdrawal before deactivate works for rewards amount - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - 10, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Ok(()) - ); - - // simulate rewards - stake_account.borrow_mut().checked_add_lamports(10).unwrap(); - // withdrawal of rewards fails if not in excess of stake - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - 10 + 1, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(InstructionError::InsufficientFunds) - ); - - // deactivate the stake before withdrawal - assert_eq!(stake_keyed_account.deactivate(&clock, &signers), Ok(())); - // simulate time passing - clock.epoch += 100; - - // Try to withdraw more than what's available - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports + 10 + 1, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(InstructionError::InsufficientFunds) - ); - - // Try to withdraw all lamports - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports + 10, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Ok(()) - ); - assert_eq!(stake_account.borrow().lamports(), 0); - assert_eq!(stake_keyed_account.state(), Ok(StakeState::Uninitialized)); - - // overflow - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let authority_pubkey = Pubkey::new_unique(); - let stake_pubkey = Pubkey::new_unique(); - let stake_account = AccountSharedData::new_ref_data_with_space( - 1_000_000_000, - &StakeState::Initialized(Meta { - rent_exempt_reserve, - authorized: Authorized { - staker: authority_pubkey, - withdrawer: authority_pubkey, - }, - lockup: Lockup::default(), - }), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let stake2_pubkey = Pubkey::new_unique(); - let stake2_account = AccountSharedData::new_ref_data_with_space( - 1_000_000_000, - &StakeState::Initialized(Meta { - rent_exempt_reserve, - authorized: Authorized { - staker: authority_pubkey, - withdrawer: authority_pubkey, - }, - lockup: Lockup::default(), - }), - std::mem::size_of::(), - &id(), - ) - .expect("stake2_account"); - - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let stake2_keyed_account = KeyedAccount::new(&stake2_pubkey, false, &stake2_account); - let authority_account = AccountSharedData::new_ref(42, 0, &system_program::id()); - let authority_keyed_account = - KeyedAccount::new(&authority_pubkey, true, &authority_account); - - assert_eq!( - stake_keyed_account.withdraw( - u64::MAX - 10, - &stake2_keyed_account, - &clock, - &StakeHistory::default(), - &authority_keyed_account, - None, - ), - Err(InstructionError::InsufficientFunds), - ); - } - - #[test] - fn test_withdraw_stake_before_warmup() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let total_lamports = 100; - let stake_lamports = 42; - let stake_account = AccountSharedData::new_ref_data_with_space( - total_lamports, - &StakeState::Initialized(Meta::auto(&stake_pubkey)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let clock = Clock::default(); - let mut future = Clock::default(); - future.epoch += 16; - - let to = solana_sdk::pubkey::new_rand(); - let to_account = AccountSharedData::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - // Stake some lamports (available lamports for withdrawals will reduce) - let vote_pubkey = solana_sdk::pubkey::new_rand(); - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - vote_keyed_account - .set_state(&VoteStateVersions::new_current(VoteState::default())) - .unwrap(); - let signers = vec![stake_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.delegate( - &vote_keyed_account, - &future, - &StakeHistory::default(), - &Config::default(), - &signers, - ), - Ok(()) - ); - - let stake_history = create_stake_history_from_delegations( - None, - 0..future.epoch, - &[stake_from(&stake_keyed_account.account.borrow()) - .unwrap() - .delegation], - ); - - // Try to withdraw stake - assert_eq!( - stake_keyed_account.withdraw( - total_lamports - stake_lamports + 1, - &to_keyed_account, - &clock, - &stake_history, - &stake_keyed_account, - None, - ), - Err(InstructionError::InsufficientFunds) - ); - } - - #[test] - fn test_withdraw_stake_invalid_state() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let total_lamports = 100; - let stake_account = AccountSharedData::new_ref_data_with_space( - total_lamports, - &StakeState::RewardsPool, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let to = solana_sdk::pubkey::new_rand(); - let to_account = AccountSharedData::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - assert_eq!( - stake_keyed_account.withdraw( - total_lamports, - &to_keyed_account, - &Clock::default(), - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(InstructionError::InvalidAccountData) - ); - } - - #[test] - fn test_withdraw_lockup() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let custodian = solana_sdk::pubkey::new_rand(); - let total_lamports = 100; - let stake_account = AccountSharedData::new_ref_data_with_space( - total_lamports, - &StakeState::Initialized(Meta { - lockup: Lockup { - unix_timestamp: 0, - epoch: 1, - custodian, - }, - ..Meta::auto(&stake_pubkey) - }), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let to = solana_sdk::pubkey::new_rand(); - let to_account = AccountSharedData::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - let mut clock = Clock::default(); - - // lockup is still in force, can't withdraw - assert_eq!( - stake_keyed_account.withdraw( - total_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(StakeError::LockupInForce.into()) - ); - - { - let custodian_account = AccountSharedData::new_ref(1, 0, &system_program::id()); - let custodian_keyed_account = KeyedAccount::new(&custodian, true, &custodian_account); - assert_eq!( - stake_keyed_account.withdraw( - total_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - Some(&custodian_keyed_account), - ), - Ok(()) - ); - } - // reset balance - stake_keyed_account - .account - .borrow_mut() - .set_lamports(total_lamports); - - // lockup has expired - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - clock.epoch += 1; - assert_eq!( - stake_keyed_account.withdraw( - total_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Ok(()) - ); - assert_eq!(stake_keyed_account.state(), Ok(StakeState::Uninitialized)); - } - - #[test] - fn test_withdraw_identical_authorities() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let custodian = stake_pubkey; - let total_lamports = 100; - let stake_account = AccountSharedData::new_ref_data_with_space( - total_lamports, - &StakeState::Initialized(Meta { - lockup: Lockup { - unix_timestamp: 0, - epoch: 1, - custodian, - }, - ..Meta::auto(&stake_pubkey) - }), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let to = solana_sdk::pubkey::new_rand(); - let to_account = AccountSharedData::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - let clock = Clock::default(); - - // lockup is still in force, even though custodian is the same as the withdraw authority - assert_eq!( - stake_keyed_account.withdraw( - total_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(StakeError::LockupInForce.into()) - ); - - { - let custodian_keyed_account = KeyedAccount::new(&custodian, true, &stake_account); - assert_eq!( - stake_keyed_account.withdraw( - total_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - Some(&custodian_keyed_account), - ), - Ok(()) - ); - assert_eq!(stake_keyed_account.state(), Ok(StakeState::Uninitialized)); - } - } - - #[test] - fn test_withdraw_rent_exempt() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let clock = Clock::default(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let stake = 7 * MINIMUM_STAKE_DELEGATION; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake + rent_exempt_reserve, - &StakeState::Initialized(Meta { - rent_exempt_reserve, - ..Meta::auto(&stake_pubkey) - }), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let to = solana_sdk::pubkey::new_rand(); - let to_account = AccountSharedData::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - - // Withdrawing account down to minimum balance should succeed - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - assert_eq!( - stake_keyed_account.withdraw( - stake - MINIMUM_STAKE_DELEGATION, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Ok(()) - ); - - // Withdrawing account down to only rent-exempt reserve should fail - stake_account - .borrow_mut() - .checked_add_lamports(stake - MINIMUM_STAKE_DELEGATION) - .unwrap(); // top up account - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - assert_eq!( - stake_keyed_account.withdraw( - stake, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(InstructionError::InsufficientFunds) - ); - - // Withdrawal that would leave less than rent-exempt reserve should fail - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - assert_eq!( - stake_keyed_account.withdraw( - stake + MINIMUM_STAKE_DELEGATION, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(InstructionError::InsufficientFunds) - ); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - assert_eq!( - stake_keyed_account.withdraw( - stake + MINIMUM_STAKE_DELEGATION, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(InstructionError::InsufficientFunds) - ); - - // Withdrawal of complete account should succeed - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - assert_eq!( - stake_keyed_account.withdraw( - stake + rent_exempt_reserve, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Ok(()) - ); - } - #[test] fn test_stake_state_redeem_rewards() { let mut vote_state = VoteState::default(); @@ -3578,352 +2405,6 @@ mod tests { ); } - #[test] - fn test_authorize_uninit() { - let new_authority = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::default(), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let stake_keyed_account = KeyedAccount::new(&new_authority, true, &stake_account); - let signers = vec![new_authority].into_iter().collect(); - assert_eq!( - stake_keyed_account.authorize( - &signers, - &new_authority, - StakeAuthorize::Staker, - false, - &Clock::default(), - None - ), - Err(InstructionError::InvalidAccountData) - ); - } - - #[test] - fn test_authorize_lockup() { - let stake_authority = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(Meta::auto(&stake_authority)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let to = solana_sdk::pubkey::new_rand(); - let to_account = AccountSharedData::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - - let clock = Clock::default(); - let stake_keyed_account = KeyedAccount::new(&stake_authority, true, &stake_account); - - let stake_pubkey0 = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_authority].into_iter().collect(); - assert_eq!( - stake_keyed_account.authorize( - &signers, - &stake_pubkey0, - StakeAuthorize::Staker, - false, - &Clock::default(), - None - ), - Ok(()) - ); - assert_eq!( - stake_keyed_account.authorize( - &signers, - &stake_pubkey0, - StakeAuthorize::Withdrawer, - false, - &Clock::default(), - None - ), - Ok(()) - ); - if let StakeState::Initialized(Meta { authorized, .. }) = - from(&stake_keyed_account.account.borrow()).unwrap() - { - assert_eq!(authorized.staker, stake_pubkey0); - assert_eq!(authorized.withdrawer, stake_pubkey0); - } else { - panic!(); - } - - // A second authorization signed by the stake_keyed_account should fail - let stake_pubkey1 = solana_sdk::pubkey::new_rand(); - assert_eq!( - stake_keyed_account.authorize( - &signers, - &stake_pubkey1, - StakeAuthorize::Staker, - false, - &Clock::default(), - None - ), - Err(InstructionError::MissingRequiredSignature) - ); - - let signers0 = vec![stake_pubkey0].into_iter().collect(); - - // Test a second authorization by the newly authorized pubkey - let stake_pubkey2 = solana_sdk::pubkey::new_rand(); - assert_eq!( - stake_keyed_account.authorize( - &signers0, - &stake_pubkey2, - StakeAuthorize::Staker, - false, - &Clock::default(), - None - ), - Ok(()) - ); - if let StakeState::Initialized(Meta { authorized, .. }) = - from(&stake_keyed_account.account.borrow()).unwrap() - { - assert_eq!(authorized.staker, stake_pubkey2); - } - - assert_eq!( - stake_keyed_account.authorize( - &signers0, - &stake_pubkey2, - StakeAuthorize::Withdrawer, - false, - &Clock::default(), - None - ), - Ok(()) - ); - if let StakeState::Initialized(Meta { authorized, .. }) = - from(&stake_keyed_account.account.borrow()).unwrap() - { - assert_eq!(authorized.staker, stake_pubkey2); - } - - // Test that withdrawal to account fails without authorized withdrawer - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, // old signer - None, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - let stake_keyed_account2 = KeyedAccount::new(&stake_pubkey2, true, &stake_account); - - // Test a successful action by the currently authorized withdrawer - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account2, - None, - ), - Ok(()) - ); - assert_eq!(stake_keyed_account.state(), Ok(StakeState::Uninitialized)); - } - - #[test] - fn test_authorize_with_seed() { - let base_pubkey = solana_sdk::pubkey::new_rand(); - let seed = "42"; - let withdrawer_pubkey = Pubkey::create_with_seed(&base_pubkey, seed, &id()).unwrap(); - let stake_lamports = 42; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(Meta::auto(&withdrawer_pubkey)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let base_account = AccountSharedData::new_ref(1, 0, &id()); - let base_keyed_account = KeyedAccount::new(&base_pubkey, true, &base_account); - - let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account); - - let new_authority = solana_sdk::pubkey::new_rand(); - - // Wrong seed - assert_eq!( - stake_keyed_account.authorize_with_seed( - &base_keyed_account, - "", - &id(), - &new_authority, - StakeAuthorize::Staker, - false, - &Clock::default(), - None, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - // Wrong base - assert_eq!( - stake_keyed_account.authorize_with_seed( - &stake_keyed_account, - seed, - &id(), - &new_authority, - StakeAuthorize::Staker, - false, - &Clock::default(), - None, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - // Set stake authority - assert_eq!( - stake_keyed_account.authorize_with_seed( - &base_keyed_account, - seed, - &id(), - &new_authority, - StakeAuthorize::Staker, - false, - &Clock::default(), - None, - ), - Ok(()) - ); - - // Set withdraw authority - assert_eq!( - stake_keyed_account.authorize_with_seed( - &base_keyed_account, - seed, - &id(), - &new_authority, - StakeAuthorize::Withdrawer, - false, - &Clock::default(), - None, - ), - Ok(()) - ); - - // No longer withdraw authority - assert_eq!( - stake_keyed_account.authorize_with_seed( - &stake_keyed_account, - seed, - &id(), - &new_authority, - StakeAuthorize::Withdrawer, - false, - &Clock::default(), - None, - ), - Err(InstructionError::MissingRequiredSignature) - ); - } - - #[test] - fn test_authorize_override() { - let withdrawer_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(Meta::auto(&withdrawer_pubkey)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account); - - // Authorize a staker pubkey and move the withdrawer key into cold storage. - let new_authority = solana_sdk::pubkey::new_rand(); - let signers = vec![withdrawer_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.authorize( - &signers, - &new_authority, - StakeAuthorize::Staker, - false, - &Clock::default(), - None - ), - Ok(()) - ); - - // Attack! The stake key (a hot key) is stolen and used to authorize a new staker. - let mallory_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![new_authority].into_iter().collect(); - assert_eq!( - stake_keyed_account.authorize( - &signers, - &mallory_pubkey, - StakeAuthorize::Staker, - false, - &Clock::default(), - None - ), - Ok(()) - ); - - // Verify the original staker no longer has access. - let new_stake_pubkey = solana_sdk::pubkey::new_rand(); - assert_eq!( - stake_keyed_account.authorize( - &signers, - &new_stake_pubkey, - StakeAuthorize::Staker, - false, - &Clock::default(), - None - ), - Err(InstructionError::MissingRequiredSignature) - ); - - // Verify the withdrawer (pulled from cold storage) can save the day. - let signers = vec![withdrawer_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.authorize( - &signers, - &new_stake_pubkey, - StakeAuthorize::Withdrawer, - false, - &Clock::default(), - None - ), - Ok(()) - ); - - // Attack! Verify the staker cannot be used to authorize a withdraw. - let signers = vec![new_stake_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.authorize( - &signers, - &mallory_pubkey, - StakeAuthorize::Withdrawer, - false, - &Clock::default(), - None - ), - Ok(()) - ); - } - #[test] fn test_split_source_uninitialized() { let stake_pubkey = solana_sdk::pubkey::new_rand(); @@ -4179,135 +2660,6 @@ mod tests { } } - #[test] - fn test_split() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - // test splitting both an Initialized stake and a Staked stake - for state in &[ - StakeState::Initialized(Meta::auto(&stake_pubkey)), - StakeState::Stake(Meta::auto(&stake_pubkey), just_stake(stake_lamports)), - ] { - let split_stake_account = AccountSharedData::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - state, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - // split more than available fails - assert_eq!( - stake_keyed_account.split(stake_lamports + 1, &split_stake_keyed_account, &signers), - Err(InstructionError::InsufficientFunds) - ); - - // should work - assert_eq!( - stake_keyed_account.split(stake_lamports / 2, &split_stake_keyed_account, &signers), - Ok(()) - ); - // no lamport leakage - assert_eq!( - stake_keyed_account.account.borrow().lamports() - + split_stake_keyed_account.account.borrow().lamports(), - stake_lamports - ); - - match state { - StakeState::Initialized(_) => { - assert_eq!(Ok(*state), split_stake_keyed_account.state()); - assert_eq!(Ok(*state), stake_keyed_account.state()); - } - StakeState::Stake(meta, stake) => { - assert_eq!( - Ok(StakeState::Stake( - *meta, - Stake { - delegation: Delegation { - stake: stake_lamports / 2, - ..stake.delegation - }, - ..*stake - } - )), - split_stake_keyed_account.state() - ); - assert_eq!( - Ok(StakeState::Stake( - *meta, - Stake { - delegation: Delegation { - stake: stake_lamports / 2, - ..stake.delegation - }, - ..*stake - } - )), - stake_keyed_account.state() - ); - } - _ => unreachable!(), - } - - // reset - stake_keyed_account - .account - .borrow_mut() - .set_lamports(stake_lamports); - } - } - - #[test] - fn test_split_fake_stake_dest() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let split_stake_account = AccountSharedData::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::(), - &solana_sdk::pubkey::new_rand(), - ) - .expect("stake_account"); - - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Stake(Meta::auto(&stake_pubkey), just_stake(stake_lamports)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - assert_eq!( - stake_keyed_account.split(stake_lamports / 2, &split_stake_keyed_account, &signers), - Err(InstructionError::IncorrectProgramId), - ); - } - #[test] fn test_split_to_account_with_rent_exempt_reserve() { let stake_pubkey = solana_sdk::pubkey::new_rand(); @@ -5522,216 +3874,6 @@ mod tests { ); } - #[test] - fn test_authorize_delegated_stake() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(Meta::auto(&stake_pubkey)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let clock = Clock::default(); - - let vote_pubkey = solana_sdk::pubkey::new_rand(); - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let signers = vec![stake_pubkey].into_iter().collect(); - stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ) - .unwrap(); - - // deactivate, so we can re-delegate - stake_keyed_account.deactivate(&clock, &signers).unwrap(); - - let new_staker_pubkey = solana_sdk::pubkey::new_rand(); - assert_eq!( - stake_keyed_account.authorize( - &signers, - &new_staker_pubkey, - StakeAuthorize::Staker, - false, - &Clock::default(), - None - ), - Ok(()) - ); - let authorized = authorized_from(&stake_keyed_account.try_account_ref().unwrap()).unwrap(); - assert_eq!(authorized.staker, new_staker_pubkey); - - let other_pubkey = solana_sdk::pubkey::new_rand(); - let other_signers = vec![other_pubkey].into_iter().collect(); - - // Use unsigned stake_keyed_account to test other signers - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - - let new_voter_pubkey = solana_sdk::pubkey::new_rand(); - let vote_state = VoteState::default(); - let new_vote_account = RefCell::new(vote_state::create_account( - &new_voter_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let new_vote_keyed_account = KeyedAccount::new(&new_voter_pubkey, false, &new_vote_account); - new_vote_keyed_account.set_state(&vote_state).unwrap(); - - // Random other account should fail - assert_eq!( - stake_keyed_account.delegate( - &new_vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &other_signers, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - let new_signers = vec![new_staker_pubkey].into_iter().collect(); - // Authorized staker should succeed - assert_eq!( - stake_keyed_account.delegate( - &new_vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &new_signers, - ), - Ok(()) - ); - let stake = stake_from(&stake_keyed_account.try_account_ref().unwrap()).unwrap(); - assert_eq!(stake.delegation.voter_pubkey, new_voter_pubkey); - - // Test another staking action - assert_eq!(stake_keyed_account.deactivate(&clock, &new_signers), Ok(())); - } - - #[test] - fn test_redelegate_consider_balance_changes() { - let initial_lamports = 4242424242; - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let withdrawer_pubkey = Pubkey::new_unique(); - let stake_lamports = rent_exempt_reserve + initial_lamports; - - let meta = Meta { - rent_exempt_reserve, - ..Meta::auto(&withdrawer_pubkey) - }; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(meta), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account); - - let vote_pubkey = Pubkey::new_unique(); - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &Pubkey::new_unique(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - - let signers = HashSet::from_iter(vec![withdrawer_pubkey]); - let config = Config::default(); - let stake_history = StakeHistory::default(); - let mut clock = Clock::default(); - stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &stake_history, - &config, - &signers, - ) - .unwrap(); - - clock.epoch += 1; - stake_keyed_account.deactivate(&clock, &signers).unwrap(); - - clock.epoch += 1; - // Once deactivated, we withdraw stake to new keyed account - let to = Pubkey::new_unique(); - let to_account = AccountSharedData::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - let withdraw_lamports = initial_lamports / 2; - stake_keyed_account - .withdraw( - withdraw_lamports, - &to_keyed_account, - &clock, - &stake_history, - &stake_keyed_account, - None, - ) - .unwrap(); - let expected_balance = rent_exempt_reserve + initial_lamports - withdraw_lamports; - assert_eq!(stake_keyed_account.lamports().unwrap(), expected_balance); - - clock.epoch += 1; - stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &stake_history, - &config, - &signers, - ) - .unwrap(); - let stake = stake_from(&stake_account.borrow()).unwrap(); - assert_eq!( - stake.delegation.stake, - stake_keyed_account.lamports().unwrap() - rent_exempt_reserve, - ); - - clock.epoch += 1; - stake_keyed_account.deactivate(&clock, &signers).unwrap(); - - // Out of band deposit - stake_keyed_account - .try_account_ref_mut() - .unwrap() - .checked_add_lamports(withdraw_lamports) - .unwrap(); - - clock.epoch += 1; - stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &stake_history, - &config, - &signers, - ) - .unwrap(); - let stake = stake_from(&stake_account.borrow()).unwrap(); - assert_eq!( - stake.delegation.stake, - stake_keyed_account.lamports().unwrap() - rent_exempt_reserve, - ); - } - #[test] fn test_calculate_lamports_per_byte_year() { let rent = Rent::default();