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.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.