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

After the good-faith bounty window, `claimCorrupted` stays blocked

Author Revealed upon completion

Root + Impact

Description

On good faith CORRUPTED, claimCorrupted requires bountyClaimed == bountyEntitlement (MustClaimBountyFirst) and never checks corruptedClaimDeadline. After the 180-day window, claimAttackerBounty reverts ClaimWindowExpired, so the whitehat can no longer fill that gate.

Post window leftovers are only recoverable via sweepUnclaimedCorrupted, which forges bountyClaimed = bountyEntitlement and sweeps to recovery. Callers that reuse the bad faith twin claimCorrupted hit a permanent revert that still blames an unfinished bounty.

function claimCorrupted() external nonReentrant {
// @> No deadline exception — stays true forever once the whitehat window closed unpaid.
if (goodFaith && bountyClaimed < bountyEntitlement) revert MustClaimBountyFirst();
// ...
}
function sweepUnclaimedCorrupted() external nonReentrant {
if (block.timestamp <= corruptedClaimDeadline) revert ClaimWindowNotExpired();
// @> Only this path clears the bounty gate after the window.
bountyClaimed = bountyEntitlement;
stakeToken.safeTransfer(recoveryAddress, amount);
}

Risk

Likelihood:

  • Good faith CORRUPTED with an absent / late whitehat is an intended ops path after CORRUPTED_CLAIM_WINDOW.

  • Keepers and integrators that only call claimCorrupted for “send leftovers to recovery” (works for bad-faith) reuse that entrypoint after the GF window.

Impact:

  • Funds remain idle until someone discovers sweepUnclaimedCorrupted.

  • Error message MustClaimBountyFirst is misleading once the attacker can no longer claim.

Proof of Concept

Run:

forge test --match-test test_PoC_claimCorruptedAfterDeadline_requiresSweepUnclaimedCorrupted -vv
// test/unit/ConfidencePool.t.sol
function test_PoC_claimCorruptedAfterDeadline_requiresSweepUnclaimedCorrupted() external {
_stake(alice, 100 * ONE);
_stake(bob, 30 * ONE);
_contributeBonus(carol, 40 * ONE);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
vm.warp(pool.corruptedClaimDeadline() + 1);
vm.prank(attacker);
vm.expectRevert(IConfidencePool.ClaimWindowExpired.selector);
pool.claimAttackerBounty(); // whitehat path closed
vm.expectRevert(IConfidencePool.MustClaimBountyFirst.selector);
pool.claimCorrupted(); // dead-end — gate ignores deadline
pool.sweepUnclaimedCorrupted(); // only twin that works
assertEq(token.balanceOf(recovery), 170 * ONE);
}

Recommended Mitigation

function claimCorrupted() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
- if (goodFaith && bountyClaimed < bountyEntitlement) revert MustClaimBountyFirst();
+ if (goodFaith && bountyClaimed < bountyEntitlement) {
+ if (block.timestamp <= corruptedClaimDeadline) revert MustClaimBountyFirst();
+ // post-window: fall through (same as sweepUnclaimedCorrupted)
+ }

Or revert with a dedicated UseSweepUnclaimedCorrupted error after the deadline.

Support

FAQs

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

Give us feedback!