## Description
Normally, the pool should only sweep the unclaimed portion of the calculated bounty entitlement (`corruptedReserve`) to the recovery address after the attacker claim window expires.
In the contract, the `sweepUnclaimedCorrupted()` function queries the absolute pool balance using `stakeToken.balanceOf(address(this))` and sweeps the entire amount to the recovery address, causing any extra funds/donations sent to the pool to be swept out as well.
```solidity
// In ConfidencePool.sol: sweepUnclaimedCorrupted()
function sweepUnclaimedCorrupted() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
if (!goodFaith) revert NotGoodFaithCorrupted();
if (block.timestamp <= corruptedClaimDeadline) revert ClaimWindowNotExpired();
// aderyn-fp-next-line(reentrancy-state-change)
@> uint256 amount = stakeToken.balanceOf(address(this));
if (amount == 0) revert NothingToSweep();
corruptedReserve = 0;
bountyClaimed = bountyEntitlement;
if (!claimsStarted) claimsStarted = true;
@> stakeToken.safeTransfer(recoveryAddress, amount);
emit UnclaimedCorruptedSwept(msg.sender, recoveryAddress, amount);
}
```
## Risk
Likelihood:
- The pool is set to `CORRUPTED` state.
- Additional tokens are directly transferred to the pool post-resolution.
- The claim window expires and `sweepUnclaimedCorrupted` is called.
Impact:
- Direct donations or accidental transfers to the pool are swept to the recovery address.
## Proof of Concept
Here is a test showing how an excess direct donation is swept entirely to the recovery address instead of remaining in the pool. Place this test in your test suite:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
contract M02SweepCorruptedExploit is BaseConfidencePoolTest {
function testExploit_SweepCorruptedDrainsDonations() public {
// Alice stakes 100 tokens
_stake(alice, 100 * ONE);
// Flag corrupted
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, address(0));
// Excess donation sent directly to the pool
token.mint(address(pool), 500 * ONE);
// Claim window expires
vm.warp(block.timestamp + 181 days);
// Sweep
uint256 recoveryPre = token.balanceOf(recovery);
pool.sweepUnclaimedCorrupted();
uint256 swept = token.balanceOf(recovery) - recoveryPre;
// Drains both the 100e18 bounty reserve AND the 500e18 donation
assertEq(swept, 600 * ONE);
}
}
```
## Recommended Mitigation
Update `sweepUnclaimedCorrupted` to sweep `corruptedReserve` instead of querying the dynamic pool balance.
```diff
function sweepUnclaimedCorrupted() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
if (!goodFaith) revert NotGoodFaithCorrupted();
if (block.timestamp <= corruptedClaimDeadline) revert ClaimWindowNotExpired();
- // aderyn-fp-next-line(reentrancy-state-change)
- uint256 amount = stakeToken.balanceOf(address(this));
+ uint256 amount = corruptedReserve;
if (amount == 0) revert NothingToSweep();
corruptedReserve = 0;
bountyClaimed = bountyEntitlement;
if (!claimsStarted) claimsStarted = true;
stakeToken.safeTransfer(recoveryAddress, amount);
```