Independent triage conclusion: valid issue.
In no-risk-window corrupted scenarios, sweepUnclaimedBonus() can transfer the entire accounted bonus to recoveryAddress while the documented pre-claim re-flag window is still open. If the moderator then corrects the outcome from SURVIVED to good-faith CORRUPTED, the new bounty snapshot is taken from the reduced live accounting, so the named attacker permanently loses the swept bonus.
This is a distinct root cause from the previously retained scope-lock finding. It is also distinct from the accepted claimExpired no-risk-window race and from the accepted "re-flag closes on first claim" model, because here real value movement happens without closing the correction window.
Design statement that correction exists before value movement locks the wrong distribution: docs/DESIGN.md
Design statement that moderator CORRUPTED in no-risk-window cases may still sweep the whole pool, bonus included: protocol-readme.md
The contract allows pre-claim re-flagging so the moderator can correct an initially wrong outcome or attacker address before anyone locks in the wrong distribution.
However, sweepUnclaimedBonus() intentionally does not set claimsStarted. In normal donation-only cases that makes sense: a trivial post-resolution donation should not let an outsider close the moderator's correction window.
The problem is that the same function also handles the riskWindowStart == 0 branch, where the entire accounted bonus is treated as unreserved and is transferred to recoveryAddress:
The registry is CORRUPTED, but no active-risk state was ever observed, so riskWindowStart == 0.
The moderator initially flags SURVIVED, leaving the correction window open because claimsStarted == false.
Anyone calls sweepUnclaimedBonus(). Because no staker is owed bonus in the riskWindowStart == 0 branch, the function transfers the full bonus out of the pool and also decrements live totalBonus.
claimsStarted still remains false, so the moderator is allowed to re-flag to good-faith CORRUPTED.
On re-flag, flagOutcome() takes a fresh snapshot from the now-reduced live accounting.
The named attacker can only claim principal, not principal plus bonus. The swept bonus is gone permanently.
So the protocol simultaneously claims:
the moderator can still correct the outcome before value movement locks the wrong distribution, and
the no-risk-window CORRUPTED path may still send the whole pool, bonus included, to the moderator-named attacker.
But sweepUnclaimedBonus() breaks both guarantees by moving real value without closing the correction window.
The existing unit test around this path proves solvency only: it checks that the later corrupted path does not brick and that entitlement shrinks to the remaining live balance. That does not make the economic loss acceptable.
The registry is CORRUPTED.
No active-risk state was ever observed by the pool, so riskWindowStart == 0.
The moderator first flags SURVIVED.
Before the moderator corrects the outcome, someone calls sweepUnclaimedBonus().
The moderator then re-flags to good-faith CORRUPTED.
The full bonus pool can be irreversibly redirected to the sponsor-controlled recoveryAddress even though the later corrected good-faith corrupted outcome should have awarded the entire pool to the named attacker.
This is direct fund loss with constraints:
the amount is unbounded because totalBonus is unbounded,
the transfer is irreversible,
the later corrected bounty cannot restore the lost bonus.
Baseline severity assessment: High.
Exploit cost is low.
Attack complexity is low to medium:
the sweep is permissionless and cheap,
the beneficiary is deterministic because recoveryAddress is already set,
the main constraints are state/timing constraints: a no-risk-window corrupted path and an open correction window after an initial SURVIVED flag.
Scenario:
A staker deposits and a bonus contributor funds the bonus pool.
The registry moves directly to CORRUPTED without any observed active-risk state, so riskWindowStart == 0.
The moderator initially flags SURVIVED, leaving claimsStarted == false.
sweepUnclaimedBonus() transfers the full bonus to recoveryAddress but still leaves the re-flag window open.
The moderator corrects the outcome to good-faith CORRUPTED.
The attacker can only claim principal because the bounty snapshot was taken after the bonus had already been swept away.
How to run:
Full source code PoC:
Last validated result:
Do not allow sweepUnclaimedBonus() to move accounted bonus while the moderator's re-flag window is still open in scenarios where a later corrected good-faith CORRUPTED outcome would need that value.
Practical fixes include:
Treat any real value-moving bonus sweep as finality and set claimsStarted = true, but only after separating pure-donation sweeps from accounted-bonus sweeps.
Alternatively, forbid sweepUnclaimedBonus() from sweeping accounted bonus when outcome == SURVIVED, claimsStarted == false, and a later moderator correction could still reclassify the pool into CORRUPTED.
Another safe option is to preserve a separate immutable resolution snapshot for the open re-flag window so that later good-faith CORRUPTED re-flags cannot be based on already-reduced live accounting.
One practical mitigation is to distinguish between:
pure donation sweeps that should not close the moderator's correction window, and
sweeps that move accounted bonus which could still be needed by a later corrected CORRUPTED outcome.
If the sweep is consuming accounted bonus rather than unrelated donated balance, it should either:
close the re-flag window by setting claimsStarted = true, or
revert while the correction window is still open.
A minimal mitigation pattern is:
The exact final patch can differ, but the fix must guarantee that a later valid good-faith CORRUPTED re-flag cannot be forced to snapshot from already-reduced live accounting after real bonus value has been swept away.
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.