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

Unreachable guard in `claimAttackerBounty` makes a bad-faith bounty revert with the wrong error

Author Revealed upon completion

Root + Impact

Description

  • A bad-faith CORRUPTED pool has no attacker bounty, so a bounty call on it should be rejected with InvalidGoodFaithParams.

  • On a bad-faith pool bountyEntitlement == 0 and bountyClaimed starts at 0, so the bountyClaimed == bountyEntitlement check (0 == 0) always fires first; the !goodFaith guard below it is therefore dead code, and the call reverts with the semantically-wrong BountyAlreadyClaimed.

// src/ConfidencePool.sol — claimAttackerBounty()
@> if (bountyClaimed == bountyEntitlement) revert BountyAlreadyClaimed(); // 0 == 0 for bad-faith → fires first
@> if (!goodFaith) revert InvalidGoodFaithParams(); // unreachable dead code

Risk

Likelihood:

  • Triggered on every bad-faith-CORRUPTED bounty call attempt.

Impact:

  • Callers/integrators receive a misleading revert reason (BountyAlreadyClaimed instead of InvalidGoodFaithParams), and the contract carries an unreachable branch. No funds move.

Proof of Concept

The bounty path is only meaningful for a good-faith CORRUPTED resolution: flagOutcome sets
bountyEntitlement = snapshotTotalStaked + snapshotTotalBonus for good-faith, but bountyEntitlement = 0
for bad-faith (no named attacker, funds sweep to recoveryAddress instead). bountyClaimed is 0
until a payout occurs. So on a bad-faith pool, when anyone calls claimAttackerBounty, the guards evaluate
top-to-bottom as:

  1. outcome != CORRUPTED → false (pool is CORRUPTED), continue.

  2. bountyClaimed == bountyEntitlement0 == 0 is true → reverts BountyAlreadyClaimed.

  3. !goodFaithnever reached — this is the branch that was meant to reject a bad-faith bounty call.

The caller is told the bounty was "already claimed" when in fact no bounty ever existed. The intended
error InvalidGoodFaithParams is unreachable for the entire bad-faith outcome.

Step-by-step reproduction (from test/fuzz/ConfidencePool.invariants.t.sol :: test_bounty_revert_matrix):

// 1. A staker deposits and the risk window is observed.
_stake(alice, 10 * ONE);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
// 2. Moderator flags BAD-FAITH CORRUPTED (goodFaith = false, no attacker).
// -> bountyEntitlement is set to 0.
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, false, address(0));
// 3. Anyone calls the bounty path. It SHOULD revert InvalidGoodFaithParams
// (there is no bounty on a bad-faith pool). Instead it reverts BountyAlreadyClaimed
// because `bountyClaimed (0) == bountyEntitlement (0)` fires first.
vm.prank(attacker);
vm.expectRevert(IConfidencePool.BountyAlreadyClaimed.selector); // NOT InvalidGoodFaithParams
pool.claimAttackerBounty();

Run it with:

forge test --match-test test_bounty_revert_matrix

The test passes with the BountyAlreadyClaimed expectation, confirming the good-faith guard is dead code for
the bad-faith case and the revert reason is misleading.

Recommended Mitigation

Move the good-faith check above the entitlement check so the bad-faith case is rejected for the correct
reason before the 0 == 0 entitlement comparison can fire:

+ if (!goodFaith) revert InvalidGoodFaithParams();
if (bountyClaimed == bountyEntitlement) revert BountyAlreadyClaimed();
- if (!goodFaith) revert InvalidGoodFaithParams();

Ordering matters here because bountyEntitlement == 0 is a legitimate resting state for a bad-faith pool,
not evidence that a bounty was consumed. Checking !goodFaith first cleanly separates the two conditions:
"this pool has no bounty path" (InvalidGoodFaithParams) versus "the good-faith bounty is fully claimed"
(BountyAlreadyClaimed). After the reorder, the entitlement check only governs genuine good-faith pools,
where bountyClaimed == bountyEntitlement correctly means the bounty has been exhausted, and no branch is
left unreachable.

Support

FAQs

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

Give us feedback!