Skip to content

Commit 78cd9d4

Browse files
committed
migrate Split tests
1 parent a135cb1 commit 78cd9d4

File tree

4 files changed

+229
-132
lines changed

4 files changed

+229
-132
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/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+
}

program/tests/stake_instruction.rs

Lines changed: 0 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -2288,138 +2288,6 @@ fn test_redelegate_consider_balance_changes() {
22882288
);
22892289
}
22902290

2291-
#[test]
2292-
fn test_split() {
2293-
let mollusk = mollusk_bpf();
2294-
2295-
let stake_history = StakeHistory::default();
2296-
let current_epoch = 100;
2297-
let clock = Clock {
2298-
epoch: current_epoch,
2299-
..Clock::default()
2300-
};
2301-
let stake_address = solana_pubkey::new_rand();
2302-
let minimum_delegation = crate::get_minimum_delegation();
2303-
let stake_lamports = minimum_delegation * 2;
2304-
let split_to_address = solana_pubkey::new_rand();
2305-
let split_to_account = AccountSharedData::new_data_with_space(
2306-
0,
2307-
&StakeStateV2::Uninitialized,
2308-
StakeStateV2::size_of(),
2309-
&id(),
2310-
)
2311-
.unwrap();
2312-
let mut transaction_accounts = vec![
2313-
(stake_address, AccountSharedData::default()),
2314-
(split_to_address, split_to_account.clone()),
2315-
(
2316-
rent::id(),
2317-
create_account_shared_data_for_test(&Rent {
2318-
lamports_per_byte_year: 0,
2319-
..Rent::default()
2320-
}),
2321-
),
2322-
(
2323-
StakeHistory::id(),
2324-
create_account_shared_data_for_test(&stake_history),
2325-
),
2326-
(clock::id(), create_account_shared_data_for_test(&clock)),
2327-
(
2328-
epoch_schedule::id(),
2329-
create_account_shared_data_for_test(&EpochSchedule::default()),
2330-
),
2331-
];
2332-
let instruction_accounts = vec![
2333-
AccountMeta {
2334-
pubkey: stake_address,
2335-
is_signer: true,
2336-
is_writable: true,
2337-
},
2338-
AccountMeta {
2339-
pubkey: split_to_address,
2340-
is_signer: false,
2341-
is_writable: true,
2342-
},
2343-
];
2344-
2345-
for state in [
2346-
StakeStateV2::Initialized(Meta::auto(&stake_address)),
2347-
just_stake(Meta::auto(&stake_address), stake_lamports),
2348-
] {
2349-
let stake_account = AccountSharedData::new_data_with_space(
2350-
stake_lamports,
2351-
&state,
2352-
StakeStateV2::size_of(),
2353-
&id(),
2354-
)
2355-
.unwrap();
2356-
let expected_active_stake = get_active_stake_for_tests(
2357-
&[stake_account.clone(), split_to_account.clone()],
2358-
&clock,
2359-
&stake_history,
2360-
);
2361-
transaction_accounts[0] = (stake_address, stake_account);
2362-
2363-
// should fail, split more than available
2364-
process_instruction(
2365-
&mollusk,
2366-
&serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(),
2367-
transaction_accounts.clone(),
2368-
instruction_accounts.clone(),
2369-
Err(ProgramError::InsufficientFunds),
2370-
);
2371-
2372-
// should pass
2373-
let accounts = process_instruction(
2374-
&mollusk,
2375-
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
2376-
transaction_accounts.clone(),
2377-
instruction_accounts.clone(),
2378-
Ok(()),
2379-
);
2380-
// no lamport leakage
2381-
assert_eq!(
2382-
accounts[0].lamports() + accounts[1].lamports(),
2383-
stake_lamports
2384-
);
2385-
2386-
// no deactivated stake
2387-
assert_eq!(
2388-
expected_active_stake,
2389-
get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history)
2390-
);
2391-
2392-
assert_eq!(from(&accounts[0]).unwrap(), from(&accounts[1]).unwrap());
2393-
match state {
2394-
StakeStateV2::Initialized(_meta) => {
2395-
assert_eq!(from(&accounts[0]).unwrap(), state);
2396-
}
2397-
StakeStateV2::Stake(_meta, _stake, _) => {
2398-
let stake_0 = from(&accounts[0]).unwrap().stake();
2399-
assert_eq!(stake_0.unwrap().delegation.stake, stake_lamports / 2);
2400-
}
2401-
_ => unreachable!(),
2402-
}
2403-
}
2404-
2405-
// should fail, fake owner of destination
2406-
let split_to_account = AccountSharedData::new_data_with_space(
2407-
0,
2408-
&StakeStateV2::Uninitialized,
2409-
StakeStateV2::size_of(),
2410-
&solana_pubkey::new_rand(),
2411-
)
2412-
.unwrap();
2413-
transaction_accounts[1] = (split_to_address, split_to_account);
2414-
process_instruction(
2415-
&mollusk,
2416-
&serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(),
2417-
transaction_accounts,
2418-
instruction_accounts,
2419-
Err(ProgramError::InvalidAccountOwner),
2420-
);
2421-
}
2422-
24232291
#[test]
24242292
fn test_withdraw_stake() {
24252293
let mollusk = mollusk_bpf();

0 commit comments

Comments
 (0)