function sweepUnclaimedBonus() external nonReentrant {
if (outcome != PoolStates.Outcome.SURVIVED && outcome != PoolStates.Outcome.EXPIRED) {
revert OutcomeNotEligibleForSweep();
}
uint256 reserved;
if (totalEligibleStake != 0) {
reserved = totalEligibleStake;
if (riskWindowStart != 0) {
reserved += snapshotTotalBonus - claimedBonus;
}
@>
}
uint256 freeBalance = stakeToken.balanceOf(address(this));
uint256 amount = freeBalance > reserved ? freeBalance - reserved : 0;
if (amount == 0) revert NothingToSweep();
@> if (totalEligibleStake == 0 || riskWindowStart == 0) {
@> totalBonus -= amount <= totalBonus ? amount : totalBonus;
@> }
@>
@>
stakeToken.safeTransfer(recoveryAddress, amount);
emit BonusSwept(msg.sender, recoveryAddress, amount);
}
```
```solidity
function flagOutcome(PoolStates.Outcome newOutcome, bool goodFaith_, address attacker_) external onlyModerator {
if (outcome != PoolStates.Outcome.UNRESOLVED && claimsStarted) revert OutcomeAlreadySet();
snapshotTotalStaked = totalEligibleStake;
@> snapshotTotalBonus = totalBonus;
snapshotSumStakeTime = sumStakeTime;
snapshotSumStakeTimeSq = sumStakeTimeSq;
corruptedReserve = newOutcome == PoolStates.Outcome.CORRUPTED ? snapshotTotalStaked + snapshotTotalBonus : 0;
@> bountyEntitlement = willBeGoodFaithCorrupted ? snapshotTotalStaked + snapshotTotalBonus : 0;
}
pragma solidity 0.8.26;
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
contract SweepBonusCorrectionWindowTest is BaseConfidencePoolTest {
uint256 constant STAKE_AMOUNT = 1_000_000e18;
uint256 constant BONUS_AMOUNT = 200_000e18;
address whitehat = makeAddr("whitehat");
address griefingCaller = makeAddr("griefingCaller");
function setUp() public override {
super.setUp();
_stake(alice, STAKE_AMOUNT);
_contributeBonus(address(this), BONUS_AMOUNT);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
assertEq(pool.riskWindowStart(), 0, "riskWindowStart should be 0");
}
function test_sweepBonusDuringCorrectionWindow_reducesAttackerBounty() external {
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
assertEq(uint8(pool.outcome()), uint8(PoolStates.Outcome.SURVIVED));
assertFalse(pool.claimsStarted(), "correction window open");
assertEq(pool.totalBonus(), BONUS_AMOUNT, "bonus intact before sweep");
vm.prank(griefingCaller);
pool.sweepUnclaimedBonus();
assertEq(pool.totalBonus(), 0, "totalBonus depleted");
assertFalse(pool.claimsStarted(), "claimsStarted still false — window still open");
assertEq(token.balanceOf(recovery), BONUS_AMOUNT, "bonus sent to recoveryAddress");
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, whitehat);
assertEq(pool.snapshotTotalBonus(), 0, "BUG: snapshotTotalBonus is 0");
assertEq(pool.bountyEntitlement(), STAKE_AMOUNT, "BUG: bounty missing bonus");
vm.prank(whitehat);
pool.claimAttackerBounty();
uint256 expectedCorrectBounty = STAKE_AMOUNT + BONUS_AMOUNT;
uint256 actualReceived = token.balanceOf(whitehat);
uint256 loss = expectedCorrectBounty - actualReceived;
assertEq(actualReceived, STAKE_AMOUNT, "Whitehat only received stake");
assertEq(loss, BONUS_AMOUNT, "Whitehat permanently lost full bonus (200,000e18)");
assertEq(token.balanceOf(recovery), BONUS_AMOUNT, "Sponsor kept the bonus");
}
function test_withoutSweep_whitehatGetsFullBounty() external {
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, whitehat);
assertEq(pool.snapshotTotalBonus(), BONUS_AMOUNT, "bonus preserved");
assertEq(pool.bountyEntitlement(), STAKE_AMOUNT + BONUS_AMOUNT, "full bounty");
vm.prank(whitehat);
pool.claimAttackerBounty();
assertEq(token.balanceOf(whitehat), STAKE_AMOUNT + BONUS_AMOUNT, "full payout");
}
}