Reported by @mirze0x01
Problem
WithdrawStakeWithSlippage checks minimum_lamports_out before the ValidatorRemoval withdrawal clamp. In the ValidatorRemoval branch, the program can later reduce withdraw_lamports to the validator stake account balance. The instruction then succeeds without rechecking whether the final clamped amount still satisfies minimum_lamports_out.
As a result, a withdrawal can succeed even though the actual stake lamports delivered to the user are lower than the instruction's declared minimum output.
Proposed solution
Revalidate the final withdraw_lamports after the ValidatorRemoval clamp and before the stake split and token burn path is allowed to complete.
The recheck should preserve the existing Option behavior because plain WithdrawStake calls can pass None:
if let Some(minimum_lamports_out) = minimum_lamports_out {
if withdraw_lamports < minimum_lamports_out {
return Err(StakePoolError::ExceededSlippage.into());
}
}
This check should be performed after all branches that can mutate withdraw_lamports, including the ValidatorRemoval clamp.
Reported by @mirze0x01
Problem
WithdrawStakeWithSlippage checks minimum_lamports_out before the ValidatorRemoval withdrawal clamp. In the ValidatorRemoval branch, the program can later reduce withdraw_lamports to the validator stake account balance. The instruction then succeeds without rechecking whether the final clamped amount still satisfies minimum_lamports_out.
As a result, a withdrawal can succeed even though the actual stake lamports delivered to the user are lower than the instruction's declared minimum output.
Proposed solution
Revalidate the final withdraw_lamports after the ValidatorRemoval clamp and before the stake split and token burn path is allowed to complete.
The recheck should preserve the existing Option behavior because plain WithdrawStake calls can pass None:
This check should be performed after all branches that can mutate withdraw_lamports, including the ValidatorRemoval clamp.