withdraw() calls _clampUserSums(msg.sender) at line 305, but the function's own gate at line 293 ensures riskWindowStart == 0 before allowing execution. _clampUserSums immediately returns when riskWindowStart == 0 (line 680), making the call a guaranteed no-op. This wastes gas on every withdrawal and introduces a silent regression trap: if the withdrawal gate is ever relaxed in a future upgrade, the live-but-wrong clamp would corrupt global accounting.
The developer intended _clampUserSums to adjust a staker's per-user sums to reflect a non-zero riskWindowStart before any arithmetic that uses those sums. This is correct when called from stake() or claimSurvived() — in those contexts riskWindowStart may be non-zero.
In withdraw(), however, the intent is moot. The withdrawal gate (riskWindowStart != 0 → WithdrawsDisabled) is a one-way latch: once the risk window opens, riskWindowStart can never return to 0. Therefore, any call that reaches _clampUserSums inside withdraw() is provably in a state where start == 0, causing an immediate early return. The clamp body — the part that actually adjusts values — is physically unreachable from withdraw().
Latent regression hazard: The comments above line 309 ("subtract their full contribution from the global accumulators") imply the clamp ran first. If a future developer relaxes the withdrawal gate (e.g., adds a new PREVIEW state where risk is open but withdrawal is allowed), the call becomes live — but now it adjusts per-user sums without updating the already-eagerly-reset globals. The subtraction on lines 309-310 would then use stale unclamped values against the post-reset globals, causing sumStakeTime to underflow and brick the pool's accounting.
Any staker calls withdraw() while riskWindowStart == 0. This is the only condition under which withdraw() can succeed — the call is always a no-op.
None. This is a pure code-level defect observable on any withdrawal call.
This finding has no direct attack path (dead code, not exploitable as-is). The risk is:
A developer ships a future upgrade that introduces a new registry state (e.g., PREVIEW) treated as non-risk but still allowing withdrawals.
A staker calls withdraw() after riskWindowStart is set.
_clampUserSums runs and updates the staker's userSumStakeTime to eligibleStake * riskWindowStart.
Line 309: sumStakeTime -= userSumStakeTime[msg.sender] — the global was already eagerly reset to totalEligibleStake * riskWindowStart by _markRiskWindowStart. Subtracting the newly-clamped value causes an underflow.
sumStakeTime wraps to type(uint256).max, corrupting the bonus denominator for all future claimants permanently.
Current: ~600 gas wasted per withdraw() call (one SLOAD + one conditional branch). Misleading code that creates false confidence in reviewers that per-user sums are being adjusted.
Potential (upgrade regression): Complete corruption of sumStakeTime global via underflow → all bonus shares computed from a garbage denominator → some stakers receive 0 bonus, others receive amounts exceeding snapshotTotalBonus, breaking the pool's solvency invariant.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.