function claimAttackerBounty() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
@> if (bountyClaimed == bountyEntitlement) revert BountyAlreadyClaimed();
if (!goodFaith) revert InvalidGoodFaithParams();
if (msg.sender != attacker) revert NotAttacker();
if (block.timestamp > corruptedClaimDeadline) revert ClaimWindowExpired();
function sweepUnclaimedCorrupted() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
if (!goodFaith) revert NotGoodFaithCorrupted();
if (block.timestamp <= corruptedClaimDeadline) revert ClaimWindowNotExpired();
...
@> bountyClaimed = bountyEntitlement;
The first test proves the error is misleading, not just imprecise, by showing the deadline check would have produced the same outcome on its own, this is purely about which message the user actually sees. The second test confirms a real double-claim attempt still shows the same error correctly, so this isn't a general break of the message, just these two specific triggers. The third test is the sharper one, it shows the same wrong error firing on a completely fresh pool, first interaction ever, while the window is still wide open.
function testSweepThenLateClaimRevertsWithMisleadingBountyAlreadyClaimedNotDeadlineExpired() external {
_stake(alice, 100 * ONE);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
vm.warp(pool.corruptedClaimDeadline() + 1);
pool.sweepUnclaimedCorrupted();
assertEq(pool.bountyClaimed(), pool.bountyEntitlement(), "sweep marks bountyClaimed as if fully paid");
assertEq(token.balanceOf(attacker), 0, "attacker received literally nothing");
vm.prank(attacker);
vm.expectRevert(IConfidencePool.BountyAlreadyClaimed.selector);
pool.claimAttackerBounty();
}
function testZeroEntitlementCaseShowsSameMisleadingErrorOnFirstEverAttempt() external {
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
assertEq(pool.bountyEntitlement(), 0);
assertEq(pool.bountyClaimed(), 0, "attacker has made zero attempts so far");
vm.prank(attacker);
vm.expectRevert(IConfidencePool.BountyAlreadyClaimed.selector);
pool.claimAttackerBounty();
}
The token.balanceOf(attacker), 0 assertion in the first test is the key proof, the error name and reality directly contradict each other. The third test needs no balance assertion at all to make its point, the deadline in that run hasn't even started to matter yet, and the message still tells the attacker they're too late.
Check the deadline before checking whether the bounty was already claimed, so a caller who's actually out of time sees that instead:
function claimAttackerBounty() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
- if (bountyClaimed == bountyEntitlement) revert BountyAlreadyClaimed();
if (!goodFaith) revert InvalidGoodFaithParams();
if (msg.sender != attacker) revert NotAttacker();
if (block.timestamp > corruptedClaimDeadline) revert ClaimWindowExpired();
+ if (bountyClaimed == bountyEntitlement) revert BountyAlreadyClaimed();
This doesn't fix the zero-entitlement case on its own, that one fires before the deadline even matters, and fixing it properly means addressing how bountyEntitlement gets computed in the first place, which is the separate finding this connects to. What this reordering does fix cleanly is the post-sweep case, and it costs nothing, a genuine double claim within the window still falls through to the correct BountyAlreadyClaimed exactly as it does today, verified directly by the second test above.