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 {
if (goodFaith && bountyClaimed < bountyEntitlement) revert MustClaimBountyFirst();
}
function sweepUnclaimedCorrupted() external nonReentrant {
if (block.timestamp <= corruptedClaimDeadline) revert ClaimWindowNotExpired();
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:
Proof of Concept
Run:
forge test --match-test test_PoC_claimCorruptedAfterDeadline_requiresSweepUnclaimedCorrupted -vv
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();
vm.expectRevert(IConfidencePool.MustClaimBountyFirst.selector);
pool.claimCorrupted();
pool.sweepUnclaimedCorrupted();
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.