Normal Behavior
The protocol intentionally allows the moderator to re-flag an outcome before the first genuine claim. The documentation states that flagOutcome() may be re-flagged before the first claim so the moderator can correct a typo or incorrect outcome.
The core finality condition is claimsStarted, not merely that an outcome was flagged. flagOutcome() only blocks re-flagging when an outcome is already set and claimsStarted is true.
sweepUnclaimedBonus() is intentionally excluded from this finality latch. The code comment explains that it does not set claimsStarted because a trivial direct-transfer donation could otherwise let anyone lock the moderator’s re-flag window.
Specific Issue
sweepUnclaimedBonus() can move tokens to recoveryAddress while leaving claimsStarted == false. Because claimsStarted remains false, the moderator can still re-flag the outcome afterward.
This is not a direct on-chain exploit by itself. The issue is the complex state it creates:
OutcomeFlagged says one outcome.
BonusSwept moves value under that apparent outcome.
claimsStarted remains false.
The moderator can still re-flag to another outcome.
Off-chain systems that assumed the sweep made the outcome final are wrong.
The project already has a regression test proving this exact behavior.
sweepUnclaimedBonus() is allowed only for SURVIVED or EXPIRED.
It computes reserved principal/bonus and sweeps excess balance.
It intentionally does not set claimsStarted.
It transfers tokens and emits BonusSwept.
The re-flag gate in flagOutcome() only blocks changes when claimsStarted is true.
Likelihood:
Likelihood: Medium
Reason 1 — The behavior is intentionally reachable
The test suite includes a regression test proving that a 1 wei donation can make sweepUnclaimedBonus() succeed after SURVIVED is flagged, while claimsStarted remains false.
Reason 2 — Re-flagging after the sweep remains possible
After sweepUnclaimedBonus(), the same test re-flags from SURVIVED to CORRUPTED, proving that the outcome remains mutable after the sweep.
Reason 3 — Off-chain systems commonly treat value-moving events as finality signals
A BonusSwept event can look like an end-of-life pool action. However, in this protocol, it is explicitly not finality. Finality comes from claim paths, not this sweep.
Impact:
Impact: Medium
Impact 1 — Incorrect finalized outcome in indexers/frontends
A frontend or indexer may display the pool as finalized after OutcomeFlagged(SURVIVED) plus BonusSwept, but the pool can still be re-flagged to CORRUPTED.
The PoC test demonstrates exactly this transition
Impact 2 — Incorrect accounting assumptions
An accounting system may treat the swept bonus as final surplus distribution under SURVIVED/EXPIRED. Later re-flagging can change the pool’s semantic outcome, even though a sweep already happened.
sweepUnclaimedBonus() can transfer excess funds while leaving claimsStarted false.
Impact 3 — Misleading monitoring / alerting
Automated bots may interpret BonusSwept as evidence that the moderator re-flag window is closed. In reality, flagOutcome() can still overwrite the outcome until a genuine claim sets claimsStarted.
The existing codebase already contains a direct PoC:
PoC source:
Alice stakes into the pool.
The registry passes through active risk and then reaches CORRUPTED.
Moderator flags SURVIVED.
A 1 wei donation is sent directly to the pool to create sweepable excess.
sweepUnclaimedBonus() succeeds.
claimsStarted remains false.
Moderator re-flags to CORRUPTED.
The final on-chain outcome is CORRUPTED.
This proves the complex finality surface: a sweep can happen while the outcome is still mutable.
Because the behavior is intentional, the mitigation should focus on making finality explicit for off-chain consumers rather than blindly setting claimsStarted inside sweepUnclaimedBonus().
Add a field to the sweep event or emit a new event indicating that the sweep does not finalize the outcome.
Since sweepUnclaimedBonus() intentionally does not set claimsStarted, this would emit outcomeFinalized = false.
OutcomeFinalized event when finality actually occursAdd a dedicated finality event for the first time claimsStarted flips to true.
Apply this pattern to all genuine finality paths where claimsStarted is currently set:
claimSurvived()
claimCorrupted()
claimAttackerBounty() when payout succeeds
sweepUnclaimedCorrupted()
mechanical claimExpired() resolution branches
This avoids changing the intended behavior of sweepUnclaimedBonus() while giving off-chain systems a safe finality signal.
If event schema changes are not desired, document this explicitly on BonusSwept and sweepUnclaimedBonus().
This is the lowest-risk mitigation but depends on integrators reading documentation.
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.