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

Blacklisted Attacker Address Cannot Claim Bounty, Entire Good-Faith CORRUPTED Pool Lost to `recoveryAddress`

Author Revealed upon completion

Summary

claimAttackerBounty() always sends to the hardcoded attacker address. If the attacker's address gets blacklisted by the stake token (e.g. USDC) after being named by the moderator, every claim attempt reverts. The attacker cannot redirect funds to another address. After corruptedClaimDeadline passes, the entire pool sweeps to recoveryAddress the attacker loses their full legitimate bounty entitlement permanently.

Root Cause

function claimAttackerBounty() external nonReentrant {
...
if (msg.sender != attacker) revert NotAttacker(); // only attacker can call
...
stakeToken.safeTransfer(attacker, payout); // hardcoded attacker address
// if attacker is blacklisted always reverts
}

After deadline, funds go elsewhere permanently:

function sweepUnclaimedCorrupted() external nonReentrant {
...
if (block.timestamp <= corruptedClaimDeadline) revert ClaimWindowNotExpired();
// Sweeps everything to recoveryAddress — attacker gets nothing
stakeToken.safeTransfer(recoveryAddress, amount);
}

No alternative path exists for the attacker to redirect their bounty.
Code snippet-
https://github.com/CodeHawks-Contests/2026-07-bc-confidence-pools/blob/58e8ba4ce3f3277866e4926f3140e597f9554a1e/src/ConfidencePool.sol#L432

Impact

Blacklisted attacker permanently loses entire bounty entitlement.

Mitigation

Add optional receiver parameter to claimAttackerBounty():

function claimAttackerBounty(address receiver) 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();
// Allow attacker to redirect to non-blacklisted address
if (receiver == address(0)) receiver = attacker;
uint256 remaining = bountyEntitlement - bountyClaimed;
uint256 freeBalance = stakeToken.balanceOf(address(this));
uint256 payout = remaining <= freeBalance ? remaining : freeBalance;
uint256 newBountyClaimed = bountyClaimed + payout;
bountyClaimed = newBountyClaimed;
if (payout > 0) {
corruptedReserve -= payout;
if (!claimsStarted) claimsStarted = true;
// Send to receiver, not hardcoded attacker
stakeToken.safeTransfer(receiver, payout);
}
emit AttackerBountyClaimed(attacker, receiver, payout, newBountyClaimed, bountyEntitlement);
}

This allows the named attacker (msg.sender check unchanged) to redirect their bounty to any non-blacklisted address they control, preventing permanent fund loss from token blacklisting while preserving all existing security guarantees.

Support

FAQs

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

Give us feedback!