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

sweepUnclaimedCorrupted() Incorrectly Marks Bounty as Fully Paid Without Verifying Actual Payout

Author Revealed upon completion

Root + Impact

Description

The sweepUnclaimedCorrupted() function unconditionally sets bountyClaimed = bountyEntitlement without checking that the attacker actually received their full bounty. If the attacker received less due to:

• Fee-on-transfer tokens
• Being blocked by another vulnerability (e.g., Token Hook DoS - My 1st Finding)
• Insufficient pool balance

Then the difference is permanently lost when the sweep executes.

function sweepUnclaimedCorrupted() external nonReentrant {
// ... validations ...
uint256 amount = stakeToken.balanceOf(address(this));
if (amount == 0) revert NothingToSweep();
corruptedReserve = 0;
bountyClaimed = bountyEntitlement; // ← BUG: Never verified actual payout
if (!claimsStarted) claimsStarted = true;
stakeToken.safeTransfer(recoveryAddress, amount);
}

Risk

Likelihood:

• Reason 1: Factory owner may incorrectly whitelist fee-on-transfer tokens or upgradeable tokens that add hooks later. The DESIGN.md states "no fee tokens" but lacks runtime checks.

• Reason 2: If my Finding (Token Hook DoS) prevents claimAttackerBounty() from succeeding, the attacker receives 0 tokens but sweepUnclaimedCorrupted() still marks bounty as fully paid.

Impact:

• Impact 1: User permanently loses the difference between entitlement and actual payout, can be 100% of bounty if compounded with DoS vulnerability.

• Impact 2: Contract accounting becomes inconsistent, bountyClaimed no longer reflects actual amounts paid, breaking future logic and transparency.

Proof of Concept

This is a logic/accounting bug, the PoC demonstrates the state transition that causes permanent fund loss. Found in line 466: bountyClaimed = bountyEntitlement executes without verifying actual payout

#!/usr/bin/env python3
# SCENARIO 1: Fee-on-transfer token causes partial payment
Initial State:
bountyEntitlement: 1000 tokens
bountyClaimed: 900 tokens (attacker only received due to 10% fees)
poolBalance: 100 tokens
After sweepUnclaimedCorrupted():
bountyClaimed: 9001000 tokens (marked FULLY PAID!)
Transfer to recovery: 100 tokens
Result: Attacker LOST 100 tokens permanently.
# SCENARIO 2: Attacker blocked by Token Hook DoS (My Finding #1)
Initial State:
bountyEntitlement: 1000 tokens
bountyClaimed: 0 tokens (DoS prevented any claims)
poolBalance: 1000 tokens
After sweepUnclaimedCorrupted():
bountyClaimed: 01000 tokens (marked FULLY PAID!)
Transfer to recovery: 1000 tokens
Result: Attacker receives NOTHING, loses 100% of bounty.
# Root Cause (ConfidencePool.sol line 466):
bountyClaimed = bountyEntitlement; // BUG: Never checks actual payout

Run poc_sweep_bug.py in battlechain-confidence-pool/test/poc/ to see the calculations.

Recommended Mitigation

function sweepUnclaimedCorrupted() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
if (!goodFaith) revert NotGoodFaithCorrupted();
if (block.timestamp <= corruptedClaimDeadline) revert ClaimWindowNotExpired();
uint256 amount = stakeToken.balanceOf(address(this));
if (amount == 0) revert NothingToSweep();
corruptedReserve = 0;
- bountyClaimed = bountyEntitlement;
+ // Only mark as fully claimed if attacker was paid in full
+ if (bountyClaimed + amount < bountyEntitlement) {
+ // Attacker is still owed - give them partial credit
+ bountyClaimed += amount;
+ } else {
+ bountyClaimed = bountyEntitlement; // Full payment confirmed
+ }
if (!claimsStarted) claimsStarted = true;
stakeToken.safeTransfer(recoveryAddress, amount);
emit UnclaimedCorruptedSwept(msg.sender, recoveryAddress, amount);
}

Support

FAQs

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

Give us feedback!