Skip to content

Commit a2ed082

Browse files
authored
program: require onramp to exist for deposit and withdraw (#648)
1 parent 8f9055c commit a2ed082

3 files changed

Lines changed: 42 additions & 24 deletions

File tree

program/src/instruction.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub enum SinglePoolInstruction {
2727
/// Initialize the mint and main stake account for a new single-validator
2828
/// stake pool. The pool stake account must contain the rent-exempt
2929
/// minimum plus the minimum balance of 1 sol. No tokens will be minted;
30-
/// to deposit more, use `Deposit` after `InitializeStake`.
30+
/// to deposit more, use `Deposit` after `InitializeStake` and `InitializePoolOnRamp`.
3131
///
3232
/// 0. `[]` Validator vote account
3333
/// 1. `[w]` Pool account
@@ -119,7 +119,7 @@ pub enum SinglePoolInstruction {
119119
/// Create token metadata for the stake-pool token in the metaplex-token
120120
/// program. Step three of the permissionless three-stage initialization
121121
/// flow.
122-
/// Note this instruction is not necessary for the pool to operate, to
122+
/// Note this instruction is NOT necessary for the pool to operate, to
123123
/// ensure we cannot be broken by upstream.
124124
///
125125
/// 0. `[]` Pool account
@@ -156,7 +156,7 @@ pub enum SinglePoolInstruction {
156156
///
157157
/// New pools created with `initialize()` will include this instruction
158158
/// automatically. Existing pools must use `InitializePoolOnRamp` to upgrade to
159-
/// the latest version.
159+
/// the latest version. Note the on-ramp IS necessary for the pool to operate.
160160
///
161161
/// 0. `[]` Pool account
162162
/// 1. `[w]` Pool on-ramp account

program/src/processor.rs

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,39 +50,44 @@ fn pool_net_asset_value(
5050
let pool_rent_exempt_reserve = rent.minimum_balance(pool_stake_info.data_len());
5151
let onramp_rent_exempt_reserve = rent.minimum_balance(pool_onramp_info.data_len());
5252

53-
// NEV is all lamports in both accounts less rent
54-
pool_stake_info
53+
// NAV is all lamports in both accounts less rent
54+
55+
let main_stake_value = pool_stake_info
56+
.lamports()
57+
.saturating_sub(pool_rent_exempt_reserve);
58+
59+
let onramp_value = pool_onramp_info
5560
.lamports()
56-
.saturating_add(pool_onramp_info.lamports())
57-
.saturating_sub(pool_rent_exempt_reserve)
58-
.saturating_sub(onramp_rent_exempt_reserve)
61+
.saturating_sub(onramp_rent_exempt_reserve);
62+
63+
main_stake_value.saturating_add(onramp_value)
5964
}
6065

61-
/// Calculate pool tokens to mint, given outstanding token supply, pool NEV, and deposit amount
66+
/// Calculate pool tokens to mint, given outstanding token supply, pool NAV, and deposit amount
6267
fn calculate_deposit_amount(
6368
pre_token_supply: u64,
64-
pre_pool_nev: u64,
69+
pre_pool_nav: u64,
6570
user_deposit_amount: u64,
6671
) -> Option<u64> {
67-
if pre_pool_nev == 0 || pre_token_supply == 0 {
72+
if pre_pool_nav == 0 || pre_token_supply == 0 {
6873
Some(user_deposit_amount)
6974
} else {
7075
u64::try_from(
7176
(user_deposit_amount as u128)
7277
.checked_mul(pre_token_supply as u128)?
73-
.checked_div(pre_pool_nev as u128)?,
78+
.checked_div(pre_pool_nav as u128)?,
7479
)
7580
.ok()
7681
}
7782
}
7883

79-
/// Calculate pool value to return, given outstanding token supply, pool NEV, and tokens to redeem
84+
/// Calculate pool value to return, given outstanding token supply, pool NAV, and tokens to redeem
8085
fn calculate_withdraw_amount(
8186
pre_token_supply: u64,
82-
pre_pool_nev: u64,
87+
pre_pool_nav: u64,
8388
user_tokens_to_burn: u64,
8489
) -> Option<u64> {
85-
let numerator = (user_tokens_to_burn as u128).checked_mul(pre_pool_nev as u128)?;
90+
let numerator = (user_tokens_to_burn as u128).checked_mul(pre_pool_nav as u128)?;
8691
let denominator = pre_token_supply as u128;
8792
if numerator < denominator || denominator == 0 {
8893
Some(0)
@@ -1048,8 +1053,14 @@ impl Processor {
10481053
unreachable!();
10491054
};
10501055

1056+
// onramp must exist
1057+
match deserialize_stake(pool_onramp_info) {
1058+
Ok(StakeStateV2::Initialized(_)) | Ok(StakeStateV2::Stake(_, _, _)) => (),
1059+
_ => return Err(SinglePoolError::OnRampDoesntExist.into()),
1060+
};
1061+
10511062
// 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);
1063+
let pre_total_nav = pool_net_asset_value(pool_stake_info, pool_onramp_info, rent);
10531064

10541065
let pre_user_lamports = user_stake_info.lamports();
10551066
let (user_stake_meta, user_stake_status) = match deserialize_stake(user_stake_info) {
@@ -1114,7 +1125,7 @@ impl Processor {
11141125

11151126
// deposit amount is determined off stake added because we return excess lamports
11161127
let new_pool_tokens =
1117-
calculate_deposit_amount(token_supply, pre_total_nev, new_stake_added)
1128+
calculate_deposit_amount(token_supply, pre_total_nav, new_stake_added)
11181129
.ok_or(SinglePoolError::UnexpectedMathError)?;
11191130

11201131
if new_pool_tokens == 0 {
@@ -1204,12 +1215,9 @@ impl Processor {
12041215

12051216
let minimum_delegation = stake::tools::get_minimum_delegation()?;
12061217

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-
12101218
// note we deliberately do NOT validate the activation status of the pool account.
12111219
// neither warmup/cooldown nor validator delinquency prevent a user withdrawal.
1212-
// however, because we calculate NEV from all lamports in both pool accounts,
1220+
// however, because we calculate NAV from all lamports in both pool accounts,
12131221
// but can only split stake from the main account (unless inactive), we must determine whether this is possible
12141222
let (withdrawable_value, pool_is_fully_inactive) = {
12151223
let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
@@ -1235,9 +1243,19 @@ impl Processor {
12351243
}
12361244
};
12371245

1238-
// withdraw amount is determined off pool NEV just like deposit amount
1246+
// onramp must exist. this does not create an edge case where withdrawals may be blocked,
1247+
// because we also require the onramp to exist for deposits
1248+
match deserialize_stake(pool_onramp_info) {
1249+
Ok(StakeStateV2::Initialized(_)) | Ok(StakeStateV2::Stake(_, _, _)) => (),
1250+
_ => return Err(SinglePoolError::OnRampDoesntExist.into()),
1251+
};
1252+
1253+
// tokens for withdraw are determined off the total stakeable value of both pool-owned accounts
1254+
let pre_total_nav = pool_net_asset_value(pool_stake_info, pool_onramp_info, rent);
1255+
1256+
// withdraw amount is determined off pool NAV just like deposit amount
12391257
let stake_to_withdraw =
1240-
calculate_withdraw_amount(token_supply, pre_total_nev, token_amount)
1258+
calculate_withdraw_amount(token_supply, pre_total_nav, token_amount)
12411259
.ok_or(SinglePoolError::UnexpectedMathError)?;
12421260

12431261
// self-explanatory. we catch 0 deposit above so we only hit this if we rounded to 0

program/tests/withdraw.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ async fn fail_disallowed_withdraw(stake_version: StakeProgramVersion) {
431431
.unwrap_err();
432432
check_error(e, SinglePoolError::WithdrawalTooSmall);
433433

434-
// pump NEV higher. token is worth more but mostly backed by liquid sol
434+
// pump NAV higher. token is worth more but mostly backed by liquid sol
435435
transfer(
436436
&mut context.banks_client,
437437
&context.payer,

0 commit comments

Comments
 (0)