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

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.

Updates

Lead Judging Commences

inallhonesty Lead Judge 2 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

QA / Informational / Gas

inallhonesty Lead Judge 2 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

QA / Informational / Gas

Support

FAQs

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

Give us feedback!