Skip to content

Commit f6de449

Browse files
committed
StakeLifecycle, Deactivate tests
1 parent fde41bb commit f6de449

File tree

5 files changed

+348
-90
lines changed

5 files changed

+348
-90
lines changed

program/tests/deactivate.rs

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

program/tests/helpers/context.rs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use {
22
super::{
33
execution::ExecutionWithChecks,
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,
@@ -25,7 +25,7 @@ pub struct StakeAccountBuilder<'a> {
2525
stake_pubkey: Option<Pubkey>,
2626
}
2727

28-
impl<'a> StakeAccountBuilder<'a> {
28+
impl StakeAccountBuilder<'_> {
2929
/// Set the staked amount (lamports delegated to validator)
3030
pub fn staked_amount(mut self, amount: u64) -> Self {
3131
self.staked_amount = amount;
@@ -65,7 +65,24 @@ impl<'a> StakeAccountBuilder<'a> {
6565
/// Build the stake account and return (pubkey, account_data)
6666
pub fn build(self) -> (Pubkey, AccountSharedData) {
6767
let stake_pubkey = self.stake_pubkey.unwrap_or_else(Pubkey::new_unique);
68-
let account = self.lifecycle.create_uninitialized_account();
68+
let vote_account_ref = self.vote_account.as_ref().unwrap_or_else(|| {
69+
self.ctx
70+
.vote_account
71+
.as_ref()
72+
.map(|(pk, _)| pk)
73+
.expect("vote_account required for this lifecycle")
74+
});
75+
let account = self.lifecycle.create_stake_account_fully_specified(
76+
&mut self.ctx.mollusk,
77+
&stake_pubkey,
78+
vote_account_ref,
79+
self.staked_amount,
80+
self.stake_authority.as_ref().unwrap_or(&self.ctx.staker),
81+
self.withdraw_authority
82+
.as_ref()
83+
.unwrap_or(&self.ctx.withdrawer),
84+
self.lockup.as_ref().unwrap_or(&Lockup::default()),
85+
);
6986
(stake_pubkey, account)
7087
}
7188
}
@@ -75,6 +92,9 @@ pub struct StakeTestContext {
7592
pub rent_exempt_reserve: u64,
7693
pub staker: Pubkey,
7794
pub withdrawer: Pubkey,
95+
pub minimum_delegation: Option<u64>,
96+
/// Vote account (pubkey, account_data) for delegation tests
97+
pub vote_account: Option<(Pubkey, AccountSharedData)>,
7898
}
7999

80100
impl StakeTestContext {
@@ -85,6 +105,21 @@ impl StakeTestContext {
85105
rent_exempt_reserve: STAKE_RENT_EXEMPTION,
86106
staker: Pubkey::new_unique(),
87107
withdrawer: Pubkey::new_unique(),
108+
minimum_delegation: None,
109+
vote_account: None,
110+
}
111+
}
112+
113+
pub fn with_delegation() -> Self {
114+
let mollusk = Mollusk::new(&id(), "solana_stake_program");
115+
let minimum_delegation = solana_stake_program::get_minimum_delegation();
116+
Self {
117+
mollusk,
118+
rent_exempt_reserve: STAKE_RENT_EXEMPTION,
119+
staker: Pubkey::new_unique(),
120+
withdrawer: Pubkey::new_unique(),
121+
minimum_delegation: Some(minimum_delegation),
122+
vote_account: Some((Pubkey::new_unique(), create_vote_account())),
88123
}
89124
}
90125

@@ -95,6 +130,8 @@ impl StakeTestContext {
95130
/// let (stake, account) = ctx
96131
/// .stake_account(StakeLifecycle::Active)
97132
/// .staked_amount(1_000_000)
133+
/// .stake_account(StakeLifecycle::Active)
134+
/// .staked_amount(1_000_000)
98135
/// .build();
99136
/// ```
100137
pub fn stake_account(&mut self, lifecycle: StakeLifecycle) -> StakeAccountBuilder<'_> {
@@ -117,6 +154,25 @@ impl StakeTestContext {
117154
ExecutionWithChecks::new(self, checks)
118155
}
119156

157+
/// Create a lockup that expires in the future
158+
pub fn create_future_lockup(&self, epochs_ahead: u64) -> Lockup {
159+
Lockup {
160+
unix_timestamp: 0,
161+
epoch: self.mollusk.sysvars.clock.epoch + epochs_ahead,
162+
custodian: Pubkey::new_unique(),
163+
}
164+
}
165+
166+
/// Create a lockup that's currently in force (far future)
167+
pub fn create_in_force_lockup(&self) -> Lockup {
168+
self.create_future_lockup(1_000_000)
169+
}
170+
171+
/// Create a second vote account (for testing different vote accounts)
172+
pub fn create_second_vote_account(&self) -> (Pubkey, AccountSharedData) {
173+
(Pubkey::new_unique(), create_vote_account())
174+
}
175+
120176
/// Execute an instruction with default success checks and missing signer testing
121177
///
122178
/// Usage: `ctx.execute(instruction, accounts)`
@@ -154,7 +210,7 @@ impl StakeTestContext {
154210

155211
impl Default for StakeTestContext {
156212
fn default() -> Self {
157-
Self::new()
213+
Self::with_delegation()
158214
}
159215
}
160216

0 commit comments

Comments
 (0)