Skip to content

Commit b5a94f0

Browse files
committed
migrate Deactivate tests
1 parent 2ab4655 commit b5a94f0

File tree

6 files changed

+440
-99
lines changed

6 files changed

+440
-99
lines changed

program/tests/deactivate.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#![allow(clippy::arithmetic_side_effects)]
2+
3+
mod helpers;
4+
5+
use {
6+
helpers::{
7+
context::StakeTestContext,
8+
instruction_builders::{DeactivateConfig, DelegateConfig},
9+
lifecycle::StakeLifecycle,
10+
utils::parse_stake_account,
11+
},
12+
mollusk_svm::result::Check,
13+
solana_program_error::ProgramError,
14+
solana_stake_interface::{error::StakeError, state::StakeStateV2},
15+
solana_stake_program::id,
16+
test_case::test_case,
17+
};
18+
19+
#[test_case(false; "activating")]
20+
#[test_case(true; "active")]
21+
fn test_deactivate(activate: bool) {
22+
let mut ctx = StakeTestContext::with_delegation();
23+
let min_delegation = ctx.minimum_delegation.unwrap();
24+
25+
let (stake, mut stake_account) = ctx
26+
.stake_account(StakeLifecycle::Initialized)
27+
.staked_amount(min_delegation)
28+
.build();
29+
30+
// Deactivating an undelegated account fails
31+
ctx.process_with(DeactivateConfig {
32+
stake: (&stake, &stake_account),
33+
override_signer: None,
34+
})
35+
.checks(&[Check::err(ProgramError::InvalidAccountData)])
36+
.test_missing_signers(false)
37+
.execute();
38+
39+
// Delegate
40+
let result = ctx
41+
.process_with(DelegateConfig {
42+
stake: (&stake, &stake_account),
43+
vote: (
44+
ctx.vote_account.as_ref().unwrap(),
45+
ctx.vote_account_data.as_ref().unwrap(),
46+
),
47+
})
48+
.execute();
49+
stake_account = result.resulting_accounts[0].1.clone().into();
50+
51+
if activate {
52+
// Advance epoch to activate
53+
let current_slot = ctx.mollusk.sysvars.clock.slot;
54+
let slots_per_epoch = ctx.mollusk.sysvars.epoch_schedule.slots_per_epoch;
55+
ctx.mollusk.warp_to_slot(current_slot + slots_per_epoch);
56+
}
57+
58+
// Deactivate with withdrawer fails
59+
ctx.process_with(DeactivateConfig {
60+
stake: (&stake, &stake_account),
61+
override_signer: Some(&ctx.withdrawer),
62+
})
63+
.checks(&[Check::err(ProgramError::MissingRequiredSignature)])
64+
.test_missing_signers(false)
65+
.execute();
66+
67+
// Deactivate succeeds
68+
let result = ctx
69+
.process_with(DeactivateConfig {
70+
stake: (&stake, &stake_account),
71+
override_signer: None,
72+
})
73+
.checks(&[
74+
Check::success(),
75+
Check::all_rent_exempt(),
76+
Check::account(&stake)
77+
.lamports(ctx.rent_exempt_reserve + min_delegation)
78+
.owner(&id())
79+
.space(StakeStateV2::size_of())
80+
.build(),
81+
])
82+
.test_missing_signers(true)
83+
.execute();
84+
stake_account = result.resulting_accounts[0].1.clone().into();
85+
86+
let clock = ctx.mollusk.sysvars.clock.clone();
87+
let (_, stake_data, _) = parse_stake_account(&stake_account);
88+
assert_eq!(
89+
stake_data.unwrap().delegation.deactivation_epoch,
90+
clock.epoch
91+
);
92+
93+
// Deactivate again fails
94+
ctx.process_with(DeactivateConfig {
95+
stake: (&stake, &stake_account),
96+
override_signer: None,
97+
})
98+
.checks(&[Check::err(StakeError::AlreadyDeactivated.into())])
99+
.test_missing_signers(false)
100+
.execute();
101+
102+
// Advance epoch
103+
let current_slot = ctx.mollusk.sysvars.clock.slot;
104+
let slots_per_epoch = ctx.mollusk.sysvars.epoch_schedule.slots_per_epoch;
105+
ctx.mollusk.warp_to_slot(current_slot + slots_per_epoch);
106+
107+
// Deactivate again still fails
108+
ctx.process_with(DeactivateConfig {
109+
stake: (&stake, &stake_account),
110+
override_signer: None,
111+
})
112+
.checks(&[Check::err(StakeError::AlreadyDeactivated.into())])
113+
.test_missing_signers(false)
114+
.execute();
115+
}

program/tests/helpers/context.rs

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,172 @@ use {
22
super::{
33
instruction_builders::{InstructionConfig, InstructionExecution},
44
lifecycle::StakeLifecycle,
5-
utils::{add_sysvars, STAKE_RENT_EXEMPTION},
5+
utils::{add_sysvars, create_vote_account, STAKE_RENT_EXEMPTION},
66
},
77
mollusk_svm::{result::Check, Mollusk},
88
solana_account::AccountSharedData,
99
solana_instruction::Instruction,
1010
solana_pubkey::Pubkey,
11+
solana_stake_interface::state::Lockup,
1112
solana_stake_program::id,
1213
};
1314

1415
/// Builder for creating stake accounts with customizable parameters
15-
pub struct StakeAccountBuilder {
16+
pub struct StakeAccountBuilder<'a> {
17+
ctx: &'a mut StakeTestContext,
1618
lifecycle: StakeLifecycle,
19+
staked_amount: u64,
20+
stake_authority: Option<Pubkey>,
21+
withdraw_authority: Option<Pubkey>,
22+
lockup: Option<Lockup>,
23+
vote_account: Option<Pubkey>,
24+
stake_pubkey: Option<Pubkey>,
1725
}
1826

19-
impl StakeAccountBuilder {
27+
impl StakeAccountBuilder<'_> {
28+
/// Set the staked amount (lamports delegated to validator)
29+
pub fn staked_amount(mut self, amount: u64) -> Self {
30+
self.staked_amount = amount;
31+
self
32+
}
33+
34+
/// Set a custom stake authority (defaults to ctx.staker)
35+
pub fn stake_authority(mut self, authority: &Pubkey) -> Self {
36+
self.stake_authority = Some(*authority);
37+
self
38+
}
39+
40+
/// Set a custom withdraw authority (defaults to ctx.withdrawer)
41+
pub fn withdraw_authority(mut self, authority: &Pubkey) -> Self {
42+
self.withdraw_authority = Some(*authority);
43+
self
44+
}
45+
46+
/// Set a custom lockup (defaults to Lockup::default())
47+
pub fn lockup(mut self, lockup: &Lockup) -> Self {
48+
self.lockup = Some(*lockup);
49+
self
50+
}
51+
52+
/// Set a custom vote account (defaults to ctx.vote_account)
53+
pub fn vote_account(mut self, vote_account: &Pubkey) -> Self {
54+
self.vote_account = Some(*vote_account);
55+
self
56+
}
57+
58+
/// Set a specific stake account pubkey (defaults to Pubkey::new_unique())
59+
pub fn stake_pubkey(mut self, pubkey: &Pubkey) -> Self {
60+
self.stake_pubkey = Some(*pubkey);
61+
self
62+
}
63+
64+
/// Build the stake account and return (pubkey, account_data)
2065
pub fn build(self) -> (Pubkey, AccountSharedData) {
21-
let stake_pubkey = Pubkey::new_unique();
22-
let account = self.lifecycle.create_uninitialized_account();
66+
let stake_pubkey = self.stake_pubkey.unwrap_or_else(Pubkey::new_unique);
67+
let account = self.lifecycle.create_stake_account_fully_specified(
68+
&mut self.ctx.mollusk,
69+
&stake_pubkey,
70+
self.vote_account.as_ref().unwrap_or(
71+
self.ctx
72+
.vote_account
73+
.as_ref()
74+
.expect("vote_account required for this lifecycle"),
75+
),
76+
self.staked_amount,
77+
self.stake_authority.as_ref().unwrap_or(&self.ctx.staker),
78+
self.withdraw_authority
79+
.as_ref()
80+
.unwrap_or(&self.ctx.withdrawer),
81+
self.lockup.as_ref().unwrap_or(&Lockup::default()),
82+
);
2383
(stake_pubkey, account)
2484
}
2585
}
2686

27-
/// Consolidated test context for stake account tests
2887
pub struct StakeTestContext {
2988
pub mollusk: Mollusk,
3089
pub rent_exempt_reserve: u64,
3190
pub staker: Pubkey,
3291
pub withdrawer: Pubkey,
92+
pub minimum_delegation: Option<u64>,
93+
pub vote_account: Option<Pubkey>,
94+
pub vote_account_data: Option<AccountSharedData>,
3395
}
3496

3597
impl StakeTestContext {
36-
/// Create a new test context with all standard setup
37-
pub fn new() -> Self {
98+
pub fn minimal() -> Self {
3899
let mollusk = Mollusk::new(&id(), "solana_stake_program");
100+
Self {
101+
mollusk,
102+
rent_exempt_reserve: STAKE_RENT_EXEMPTION,
103+
staker: Pubkey::new_unique(),
104+
withdrawer: Pubkey::new_unique(),
105+
minimum_delegation: None,
106+
vote_account: None,
107+
vote_account_data: None,
108+
}
109+
}
39110

111+
pub fn with_delegation() -> Self {
112+
let mollusk = Mollusk::new(&id(), "solana_stake_program");
113+
let minimum_delegation = solana_stake_program::get_minimum_delegation();
40114
Self {
41115
mollusk,
42116
rent_exempt_reserve: STAKE_RENT_EXEMPTION,
43117
staker: Pubkey::new_unique(),
44118
withdrawer: Pubkey::new_unique(),
119+
minimum_delegation: Some(minimum_delegation),
120+
vote_account: Some(Pubkey::new_unique()),
121+
vote_account_data: Some(create_vote_account()),
45122
}
46123
}
47124

125+
pub fn new() -> Self {
126+
Self::with_delegation()
127+
}
128+
48129
/// Create a stake account builder for the specified lifecycle stage
49130
///
50131
/// Example:
51132
/// ```
52133
/// let (stake, account) = ctx
53-
/// .stake_account(StakeLifecycle::Uninitialized)
134+
/// .stake_account(StakeLifecycle::Active)
135+
/// .staked_amount(1_000_000)
54136
/// .build();
55137
/// ```
56138
pub fn stake_account(&mut self, lifecycle: StakeLifecycle) -> StakeAccountBuilder {
57-
StakeAccountBuilder { lifecycle }
139+
StakeAccountBuilder {
140+
ctx: self,
141+
lifecycle,
142+
staked_amount: 0,
143+
stake_authority: None,
144+
withdraw_authority: None,
145+
lockup: None,
146+
vote_account: None,
147+
stake_pubkey: None,
148+
}
149+
}
150+
151+
/// Create a lockup that expires in the future
152+
pub fn create_future_lockup(&self, epochs_ahead: u64) -> Lockup {
153+
Lockup {
154+
unix_timestamp: 0,
155+
epoch: self.mollusk.sysvars.clock.epoch + epochs_ahead,
156+
custodian: Pubkey::new_unique(),
157+
}
158+
}
159+
160+
/// Create a lockup that's currently in force (far future)
161+
pub fn create_in_force_lockup(&self) -> Lockup {
162+
self.create_future_lockup(1_000_000)
163+
}
164+
165+
/// Create a second vote account (for testing different vote accounts)
166+
pub fn create_second_vote_account(&self) -> (Pubkey, AccountSharedData) {
167+
(Pubkey::new_unique(), create_vote_account())
58168
}
59169

60-
/// Process an instruction
170+
/// Process an instruction with a config-based approach
61171
pub fn process_with<'b, C: InstructionConfig>(
62172
&self,
63173
config: C,

program/tests/helpers/instruction_builders.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ impl<'b> InstructionExecution<'_, 'b> {
3737
}
3838

3939
/// Executes the instruction. If `checks` is `None` or empty, uses `Check::success()`.
40-
/// Fail-safe default: when `test_missing_signers` is `None`, runs the missing-signers
41-
/// test (`true`). Callers must explicitly opt out with `.test_missing_signers(false)`.
40+
/// When `test_missing_signers` is `None`, runs the missing-signers tests.
41+
/// Callers must explicitly opt out with `.test_missing_signers(false)`.
4242
pub fn execute(self) -> mollusk_svm::result::InstructionResult {
4343
let default_checks = [Check::success()];
4444
let checks = match self.checks {
@@ -103,3 +103,38 @@ impl InstructionConfig for InitializeCheckedConfig<'_> {
103103
vec![(*self.stake.0, self.stake.1.clone())]
104104
}
105105
}
106+
107+
pub struct DeactivateConfig<'a> {
108+
pub stake: (&'a Pubkey, &'a AccountSharedData),
109+
/// Override signer for testing wrong signer scenarios (defaults to ctx.staker)
110+
pub override_signer: Option<&'a Pubkey>,
111+
}
112+
113+
impl InstructionConfig for DeactivateConfig<'_> {
114+
fn build_instruction(&self, ctx: &StakeTestContext) -> Instruction {
115+
let signer = self.override_signer.unwrap_or(&ctx.staker);
116+
ixn::deactivate_stake(self.stake.0, signer)
117+
}
118+
119+
fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)> {
120+
vec![(*self.stake.0, self.stake.1.clone())]
121+
}
122+
}
123+
124+
pub struct DelegateConfig<'a> {
125+
pub stake: (&'a Pubkey, &'a AccountSharedData),
126+
pub vote: (&'a Pubkey, &'a AccountSharedData),
127+
}
128+
129+
impl InstructionConfig for DelegateConfig<'_> {
130+
fn build_instruction(&self, ctx: &StakeTestContext) -> Instruction {
131+
ixn::delegate_stake(self.stake.0, &ctx.staker, self.vote.0)
132+
}
133+
134+
fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)> {
135+
vec![
136+
(*self.stake.0, self.stake.1.clone()),
137+
(*self.vote.0, self.vote.1.clone()),
138+
]
139+
}
140+
}

0 commit comments

Comments
 (0)