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

Good-Faith Corrupted Claim Deadline Can Wrap and Immediately Expire Near uint32 Ceiling

Author Revealed upon completion

Root + Impact

Description

For good-faith CORRUPTED outcomes, the intended behavior is that the named attacker has CORRUPTED_CLAIM_WINDOW to claim the bounty before unclaimed funds can be swept to recoveryAddress.

The issue is that the deadline is stored as uint32 after adding a 180 day window to the current timestamp. If the first good-faith corrupted flag is made near the uint32 timestamp ceiling, the addition wraps the deadline into the past. The attacker's claim window is therefore already expired in the same block, and anyone can immediately sweep the bounty to recoveryAddress.

function flagOutcome(PoolStates.Outcome newOutcome, bool goodFaith_, address attacker_)
external
nonReentrant
onlyModerator
{
...
if (willBeGoodFaithCorrupted) {
if (_firstGoodFaithCorruptedAt == 0) {
// @> Stores the near-ceiling block timestamp in uint32
_firstGoodFaithCorruptedAt = uint32(block.timestamp);
}
// @> Adding 180 days near type(uint32).max wraps the deadline into the past
corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW);
} else {
corruptedClaimDeadline = 0;
}
outcomeFlaggedAt = riskWindowEnd;
...
}
function claimAttackerBounty() external nonReentrant {
...
// @> The wrapped deadline makes the bounty unclaimable immediately
if (block.timestamp > corruptedClaimDeadline) revert ClaimWindowExpired();
...
}
function sweepUnclaimedCorrupted() external nonReentrant {
...
// @> The same wrapped deadline allows immediate sweeping
if (block.timestamp <= corruptedClaimDeadline) revert ClaimWindowNotExpired();
...
}

Risk

Likelihood:

  • This occurs when a good-faith corrupted outcome is first flagged within CORRUPTED_CLAIM_WINDOW of the uint32 timestamp ceiling.

  • This occurs before the attacker claims, because the wrapped corruptedClaimDeadline is lower than the current block timestamp immediately after flagOutcome().

Impact:

  • The named good-faith attacker cannot claim the bounty, even though the moderator named them as eligible.

  • The full good-faith corrupted bounty can be swept to recoveryAddress immediately instead of after the intended claim window.

Proof of Concept

Add this test to test/unit/ConfidencePool.branches.t.sol:

function testGoodFaithCorruptedDeadlineWrapsNearUint32Ceiling() external {
uint256 nearUint32Ceiling = uint256(type(uint32).max) - 31 days;
vm.warp(nearUint32Ceiling);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.NEW_DEPLOYMENT);
pool = _deployPool();
_stake(alice, 100 * ONE);
_contributeBonus(carol, 50 * ONE);
_passThroughUnderAttack();
vm.warp(uint256(type(uint32).max) - 1 days);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
assertLt(pool.corruptedClaimDeadline(), block.timestamp);
vm.prank(attacker);
vm.expectRevert(IConfidencePool.ClaimWindowExpired.selector);
pool.claimAttackerBounty();
uint256 recoveryBefore = token.balanceOf(recovery);
pool.sweepUnclaimedCorrupted();
assertEq(token.balanceOf(recovery) - recoveryBefore, 150 * ONE);
assertEq(pool.bountyClaimed(), pool.bountyEntitlement());
}

Run:

forge test --match-test testGoodFaithCorruptedDeadlineWrapsNearUint32Ceiling -vvv

Expected result:

[PASS] testGoodFaithCorruptedDeadlineWrapsNearUint32Ceiling()

Recommended Mitigation

Store corrupted claim deadlines in uint256 and validate that the computed deadline does not exceed the supported timestamp range.

- uint32 public override corruptedClaimDeadline;
+ uint256 public override corruptedClaimDeadline;
if (willBeGoodFaithCorrupted) {
if (_firstGoodFaithCorruptedAt == 0) {
- _firstGoodFaithCorruptedAt = uint32(block.timestamp);
+ _firstGoodFaithCorruptedAt = block.timestamp;
}
- corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW);
+ corruptedClaimDeadline = _firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW;
} else {
corruptedClaimDeadline = 0;
}

If packed storage is required, reject pool creation or outcome flagging when block.timestamp + CORRUPTED_CLAIM_WINDOW cannot fit in uint32.

Support

FAQs

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

Give us feedback!