The protocol intentionally allows moderators to correct an erroneous good-faith CORRUPTED designation before the first bounty claim by re-flagging the pool's outcome. During these pre-claim corrections, flagOutcome() recomputes all snapshot-related state, making repeated re-flags an expected workflow.
However, _firstGoodFaithCorruptedAt is initialized only once when the pool is first marked as good-faith CORRUPTED and is never reset. While this prevents moderators from extending the attacker claim window through repeated re-flags, it also causes corrected good-faith CORRUPTED designations to inherit the original claim deadline.
If a moderator initially flags the pool with an incorrect attacker, later restores the outcome to SURVIVED, and subsequently re-flags the pool as good-faith CORRUPTED with the correct attacker, corruptedClaimDeadline is still derived from the original _firstGoodFaithCorruptedAt timestamp rather than the corrected designation.
As a result, if the original 180-day claim window has already expired, the newly designated attacker cannot claim the bounty because claimAttackerBounty() immediately reverts with ClaimWindowExpired. At the same time, sweepUnclaimedCorrupted() becomes immediately callable, permanently preventing the rightful attacker from receiving the intended bounty.
363 if (willBeGoodFaithCorrupted) {
@>364 if (_firstGoodFaithCorruptedAt == 0) {
365
366 _firstGoodFaithCorruptedAt = uint32(block.timestamp);
367 }
368
...
@>372 corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW);
373 } else {
374 corruptedClaimDeadline = 0;
375 }
_firstGoodFaithCorruptedAt is intentionally write-once. As a result,
every subsequent good-faith CORRUPTED designation derives corruptedClaimDeadline
from the original timestamp, even if an earlier designation was reverted before
any bounty claim occurred. Unlike the snapshot fields, which are recomputed on
every flagOutcome() invocation, this timestamp persists across legitimate pre-claim corrections.
Risk
Likelihood:
Medium. The issue requires a moderator to correct an earlier good-faith CORRUPTED designation before any bounty has been claimed and subsequently identify the correct attacker after the original 180-day claim window has elapsed. Because the protocol explicitly supports pre-claim outcome corrections, this scenario is realistic during long-running investigations or when an attacker is initially misidentified.
Impact:
Medium. The rightful bounty recipient can be permanently prevented from claiming the attacker bounty despite eventually being correctly identified. Once the inherited claim deadline has expired, claimAttackerBounty() always reverts with ClaimWindowExpired, and sweepUnclaimedCorrupted() can immediately transfer the pool to recoveryAddress, permanently preventing the intended bounty payout for that corruption event.
Proof of Concept
The following PoC demonstrates that a moderator can legitimately correct a good-faith CORRUPTED
designation before any claim occurs. Despite the correction, _firstGoodFaithCorruptedAt is
not reset, causing the newly calculated corruptedClaimDeadline to remain anchored to the original designation. After the original 180-day period expires, the correct attacker is unable to claim the bounty while sweepUnclaimedCorrupted() becomes immediately executable.
function test_flagOutcome_correctingAttackerAfterSurvivedDetourCanForfeitBounty() external {
address wrongAttacker = makeAddr("wrongAttacker");
address rightAttacker = makeAddr("rightAttacker");
_stake(alice, 100 * ONE);
_contributeBonus(carol, 50 * ONE);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, wrongAttacker);
uint256 originalDeadline = pool.corruptedClaimDeadline();
vm.warp(vm.getBlockTimestamp() + 5 days);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
vm.warp(vm.getBlockTimestamp() + 195 days);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, rightAttacker);
assertEq(pool.corruptedClaimDeadline(), originalDeadline, "deadline still anchored to day 0");
assertLt(pool.corruptedClaimDeadline(), vm.getBlockTimestamp(), "window already expired on arrival");
vm.prank(rightAttacker);
vm.expectRevert(IConfidencePool.ClaimWindowExpired.selector);
pool.claimAttackerBounty();
uint256 poolBalance = token.balanceOf(address(pool));
uint256 recoveryBefore = token.balanceOf(recovery);
pool.sweepUnclaimedCorrupted();
assertEq(token.balanceOf(recovery) - recoveryBefore, poolBalance, "full pool diverted to recoveryAddress");
assertEq(token.balanceOf(rightAttacker), 0, "correct whitehat received nothing");
}
Recommended Mitigation
The mitigation should preserve the protocol's existing protection against moderators indefinitely extending
the claim window through repeated re-flagging while still allowing legitimate pre-claim corrections to
establish a fresh claim deadline for the currently designated attacker.
This can be achieved by resetting _firstGoodFaithCorruptedAt whenever a good-faith CORRUPTED designation
is reverted before any bounty has been claimed, or by recomputing corruptedClaimDeadline when a corrected
good-faith CORRUPTED designation is made.