@@ -40,33 +40,49 @@ use {
4040 spl_token_interface:: { self as spl_token, state:: Mint } ,
4141} ;
4242
43- /// Calculate pool tokens to mint, given outstanding token supply, pool active
44- /// stake, and deposit active stake
43+ /// Determine the canonical value of the pool from its staked and stake-able lamports
44+ fn pool_net_asset_value (
45+ pool_stake_info : & AccountInfo ,
46+ pool_onramp_info : & AccountInfo ,
47+ rent : & Rent ,
48+ ) -> u64 {
49+ // these numbers should typically be equal, but might differ during StakeState upgrades
50+ let pool_rent_exempt_reserve = rent. minimum_balance ( pool_stake_info. data_len ( ) ) ;
51+ let onramp_rent_exempt_reserve = rent. minimum_balance ( pool_onramp_info. data_len ( ) ) ;
52+
53+ // NEV is all lamports in both accounts less rent
54+ pool_stake_info
55+ . lamports ( )
56+ . saturating_add ( pool_onramp_info. lamports ( ) )
57+ . saturating_sub ( pool_rent_exempt_reserve)
58+ . saturating_sub ( onramp_rent_exempt_reserve)
59+ }
60+
61+ /// Calculate pool tokens to mint, given outstanding token supply, pool NEV, and deposit amount
4562fn calculate_deposit_amount (
4663 pre_token_supply : u64 ,
47- pre_pool_stake : u64 ,
48- user_stake_to_deposit : u64 ,
64+ pre_pool_nev : u64 ,
65+ user_deposit_amount : u64 ,
4966) -> Option < u64 > {
50- if pre_pool_stake == 0 || pre_token_supply == 0 {
51- Some ( user_stake_to_deposit )
67+ if pre_pool_nev == 0 || pre_token_supply == 0 {
68+ Some ( user_deposit_amount )
5269 } else {
5370 u64:: try_from (
54- ( user_stake_to_deposit as u128 )
71+ ( user_deposit_amount as u128 )
5572 . checked_mul ( pre_token_supply as u128 ) ?
56- . checked_div ( pre_pool_stake as u128 ) ?,
73+ . checked_div ( pre_pool_nev as u128 ) ?,
5774 )
5875 . ok ( )
5976 }
6077}
6178
62- /// Calculate pool stake to return, given outstanding token supply, pool active
63- /// stake, and tokens to redeem
79+ /// Calculate pool value to return, given outstanding token supply, pool NEV, and tokens to redeem
6480fn calculate_withdraw_amount (
6581 pre_token_supply : u64 ,
66- pre_pool_stake : u64 ,
82+ pre_pool_nev : u64 ,
6783 user_tokens_to_burn : u64 ,
6884) -> Option < u64 > {
69- let numerator = ( user_tokens_to_burn as u128 ) . checked_mul ( pre_pool_stake as u128 ) ?;
85+ let numerator = ( user_tokens_to_burn as u128 ) . checked_mul ( pre_pool_nev as u128 ) ?;
7086 let denominator = pre_token_supply as u128 ;
7187 if numerator < denominator || denominator == 0 {
7288 Some ( 0 )
@@ -774,8 +790,8 @@ impl Processor {
774790 let stake_config_info = next_account_info ( account_info_iter) ?;
775791 let stake_program_info = next_account_info ( account_info_iter) ?;
776792
793+ let rent = Rent :: get ( ) ?;
777794 let stake_history = & StakeHistorySysvar ( clock. epoch ) ;
778- let minimum_delegation = stake:: tools:: get_minimum_delegation ( ) ?;
779795
780796 check_vote_account ( vote_account_info) ?;
781797 check_pool_address ( program_id, vote_account_info. key , pool_info. key ) ?;
@@ -791,8 +807,9 @@ impl Processor {
791807 ) ?;
792808 check_stake_program ( stake_program_info. key ) ?;
793809
810+ let minimum_delegation = stake:: tools:: get_minimum_delegation ( ) ?;
811+
794812 // we expect these numbers to be equal but get them separately in case of future changes
795- let rent = Rent :: get ( ) ?;
796813 let pool_rent_exempt_reserve = rent. minimum_balance ( pool_stake_info. data_len ( ) ) ;
797814 let onramp_rent_exempt_reserve = rent. minimum_balance ( pool_onramp_info. data_len ( ) ) ;
798815
@@ -970,6 +987,7 @@ impl Processor {
970987 let token_program_info = next_account_info ( account_info_iter) ?;
971988 let stake_program_info = next_account_info ( account_info_iter) ?;
972989
990+ let rent = & Rent :: get ( ) ?;
973991 let stake_history = & StakeHistorySysvar ( clock. epoch ) ;
974992
975993 SinglePool :: from_account_info ( pool_info, program_id) ?;
@@ -998,9 +1016,8 @@ impl Processor {
9981016 return Err ( SinglePoolError :: InvalidPoolStakeAccountUsage . into ( ) ) ;
9991017 }
10001018
1001- let ( _, pool_stake_state) = get_stake_state ( pool_stake_info) ?;
1002-
1003- let ( pool_is_active, pool_is_activating) = {
1019+ let ( pre_pool_stake, pool_is_active, pool_is_activating) = {
1020+ let ( _, pool_stake_state) = get_stake_state ( pool_stake_info) ?;
10041021 let pool_stake_status = pool_stake_state
10051022 . delegation
10061023 . stake_activating_and_deactivating (
@@ -1010,6 +1027,7 @@ impl Processor {
10101027 ) ;
10111028
10121029 (
1030+ pool_stake_state. delegation . stake ,
10131031 is_stake_fully_active ( & pool_stake_status) ,
10141032 is_stake_newly_activating ( & pool_stake_status) ,
10151033 )
@@ -1030,10 +1048,10 @@ impl Processor {
10301048 unreachable ! ( ) ;
10311049 } ;
10321050
1033- let pre_pool_stake = pool_stake_state. delegation . stake ;
1034- let pre_pool_lamports = pool_stake_info. lamports ( ) ;
1035- msg ! ( "Available stake pre merge {}" , pre_pool_stake) ;
1051+ // tokens for deposit are determined off the total stakeable value of both pool-owned accounts
1052+ let pre_total_nev = pool_net_asset_value ( pool_stake_info, pool_onramp_info, rent) ;
10361053
1054+ let pre_user_lamports = user_stake_info. lamports ( ) ;
10371055 let ( user_stake_meta, user_stake_status) = match deserialize_stake ( user_stake_info) {
10381056 Ok ( StakeStateV2 :: Stake ( meta, stake, _) ) => (
10391057 meta,
@@ -1078,21 +1096,15 @@ impl Processor {
10781096 stake_history_info. clone ( ) ,
10791097 ) ?;
10801098
1081- let ( _, pool_stake_state) = get_stake_state ( pool_stake_info) ?;
1082- let post_pool_stake = pool_stake_state. delegation . stake ;
1083- let post_pool_lamports = pool_stake_info. lamports ( ) ;
1084- msg ! ( "Available stake post merge {}" , post_pool_stake) ;
1085-
1086- // stake lamports added, as a stake difference
1087- let stake_added = post_pool_stake
1099+ // determine new stake lamports added by merge
1100+ let post_pool_stake = get_stake_amount ( pool_stake_info) ?;
1101+ let new_stake_added = post_pool_stake
10881102 . checked_sub ( pre_pool_stake)
10891103 . ok_or ( SinglePoolError :: ArithmeticOverflow ) ?;
10901104
1091- // if there were excess lamports in the user-provided account, we return them
1092- // this includes their rent-exempt reserve if the pool is fully active
1093- let user_excess_lamports = post_pool_lamports
1094- . checked_sub ( pre_pool_lamports)
1095- . and_then ( |amount| amount. checked_sub ( stake_added) )
1105+ // return user lamports that were not added to stake
1106+ let user_excess_lamports = pre_user_lamports
1107+ . checked_sub ( new_stake_added)
10961108 . ok_or ( SinglePoolError :: ArithmeticOverflow ) ?;
10971109
10981110 // sanity check: the user stake account is empty
@@ -1101,8 +1113,9 @@ impl Processor {
11011113 }
11021114
11031115 // deposit amount is determined off stake added because we return excess lamports
1104- let new_pool_tokens = calculate_deposit_amount ( token_supply, pre_pool_stake, stake_added)
1105- . ok_or ( SinglePoolError :: UnexpectedMathError ) ?;
1116+ let new_pool_tokens =
1117+ calculate_deposit_amount ( token_supply, pre_total_nev, new_stake_added)
1118+ . ok_or ( SinglePoolError :: UnexpectedMathError ) ?;
11061119
11071120 if new_pool_tokens == 0 {
11081121 return Err ( SinglePoolError :: DepositTooSmall . into ( ) ) ;
@@ -1152,9 +1165,13 @@ impl Processor {
11521165 let user_stake_info = next_account_info ( account_info_iter) ?;
11531166 let user_token_account_info = next_account_info ( account_info_iter) ?;
11541167 let clock_info = next_account_info ( account_info_iter) ?;
1168+ let clock = & Clock :: from_account_info ( clock_info) ?;
11551169 let token_program_info = next_account_info ( account_info_iter) ?;
11561170 let stake_program_info = next_account_info ( account_info_iter) ?;
11571171
1172+ let rent = & Rent :: get ( ) ?;
1173+ let stake_history = & StakeHistorySysvar ( clock. epoch ) ;
1174+
11581175 SinglePool :: from_account_info ( pool_info, program_id) ?;
11591176
11601177 check_pool_stake_address ( program_id, pool_info. key , pool_stake_info. key ) ?;
@@ -1181,26 +1198,73 @@ impl Processor {
11811198 return Err ( SinglePoolError :: InvalidPoolStakeAccountUsage . into ( ) ) ;
11821199 }
11831200
1184- // we deliberately do NOT validate the activation status of the pool account.
1185- // neither snow nor rain nor warmup/cooldown nor validator delinquency prevents a user withdrawal
1186- //
1187- // NOTE this is fine for stake v4 but subtly wrong for stake v5 *if* the pool account was deactivated.
1188- // stake v5 declines to (meaninglessly) adjust delegations of deactivated sources.
1189- // this will (again) be correct with #581, which shifts to NEV accounting on lamports rather than stake.
1190- // we should plan another SVSP release before stake v5 activation
1191- let pre_pool_stake = get_stake_amount ( pool_stake_info) ?;
1192- msg ! ( "Available stake pre split {}" , pre_pool_stake) ;
1193-
1194- // withdraw amount is determined off stake just like deposit amount
1195- let withdraw_stake = calculate_withdraw_amount ( token_supply, pre_pool_stake, token_amount)
1196- . ok_or ( SinglePoolError :: UnexpectedMathError ) ?;
1197-
1198- if withdraw_stake == 0 {
1201+ if token_amount == 0 {
11991202 return Err ( SinglePoolError :: WithdrawalTooSmall . into ( ) ) ;
12001203 }
12011204
1202- // the second case should never be true, but its best to be sure
1203- if withdraw_stake > pre_pool_stake || withdraw_stake == pool_stake_info. lamports ( ) {
1205+ let minimum_delegation = stake:: tools:: get_minimum_delegation ( ) ?;
1206+
1207+ // tokens for withdraw are determined off the total stakeable value of both pool-owned accounts
1208+ let pre_total_nev = pool_net_asset_value ( pool_stake_info, pool_onramp_info, rent) ;
1209+
1210+ // note we deliberately do NOT validate the activation status of the pool account.
1211+ // neither warmup/cooldown nor validator delinquency prevent a user withdrawal.
1212+ // however, because we calculate NEV from all lamports in both pool accounts,
1213+ // but can only split stake from the main account (unless inactive), we must determine whether this is possible
1214+ let ( withdrawable_value, pool_is_fully_inactive) = {
1215+ let ( _, pool_stake_state) = get_stake_state ( pool_stake_info) ?;
1216+ let pool_stake_status = pool_stake_state
1217+ . delegation
1218+ . stake_activating_and_deactivating (
1219+ clock. epoch ,
1220+ stake_history,
1221+ PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH ,
1222+ ) ;
1223+
1224+ // if fully inactive, we split on lamports; otherwise, on all delegation.
1225+ // the stake program works off delegation in this way *even* for a partially deactivated stake
1226+ if pool_stake_status == StakeActivationStatus :: default ( ) {
1227+ (
1228+ pool_stake_info
1229+ . lamports ( )
1230+ . saturating_sub ( rent. minimum_balance ( pool_stake_info. data_len ( ) ) ) ,
1231+ true ,
1232+ )
1233+ } else {
1234+ ( pool_stake_state. delegation . stake , false )
1235+ }
1236+ } ;
1237+
1238+ // withdraw amount is determined off pool NEV just like deposit amount
1239+ let stake_to_withdraw =
1240+ calculate_withdraw_amount ( token_supply, pre_total_nev, token_amount)
1241+ . ok_or ( SinglePoolError :: UnexpectedMathError ) ?;
1242+
1243+ // self-explanatory. we catch 0 deposit above so we only hit this if we rounded to 0
1244+ if stake_to_withdraw == 0 {
1245+ return Err ( SinglePoolError :: WithdrawalTooSmall . into ( ) ) ;
1246+ }
1247+
1248+ // the pool must *always* meet minimum delegation, even if it is inactive.
1249+ // this error is currently impossible to hit and exists to protect pools if minimum delegation rises above 1sol
1250+ if withdrawable_value. saturating_sub ( stake_to_withdraw) < minimum_delegation {
1251+ return Err ( SinglePoolError :: WithdrawalViolatesPoolRequirements . into ( ) ) ;
1252+ }
1253+
1254+ // this is impossible but we guard explicitly because it would put the pool in an unrecoverable state
1255+ if stake_to_withdraw == pool_stake_info. lamports ( ) {
1256+ return Err ( SinglePoolError :: WithdrawalViolatesPoolRequirements . into ( ) ) ;
1257+ }
1258+
1259+ // if the destination would be in any non-inactive state it must meet minimum delegation
1260+ if !pool_is_fully_inactive && stake_to_withdraw < minimum_delegation {
1261+ return Err ( SinglePoolError :: WithdrawalTooSmall . into ( ) ) ;
1262+ }
1263+
1264+ // if we do not have enough value to service this withdrawal, the user must wait a `ReplenishPool` cycle.
1265+ // this does *not* mean the value isnt in the pool, merely that it is not duly splittable.
1266+ // this check should always come last to avoid returning it if the withdrawal is actually invalid
1267+ if stake_to_withdraw > withdrawable_value {
12041268 return Err ( SinglePoolError :: WithdrawalTooLarge . into ( ) ) ;
12051269 }
12061270
@@ -1221,7 +1285,7 @@ impl Processor {
12211285 pool_stake_info. clone ( ) ,
12221286 pool_stake_authority_info. clone ( ) ,
12231287 stake_authority_bump_seed,
1224- withdraw_stake ,
1288+ stake_to_withdraw ,
12251289 user_stake_info. clone ( ) ,
12261290 ) ?;
12271291
@@ -1235,9 +1299,6 @@ impl Processor {
12351299 clock_info. clone ( ) ,
12361300 ) ?;
12371301
1238- let post_pool_stake = get_stake_amount ( pool_stake_info) ?;
1239- msg ! ( "Available stake post split {}" , post_pool_stake) ;
1240-
12411302 Ok ( ( ) )
12421303 }
12431304
@@ -1547,7 +1608,7 @@ mod tests {
15471608 test_case:: test_case,
15481609 } ;
15491610
1550- // approximately 6%/yr assuking 146 epochs
1611+ // approximately 6%/yr assuming 146 epochs
15511612 const INFLATION_BASE_RATE : f64 = 0.0004 ;
15521613
15531614 #[ derive( Clone , Debug , Default ) ]
0 commit comments