FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: medium
Likelihood: low

Corrupted claim deadline computation can overflow uint32, truncating the attacker bounty window

Author Revealed upon completion

DESCRIPTION

The pool has a 180-day window (CORRUPTED_CLAIM_WINDOW) during which a whitehat attacker named in a good-faith CORRUPTED resolution can claim the full pool bounty. The deadline is computed from _firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW and stored in a uint32.

The problem is that the addition is performed in uint256 only after widening, but the sum is then cast to uint32. If _firstGoodFaithCorruptedAt is close to type(uint32).max (~4.29e9, corresponding to 2106-02-07), adding 180 days (15,552,000 seconds) overflows the uint32 range, causing the deadline to wrap to a small or already-expired value.

Root cause:

@> confidencePool.sol:370-374

// forge-lint: disable-next-line(unsafe-typecast)
_firstGoodFaithCorruptedAt = uint32(block.timestamp);
}
// Sum stays in uint32 unless flagged within 180 days of the 2106 ceiling; out of scope.
// forge-lint: disable-next-line(unsafe-typecast)
corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW);

The NatSpec comment acknowledges the truncation risk ("out of scope") but the downstream consequence is that the attacker's bounty claim window silently collapses — the deadline becomes a past or near-zero value, and claimAttackerBounty reverts with ClaimWindowExpired even when called within the intended 180-day window.

RISK

Likelihood: Low

  • The pool must be initialized with an expiry near type(uint32).max (after ~2105), and the moderator must flag a good-faith CORRUPTED outcome at that time. For pools created today with reasonable expiry dates, this will not trigger for decades.

  • However, setExpiry allows pushing expiry up to type(uint32).max, and the pool lifecycle can span any duration within that window.

Impact: Medium

  • The whitehat attacker's 180-day bounty claim window is silently truncated or eliminated.

  • If the deadline wraps to a past value, claimAttackerBounty is permanently blocked, and the full pool sweeps to recoveryAddress via sweepUnclaimedCorrupted after the truncated deadline. The attacker receives nothing despite being entitled to the bounty.

  • The moderator cannot fix this — _firstGoodFaithCorruptedAt is set once and never reset.


PROOF OF CONCEPT

  1. A pool is initialized with a far-future expiry (type(uint32).max - 100_000)

  2. A staker deposits so bountyEntitlement > 0

  3. Timestamp warps to type(uint32).max - 50_000 (~2105)

  4. Registry transitions through active risk to CORRUPTED

  5. Moderator flags good-faith CORRUPTED

  6. corruptedClaimDeadline = uint32(4294917295 + 15552000) = 15501999 (wrapped!)

  7. The deadline at ~15502000 corresponds to year 1970 — 135 years in the past

  8. Attacker calls claimAttackerBounty → reverts with ClaimWindowExpired

// flagTime = 4294917295
// flagTime + 180 days = 4310469295
// type(uint32).max = 4294967295
// 4310469295 > 4294967295 → overflow!
// uint32(4310469295) = 4310469295 - 2^32 = 15502000 (wrapped to 1970)

RECOMMENDED MITIGATION

Use uint256 for the intermediate arithmetic and cap the result:

+ uint256 deadline = uint256(_firstGoodFaithCorruptedAt) + CORRUPTED_CLAIM_WINDOW;
+ corruptedClaimDeadline = deadline > type(uint32).max ? type(uint32).max : uint32(deadline);

Or alternatively revert on overflow:

+ if (_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW > type(uint32).max) {
+ revert ClaimDeadlineOverflow();
+ }
+ corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW);

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!