When a pool's registry genuinely reaches CORRUPTED, the moderator calls
flagOutcome(CORRUPTED, goodFaith_, attacker_) to record whether the breach was bad-faith (full
pool swept to recoveryAddress) or good-faith, in which case attacker_ is meant to identify an
independent whitehat who is rewarded with a bounty for responsibly surfacing the corruption.
flagOutcome never checks that attacker_ is distinct from outcomeModerator (or from anyone) —
it only rejects the zero address. Since claimAttackerBounty() pays out to whichever address
msg.sender == attacker, the moderator can simply name itself as the good-faith attacker and then
call claimAttackerBounty() to collect the full snapshot — every staker's principal plus the
entire bonus pool — with no independent whitehat ever involved.
ConfidencePool.sol:322-379 (flagOutcome)
Likelihood:
This occurs whenever the registry genuinely reaches CORRUPTED and the moderator calls flagOutcome(CORRUPTED, true, <own address>) — CORRUPTED is a normal, designed-for resolution path (not a rare edge case), and nothing in flagOutcome or claimAttackerBounty distinguishes the moderator's own address from any other candidate attacker.
The re-flag window (open until claimsStarted, which only flips on the first successful claim) lets the moderator overwrite a previously-honest attacker_ with its own address at any point before anyone claims, and, when outcomeModerator is a contract, flagOutcome + claimAttackerBounty can be bundled into a single transaction — no multi-block window, favorable mempool ordering, or race condition is required.
Impact:
Complete, permanent loss of 100% of the pool's staked principal and contributed bonus (snapshotTotalStaked + snapshotTotalBonus) to the moderator in a single transaction.
Stakers have no on-chain defense or exit at the point this becomes exploitable: withdraw() is already permanently disabled once riskWindowStart != 0, which is latched before the registry can reach CORRUPTED.
Run full debug: forge test --match-contract ModeratorSelfDeal -vvvv
Relevant excerpt from the -vvvv execution trace (self-naming call, the bounty drain, and the
before/after balances — full trace available on request, trimmed here to the load-bearing calls):
The OutcomeFlagged event is the key evidence line: moderator (the caller) and attacker (the
named party) are the identical address, emitted by the contract itself — not asserted by the test.
The subsequent Transfer/AttackerBountyClaimed events and the balance deltas (0 → 2e21 for the
moderator, 2e21 → 0 for the pool) show the full drain executing exactly as claimed.
A same-address check alone doesn't stop the moderator routing the bounty to a collaborator's
address instead of its own — treat this as a minimum fix, not a complete solution to moderator
trust concentration in this function.
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.