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 {
uint256 amount = stakeToken.balanceOf(address(this));
if (amount == 0) revert NothingToSweep();
corruptedReserve = 0;
bountyClaimed = bountyEntitlement;
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: 900 → 1000 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: 0 → 1000 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;
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);
}