Skip to content

Commit 439cf91

Browse files
committed
shuffle withdraw errors
1 parent 2ddd51a commit 439cf91

2 files changed

Lines changed: 31 additions & 11 deletions

File tree

program/src/error.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ pub enum SinglePoolError {
5454

5555
// 10
5656
/// Not enough stake to cover the provided quantity of pool tokens.
57-
/// (Generally this should not happen absent user error, but may if the
58-
/// minimum delegation increases beyond 1 sol.)
57+
/// This typically means the value exists in the pool as activating stake,
58+
/// and an epoch is required for it to become available. Otherwise, it means
59+
/// active stake in the on-ramp must be moved via `ReplenishPool`.
5960
#[error("WithdrawalTooLarge")]
6061
WithdrawalTooLarge,
6162
/// Required signature is missing.
@@ -105,6 +106,10 @@ pub enum SinglePoolError {
105106
/// is in an exceptional state, or because the on-ramp account should be refreshed.
106107
#[error("ReplenishRequired")]
107108
ReplenishRequired,
109+
/// Withdrawal would render the pool stake account impossible to redelegate.
110+
/// This can only occur if the Stake Program minimum delegation increases above 1sol.
111+
#[error("WithdrawalViolatesPoolRequirements")]
112+
WithdrawalViolatesPoolRequirements,
108113
}
109114
impl From<SinglePoolError> for ProgramError {
110115
fn from(e: SinglePoolError) -> Self {
@@ -137,8 +142,9 @@ impl ToStr for SinglePoolError {
137142
"Error: Not enough pool tokens provided to withdraw stake worth one lamport.",
138143
SinglePoolError::WithdrawalTooLarge =>
139144
"Error: Not enough stake to cover the provided quantity of pool tokens. \
140-
(Generally this should not happen absent user error, but may if the minimum delegation increases \
141-
beyond 1 sol.)",
145+
This typically means the value exists in the pool as activating stake, \
146+
and an epoch is required for it to become available. Otherwise, it means \
147+
active stake in the onramp must be moved via `ReplenishPool`.",
142148
SinglePoolError::SignatureMissing => "Error: Required signature is missing.",
143149
SinglePoolError::WrongStakeState => "Error: Stake account is not in the state expected by the program.",
144150
SinglePoolError::ArithmeticOverflow => "Error: Unsigned subtraction crossed the zero.",
@@ -157,11 +163,14 @@ impl ToStr for SinglePoolError {
157163
SinglePoolError::InvalidPoolOnRampAccount =>
158164
"Error: Provided pool onramp account does not match address derived from the pool account.",
159165
SinglePoolError::OnRampDoesntExist =>
160-
"The onramp account for this pool does not exist; you must call `InitializePoolOnRamp` \
166+
"Error: The onramp account for this pool does not exist; you must call `InitializePoolOnRamp` \
161167
before you can perform this operation.",
162168
SinglePoolError::ReplenishRequired =>
163169
"Error: The present operation requires a `ReplenishPool` call, either because the pool stake account \
164170
is in an exceptional state, or because the on-ramp account should be refreshed.",
171+
SinglePoolError::WithdrawalViolatesPoolRequirements =>
172+
"Error: Withdrawal would render the pool stake account impossible to redelegate. \
173+
This can only occur if the Stake Program minimum delegation increases above 1 sol.",
165174
}
166175
}
167176
}

program/src/processor.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,10 @@ impl Processor {
11981198
return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into());
11991199
}
12001200

1201+
if token_amount == 0 {
1202+
return Err(SinglePoolError::WithdrawalTooSmall.into());
1203+
}
1204+
12011205
let minimum_delegation = stake::tools::get_minimum_delegation()?;
12021206

12031207
// tokens for withdraw are determined off the total stakeable value of both pool-owned accounts
@@ -1241,22 +1245,29 @@ impl Processor {
12411245
return Err(SinglePoolError::WithdrawalTooSmall.into());
12421246
}
12431247

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+
12441259
// if the destination would be in any non-inactive state it must meet minimum delegation
12451260
if !pool_is_fully_inactive && stake_to_withdraw < minimum_delegation {
12461261
return Err(SinglePoolError::WithdrawalTooSmall.into());
12471262
}
12481263

12491264
// if we do not have enough value to service this withdrawal, the user must wait a `ReplenishPool` cycle.
1250-
// this does *not* mean the value isnt in the pool, merely that it is not duly splittable
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
12511267
if stake_to_withdraw > withdrawable_value {
12521268
return Err(SinglePoolError::WithdrawalTooLarge.into());
12531269
}
12541270

1255-
// this is theoretically impossible but we guard because it would put the pool in an unrecoverable state
1256-
if stake_to_withdraw == pool_stake_info.lamports() {
1257-
return Err(SinglePoolError::WithdrawalTooLarge.into());
1258-
}
1259-
12601271
// burn user tokens corresponding to the amount of stake they wish to withdraw
12611272
Self::token_burn(
12621273
pool_info.key,

0 commit comments

Comments
 (0)