In the good-faith CORRUPTED path, the moderator names a whitehat attacker and the pool reserves the full bounty for that attacker during a 180-day claim window. The bug is an integer truncation edge case: the first good-faith CORRUPTED timestamp and corruptedClaimDeadline are stored as uint32, and the deadline is computed by casting _firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW back to uint32.
Near the uint32 timestamp ceiling, adding the 180-day window wraps the deadline into the past. This is not theft by an arbitrary attacker, but it breaks the intended bounty reservation and immediately enables the recovery sweep for a good-faith CORRUPTED outcome.
File: src/ConfidencePool.sol
Lines: 151-155
Function/Type: _firstGoodFaithCorruptedAt / corruptedClaimDeadline
File: src/ConfidencePool.sol
Lines: 322-379
Function/Type: flagOutcome(PoolStates.Outcome,bool,address)
File: src/ConfidencePool.sol
Lines: 432-453
Function/Type: claimAttackerBounty()
File: src/ConfidencePool.sol
Lines: 456-471
Function/Type: sweepUnclaimedCorrupted()
The moderator can flag a pool as good-faith CORRUPTED and provide the named attacker address. On the first good-faith CORRUPTED flag, the pool stores the current timestamp in _firstGoodFaithCorruptedAt as uint32. It then sets corruptedClaimDeadline by adding CORRUPTED_CLAIM_WINDOW and casting the sum back down to uint32.
When the first good-faith CORRUPTED flag occurs within 180 days of type(uint32).max, the deadline wraps below the current timestamp. claimAttackerBounty() then treats the claim window as expired, while sweepUnclaimedCorrupted() treats the same wrapped deadline as already passed. The named attacker cannot claim, and the full pool balance can be swept to recoveryAddress.
The root cause is storing the corrupted bounty deadline in uint32 and truncating a timestamp plus a 180-day window back into that type.
Step 1 A pool exists close to type(uint32).max.
Step 2 The registry reaches CORRUPTED.
Step 3 The moderator flags the pool as good-faith CORRUPTED and names the attacker.
Result: corruptedClaimDeadline wraps below the current timestamp, so the named attacker immediately fails the deadline check.
Step 1 The good-faith CORRUPTED flag stores a wrapped deadline in the past.
Step 2 The named attacker attempts claimAttackerBounty() and reverts with ClaimWindowExpired.
Step 3 Any caller invokes sweepUnclaimedCorrupted().
Step 4 The sweep sees block.timestamp > corruptedClaimDeadline.
Result: The full pool balance is transferred to recoveryAddress without the intended 180-day bounty reservation.
The pool is flagged as CORRUPTED with goodFaith_ == true.
flagOutcome() stores _firstGoodFaithCorruptedAt = uint32(block.timestamp).
flagOutcome() computes corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW).
Near the uint32 ceiling, the cast truncates the deadline to a timestamp lower than block.timestamp.
claimAttackerBounty() reverts because block.timestamp > corruptedClaimDeadline.
sweepUnclaimedCorrupted() succeeds because block.timestamp <= corruptedClaimDeadline is false.
This is a Low severity time-bound edge case. It does not give an arbitrary caller the ability to select the outcome, but it can fully bypass the promised 180-day claim window for a named good-faith attacker. The practical result is that the whitehat loses the full bounty and the recovery address receives the pool immediately.
The issue should still be fixed because the contract stores pool lifecycle values as uint32 and explicitly operates near that timestamp domain. Deadline arithmetic should not silently wrap when the protocol advertises a fixed 180-day reservation window.
Framework used: Foundry / forge
File 1: test/poc/ConfidencePoolOverflowPoC.t.sol
The passing PoC warps the timestamp to 31 days before type(uint32).max, flags good-faith CORRUPTED, and proves the stored deadline is below the current timestamp. The attacker claim then reverts as expired, while the recovery sweep transfers 110 * ONE and leaves the attacker with zero balance.
assertLt(pool.corruptedClaimDeadline(), block.timestamp) -> proves the deadline wrapped into the past -> PASS
vm.expectRevert(IConfidencePool.ClaimWindowExpired.selector) -> proves the named attacker cannot use the intended claim window -> PASS
assertEq(token.balanceOf(recovery) - recoveryBefore, 110 * ONE) -> proves the full pool can be swept immediately to recovery -> PASS
Store the first good-faith corrupted timestamp and claim deadline as uint256, or explicitly reject flags whose full 180-day deadline cannot be represented. Using uint256 for deadline arithmetic is the simpler fix and preserves the documented claim window.
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.