Normal behavior: When the moderator flags outcome = CORRUPTED with goodFaith = true and names a whitehat, the contract records corruptedClaimDeadline = _firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW (180 days). The whitehat then has that window to call claimAttackerBounty(), design-intent per docs/DESIGN.md §12. After the window, sweepUnclaimedCorrupted() is permissionlessly callable to recover any unclaimed remainder to recoveryAddress.
The specific issue: in ConfidencePool.flagOutcome, both _firstGoodFaithCorruptedAt (line 366) and corruptedClaimDeadline (line 372) are cast to uint32 without any cap on block.timestamp, unlike the neighboring _markRiskWindowStart (t = min(block.timestamp, expiry) at line 807) and _markRiskWindowEnd (line 824) which were deliberately capped to keep their uint32 casts in range. When the first good-faith CORRUPTED flag is mined with block.timestamp > uint32.max - 180 days (activation window 2105-08-11 UTC through 2106-02-07 UTC), the addition in line 372 wraps modulo 2^32, storing a deadline deep in 1970 — well below the current block.timestamp. As a result:
claimAttackerBounty() reverts ClaimWindowExpired immediately at line 437 (comparison uint256 > uint32-zero-extended resolves with block.timestamp > deadline),
sweepUnclaimedCorrupted() becomes permissionlessly callable at the same block (line 459 gate block.timestamp <= corruptedClaimDeadline fails),
the whitehat's entire bounty (the whole pool: snapshotTotalStaked + snapshotTotalBonus) is swept to recoveryAddress in the same block as the flag.
Activation window is fixed and deterministic: any first good-faith CORRUPTED flagOutcome whose block.timestamp falls in [4_279_415_295, 4_294_967_295] (i.e. 2105-08-11 06:28:15 UTC through 2106-02-07 06:28:15 UTC, exactly the last 180 days of uint32 seconds) triggers the bug. flagOutcome has no upper-bound check on block.timestamp, and expiry may legitimately be set up to type(uint32).max (ExpiryTooFar check at line 197 only enforces expiry_ <= uint32.max), so a pool can still be unresolved when the chain reaches the activation window.
The stored deadline wrap is one-directional: it always lands below block.timestamp for any t in the activation window (t + 180d - 2^32 ∈ [~63K, ~12.3M], all ≪ t ≈ 4.3e9), so the bug deterministically fires rather than being probabilistic.
The whitehat named by the moderator receives zero bounty — claimAttackerBounty() reverts ClaimWindowExpired even in the same block as the flag, contradicting the documented CORRUPTED_CLAIM_WINDOW = 180 days reservation in protocol-readme.md L95 and docs/DESIGN.md §12 ("the pool is reserved for the named whitehat for CORRUPTED_CLAIM_WINDOW (180 days)").
The whole pool (snapshotTotalStaked + snapshotTotalBonus, plus any donation dust) is permissionlessly swept to recoveryAddress by any caller via sweepUnclaimedCorrupted() — funds the moderator explicitly intended for the whitehat go to the recovery address instead.
The state is unrecoverable without a contract upgrade: _firstGoodFaithCorruptedAt is single-write (if (_firstGoodFaithCorruptedAt == 0) at line 364), so every subsequent moderator re-flag recomputes corruptedClaimDeadline from the same wrapped base at line 372 and stays broken; no on-chain action by the moderator, staker, or whitehat can restore the 180-day window.
Run: forge test --match-test test_PoC_corruptedClaimDeadline_uint32Wrap -vv
Apply the same expiry-cap pattern that already exists in _markRiskWindowStart / _markRiskWindowEnd (lines 807, 824). Either bound block.timestamp to expiry (the pool's own lifecycle) or guard the addition against the uint32 ceiling:
This widens the addition to uint256 (no wrap) and keeps the stored uint32 value in range by clamping block.timestamp first — matching the existing min(t, expiry) approach used for riskWindowStart / riskWindowEnd.
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.