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