_firstGoodFaithCorruptedAt is never refreshed, so re-designating an attacker more than 180 days after the first good-faith CORRUPTED flag can assign an already-expired corruptedClaimDeadline. The designated attacker becomes permanently unable to claim their valid bounty, and the entire pool can instead be swept to recoveryAddress.
Normal behavior: when the moderator flags a pool CORRUPTED in good faith and names a whitehat attacker, that attacker is entitled to claim the entire pool (snapshotTotalStaked + snapshotTotalBonus) via claimAttackerBounty() within CORRUPTED_CLAIM_WINDOW (180 days) of being named. flagOutcome can legitimately be called multiple times before any claim occurs, for example to correct the outcome or designate a different attacker, and each successful re-designation restores the named attacker's full bountyEntitlement.
The issue: corruptedClaimDeadline is computed from _firstGoodFaithCorruptedAt, a timestamp that is set once, on the very first good-faith CORRUPTED flag ever issued for the pool, and is never refreshed on subsequent flags. If a later good-faith CORRUPTED flag (naming the same or a different attacker) lands more than 180 days after that original anchor, the resulting corruptedClaimDeadline is already in the past at the moment the new designation is made. The flagOutcome call itself succeeds with no revert or warning, and bountyEntitlement is correctly restored to the full pool value, giving every on-chain indication that the newly-named attacker is entitled to claim. But their very first call to claimAttackerBounty() reverts ClaimWindowExpired, permanently. Since the deadline has already passed, sweepUnclaimedCorrupted() becomes immediately callable by anyone, redirecting the entire pool to recoveryAddress instead of the designated attacker who is recorded as entitled to the bounty.
Likelihood:
Occurs whenever a pool's good-faith CORRUPTED outcome is re-flagged (naming the same or a different attacker) more than CORRUPTED_CLAIM_WINDOW (180 days) after the pool's first-ever good-faith CORRUPTED flag, with no claim having occurred in the interim.
This requires no adversarial behavior from any party. An ordinary, good-faith scope dispute that takes several months to resolve (plausible for real-world security-incident adjudication) reaches this state naturally, as does a moderator correcting course after an initially-named attacker becomes unable to claim (for example, due to an unrelated token blacklist).
Impact:
The validly-named whitehat attacker permanently loses their entire bounty entitlement. claimAttackerBounty() reverts on every attempt, with no recovery path (every further re-flag recomputes the same stale deadline, and no function anywhere in the codebase resets _firstGoodFaithCorruptedAt or otherwise refreshes the window).
The full pool (stake plus bonus) is redirected to recoveryAddress via the permissionless sweepUnclaimedCorrupted(), denying the recorded claimant funds the protocol's own on-chain state says they are owed.
The protocol's state becomes internally inconsistent: attacker and bountyEntitlement indicate the designated whitehat is entitled to the bounty, while corruptedClaimDeadline simultaneously makes that entitlement impossible to realize.
Create a test file in test/unit/ folder named ConfidencePool.staleCorruptedAnchor.t.sol and paste the following below in the test file:
Run with:
Expected Output:
Refresh _firstGoodFaithCorruptedAt (or otherwise recompute corruptedClaimDeadline) whenever a new good-faith CORRUPTED designation would otherwise inherit an already-expired claim window, while preserving the invariant that moderators cannot extend an already-live claim window through repeated re-flagging. Concretely: only reuse the existing anchor if _firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW is still in the future at the time of the new flag; otherwise treat the flag as establishing a fresh window anchored to the current timestamp.
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.