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

claimAttackerBounty() emits a misleading zero-value event without reverting when pool balance is zero

Author Revealed upon completion

Root + Impact

Description

  • When freeBalance == 0, payout evaluates to 0. The function correctly skips the transfer and leaves claimsStarted untouched, but it still writes bountyClaimed with its unchanged value and unconditionally emits AttackerBountyClaimed(attacker, 0, ...) — a call that moved nothing succeeds silently rather than reverting.


// Root cause in the codebase with @> marks to highlight the relevant section
uint256 payout = remaining <= freeBalance ? remaining : freeBalance;
@> bountyClaimed = newBountyClaimed; // written even when payout == 0
if (payout > 0) { ... }
@> emit AttackerBountyClaimed(attacker, payout, newBountyClaimed, bountyEntitlement);

Risk

Likelihood:

  • Occurs whenever the pool balance is temporarily zero when the named attacker calls this function, reachable naturally after a prior partial claim drains the balance.


Impact:

  • No funds move and no invariant breaks — claimsStarted is confirmed to stay false. Impact is limited to misleading off-chain indexers and a wasted storage write per call.


Proof of Concept

The PoC forces the pool balance to zero after a good-faith CORRUPTED flag, then calls claimAttackerBounty() and confirms the event fires with amount == 0 despite no revert, bountyClaimed staying at its prior value, and claimsStarted remaining false — ruling out any escalation beyond log/gas noise.

function test_PoC_ZeroPayoutClaimEmitsMisleadingEventWithoutReverting() external
{
_stake(alice, 100 * ONE);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
deal(address(token), address(pool), 0);
uint256 bountyClaimedBefore = pool.bountyClaimed();
vm.prank(attacker);
pool.claimAttackerBounty(); // succeeds, moves nothing
assertEq(pool.bountyClaimed(), bountyClaimedBefore);
assertFalse(pool.claimsStarted());
}

Recommended Mitigation

Revert early on a zero payout using the existing NothingToSweep error, removing the wasted write and misleading emission while leaving every genuine-payout path unchanged.

uint256 payout = remaining <= freeBalance ? remaining : freeBalance;
+
+ if (payout == 0) revert NothingToSweep();
uint256 newBountyClaimed = bountyClaimed + payout;
bountyClaimed = newBountyClaimed;
- if (payout > 0) {
- corruptedReserve -= payout;
- if (!claimsStarted) claimsStarted = true;
- stakeToken.safeTransfer(attacker, payout);
- }
+ corruptedReserve -= payout;
+ if (!claimsStarted) claimsStarted = true;
+ stakeToken.safeTransfer(attacker, payout);
emit AttackerBountyClaimed(attacker, payout, newBountyClaimed, bountyEntitlement);

Support

FAQs

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

Give us feedback!