diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1700f7f8..1a97e9f0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,7 +2,7 @@ name: Main on: push: - branches: [main,mollusk-tests-7-move-lamports] + branches: [main,mollusk-tests-8-move-stake] pull_request: env: diff --git a/program/tests/helpers/instruction_builders.rs b/program/tests/helpers/instruction_builders.rs index 5b9caf80..d0dd5069 100644 --- a/program/tests/helpers/instruction_builders.rs +++ b/program/tests/helpers/instruction_builders.rs @@ -271,3 +271,73 @@ impl InstructionConfig for MoveLamportsFullConfig<'_> { accounts } } + +pub struct MoveStakeConfig<'a> { + pub source: (&'a Pubkey, &'a AccountSharedData), + pub destination: (&'a Pubkey, &'a AccountSharedData), + pub amount: u64, + /// Override signer for testing wrong signer scenarios (defaults to ctx.staker) + pub override_signer: Option<&'a Pubkey>, +} + +impl<'a> MoveStakeConfig<'a> { + /// Helper to get the default source vote account from context + pub fn with_default_vote(self, ctx: &'a StakeTestContext) -> MoveStakeWithVoteConfig<'a> { + MoveStakeWithVoteConfig { + source: self.source, + destination: self.destination, + override_signer: self.override_signer, + amount: self.amount, + source_vote: ( + ctx.vote_account.as_ref().expect("vote_account required"), + ctx.vote_account_data + .as_ref() + .expect("vote_account_data required"), + ), + dest_vote: None, + } + } +} + +impl InstructionConfig for MoveStakeConfig<'_> { + fn build_instruction(&self, ctx: &StakeTestContext) -> Instruction { + let signer = self.override_signer.unwrap_or(&ctx.staker); + ixn::move_stake(self.source.0, self.destination.0, signer, self.amount) + } + + fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)> { + vec![ + (*self.source.0, self.source.1.clone()), + (*self.destination.0, self.destination.1.clone()), + ] + } +} + +pub struct MoveStakeWithVoteConfig<'a> { + pub source: (&'a Pubkey, &'a AccountSharedData), + pub destination: (&'a Pubkey, &'a AccountSharedData), + pub amount: u64, + /// Override signer for testing wrong signer scenarios (defaults to ctx.staker) + pub override_signer: Option<&'a Pubkey>, + pub source_vote: (&'a Pubkey, &'a AccountSharedData), + pub dest_vote: Option<(&'a Pubkey, &'a AccountSharedData)>, +} + +impl InstructionConfig for MoveStakeWithVoteConfig<'_> { + fn build_instruction(&self, ctx: &StakeTestContext) -> Instruction { + let signer = self.override_signer.unwrap_or(&ctx.staker); + ixn::move_stake(self.source.0, self.destination.0, signer, self.amount) + } + + fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)> { + let mut accounts = vec![ + (*self.source.0, self.source.1.clone()), + (*self.destination.0, self.destination.1.clone()), + (*self.source_vote.0, self.source_vote.1.clone()), + ]; + if let Some((vote_pk, vote_acc)) = self.dest_vote { + accounts.push((*vote_pk, vote_acc.clone())); + } + accounts + } +} diff --git a/program/tests/move_stake.rs b/program/tests/move_stake.rs new file mode 100644 index 00000000..7d020e6f --- /dev/null +++ b/program/tests/move_stake.rs @@ -0,0 +1,457 @@ +#![allow(clippy::arithmetic_side_effects)] + +mod helpers; + +use { + helpers::{ + context::StakeTestContext, + instruction_builders::{MoveStakeConfig, MoveStakeWithVoteConfig}, + lifecycle::StakeLifecycle, + utils::{get_effective_stake, parse_stake_account, true_up_transient_stake_epoch}, + }, + mollusk_svm::result::Check, + solana_account::WritableAccount, + solana_program_error::ProgramError, + solana_stake_interface::{error::StakeError, state::Lockup}, + test_case::test_matrix, +}; + +#[test_matrix( + [StakeLifecycle::Initialized, StakeLifecycle::Activating, StakeLifecycle::Active, + StakeLifecycle::Deactivating, StakeLifecycle::Deactive], + [StakeLifecycle::Initialized, StakeLifecycle::Activating, StakeLifecycle::Active, + StakeLifecycle::Deactivating, StakeLifecycle::Deactive], + [false, true], + [false, true] +)] +fn test_move_stake( + move_source_type: StakeLifecycle, + move_dest_type: StakeLifecycle, + full_move: bool, + has_lockup: bool, +) { + let mut ctx = StakeTestContext::new(); + + // Source has 2x minimum so we can easily test partial moves + let source_staked_amount = ctx.minimum_delegation.unwrap() * 2; + + // This is the amount of *effective/activated* lamports for test assertions (not delegation amount) + // All dests are created with minimum_delegation, but only Active dests have it fully activated + let dest_staked_amount = if move_dest_type == StakeLifecycle::Active { + ctx.minimum_delegation.unwrap() + } else { + 0 // Non-Active destinations have 0 effective stake (Activating/Deactivating are transient) + }; + + // Test with and without lockup + let lockup = if has_lockup { + ctx.create_future_lockup(100) + } else { + Lockup::default() + }; + + // Extra lamports in each account to test they don't activate + let source_excess = ctx.minimum_delegation.unwrap(); + let dest_excess = ctx.minimum_delegation.unwrap(); + + // Create source and dest stakes + let min_delegation = ctx.minimum_delegation.unwrap(); + let (move_source, mut move_source_account) = ctx + .stake_account(move_source_type) + .staked_amount(source_staked_amount) + .lockup(&lockup) + .build(); + let (move_dest, mut move_dest_account) = ctx + .stake_account(move_dest_type) + .staked_amount(min_delegation) + .lockup(&lockup) + .build(); + + true_up_transient_stake_epoch( + &mut ctx.mollusk, + ctx.tracker.as_mut().unwrap(), + &move_source, + &mut move_source_account, + move_source_type, + ); + + true_up_transient_stake_epoch( + &mut ctx.mollusk, + ctx.tracker.as_mut().unwrap(), + &move_dest, + &mut move_dest_account, + move_dest_type, + ); + + // Add excess lamports + move_source_account + .checked_add_lamports(source_excess) + .unwrap(); + // Active accounts get additional excess on top of their staked amount + // Inactive accounts already have minimum_delegation as excess from creation + if move_dest_type == StakeLifecycle::Active { + move_dest_account.checked_add_lamports(dest_excess).unwrap(); + } + + // Check if this state combination is valid for MoveStake + match (move_source_type, move_dest_type) { + (StakeLifecycle::Active, StakeLifecycle::Initialized) + | (StakeLifecycle::Active, StakeLifecycle::Active) + | (StakeLifecycle::Active, StakeLifecycle::Deactive) => { + // Valid - continue with tests + } + _ => { + // Invalid state combination + let result = ctx + .process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_dest, &move_dest_account), + amount: if full_move { + source_staked_amount + } else { + ctx.minimum_delegation.unwrap() + }, + override_signer: None, + }) + .checks(&[]) + .test_missing_signers(false) + .execute(); + assert!(result.program_result.is_err()); + return; + } + } + + // The below checks need minimum_delegation > 1 + if ctx.minimum_delegation.unwrap() > 1 { + // Undershoot destination for inactive accounts + if move_dest_type != StakeLifecycle::Active { + ctx.process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_dest, &move_dest_account), + amount: ctx.minimum_delegation.unwrap() - 1, + override_signer: None, + }) + .checks(&[Check::err(ProgramError::InvalidArgument)]) + .test_missing_signers(false) + .execute(); + } + + // Overshoot source (would leave source underfunded) + ctx.process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_dest, &move_dest_account), + amount: ctx.minimum_delegation.unwrap() + 1, + override_signer: None, + }) + .checks(&[Check::err(ProgramError::InvalidArgument)]) + .test_missing_signers(false) + .execute(); + } + + let result = ctx + .process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_dest, &move_dest_account), + amount: if full_move { + source_staked_amount + } else { + ctx.minimum_delegation.unwrap() + }, + override_signer: None, + }) + .checks(&[Check::success()]) + .test_missing_signers(true) + .execute(); + + move_source_account = result.resulting_accounts[0].1.clone().into(); + move_dest_account = result.resulting_accounts[1].1.clone().into(); + + if full_move { + let (_, option_source_stake, source_lamports) = parse_stake_account(&move_source_account); + + // Source is deactivated and rent/excess stay behind + assert!(option_source_stake.is_none()); + assert_eq!(source_lamports, source_excess + ctx.rent_exempt_reserve); + + let (_, Some(dest_stake), dest_lamports) = parse_stake_account(&move_dest_account) else { + panic!("dest should be active") + }; + let dest_effective_stake = get_effective_stake(&ctx.mollusk, &move_dest_account); + + // Dest captured the entire source delegation, kept its rent/excess, didn't activate its excess + assert_eq!( + dest_stake.delegation.stake, + source_staked_amount + dest_staked_amount + ); + assert_eq!(dest_effective_stake, dest_stake.delegation.stake); + assert_eq!( + dest_lamports, + dest_effective_stake + dest_excess + ctx.rent_exempt_reserve + ); + } else { + let (_, Some(source_stake), source_lamports) = parse_stake_account(&move_source_account) + else { + panic!("source should be active") + }; + let source_effective_stake = get_effective_stake(&ctx.mollusk, &move_source_account); + + // Half of source delegation moved over, excess stayed behind + assert_eq!(source_stake.delegation.stake, source_staked_amount / 2); + assert_eq!(source_effective_stake, source_stake.delegation.stake); + assert_eq!( + source_lamports, + source_effective_stake + source_excess + ctx.rent_exempt_reserve + ); + + let (_, Some(dest_stake), dest_lamports) = parse_stake_account(&move_dest_account) else { + panic!("dest should be active") + }; + let dest_effective_stake = get_effective_stake(&ctx.mollusk, &move_dest_account); + + // Dest mirrors our observations + assert_eq!( + dest_stake.delegation.stake, + source_staked_amount / 2 + dest_staked_amount + ); + assert_eq!(dest_effective_stake, dest_stake.delegation.stake); + assert_eq!( + dest_lamports, + dest_effective_stake + dest_excess + ctx.rent_exempt_reserve + ); + } +} + +#[test_matrix( + [(StakeLifecycle::Active, StakeLifecycle::Uninitialized), + (StakeLifecycle::Uninitialized, StakeLifecycle::Initialized), + (StakeLifecycle::Uninitialized, StakeLifecycle::Uninitialized)] +)] +fn test_move_stake_uninitialized_fail(move_types: (StakeLifecycle, StakeLifecycle)) { + let mut ctx = StakeTestContext::new(); + let min_delegation = ctx.minimum_delegation.unwrap(); + let source_staked_amount = min_delegation * 2; + let (move_source_type, move_dest_type) = move_types; + + let (move_source, move_source_account) = ctx + .stake_account(move_source_type) + .staked_amount(source_staked_amount) + .build(); + let (move_dest, move_dest_account) = ctx.stake_account(move_dest_type).staked_amount(0).build(); + + let source_signer = if move_source_type == StakeLifecycle::Uninitialized { + move_source + } else { + ctx.staker + }; + + ctx.process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_dest, &move_dest_account), + override_signer: Some(&source_signer), + amount: min_delegation, + }) + .checks(&[Check::err(ProgramError::InvalidAccountData)]) + .test_missing_signers(false) + .execute(); +} + +#[test_matrix( + [StakeLifecycle::Initialized, StakeLifecycle::Active, StakeLifecycle::Deactive], + [StakeLifecycle::Initialized, StakeLifecycle::Activating, StakeLifecycle::Active, StakeLifecycle::Deactive] +)] +fn test_move_stake_general_fail(move_source_type: StakeLifecycle, move_dest_type: StakeLifecycle) { + let mut ctx = StakeTestContext::new(); + let min_delegation = ctx.minimum_delegation.unwrap(); + let source_staked_amount = min_delegation * 2; + + // Only test valid MoveStake combinations + if move_source_type != StakeLifecycle::Active || move_dest_type == StakeLifecycle::Activating { + return; + } + + let in_force_lockup = ctx.create_in_force_lockup(); + + // Create source + let (move_source, mut move_source_account) = ctx + .stake_account(move_source_type) + .staked_amount(source_staked_amount) + .build(); + move_source_account + .checked_add_lamports(min_delegation) + .unwrap(); + + // Self-move fails + ctx.process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_source, &move_source_account), + amount: min_delegation, + override_signer: None, + }) + .checks(&[Check::err(ProgramError::InvalidInstructionData)]) + .test_missing_signers(false) + .execute(); + + // Zero move fails + let (move_dest, move_dest_account) = ctx + .stake_account(move_dest_type) + .staked_amount(min_delegation) + .build(); + + ctx.process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_dest, &move_dest_account), + amount: 0, + override_signer: None, + }) + .checks(&[Check::err(ProgramError::InvalidArgument)]) + .test_missing_signers(false) + .execute(); + + // Sign with withdrawer fails + let withdrawer = ctx.withdrawer; + ctx.process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_dest, &move_dest_account), + amount: min_delegation, + override_signer: Some(&withdrawer), + }) + .checks(&[Check::err(ProgramError::MissingRequiredSignature)]) + .test_missing_signers(false) + .execute(); + + // Source lockup fails + let (move_locked_source, mut move_locked_source_account) = ctx + .stake_account(move_source_type) + .staked_amount(source_staked_amount) + .lockup(&in_force_lockup) + .build(); + move_locked_source_account + .checked_add_lamports(min_delegation) + .unwrap(); + + let (move_dest2, move_dest2_account) = ctx + .stake_account(move_dest_type) + .staked_amount(min_delegation) + .build(); + + ctx.process_with(MoveStakeConfig { + source: (&move_locked_source, &move_locked_source_account), + destination: (&move_dest2, &move_dest2_account), + amount: min_delegation, + override_signer: None, + }) + .checks(&[Check::err(StakeError::MergeMismatch.into())]) + .test_missing_signers(false) + .execute(); + + // Staker mismatch fails + let throwaway_staker = solana_pubkey::Pubkey::new_unique(); + let (move_dest3, move_dest3_account) = ctx + .stake_account(move_dest_type) + .staked_amount(min_delegation) + .stake_authority(&throwaway_staker) + .withdraw_authority(&withdrawer) + .build(); + + ctx.process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_dest3, &move_dest3_account), + amount: min_delegation, + override_signer: None, + }) + .checks(&[Check::err(StakeError::MergeMismatch.into())]) + .test_missing_signers(false) + .execute(); + + // Also verify signing with dest's staker fails (wrong signer for source) + ctx.process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_dest3, &move_dest3_account), + amount: min_delegation, + override_signer: Some(&throwaway_staker), + }) + .checks(&[Check::err(ProgramError::MissingRequiredSignature)]) + .test_missing_signers(false) + .execute(); + + // Withdrawer mismatch fails + let throwaway_withdrawer = solana_pubkey::Pubkey::new_unique(); + let staker = ctx.staker; + let (move_dest4, move_dest4_account) = ctx + .stake_account(move_dest_type) + .staked_amount(min_delegation) + .stake_authority(&staker) + .withdraw_authority(&throwaway_withdrawer) + .build(); + + ctx.process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_dest4, &move_dest4_account), + amount: min_delegation, + override_signer: None, + }) + .checks(&[Check::err(StakeError::MergeMismatch.into())]) + .test_missing_signers(false) + .execute(); + + // Also verify signing with dest's withdrawer fails (wrong signer for source) + ctx.process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_dest4, &move_dest4_account), + amount: min_delegation, + override_signer: Some(&throwaway_withdrawer), + }) + .checks(&[Check::err(ProgramError::MissingRequiredSignature)]) + .test_missing_signers(false) + .execute(); + + // Dest lockup fails + let (move_dest5, move_dest5_account) = ctx + .stake_account(move_dest_type) + .staked_amount(min_delegation) + .lockup(&in_force_lockup) + .build(); + + ctx.process_with(MoveStakeConfig { + source: (&move_source, &move_source_account), + destination: (&move_dest5, &move_dest5_account), + amount: min_delegation, + override_signer: None, + }) + .checks(&[Check::err(StakeError::MergeMismatch.into())]) + .test_missing_signers(false) + .execute(); + + // Different vote accounts for active dest + if move_dest_type == StakeLifecycle::Active { + let (dest_vote_account, dest_vote_account_data) = ctx.create_second_vote_account(); + + let move_dest6_pubkey = solana_pubkey::Pubkey::new_unique(); + let (_, move_dest6_account) = ctx + .stake_account(move_dest_type) + .staked_amount(min_delegation) + .vote_account(&dest_vote_account) + .stake_pubkey(&move_dest6_pubkey) + .build(); + + let (move_source2, move_source2_account) = ctx + .stake_account(move_source_type) + .staked_amount(source_staked_amount) + .build(); + + let staker = ctx.staker; + let vote_account = ctx.vote_account.unwrap(); + let vote_account_data = ctx.vote_account_data.as_ref().unwrap(); + ctx.process_with(MoveStakeWithVoteConfig { + source: (&move_source2, &move_source2_account), + destination: (&move_dest6_pubkey, &move_dest6_account), + override_signer: Some(&staker), + amount: min_delegation, + source_vote: (&vote_account, vote_account_data), + dest_vote: Some((&dest_vote_account, &dest_vote_account_data)), + }) + .checks(&[Check::err(StakeError::VoteAddressMismatch.into())]) + .test_missing_signers(false) + .execute(); + } +} diff --git a/program/tests/program_test.rs b/program/tests/program_test.rs index 9686f588..ab78e415 100644 --- a/program/tests/program_test.rs +++ b/program/tests/program_test.rs @@ -23,7 +23,6 @@ use { instruction as vote_instruction, state::{VoteInit, VoteStateV4}, }, - test_case::test_matrix, }; pub const USER_STARTING_LAMPORTS: u64 = 10_000_000_000_000; // 10k sol @@ -203,28 +202,6 @@ pub async fn get_effective_stake(banks_client: &mut BanksClient, pubkey: &Pubkey } } -async fn get_minimum_delegation(context: &mut ProgramTestContext) -> u64 { - let transaction = Transaction::new_signed_with_payer( - &[ixn::get_minimum_delegation()], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let mut data = context - .banks_client - .simulate_transaction(transaction) - .await - .unwrap() - .simulation_details - .unwrap() - .return_data - .unwrap() - .data; - data.resize(8, 0); - - data.try_into().map(u64::from_le_bytes).unwrap() -} - pub async fn create_independent_stake_account( context: &mut ProgramTestContext, authorized: &Authorized, @@ -748,256 +725,3 @@ impl StakeLifecycle { } } } - -#[test_matrix( - [StakeLifecycle::Initialized, StakeLifecycle::Activating, StakeLifecycle::Active, - StakeLifecycle::Deactivating, StakeLifecycle::Deactive], - [StakeLifecycle::Initialized, StakeLifecycle::Activating, StakeLifecycle::Active, - StakeLifecycle::Deactivating, StakeLifecycle::Deactive], - [false, true], - [false, true] -)] -#[tokio::test] -async fn program_test_move_stake( - move_source_type: StakeLifecycle, - move_dest_type: StakeLifecycle, - full_move: bool, - has_lockup: bool, -) { - let mut context = program_test().start_with_context().await; - let accounts = Accounts::default(); - accounts.initialize(&mut context).await; - - let rent_exempt_reserve = get_stake_account_rent(&mut context.banks_client).await; - let minimum_delegation = get_minimum_delegation(&mut context).await; - - // source has 2x minimum so we can easily test an unfunded destination - let source_staked_amount = minimum_delegation * 2; - - // this is the amount of *staked* lamports for test checks - // destinations may have excess lamports but these are *never* activated by move - let dest_staked_amount = if move_dest_type == StakeLifecycle::Active { - minimum_delegation - } else { - 0 - }; - - // test with and without lockup. both of these cases pass, we test failures - // elsewhere - let lockup = if has_lockup { - let clock = context.banks_client.get_sysvar::().await.unwrap(); - let lockup = Lockup { - unix_timestamp: 0, - epoch: clock.epoch + 100, - custodian: Pubkey::new_unique(), - }; - - assert!(lockup.is_in_force(&clock, None)); - lockup - } else { - Lockup::default() - }; - - // we put an extra minimum in every account, unstaked, to test that no new - // lamports activate name them here so our asserts are readable - let source_excess = minimum_delegation; - let dest_excess = minimum_delegation; - - let move_source_keypair = Keypair::new(); - let move_dest_keypair = Keypair::new(); - let staker_keypair = Keypair::new(); - let withdrawer_keypair = Keypair::new(); - - // create source stake - move_source_type - .new_stake_account_fully_specified( - &mut context, - &accounts.vote_account.pubkey(), - source_staked_amount, - &move_source_keypair, - &staker_keypair, - &withdrawer_keypair, - &lockup, - ) - .await; - let move_source = move_source_keypair.pubkey(); - let mut source_account = get_account(&mut context.banks_client, &move_source).await; - let mut source_stake_state: StakeStateV2 = bincode::deserialize(&source_account.data).unwrap(); - - // create dest stake with same authorities - move_dest_type - .new_stake_account_fully_specified( - &mut context, - &accounts.vote_account.pubkey(), - minimum_delegation, - &move_dest_keypair, - &staker_keypair, - &withdrawer_keypair, - &lockup, - ) - .await; - let move_dest = move_dest_keypair.pubkey(); - - // true up source epoch if transient - if move_source_type == StakeLifecycle::Activating - || move_source_type == StakeLifecycle::Deactivating - { - let clock = context.banks_client.get_sysvar::().await.unwrap(); - if let StakeStateV2::Stake(_, ref mut stake, _) = &mut source_stake_state { - match move_source_type { - StakeLifecycle::Activating => stake.delegation.activation_epoch = clock.epoch, - StakeLifecycle::Deactivating => stake.delegation.deactivation_epoch = clock.epoch, - _ => (), - } - } - - source_account.data = bincode::serialize(&source_stake_state).unwrap(); - context.set_account(&move_source, &source_account.into()); - } - - // our inactive accounts have extra lamports, lets not let active feel left out - if move_dest_type == StakeLifecycle::Active { - transfer(&mut context, &move_dest, dest_excess).await; - } - - // hey why not spread the love around to everyone - transfer(&mut context, &move_source, source_excess).await; - - // alright first things first, clear out all the state failures - match (move_source_type, move_dest_type) { - // valid - (StakeLifecycle::Active, StakeLifecycle::Initialized) - | (StakeLifecycle::Active, StakeLifecycle::Active) - | (StakeLifecycle::Active, StakeLifecycle::Deactive) => (), - // invalid! get outta my test - _ => { - let instruction = ixn::move_stake( - &move_source, - &move_dest, - &staker_keypair.pubkey(), - if full_move { - source_staked_amount - } else { - minimum_delegation - }, - ); - - // this is InvalidAccountData sometimes and Custom(5) sometimes but i dont care - process_instruction(&mut context, &instruction, &vec![&staker_keypair]) - .await - .unwrap_err(); - return; - } - } - - // the below checks are conceptually incoherent with a 1 lamport minimum - // the undershoot fails successfully (but because its a zero move, not because - // the destination ends underfunded) then the second one succeeds failedly - // (because its a full move, so the "underfunded" source is actually closed) - if minimum_delegation > 1 { - // first for inactive accounts lets undershoot and fail for underfunded dest - if move_dest_type != StakeLifecycle::Active { - let instruction = ixn::move_stake( - &move_source, - &move_dest, - &staker_keypair.pubkey(), - minimum_delegation - 1, - ); - - let e = process_instruction(&mut context, &instruction, &vec![&staker_keypair]) - .await - .unwrap_err(); - assert_eq!(e, ProgramError::InvalidArgument); - } - - // now lets overshoot and fail for underfunded source - let instruction = ixn::move_stake( - &move_source, - &move_dest, - &staker_keypair.pubkey(), - minimum_delegation + 1, - ); - - let e = process_instruction(&mut context, &instruction, &vec![&staker_keypair]) - .await - .unwrap_err(); - assert_eq!(e, ProgramError::InvalidArgument); - } - - // now we do it juuust right - let instruction = ixn::move_stake( - &move_source, - &move_dest, - &staker_keypair.pubkey(), - if full_move { - source_staked_amount - } else { - minimum_delegation - }, - ); - - process_instruction_test_missing_signers(&mut context, &instruction, &vec![&staker_keypair]) - .await; - - if full_move { - let (_, option_source_stake, source_lamports) = - get_stake_account(&mut context.banks_client, &move_source).await; - - // source is deactivated and rent/excess stay behind - assert!(option_source_stake.is_none()); - assert_eq!(source_lamports, source_excess + rent_exempt_reserve); - - let (_, Some(dest_stake), dest_lamports) = - get_stake_account(&mut context.banks_client, &move_dest).await - else { - panic!("dest should be active") - }; - let dest_effective_stake = get_effective_stake(&mut context.banks_client, &move_dest).await; - - // dest captured the entire source delegation, kept its rent/excess, didnt - // activate its excess - assert_eq!( - dest_stake.delegation.stake, - source_staked_amount + dest_staked_amount - ); - assert_eq!(dest_effective_stake, dest_stake.delegation.stake); - assert_eq!( - dest_lamports, - dest_effective_stake + dest_excess + rent_exempt_reserve - ); - } else { - let (_, Some(source_stake), source_lamports) = - get_stake_account(&mut context.banks_client, &move_source).await - else { - panic!("source should be active") - }; - let source_effective_stake = - get_effective_stake(&mut context.banks_client, &move_source).await; - - // half of source delegation moved over, excess stayed behind - assert_eq!(source_stake.delegation.stake, source_staked_amount / 2); - assert_eq!(source_effective_stake, source_stake.delegation.stake); - assert_eq!( - source_lamports, - source_effective_stake + source_excess + rent_exempt_reserve - ); - - let (_, Some(dest_stake), dest_lamports) = - get_stake_account(&mut context.banks_client, &move_dest).await - else { - panic!("dest should be active") - }; - let dest_effective_stake = get_effective_stake(&mut context.banks_client, &move_dest).await; - - // dest mirrors our observations - assert_eq!( - dest_stake.delegation.stake, - source_staked_amount / 2 + dest_staked_amount - ); - assert_eq!(dest_effective_stake, dest_stake.delegation.stake); - assert_eq!( - dest_lamports, - dest_effective_stake + dest_excess + rent_exempt_reserve - ); - } -}