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