From e051c7c16279e41ca16d18c78f49ce408ba3f65d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Mei=C3=9Fner?= Date: Tue, 5 Apr 2022 12:36:01 +0200 Subject: [PATCH] Removes KeyedAccount from tests in stake instruction. (Part 3) (#24110) * Moves test from stake state to stake instruction. * Migrates test_split_source_uninitialized. * Migrates test_split_split_not_uninitialized. * Migrates test_split_more_than_staked. * Migrates test_split_with_rent. * Migrates test_split_to_account_with_rent_exempt_reserve. * Migrates test_split_from_larger_sized_account. * Migrates test_split_from_smaller_sized_account. * Migrates test_split_100_percent_of_source. * Migrates test_split_100_percent_of_source_to_account_with_lamports. * Migrates test_split_rent_exemptness. --- programs/stake/src/stake_instruction.rs | 956 ++++++++++++++++++++++++ programs/stake/src/stake_state.rs | 944 +---------------------- 2 files changed, 957 insertions(+), 943 deletions(-) diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 33da4ac44d..6d177e984d 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -4220,4 +4220,960 @@ mod tests { Ok(()), ); } + + #[test] + fn test_split_source_uninitialized() { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + 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 transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + ]; + + // splitting an uninitialized account where the destination is the same as the source + { + // splitting should work when... + // - when split amount is the full balance + // - when split amount is zero + // - when split amount is non-zero and less than the full balance + // + // and splitting should fail when the split amount is greater than the balance + process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + process_instruction( + &serialize(&StakeInstruction::Split(0)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + } + + // this should work + instruction_accounts[1].pubkey = split_to_address; + let accounts = process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(accounts[0].lamports(), accounts[1].lamports()); + + // no signers should fail + instruction_accounts[0].is_signer = false; + process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::MissingRequiredSignature), + ); + } + + #[test] + fn test_split_split_not_uninitialized() { + let stake_lamports = 42; + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &just_stake(Meta::auto(&stake_address), stake_lamports), + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + ]; + + for split_to_state in &[ + StakeState::Initialized(Meta::default()), + StakeState::Stake(Meta::default(), Stake::default()), + StakeState::RewardsPool, + ] { + let split_to_account = AccountSharedData::new_data_with_space( + 0, + split_to_state, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + vec![ + (stake_address, stake_account.clone()), + (split_to_address, split_to_account), + ], + instruction_accounts.clone(), + Err(InstructionError::InvalidAccountData), + ); + } + } + + #[test] + fn test_split_more_than_staked() { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &just_stake( + Meta { + rent_exempt_reserve, + ..Meta::auto(&stake_address) + }, + stake_lamports / 2 - 1, + ), + std::mem::size_of::(), + &id(), + ) + .unwrap(); + 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 transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + 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, + }, + ]; + + process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts, + Err(StakeError::InsufficientStake.into()), + ); + } + + #[test] + fn test_split_with_rent() { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + let minimum_balance = rent_exempt_reserve + minimum_delegation; + let stake_lamports = minimum_balance * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + 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, + }, + ]; + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + ..Meta::default() + }; + + // test splitting both an Initialized stake and a Staked stake + for state in &[ + StakeState::Initialized(meta), + just_stake(meta, stake_lamports - rent_exempt_reserve), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + state, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account.clone()), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + + // not enough to make a non-zero stake account + process_instruction( + &serialize(&StakeInstruction::Split(rent_exempt_reserve)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // doesn't leave enough for initial stake to be non-zero + process_instruction( + &serialize(&StakeInstruction::Split( + stake_lamports - rent_exempt_reserve, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // split account already has way enough lamports + transaction_accounts[1].1.set_lamports(minimum_balance); + let accounts = process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports - minimum_balance)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + + // verify no stake leakage in the case of a stake + if let StakeState::Stake(meta, stake) = state { + assert_eq!( + accounts[1].state(), + Ok(StakeState::Stake( + *meta, + Stake { + delegation: Delegation { + stake: stake_lamports - minimum_balance, + ..stake.delegation + }, + ..*stake + } + )) + ); + assert_eq!(accounts[0].lamports(), minimum_balance,); + assert_eq!(accounts[1].lamports(), stake_lamports,); + } + } + } + + #[test] + fn test_split_to_account_with_rent_exempt_reserve() { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + ..Meta::default() + }; + let state = just_stake(meta, stake_lamports - rent_exempt_reserve); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly + // rent_exempt_reserve, and more than rent_exempt_reserve. The empty case is not covered in + // test_split, since that test uses a Meta with rent_exempt_reserve = 0 + let split_lamport_balances = vec![ + 0, + rent_exempt_reserve - 1, + rent_exempt_reserve, + rent_exempt_reserve + minimum_delegation - 1, + rent_exempt_reserve + minimum_delegation, + ]; + for initial_balance in split_lamport_balances { + let split_to_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + + // split more than available fails + process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // should work + let accounts = process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + // no lamport leakage + assert_eq!( + accounts[0].lamports() + accounts[1].lamports(), + stake_lamports + initial_balance, + ); + + if let StakeState::Stake(meta, stake) = state { + let expected_stake = + stake_lamports / 2 - (rent_exempt_reserve.saturating_sub(initial_balance)); + assert_eq!( + Ok(StakeState::Stake( + meta, + Stake { + delegation: Delegation { + stake: stake_lamports / 2 + - (rent_exempt_reserve.saturating_sub(initial_balance)), + ..stake.delegation + }, + ..stake + } + )), + accounts[1].state(), + ); + assert_eq!( + accounts[1].lamports(), + expected_stake + + rent_exempt_reserve + + initial_balance.saturating_sub(rent_exempt_reserve), + ); + assert_eq!( + Ok(StakeState::Stake( + meta, + Stake { + delegation: Delegation { + stake: stake_lamports / 2 - rent_exempt_reserve, + ..stake.delegation + }, + ..stake + } + )), + accounts[0].state(), + ); + } + } + } + + #[test] + fn test_split_from_larger_sized_account() { + let rent = Rent::default(); + let source_larger_rent_exempt_reserve = + rent.minimum_balance(std::mem::size_of::() + 100); + let split_rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + let stake_lamports = (source_larger_rent_exempt_reserve + minimum_delegation) * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: source_larger_rent_exempt_reserve, + ..Meta::default() + }; + let state = just_stake(meta, stake_lamports - source_larger_rent_exempt_reserve); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + std::mem::size_of::() + 100, + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly + // rent_exempt_reserve, and more than rent_exempt_reserve. The empty case is not covered in + // test_split, since that test uses a Meta with rent_exempt_reserve = 0 + let split_lamport_balances = vec![ + 0, + split_rent_exempt_reserve - 1, + split_rent_exempt_reserve, + split_rent_exempt_reserve + minimum_delegation - 1, + split_rent_exempt_reserve + minimum_delegation, + ]; + for initial_balance in split_lamport_balances { + let split_to_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + + // split more than available fails + process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // should work + 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 + initial_balance + ); + + if let StakeState::Stake(meta, stake) = state { + let expected_split_meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: split_rent_exempt_reserve, + ..Meta::default() + }; + let expected_stake = stake_lamports / 2 + - (split_rent_exempt_reserve.saturating_sub(initial_balance)); + + assert_eq!( + Ok(StakeState::Stake( + expected_split_meta, + Stake { + delegation: Delegation { + stake: expected_stake, + ..stake.delegation + }, + ..stake + } + )), + accounts[1].state() + ); + assert_eq!( + accounts[1].lamports(), + expected_stake + + split_rent_exempt_reserve + + initial_balance.saturating_sub(split_rent_exempt_reserve) + ); + assert_eq!( + Ok(StakeState::Stake( + meta, + Stake { + delegation: Delegation { + stake: stake_lamports / 2 - source_larger_rent_exempt_reserve, + ..stake.delegation + }, + ..stake + } + )), + accounts[0].state() + ); + } + } + } + + #[test] + fn test_split_from_smaller_sized_account() { + let rent = Rent::default(); + let source_smaller_rent_exempt_reserve = + rent.minimum_balance(std::mem::size_of::()); + let split_rent_exempt_reserve = + rent.minimum_balance(std::mem::size_of::() + 100); + let stake_lamports = split_rent_exempt_reserve + 1; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: source_smaller_rent_exempt_reserve, + ..Meta::default() + }; + let state = just_stake(meta, stake_lamports - source_smaller_rent_exempt_reserve); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + let split_amount = stake_lamports - (source_smaller_rent_exempt_reserve + 1); // Enough so that split stake is > 0 + let split_lamport_balances = vec![ + 0, + 1, + split_rent_exempt_reserve, + split_rent_exempt_reserve + 1, + ]; + for initial_balance in split_lamport_balances { + let split_to_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeState::Uninitialized, + std::mem::size_of::() + 100, + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + + // should always return error when splitting to larger account + process_instruction( + &serialize(&StakeInstruction::Split(split_amount)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InvalidAccountData), + ); + + // Splitting 100% of source should not make a difference + process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Err(InstructionError::InvalidAccountData), + ); + } + } + + #[test] + fn test_split_100_percent_of_source() { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + let stake_lamports = rent_exempt_reserve + minimum_delegation; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + ..Meta::default() + }; + let split_to_address = solana_sdk::pubkey::new_rand(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + 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, + }, + ]; + + // test splitting both an Initialized stake and a Staked stake + for state in &[ + StakeState::Initialized(meta), + just_stake(meta, stake_lamports - rent_exempt_reserve), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account.clone()), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + + // split 100% over to dest + let accounts = process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + + // no lamport leakage + assert_eq!( + accounts[0].lamports() + accounts[1].lamports(), + stake_lamports + ); + + match state { + StakeState::Initialized(_) => { + assert_eq!(Ok(*state), accounts[1].state()); + assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); + } + StakeState::Stake(meta, stake) => { + assert_eq!( + Ok(StakeState::Stake( + *meta, + Stake { + delegation: Delegation { + stake: stake_lamports - rent_exempt_reserve, + ..stake.delegation + }, + ..*stake + } + )), + accounts[1].state() + ); + assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); + } + _ => unreachable!(), + } + } + } + + #[test] + fn test_split_100_percent_of_source_to_account_with_lamports() { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + let stake_lamports = rent_exempt_reserve + minimum_delegation; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + ..Meta::default() + }; + let state = just_stake(meta, stake_lamports - rent_exempt_reserve); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly + // rent_exempt_reserve, and more than rent_exempt_reserve. Technically, the empty case is + // covered in test_split_100_percent_of_source, but included here as well for readability + let split_lamport_balances = vec![ + 0, + rent_exempt_reserve - 1, + rent_exempt_reserve, + rent_exempt_reserve + minimum_delegation - 1, + rent_exempt_reserve + minimum_delegation, + ]; + for initial_balance in split_lamport_balances { + let split_to_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + + // split 100% over to dest + let accounts = process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + + // no lamport leakage + assert_eq!( + accounts[0].lamports() + accounts[1].lamports(), + stake_lamports + initial_balance + ); + + if let StakeState::Stake(meta, stake) = state { + assert_eq!( + Ok(StakeState::Stake( + meta, + Stake { + delegation: Delegation { + stake: stake_lamports - rent_exempt_reserve, + ..stake.delegation + }, + ..stake + } + )), + accounts[1].state() + ); + assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); + } + } + } + + #[test] + fn test_split_rent_exemptness() { + let rent = Rent::default(); + let source_rent_exempt_reserve = + rent.minimum_balance(std::mem::size_of::() + 100); + let split_rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + let minimum_delegation = crate::get_minimum_delegation(&FeatureSet::all_enabled()); + let stake_lamports = source_rent_exempt_reserve + minimum_delegation; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: source_rent_exempt_reserve, + ..Meta::default() + }; + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + for state in &[ + StakeState::Initialized(meta), + just_stake(meta, stake_lamports - source_rent_exempt_reserve), + ] { + // Test that splitting to a larger account fails + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + std::mem::size_of::() + 10000, + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Err(InstructionError::InvalidAccountData), + ); + + // Test that splitting from a larger account to a smaller one works. + // Split amount should not matter, assuming other fund criteria are met + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + std::mem::size_of::() + 100, + &id(), + ) + .unwrap(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + let accounts = process_instruction( + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(accounts[1].lamports(), stake_lamports); + + let expected_split_meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: split_rent_exempt_reserve, + ..Meta::default() + }; + match state { + StakeState::Initialized(_) => { + assert_eq!( + Ok(StakeState::Initialized(expected_split_meta)), + accounts[1].state() + ); + assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); + } + StakeState::Stake(_meta, stake) => { + // Expected stake should reflect original stake amount so that extra lamports + // from the rent_exempt_reserve inequality do not magically activate + let expected_stake = stake_lamports - source_rent_exempt_reserve; + + assert_eq!( + Ok(StakeState::Stake( + expected_split_meta, + Stake { + delegation: Delegation { + stake: expected_stake, + ..stake.delegation + }, + ..*stake + } + )), + accounts[1].state() + ); + assert_eq!( + accounts[1].lamports(), + expected_stake + source_rent_exempt_reserve, + ); + assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); + } + _ => unreachable!(), + } + } + } } diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 126ea0a85f..f1cbb31429 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -1444,7 +1444,7 @@ mod tests { proptest::prelude::*, solana_program_runtime::invoke_context::InvokeContext, solana_sdk::{ - account::{create_account_shared_data_for_test, AccountSharedData, WritableAccount}, + account::{create_account_shared_data_for_test, AccountSharedData}, native_token, pubkey::Pubkey, sysvar::SysvarId, @@ -2529,149 +2529,6 @@ mod tests { ) } - #[test] - fn test_split_source_uninitialized() { - let mut transaction_context = create_mock_tx_context(); - let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set); - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let split_stake_account = AccountSharedData::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, false, &split_stake_account); - - // no signers should fail - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports / 2, - &split_stake_keyed_account, - &HashSet::default(), // no signers - ), - Err(InstructionError::MissingRequiredSignature) - ); - - let signers = HashSet::from([stake_pubkey]); - - // splitting an uninitialized account where the destination is the same as the source - { - // splitting should work when... - // - when split amount is the full balance - // - when split amount is zero - // - when split amount is non-zero and less than the full balance - // - // and splitting should fail when the split amount is greater than the balance - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports, - &stake_keyed_account, - &signers - ), - Ok(()), - ); - assert_eq!( - stake_keyed_account.split(&invoke_context, 0, &stake_keyed_account, &signers), - Ok(()), - ); - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports / 2, - &stake_keyed_account, - &signers - ), - Ok(()), - ); - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports + 1, - &stake_keyed_account, - &signers - ), - Err(InstructionError::InsufficientFunds), - ); - } - - // this should work - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports / 2, - &split_stake_keyed_account, - &signers - ), - Ok(()) - ); - assert_eq!( - stake_keyed_account.account.borrow().lamports(), - split_stake_keyed_account.account.borrow().lamports() - ); - } - - #[test] - fn test_split_split_not_uninitialized() { - let mut transaction_context = create_mock_tx_context(); - let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]); - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - 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); - let signers = vec![stake_pubkey].into_iter().collect(); - - for split_stake_state in &[ - StakeState::Initialized(Meta::default()), - StakeState::Stake(Meta::default(), Stake::default()), - StakeState::RewardsPool, - ] { - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let split_stake_account = AccountSharedData::new_ref_data_with_space( - 0, - split_stake_state, - std::mem::size_of::(), - &id(), - ) - .expect("split_stake_account"); - - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports / 2, - &split_stake_keyed_account, - &signers - ), - Err(InstructionError::InvalidAccountData) - ); - } - } fn just_stake(stake: u64) -> Stake { Stake { delegation: Delegation { @@ -2682,805 +2539,6 @@ mod tests { } } - #[test] - fn test_split_more_than_staked() { - let mut transaction_context = create_mock_tx_context(); - let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set); - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; - let stake_account = AccountSharedData::new_ref_data_with_space( - stake_lamports, - &StakeState::Stake( - Meta { - rent_exempt_reserve, - ..Meta::auto(&stake_pubkey) - }, - just_stake(stake_lamports / 2 - 1), - ), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let split_stake_account = AccountSharedData::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let signers = vec![stake_pubkey].into_iter().collect(); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports / 2, - &split_stake_keyed_account, - &signers - ), - Err(StakeError::InsufficientStake.into()) - ); - } - - #[test] - fn test_split_with_rent() { - let mut transaction_context = create_mock_tx_context(); - let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set); - let minimum_balance = rent_exempt_reserve + minimum_delegation; - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = minimum_balance * 2; - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve, - ..Meta::default() - }; - - // test splitting both an Initialized stake and a Staked stake - for state in &[ - StakeState::Initialized(meta), - StakeState::Stake(meta, just_stake(stake_lamports - rent_exempt_reserve)), - ] { - 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); - - 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); - - // not enough to make a non-zero stake account - assert_eq!( - stake_keyed_account.split( - &invoke_context, - rent_exempt_reserve, - &split_stake_keyed_account, - &signers, - ), - Err(InstructionError::InsufficientFunds) - ); - - // doesn't leave enough for initial stake to be non-zero - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports - rent_exempt_reserve, - &split_stake_keyed_account, - &signers, - ), - Err(InstructionError::InsufficientFunds) - ); - - // split account already has way enough lamports - split_stake_keyed_account - .account - .borrow_mut() - .set_lamports(minimum_balance); - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports - minimum_balance, - &split_stake_keyed_account, - &signers, - ), - Ok(()) - ); - - // verify no stake leakage in the case of a stake - if let StakeState::Stake(meta, stake) = state { - assert_eq!( - split_stake_keyed_account.state(), - Ok(StakeState::Stake( - *meta, - Stake { - delegation: Delegation { - stake: stake_lamports - minimum_balance, - ..stake.delegation - }, - ..*stake - } - )) - ); - assert_eq!( - stake_keyed_account.account.borrow().lamports(), - minimum_balance, - ); - assert_eq!( - split_stake_keyed_account.account.borrow().lamports(), - stake_lamports, - ); - } - } - } - - #[test] - fn test_split_to_account_with_rent_exempt_reserve() { - let mut transaction_context = create_mock_tx_context(); - let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]); - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set); - let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve, - ..Meta::default() - }; - - let state = StakeState::Stake(meta, just_stake(stake_lamports - rent_exempt_reserve)); - // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly - // rent_exempt_reserve, and more than rent_exempt_reserve. The empty case is not covered in - // test_split, since that test uses a Meta with rent_exempt_reserve = 0 - let split_lamport_balances = vec![ - 0, - rent_exempt_reserve - 1, - rent_exempt_reserve, - rent_exempt_reserve + minimum_delegation - 1, - rent_exempt_reserve + minimum_delegation, - ]; - for initial_balance in split_lamport_balances { - let split_stake_account = AccountSharedData::new_ref_data_with_space( - initial_balance, - &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( - &invoke_context, - stake_lamports + 1, - &split_stake_keyed_account, - &signers - ), - Err(InstructionError::InsufficientFunds) - ); - - // should work - assert_eq!( - stake_keyed_account.split( - &invoke_context, - 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 + initial_balance - ); - - if let StakeState::Stake(meta, stake) = state { - let expected_stake = - stake_lamports / 2 - (rent_exempt_reserve.saturating_sub(initial_balance)); - assert_eq!( - Ok(StakeState::Stake( - meta, - Stake { - delegation: Delegation { - stake: stake_lamports / 2 - - (rent_exempt_reserve.saturating_sub(initial_balance)), - ..stake.delegation - }, - ..stake - } - )), - split_stake_keyed_account.state() - ); - assert_eq!( - split_stake_keyed_account.account.borrow().lamports(), - expected_stake - + rent_exempt_reserve - + initial_balance.saturating_sub(rent_exempt_reserve) - ); - assert_eq!( - Ok(StakeState::Stake( - meta, - Stake { - delegation: Delegation { - stake: stake_lamports / 2 - rent_exempt_reserve, - ..stake.delegation - }, - ..stake - } - )), - stake_keyed_account.state() - ); - } - } - } - - #[test] - fn test_split_from_larger_sized_account() { - let mut transaction_context = create_mock_tx_context(); - let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]); - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let source_larger_rent_exempt_reserve = - rent.minimum_balance(std::mem::size_of::() + 100); - let split_rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set); - let stake_lamports = (source_larger_rent_exempt_reserve + minimum_delegation) * 2; - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve: source_larger_rent_exempt_reserve, - ..Meta::default() - }; - - let state = StakeState::Stake( - meta, - just_stake(stake_lamports - source_larger_rent_exempt_reserve), - ); - - // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly - // rent_exempt_reserve, and more than rent_exempt_reserve. The empty case is not covered in - // test_split, since that test uses a Meta with rent_exempt_reserve = 0 - let split_lamport_balances = vec![ - 0, - split_rent_exempt_reserve - 1, - split_rent_exempt_reserve, - split_rent_exempt_reserve + minimum_delegation - 1, - split_rent_exempt_reserve + minimum_delegation, - ]; - for initial_balance in split_lamport_balances { - let split_stake_account = AccountSharedData::new_ref_data_with_space( - initial_balance, - &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::() + 100, - &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( - &invoke_context, - stake_lamports + 1, - &split_stake_keyed_account, - &signers - ), - Err(InstructionError::InsufficientFunds) - ); - - // should work - assert_eq!( - stake_keyed_account.split( - &invoke_context, - 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 + initial_balance - ); - - if let StakeState::Stake(meta, stake) = state { - let expected_split_meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve: split_rent_exempt_reserve, - ..Meta::default() - }; - let expected_stake = stake_lamports / 2 - - (split_rent_exempt_reserve.saturating_sub(initial_balance)); - - assert_eq!( - Ok(StakeState::Stake( - expected_split_meta, - Stake { - delegation: Delegation { - stake: expected_stake, - ..stake.delegation - }, - ..stake - } - )), - split_stake_keyed_account.state() - ); - assert_eq!( - split_stake_keyed_account.account.borrow().lamports(), - expected_stake - + split_rent_exempt_reserve - + initial_balance.saturating_sub(split_rent_exempt_reserve) - ); - assert_eq!( - Ok(StakeState::Stake( - meta, - Stake { - delegation: Delegation { - stake: stake_lamports / 2 - source_larger_rent_exempt_reserve, - ..stake.delegation - }, - ..stake - } - )), - stake_keyed_account.state() - ); - } - } - } - - #[test] - fn test_split_from_smaller_sized_account() { - let mut transaction_context = create_mock_tx_context(); - let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]); - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let source_smaller_rent_exempt_reserve = - rent.minimum_balance(std::mem::size_of::()); - let split_rent_exempt_reserve = - rent.minimum_balance(std::mem::size_of::() + 100); - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve: source_smaller_rent_exempt_reserve, - ..Meta::default() - }; - - let stake_lamports = split_rent_exempt_reserve + 1; - let split_amount = stake_lamports - (source_smaller_rent_exempt_reserve + 1); // Enough so that split stake is > 0 - - let state = StakeState::Stake( - meta, - just_stake(stake_lamports - source_smaller_rent_exempt_reserve), - ); - - let split_lamport_balances = vec![ - 0, - 1, - split_rent_exempt_reserve, - split_rent_exempt_reserve + 1, - ]; - for initial_balance in split_lamport_balances { - let split_stake_account = AccountSharedData::new_ref_data_with_space( - initial_balance, - &StakeState::Uninitialized, - std::mem::size_of::() + 100, - &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); - - // should always return error when splitting to larger account - let split_result = stake_keyed_account.split( - &invoke_context, - split_amount, - &split_stake_keyed_account, - &signers, - ); - assert_eq!(split_result, Err(InstructionError::InvalidAccountData)); - - // Splitting 100% of source should not make a difference - let split_result = stake_keyed_account.split( - &invoke_context, - stake_lamports, - &split_stake_keyed_account, - &signers, - ); - assert_eq!(split_result, Err(InstructionError::InvalidAccountData)); - } - } - - #[test] - fn test_split_100_percent_of_source() { - let mut transaction_context = create_mock_tx_context(); - let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]); - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set); - let stake_lamports = rent_exempt_reserve + minimum_delegation; - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve, - ..Meta::default() - }; - - // test splitting both an Initialized stake and a Staked stake - for state in &[ - StakeState::Initialized(meta), - StakeState::Stake(meta, just_stake(stake_lamports - rent_exempt_reserve)), - ] { - 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 100% over to dest - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports, - &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(StakeState::Uninitialized), stake_keyed_account.state()); - } - StakeState::Stake(meta, stake) => { - assert_eq!( - Ok(StakeState::Stake( - *meta, - Stake { - delegation: Delegation { - stake: stake_lamports - rent_exempt_reserve, - ..stake.delegation - }, - ..*stake - } - )), - split_stake_keyed_account.state() - ); - assert_eq!(Ok(StakeState::Uninitialized), stake_keyed_account.state()); - } - _ => unreachable!(), - } - - // reset - stake_keyed_account - .account - .borrow_mut() - .set_lamports(stake_lamports); - } - } - - #[test] - fn test_split_100_percent_of_source_to_account_with_lamports() { - let mut transaction_context = create_mock_tx_context(); - let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]); - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set); - let stake_lamports = rent_exempt_reserve + minimum_delegation; - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve, - ..Meta::default() - }; - - let state = StakeState::Stake(meta, just_stake(stake_lamports - rent_exempt_reserve)); - // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly - // rent_exempt_reserve, and more than rent_exempt_reserve. Technically, the empty case is - // covered in test_split_100_percent_of_source, but included here as well for readability - let split_lamport_balances = vec![ - 0, - rent_exempt_reserve - 1, - rent_exempt_reserve, - rent_exempt_reserve + minimum_delegation - 1, - rent_exempt_reserve + minimum_delegation, - ]; - for initial_balance in split_lamport_balances { - let split_stake_account = AccountSharedData::new_ref_data_with_space( - initial_balance, - &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 100% over to dest - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports, - &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 + initial_balance - ); - - if let StakeState::Stake(meta, stake) = state { - assert_eq!( - Ok(StakeState::Stake( - meta, - Stake { - delegation: Delegation { - stake: stake_lamports - rent_exempt_reserve, - ..stake.delegation - }, - ..stake - } - )), - split_stake_keyed_account.state() - ); - assert_eq!(Ok(StakeState::Uninitialized), stake_keyed_account.state()); - } - } - } - - #[test] - fn test_split_rent_exemptness() { - let mut transaction_context = create_mock_tx_context(); - let invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]); - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let source_rent_exempt_reserve = - rent.minimum_balance(std::mem::size_of::() + 100); - let split_rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set); - let stake_lamports = source_rent_exempt_reserve + minimum_delegation; - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve: source_rent_exempt_reserve, - ..Meta::default() - }; - - for state in &[ - StakeState::Initialized(meta), - StakeState::Stake( - meta, - just_stake(stake_lamports - source_rent_exempt_reserve), - ), - ] { - // Test that splitting to a larger account fails - let split_stake_account = AccountSharedData::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::() + 10000, - &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); - - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports, - &split_stake_keyed_account, - &signers - ), - Err(InstructionError::InvalidAccountData) - ); - - // Test that splitting from a larger account to a smaller one works. - // Split amount should not matter, assuming other fund criteria are met - let 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::() + 100, - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - assert_eq!( - stake_keyed_account.split( - &invoke_context, - stake_lamports, - &split_stake_keyed_account, - &signers - ), - Ok(()) - ); - - assert_eq!( - split_stake_keyed_account.account.borrow().lamports(), - stake_lamports - ); - - let expected_split_meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve: split_rent_exempt_reserve, - ..Meta::default() - }; - - match state { - StakeState::Initialized(_) => { - assert_eq!( - Ok(StakeState::Initialized(expected_split_meta)), - split_stake_keyed_account.state() - ); - assert_eq!(Ok(StakeState::Uninitialized), stake_keyed_account.state()); - } - StakeState::Stake(_meta, stake) => { - // Expected stake should reflect original stake amount so that extra lamports - // from the rent_exempt_reserve inequality do not magically activate - let expected_stake = stake_lamports - source_rent_exempt_reserve; - - assert_eq!( - Ok(StakeState::Stake( - expected_split_meta, - Stake { - delegation: Delegation { - stake: expected_stake, - ..stake.delegation - }, - ..*stake - } - )), - split_stake_keyed_account.state() - ); - assert_eq!( - split_stake_keyed_account.account.borrow().lamports(), - expected_stake + source_rent_exempt_reserve, - ); - assert_eq!(Ok(StakeState::Uninitialized), stake_keyed_account.state()); - } - _ => unreachable!(), - } - } - } - #[test] fn test_merge() { let mut transaction_context = TransactionContext::new(Vec::new(), 1, 1);