Root Cause & Impact Summary
The sweepUnclaimedBonus() function is implemented to sweep only excess leftover donation/dust funds to recoveryAddress after a pool settles to SURVIVED or EXPIRED. Per protocol design, funds reserved for users must cover staker principal + all unclaimed bonus rewards; only balance above this reserved amount can be swept.
When calculating reserved funds for the pool, the contract must always account for all unclaimed bonus (snapshotTotalBonus - claimedBonus) on top of totalEligibleStake. This prevents user bonus rewards from being classified as excess sweepable funds.
The logic that adds unclaimed bonus to reserved is wrapped inside an if (riskWindowStart != 0) conditional block. When riskWindowStart == 0 (the pool never enters an attack risk window, a common normal pool lifecycle), this entire branch is skipped.
reserved only equals totalEligibleStake
All unclaimed bonus is excluded from reserved fund calculation
The full unclaimed bonus pool is falsely marked as excess funds and permanently transferred to the sponsor-controlled recoveryAddress
When riskWindowStart == 0, the bonus addition logic never runs, omitting all user bonus from reserved funds.
Likelihood:
Trigger condition riskWindowStart == 0 occurs for any pool that never detects an attack during its lifecycle (the majority of normal pool deployments).
Exploitable immediately after the pool settles to EXPIRED or SURVIVED.
Zero access control: any external address may call sweepUnclaimedBonus() with no permissions required.
Impact:
The full unclaimed bonus reward pool is permanently transferred to the sponsor-controlled recoveryAddress.
Bonus contributors and eligible stakers lose all entitled bonus rewards against the protocol’s intended logic.
Fund misallocation is irreversible; there is no built-in mechanism to recover swept bonus funds for affected userse
Test Explanation
Staker deposits 10 ETH eligible stake principal
Bonus contributor adds 5 ETH to the bonus reward pool
Validate pool state riskWindowStart = 0 (no attack window triggered)
Fast-forward block timestamp past pool expiry period
Staker withdraws principal via claimExpired()
Random unpermissioned external address calls sweepUnclaimedBonus()
Assert confirms full 5 ETH bonus is sent to recoveryAddress
The core vulnerability stems from a misplaced conditional gate that limits unclaimed bonus calculation to pools with a non-zero riskWindowStart value. When a pool never triggers an attack window (riskWindowStart = 0), unclaimed bonus funds are excluded from the reserved fund tally, which incorrectly classifies all user bonus as excess sweepable funds sent to the sponsor-controlled recoveryAddress.
This fix removes the riskWindowStart != 0 restriction entirely. We replace it with a safe arithmetic check snapshotTotalBonus > claimedBonus to ensure we only add positive unclaimed bonus values to the reserved pool. This check also prevents uint underflow reverts that would occur if claimedBonus exceeds snapshotTotalBonus. After the fix, all user-owed bonus rewards are permanently included in reserved funds for every pool lifecycle state, eliminating the unauthorized bonus sweep vulnerability.
Remove the original if (riskWindowStart != 0) conditional wrapper that blocked bonus calculation for normal pools with no attack window.
Implement a safe positive balance check before adding unclaimed bonus to the reserved fund count.
Ensure reserved funds always sum staker principal + all outstanding unclaimed bonus, regardless of pool risk window status.
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.