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

Good-faith corrupted claim window can wrap near uint32 timestamp ceiling

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior: when the moderator flags a good-faith CORRUPTED outcome, the named attacker receives an exclusive CORRUPTED_CLAIM_WINDOW of 180 days to claim the full pool bounty. After this window, anyone can sweep the unclaimed corrupted funds to recoveryAddress.

  • Normal behavior: the contract stores several timestamps as uint32, and pool expiry is bounded by type(uint32).max.

  • Specific issue: flagOutcome() computes corruptedClaimDeadline as uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW). When the first good-faith corrupted flag occurs within 180 days of type(uint32).max, this addition exceeds the uint32 range and truncates into the past.

  • Specific issue: once the deadline wraps into the past, claimAttackerBounty() immediately reverts with ClaimWindowExpired, while sweepUnclaimedCorrupted() immediately becomes callable. This contradicts the documented 180-day exclusive bounty window.

function flagOutcome(PoolStates.Outcome newOutcome, bool goodFaith_, address attacker_) external onlyModerator {
...
if (willBeGoodFaithCorrupted) {
if (_firstGoodFaithCorruptedAt == 0) {
_firstGoodFaithCorruptedAt = uint32(block.timestamp);
}
// @> Adding 180 days can exceed uint32.max near the timestamp ceiling.
// @> The uint32 cast truncates the intended deadline into the past.
corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW);
} else {
corruptedClaimDeadline = 0;
}
}
function claimAttackerBounty() external nonReentrant {
...
// @> Wrapped deadline makes this revert immediately.
if (block.timestamp > corruptedClaimDeadline) revert ClaimWindowExpired();
...
}
function sweepUnclaimedCorrupted() external nonReentrant {
...
// @> Wrapped deadline makes this check pass immediately.
if (block.timestamp <= corruptedClaimDeadline) revert ClaimWindowNotExpired();
...
}

Risk

Likelihood:

  • A pool remains valid near the uint32 timestamp ceiling, which the contract otherwise supports by allowing expiry <= type(uint32).max.

  • The moderator flags a good-faith CORRUPTED outcome within 180 days of type(uint32).max.

  • The named attacker tries to claim after the wrapped deadline has already been stored in the past.

Impact:

  • The named good-faith attacker loses the documented 180-day exclusive bounty window.

  • The pool can be swept to recoveryAddress immediately instead of remaining reserved for the attacker.

  • The code behavior diverges from docs/DESIGN.md, which states that the pool is reserved for the named whitehat for CORRUPTED_CLAIM_WINDOW.

Proof of Concept

// Constants:
// CORRUPTED_CLAIM_WINDOW = 180 days = 15,552,000 seconds
// type(uint32).max = 4,294,967,295
// Assume the moderator flags good-faith CORRUPTED at:
uint256 nowTs = type(uint32).max - 31 days;
// nowTs = 4,292,288,895
// Intended deadline:
uint256 intendedDeadline = nowTs + 180 days;
// intendedDeadline = 4,307,840,895
// Actual stored deadline:
uint32 storedDeadline = uint32(intendedDeadline);
// storedDeadline = 12,873,599
// claimAttackerBounty() checks:
require(block.timestamp <= storedDeadline, "ClaimWindowExpired");
// At the current timestamp:
// 4,292,288,895 <= 12,873,599 is false
// Therefore, the attacker claim is immediately expired.
// sweepUnclaimedCorrupted() checks:
require(block.timestamp > storedDeadline, "ClaimWindowNotExpired");
// At the current timestamp:
// 4,292,288,895 > 12,873,599 is true
// Therefore, the corrupted funds can be swept immediately.

Recommended Mitigation

- uint32 internal _firstGoodFaithCorruptedAt;
- uint32 public override corruptedClaimDeadline;
+ uint64 internal _firstGoodFaithCorruptedAt;
+ uint64 public override corruptedClaimDeadline;

Or reject good-faith corrupted flags that cannot preserve the full claim window:

if (willBeGoodFaithCorrupted) {
if (_firstGoodFaithCorruptedAt == 0) {
_firstGoodFaithCorruptedAt = uint32(block.timestamp);
}
+ if (block.timestamp + CORRUPTED_CLAIM_WINDOW > type(uint32).max) {
+ revert ExpiryTooFar();
+ }
corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW);
}

Using uint64 or uint256 for corrupted claim timestamps is preferable because it preserves the full documented claim window without adding boundary-case policy logic.

Support

FAQs

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

Give us feedback!