Skip to content

Commit 647046f

Browse files
committed
migrate Split tests
1 parent 7d34dd5 commit 647046f

File tree

4 files changed

+229
-150
lines changed

4 files changed

+229
-150
lines changed

program/tests/helpers/instruction_builders.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,24 @@ impl InstructionConfig for DelegateConfig<'_> {
138138
]
139139
}
140140
}
141+
142+
pub struct SplitConfig<'a> {
143+
pub source: (&'a Pubkey, &'a AccountSharedData),
144+
pub destination: (&'a Pubkey, &'a AccountSharedData),
145+
pub amount: u64,
146+
pub signer: &'a Pubkey,
147+
}
148+
149+
impl InstructionConfig for SplitConfig<'_> {
150+
fn build_instruction(&self, _ctx: &StakeTestContext) -> Instruction {
151+
let instructions = ixn::split(self.source.0, self.signer, self.amount, self.destination.0);
152+
instructions[2].clone() // The actual split instruction
153+
}
154+
155+
fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)> {
156+
vec![
157+
(*self.source.0, self.source.1.clone()),
158+
(*self.destination.0, self.destination.1.clone()),
159+
]
160+
}
161+
}

program/tests/helpers/utils.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,21 @@ pub fn increment_vote_account_credits(
108108

109109
vote_account.set_data(bincode::serialize(&vote_state).unwrap());
110110
}
111+
112+
/// Get the effective stake for an account
113+
pub fn get_effective_stake(mollusk: &Mollusk, stake_account: &AccountSharedData) -> u64 {
114+
let stake_state: StakeStateV2 = bincode::deserialize(stake_account.data()).unwrap();
115+
116+
if let StakeStateV2::Stake(_, stake, _) = stake_state {
117+
stake
118+
.delegation
119+
.stake_activating_and_deactivating(
120+
mollusk.sysvars.clock.epoch,
121+
&mollusk.sysvars.stake_history,
122+
Some(0),
123+
)
124+
.effective
125+
} else {
126+
0
127+
}
128+
}

program/tests/program_test.rs

Lines changed: 0 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -740,156 +740,6 @@ impl StakeLifecycle {
740740
}
741741
}
742742

743-
#[test_case(StakeLifecycle::Uninitialized; "uninitialized")]
744-
#[test_case(StakeLifecycle::Initialized; "initialized")]
745-
#[test_case(StakeLifecycle::Activating; "activating")]
746-
#[test_case(StakeLifecycle::Active; "active")]
747-
#[test_case(StakeLifecycle::Deactivating; "deactivating")]
748-
#[test_case(StakeLifecycle::Deactive; "deactive")]
749-
#[tokio::test]
750-
async fn program_test_split(split_source_type: StakeLifecycle) {
751-
let mut context = program_test().start_with_context().await;
752-
let accounts = Accounts::default();
753-
accounts.initialize(&mut context).await;
754-
755-
let rent_exempt_reserve = get_stake_account_rent(&mut context.banks_client).await;
756-
let minimum_delegation = get_minimum_delegation(&mut context).await;
757-
let staked_amount = minimum_delegation * 2;
758-
759-
let (split_source_keypair, staker_keypair, _) = split_source_type
760-
.new_stake_account(&mut context, &accounts.vote_account.pubkey(), staked_amount)
761-
.await;
762-
763-
let split_source = split_source_keypair.pubkey();
764-
let split_dest = create_blank_stake_account(&mut context).await;
765-
766-
let signers = match split_source_type {
767-
StakeLifecycle::Uninitialized => vec![&split_source_keypair],
768-
_ => vec![&staker_keypair],
769-
};
770-
771-
// fail, split more than available (even if not active, would kick source out of
772-
// rent exemption)
773-
let instruction = &ixn::split(
774-
&split_source,
775-
&signers[0].pubkey(),
776-
staked_amount + 1,
777-
&split_dest,
778-
)[2];
779-
780-
let e = process_instruction(&mut context, instruction, &signers)
781-
.await
782-
.unwrap_err();
783-
assert_eq!(e, ProgramError::InsufficientFunds);
784-
785-
// an active or transitioning stake account cannot have less than the minimum
786-
// delegation note this is NOT dependent on the minimum delegation feature.
787-
// there was ALWAYS a minimum. it was one lamport!
788-
if split_source_type.split_minimum_enforced() {
789-
// zero split fails
790-
let instruction = &ixn::split(&split_source, &signers[0].pubkey(), 0, &split_dest)[2];
791-
let e = process_instruction(&mut context, instruction, &signers)
792-
.await
793-
.unwrap_err();
794-
assert_eq!(e, ProgramError::InsufficientFunds);
795-
796-
// underfunded destination fails
797-
let instruction = &ixn::split(
798-
&split_source,
799-
&signers[0].pubkey(),
800-
minimum_delegation - 1,
801-
&split_dest,
802-
)[2];
803-
804-
let e = process_instruction(&mut context, instruction, &signers)
805-
.await
806-
.unwrap_err();
807-
assert_eq!(e, ProgramError::InsufficientFunds);
808-
809-
// underfunded source fails
810-
let instruction = &ixn::split(
811-
&split_source,
812-
&signers[0].pubkey(),
813-
minimum_delegation + 1,
814-
&split_dest,
815-
)[2];
816-
817-
let e = process_instruction(&mut context, instruction, &signers)
818-
.await
819-
.unwrap_err();
820-
assert_eq!(e, ProgramError::InsufficientFunds);
821-
}
822-
823-
// split to non-owned account fails
824-
let mut fake_split_dest_account = get_account(&mut context.banks_client, &split_dest).await;
825-
fake_split_dest_account.owner = Pubkey::new_unique();
826-
let fake_split_dest = Pubkey::new_unique();
827-
context.set_account(&fake_split_dest, &fake_split_dest_account.into());
828-
829-
let instruction = &ixn::split(
830-
&split_source,
831-
&signers[0].pubkey(),
832-
staked_amount / 2,
833-
&fake_split_dest,
834-
)[2];
835-
836-
let e = process_instruction(&mut context, instruction, &signers)
837-
.await
838-
.unwrap_err();
839-
assert_eq!(e, ProgramError::InvalidAccountOwner);
840-
841-
// success
842-
let instruction = &ixn::split(
843-
&split_source,
844-
&signers[0].pubkey(),
845-
staked_amount / 2,
846-
&split_dest,
847-
)[2];
848-
process_instruction_test_missing_signers(&mut context, instruction, &signers).await;
849-
850-
// source lost split amount
851-
let source_lamports = get_account(&mut context.banks_client, &split_source)
852-
.await
853-
.lamports;
854-
assert_eq!(source_lamports, staked_amount / 2 + rent_exempt_reserve);
855-
856-
// destination gained split amount
857-
let dest_lamports = get_account(&mut context.banks_client, &split_dest)
858-
.await
859-
.lamports;
860-
assert_eq!(dest_lamports, staked_amount / 2 + rent_exempt_reserve);
861-
862-
// destination meta has been set properly if ever delegated
863-
if split_source_type >= StakeLifecycle::Initialized {
864-
let (source_meta, source_stake, _) =
865-
get_stake_account(&mut context.banks_client, &split_source).await;
866-
let (dest_meta, dest_stake, _) =
867-
get_stake_account(&mut context.banks_client, &split_dest).await;
868-
assert_eq!(dest_meta, source_meta);
869-
870-
// delegations are set properly if activating or active
871-
if split_source_type >= StakeLifecycle::Activating
872-
&& split_source_type < StakeLifecycle::Deactive
873-
{
874-
assert_eq!(source_stake.unwrap().delegation.stake, staked_amount / 2);
875-
assert_eq!(dest_stake.unwrap().delegation.stake, staked_amount / 2);
876-
}
877-
}
878-
879-
// nothing has been deactivated if active
880-
if split_source_type >= StakeLifecycle::Active && split_source_type < StakeLifecycle::Deactive {
881-
assert_eq!(
882-
get_effective_stake(&mut context.banks_client, &split_source).await,
883-
staked_amount / 2,
884-
);
885-
886-
assert_eq!(
887-
get_effective_stake(&mut context.banks_client, &split_dest).await,
888-
staked_amount / 2,
889-
);
890-
}
891-
}
892-
893743
#[test_case(StakeLifecycle::Uninitialized; "uninitialized")]
894744
#[test_case(StakeLifecycle::Initialized; "initialized")]
895745
#[test_case(StakeLifecycle::Activating; "activating")]

program/tests/split.rs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#![allow(clippy::arithmetic_side_effects)]
2+
3+
mod helpers;
4+
5+
use {
6+
helpers::{
7+
context::StakeTestContext,
8+
instruction_builders::SplitConfig,
9+
lifecycle::StakeLifecycle,
10+
utils::{get_effective_stake, parse_stake_account},
11+
},
12+
mollusk_svm::result::Check,
13+
solana_account::{AccountSharedData, WritableAccount},
14+
solana_program_error::ProgramError,
15+
solana_pubkey::Pubkey,
16+
solana_stake_interface::state::StakeStateV2,
17+
solana_stake_program::id,
18+
test_case::test_case,
19+
};
20+
21+
#[test_case(StakeLifecycle::Uninitialized; "uninitialized")]
22+
#[test_case(StakeLifecycle::Initialized; "initialized")]
23+
#[test_case(StakeLifecycle::Activating; "activating")]
24+
#[test_case(StakeLifecycle::Active; "active")]
25+
#[test_case(StakeLifecycle::Deactivating; "deactivating")]
26+
#[test_case(StakeLifecycle::Deactive; "deactive")]
27+
fn test_split(split_source_type: StakeLifecycle) {
28+
let mut ctx = StakeTestContext::new();
29+
let staked_amount = ctx.minimum_delegation.unwrap() * 2;
30+
31+
// Create source stake account at the specified lifecycle stage
32+
let (split_source, mut split_source_account) = ctx
33+
.stake_account(split_source_type)
34+
.staked_amount(staked_amount)
35+
.build();
36+
37+
// Create destination stake account matching what create_blank_stake_account does:
38+
// rent-exempt lamports, correct size, stake program owner, uninitialized data
39+
let split_dest = Pubkey::new_unique();
40+
let split_dest_account =
41+
AccountSharedData::new(ctx.rent_exempt_reserve, StakeStateV2::size_of(), &id());
42+
43+
// Determine signer based on lifecycle stage
44+
let signer = if split_source_type == StakeLifecycle::Uninitialized {
45+
split_source
46+
} else {
47+
ctx.staker
48+
};
49+
50+
// Fail: split more than available (would violate rent exemption)
51+
// Note: Behavior differs between program-test and Mollusk:
52+
// - program-test: Transaction-level rent check returns InsufficientFunds before program runs
53+
// - Mollusk: Program succeeds for uninitialized (no program-level check), but violates rent
54+
// For initialized+ accounts, the program itself checks and returns InsufficientFunds
55+
if split_source_type == StakeLifecycle::Uninitialized {
56+
// Mollusk: Program succeeds, but resulting accounts violate rent exemption
57+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
58+
ctx.process_with(SplitConfig {
59+
source: (&split_source, &split_source_account),
60+
destination: (&split_dest, &split_dest_account),
61+
signer: &signer,
62+
amount: staked_amount + 1,
63+
})
64+
.checks(&[Check::success(), Check::all_rent_exempt()])
65+
.execute()
66+
}));
67+
assert!(
68+
result.is_err(),
69+
"Expected rent exemption check to fail for uninitialized split"
70+
);
71+
} else {
72+
// Program-level check returns InsufficientFunds for initialized+ accounts
73+
ctx.process_with(SplitConfig {
74+
source: (&split_source, &split_source_account),
75+
destination: (&split_dest, &split_dest_account),
76+
signer: &signer,
77+
amount: staked_amount + 1,
78+
})
79+
.checks(&[Check::err(ProgramError::InsufficientFunds)])
80+
.test_missing_signers(false)
81+
.execute();
82+
}
83+
84+
// Test minimum delegation enforcement for active/transitioning stakes
85+
if split_source_type.split_minimum_enforced() {
86+
// Zero split fails
87+
ctx.process_with(SplitConfig {
88+
source: (&split_source, &split_source_account),
89+
destination: (&split_dest, &split_dest_account),
90+
signer: &signer,
91+
amount: 0,
92+
})
93+
.checks(&[Check::err(ProgramError::InsufficientFunds)])
94+
.test_missing_signers(false)
95+
.execute();
96+
97+
// Underfunded destination fails
98+
ctx.process_with(SplitConfig {
99+
source: (&split_source, &split_source_account),
100+
destination: (&split_dest, &split_dest_account),
101+
signer: &signer,
102+
amount: ctx.minimum_delegation.unwrap() - 1,
103+
})
104+
.checks(&[Check::err(ProgramError::InsufficientFunds)])
105+
.test_missing_signers(false)
106+
.execute();
107+
108+
// Underfunded source fails
109+
ctx.process_with(SplitConfig {
110+
source: (&split_source, &split_source_account),
111+
destination: (&split_dest, &split_dest_account),
112+
signer: &signer,
113+
amount: ctx.minimum_delegation.unwrap() + 1,
114+
})
115+
.checks(&[Check::err(ProgramError::InsufficientFunds)])
116+
.test_missing_signers(false)
117+
.execute();
118+
}
119+
120+
// Split to account with wrong owner fails
121+
let fake_split_dest = Pubkey::new_unique();
122+
let mut fake_split_dest_account = split_dest_account.clone();
123+
fake_split_dest_account.set_owner(Pubkey::new_unique());
124+
125+
ctx.process_with(SplitConfig {
126+
source: (&split_source, &split_source_account),
127+
destination: (&fake_split_dest, &fake_split_dest_account),
128+
signer: &signer,
129+
amount: staked_amount / 2,
130+
})
131+
.checks(&[Check::err(ProgramError::InvalidAccountOwner)])
132+
.test_missing_signers(false)
133+
.execute();
134+
135+
// Success: split half
136+
let result = ctx
137+
.process_with(SplitConfig {
138+
source: (&split_source, &split_source_account),
139+
destination: (&split_dest, &split_dest_account),
140+
signer: &signer,
141+
amount: staked_amount / 2,
142+
})
143+
.checks(&[
144+
Check::success(),
145+
Check::all_rent_exempt(),
146+
Check::account(&split_source)
147+
.lamports(staked_amount / 2 + ctx.rent_exempt_reserve)
148+
.owner(&id())
149+
.space(StakeStateV2::size_of())
150+
.build(),
151+
Check::account(&split_dest)
152+
.lamports(staked_amount / 2 + ctx.rent_exempt_reserve)
153+
.owner(&id())
154+
.space(StakeStateV2::size_of())
155+
.build(),
156+
])
157+
.test_missing_signers(true)
158+
.execute();
159+
160+
split_source_account = result.resulting_accounts[0].1.clone().into();
161+
let split_dest_account: AccountSharedData = result.resulting_accounts[1].1.clone().into();
162+
163+
// Verify metadata is copied for initialized and above
164+
if split_source_type >= StakeLifecycle::Initialized {
165+
let (source_meta, source_stake, _) = parse_stake_account(&split_source_account);
166+
let (dest_meta, dest_stake, _) = parse_stake_account(&split_dest_account);
167+
assert_eq!(dest_meta, source_meta);
168+
169+
// Verify delegations are set properly for activating/active/deactivating
170+
if split_source_type >= StakeLifecycle::Activating
171+
&& split_source_type < StakeLifecycle::Deactive
172+
{
173+
assert_eq!(source_stake.unwrap().delegation.stake, staked_amount / 2);
174+
assert_eq!(dest_stake.unwrap().delegation.stake, staked_amount / 2);
175+
}
176+
}
177+
178+
// Verify nothing has been deactivated for active stakes
179+
if split_source_type >= StakeLifecycle::Active && split_source_type < StakeLifecycle::Deactive {
180+
assert_eq!(
181+
get_effective_stake(&ctx.mollusk, &split_source_account),
182+
staked_amount / 2,
183+
);
184+
185+
assert_eq!(
186+
get_effective_stake(&ctx.mollusk, &split_dest_account),
187+
staked_amount / 2,
188+
);
189+
}
190+
}

0 commit comments

Comments
 (0)