1919 borsh:: BorshDeserialize ,
2020 solana_account_info:: { next_account_info, AccountInfo } ,
2121 solana_borsh:: v1:: try_from_slice_unchecked,
22- solana_clock:: { Clock , Epoch } ,
22+ solana_clock:: Clock ,
2323 solana_cpi:: invoke_signed,
2424 solana_msg:: msg,
2525 solana_native_token:: LAMPORTS_PER_SOL ,
@@ -76,10 +76,8 @@ fn calculate_withdraw_amount(
7676
7777/// Deserialize the stake state from `AccountInfo`
7878fn get_stake_state ( stake_account_info : & AccountInfo ) -> Result < ( Meta , Stake ) , ProgramError > {
79- let stake_state = try_from_slice_unchecked :: < StakeStateV2 > ( & stake_account_info. data . borrow ( ) ) ?;
80-
81- match stake_state {
82- StakeStateV2 :: Stake ( meta, stake, _) => Ok ( ( meta, stake) ) ,
79+ match deserialize_stake ( stake_account_info) {
80+ Ok ( StakeStateV2 :: Stake ( meta, stake, _) ) => Ok ( ( meta, stake) ) ,
8381 _ => Err ( SinglePoolError :: WrongStakeState . into ( ) ) ,
8482 }
8583}
@@ -89,10 +87,11 @@ fn get_stake_amount(stake_account_info: &AccountInfo) -> Result<u64, ProgramErro
8987 Ok ( get_stake_state ( stake_account_info) ?. 1 . delegation . stake )
9088}
9189
92- /// Determine if stake is active
93- fn is_stake_active_without_history ( stake : & Stake , current_epoch : Epoch ) -> bool {
94- stake. delegation . activation_epoch < current_epoch
95- && stake. delegation . deactivation_epoch == Epoch :: MAX
90+ /// Wrapper so if we ever change stake deserialization we have it in one place
91+ fn deserialize_stake ( stake_account_info : & AccountInfo ) -> Result < StakeStateV2 , ProgramError > {
92+ Ok ( try_from_slice_unchecked :: < StakeStateV2 > (
93+ & stake_account_info. data . borrow ( ) ,
94+ ) ?)
9695}
9796
9897/// Determine if stake is fully active with history
@@ -104,6 +103,15 @@ fn is_stake_fully_active(stake_activation_status: &StakeActivationStatus) -> boo
104103 } if * effective > 0 )
105104}
106105
106+ /// Determine if stake is newly activating with history
107+ fn is_stake_newly_activating ( stake_activation_status : & StakeActivationStatus ) -> bool {
108+ matches ! ( stake_activation_status, StakeActivationStatus {
109+ effective: 0 ,
110+ activating,
111+ deactivating: 0 ,
112+ } if * activating > 0 )
113+ }
114+
107115/// Check pool account address for the validator vote account
108116fn check_pool_address (
109117 program_id : & Pubkey ,
@@ -785,7 +793,7 @@ impl Processor {
785793 // get on-ramp and its status. we have to match because unlike the main account it could be Initialized
786794 // if it doesnt exist, it must first be created with InitializePoolOnRamp
787795 let ( option_onramp_status, onramp_deactivation_epoch) =
788- match try_from_slice_unchecked :: < StakeStateV2 > ( & pool_onramp_info. data . borrow ( ) ) {
796+ match deserialize_stake ( pool_onramp_info) {
789797 Ok ( StakeStateV2 :: Initialized ( _) ) => ( None , u64:: MAX ) ,
790798 Ok ( StakeStateV2 :: Stake ( _, stake, _) ) => (
791799 Some ( stake. delegation . stake_activating_and_deactivating (
@@ -945,6 +953,8 @@ impl Processor {
945953 let token_program_info = next_account_info ( account_info_iter) ?;
946954 let stake_program_info = next_account_info ( account_info_iter) ?;
947955
956+ let stake_history = & StakeHistorySysvar ( clock. epoch ) ;
957+
948958 SinglePool :: from_account_info ( pool_info, program_id) ?;
949959
950960 check_pool_stake_address ( program_id, pool_info. key , pool_stake_info. key ) ?;
@@ -977,6 +987,37 @@ impl Processor {
977987 let minimum_pool_balance = minimum_pool_balance ( ) ?;
978988
979989 let ( _, pool_stake_state) = get_stake_state ( pool_stake_info) ?;
990+
991+ let ( pool_is_active, pool_is_activating) = {
992+ let pool_stake_status = pool_stake_state
993+ . delegation
994+ . stake_activating_and_deactivating (
995+ clock. epoch ,
996+ stake_history,
997+ PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH ,
998+ ) ;
999+
1000+ (
1001+ is_stake_fully_active ( & pool_stake_status) ,
1002+ is_stake_newly_activating ( & pool_stake_status) ,
1003+ )
1004+ } ;
1005+
1006+ // if pool is inactive or deactivating, it does not accept deposits.
1007+ // a user must call `ReplenishPool` to reactivate it. this condition is exceptional:
1008+ // in practice, it can only happen if the vote account is delinquent. under normal operation,
1009+ // a new pool is activating for one epoch then active forevermore
1010+ //
1011+ // this branch would also be hit for a pool in warmup/cooldown, but this should never happen,
1012+ // because it would require cluster warmup/cooldown *and* a first-epoch pool or delinquent validator.
1013+ // a `ReplenishRequired` error in that case is misleading, but it is still properly an error
1014+ if !pool_is_active && !pool_is_activating {
1015+ return Err ( SinglePoolError :: ReplenishRequired . into ( ) ) ;
1016+ } else if pool_is_active && pool_is_activating {
1017+ // this is impossible, but assert since we assume `pool_is_active == !pool_is_activating` later
1018+ unreachable ! ( ) ;
1019+ } ;
1020+
9801021 let pre_pool_stake = pool_stake_state
9811022 . delegation
9821023 . stake
@@ -988,20 +1029,40 @@ impl Processor {
9881029 . ok_or ( SinglePoolError :: ArithmeticOverflow ) ?;
9891030 msg ! ( "Available stake pre merge {}" , pre_pool_stake) ;
9901031
991- // user can deposit active stake into an active pool or inactive stake into an
992- // activating pool
993- let ( user_stake_meta, user_stake_state) = get_stake_state ( user_stake_info) ?;
1032+ let ( user_stake_meta, user_stake_status) = match deserialize_stake ( user_stake_info) {
1033+ Ok ( StakeStateV2 :: Stake ( meta, stake, _) ) => (
1034+ meta,
1035+ stake. delegation . stake_activating_and_deactivating (
1036+ clock. epoch ,
1037+ stake_history,
1038+ PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH ,
1039+ ) ,
1040+ ) ,
1041+ Ok ( StakeStateV2 :: Initialized ( meta) ) => ( meta, StakeActivationStatus :: default ( ) ) ,
1042+ _ => return Err ( SinglePoolError :: WrongStakeState . into ( ) ) ,
1043+ } ;
1044+
1045+ // user must have set authority to pool and have no lockup for merge to succeed
9941046 if user_stake_meta. authorized
9951047 != stake:: state:: Authorized :: auto ( pool_stake_authority_info. key )
996- || is_stake_active_without_history ( & pool_stake_state, clock. epoch )
997- != is_stake_active_without_history ( & user_stake_state, clock. epoch )
1048+ || user_stake_meta. lockup . is_in_force ( clock, None )
9981049 {
9991050 return Err ( SinglePoolError :: WrongStakeState . into ( ) ) ;
10001051 }
10011052
1002- // merge the user stake account, which is preauthed to us, into the pool stake
1003- // account this merge succeeding implicitly validates authority/lockup
1004- // of the user stake account
1053+ // user can deposit active stake into an active pool, or activating or inactive stake into an activating pool
1054+ if pool_is_active && is_stake_fully_active ( & user_stake_status) {
1055+ // ok: active <- active
1056+ } else if pool_is_activating && is_stake_newly_activating ( & user_stake_status) {
1057+ // ok: activating <- activating
1058+ } else if pool_is_activating && user_stake_status == StakeActivationStatus :: default ( ) {
1059+ // ok: activating <- inactive
1060+ } else {
1061+ // all other transitions are disallowed
1062+ return Err ( SinglePoolError :: WrongStakeState . into ( ) ) ;
1063+ }
1064+
1065+ // merge the user stake account, which is preauthed to us, into the pool stake account
10051066 Self :: stake_merge (
10061067 pool_info. key ,
10071068 user_stake_info. clone ( ) ,
@@ -1044,7 +1105,7 @@ impl Processor {
10441105 pool_mint. supply
10451106 } ;
10461107
1047- // deposit amount is determined off stake because we return excess rent
1108+ // deposit amount is determined off stake added because we return excess lamports
10481109 let new_pool_tokens = calculate_deposit_amount ( token_supply, pre_pool_stake, stake_added)
10491110 . ok_or ( SinglePoolError :: UnexpectedMathError ) ?;
10501111
@@ -1127,6 +1188,13 @@ impl Processor {
11271188
11281189 let minimum_pool_balance = minimum_pool_balance ( ) ?;
11291190
1191+ // we deliberately do NOT validate the activation status of the pool account.
1192+ // neither snow nor rain nor warmup/cooldown nor validator delinquency prevents a user withdrawal
1193+ //
1194+ // NOTE this is fine for stake v4 but subtly wrong for stake v5 *if* the pool account was deactivated.
1195+ // stake v5 declines to (meaninglessly) adjust delegations of deactivated sources.
1196+ // this will (again) be correct with #581, which shifts to NEV accounting on lamports rather than stake.
1197+ // we should plan another SVSP release before stake v5 activation
11301198 let pre_pool_stake =
11311199 get_stake_amount ( pool_stake_info) ?. saturating_sub ( minimum_pool_balance) ;
11321200 msg ! ( "Available stake pre split {}" , pre_pool_stake) ;
0 commit comments