## Description
The protocol lets the outcome moderator resolve a good-faith `CORRUPTED` pool by naming a whitehat `attacker`, who may then withdraw the entire pool via `claimAttackerBounty`. `docs/DESIGN.md` explicitly promises the moderator a correction window: *"`flagOutcome` may be re-flagged **pre-claim** so the moderator can fix a typo'd outcome/**attacker** before any participant locks in the wrong distribution."* That window is gated by the one-way `claimsStarted` latch — re-flagging is blocked once `claimsStarted == true`.
**Root cause (GitHub permalinks):**
- `claimAttackerBounty` — no lower-bound/challenge delay: https://github.com/CodeHawks-Contests/2026-07-bc-confidence-pools/blob/58e8ba4ce3f3277866e4926f3140e597f9554a1e/src/ConfidencePool.sol#L432-L453
- `claimAttackerBounty` sets `claimsStarted` on first payout: https://github.com/CodeHawks-Contests/2026-07-bc-confidence-pools/blob/58e8ba4ce3f3277866e4926f3140e597f9554a1e/src/ConfidencePool.sol#L448
- `flagOutcome` re-flag gate on `claimsStarted`: https://github.com/CodeHawks-Contests/2026-07-bc-confidence-pools/blob/58e8ba4ce3f3277866e4926f3140e597f9554a1e/src/ConfidencePool.sol#L327
---
The problem is that `claimAttackerBounty` enforces only an **upper** time bound (`corruptedClaimDeadline`) and **no lower bound / challenge delay**. The named `attacker` can therefore claim in the very next block after `flagOutcome`, and that claim sets `claimsStarted = true`. If the moderator named the *wrong* address (a fat-finger, or a socially-engineered attacker parameter) and that address is live and adversarial, it front-runs the moderator's correction: the bounty is drained and the subsequent corrective `flagOutcome` reverts `OutcomeAlreadySet`. The §4 correction guarantee does not hold for a mis-named *live* attacker.
```solidity
function claimAttackerBounty() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
if (bountyClaimed == bountyEntitlement) revert BountyAlreadyClaimed();
if (!goodFaith) revert InvalidGoodFaithParams();
if (msg.sender != attacker) revert NotAttacker();
@> if (block.timestamp > corruptedClaimDeadline) revert ClaimWindowExpired();
uint256 remaining = bountyEntitlement - bountyClaimed;
uint256 freeBalance = stakeToken.balanceOf(address(this));
uint256 payout = remaining <= freeBalance ? remaining : freeBalance;
uint256 newBountyClaimed = bountyClaimed + payout;
bountyClaimed = newBountyClaimed;
if (payout > 0) {
corruptedReserve -= payout;
@> if (!claimsStarted) claimsStarted = true;
stakeToken.safeTransfer(attacker, payout);
}
emit AttackerBountyClaimed(attacker, payout, newBountyClaimed, bountyEntitlement);
}
```
```solidity
function flagOutcome(PoolStates.Outcome newOutcome, bool goodFaith_, address attacker_) external onlyModerator {
@> if (outcome != PoolStates.Outcome.UNRESOLVED && claimsStarted) revert OutcomeAlreadySet();
...
}
```
Risk
Likelihood:
Occurs whenever the moderator names an incorrect attacker_ in a good-faith CORRUPTED flag and that address is a live, mempool-watching adversary (e.g. a transposed-but-live address, or an attacker parameter injected via social engineering of the moderator).
Occurs at good-faith CORRUPTED resolution — the exact moment a large pool becomes claimable — so the mis-named party is maximally incentivised to claim before the moderator notices.
Impact:
The entire good-faith CORRUPTED pool (snapshotTotalStaked + snapshotTotalBonus) is paid to the wrong address, irreversibly.
The moderator's docs/DESIGN.md §4 "fix a typo'd attacker before any claim" guarantee is void for a live mis-name; the corrective flagOutcome reverts OutcomeAlreadySet.
Proof of Concept
The first test shows a mis-named **live** address front-running the correction; the second shows that a **dead** mis-name (the common typo case) *is* still correctable — bounding the risk to live/hostile addresses.
```solidity
function test_edge_misNamedLiveAttacker_frontrunsCorrection() public {
_stake(alice, 100 ether);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, mallory);
vm.prank(mallory);
pool.claimAttackerBounty();
assertEq(token.balanceOf(mallory), 100 ether, "wrongly-named live attacker drained the pool");
vm.prank(moderator);
vm.expectRevert(IConfidencePool.OutcomeAlreadySet.selector);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
}
function test_edge_misNamedDeadAttacker_correctionStillWorks() public {
_stake(alice, 100 ether);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, address(0xDEAD));
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
assertEq(pool.attacker(), attacker, "correction succeeds against a dead mis-name");
vm.prank(attacker);
pool.claimAttackerBounty();
assertEq(token.balanceOf(attacker), 100 ether, "correct attacker paid");
}
```
**Outcome (as run):**
```
Ran 2 tests for test/poc/PoCEdgeCases.t.sol:PoCEdgeCases
[PASS] test_edge_misNamedDeadAttacker_correctionStillWorks() (gas: 522110)
[PASS] test_edge_misNamedLiveAttacker_frontrunsCorrection() (gas: 519554)
```
Both pass: the live mis-name drains the pool and blocks correction (`OutcomeAlreadySet`), while the dead-address case remains correctable.
---
Recommended Mitigation
Add a short **challenge/timelock window** on `claimAttackerBounty`, measured from the first good-faith flag, so the moderator has a bounded period to re-flag a corrected attacker before any claim can latch finality. (Alternatively, or in addition, scope the `docs/DESIGN.md` §4 wording so it no longer promises correction of a mis-named *live* attacker.)
```diff
+ /// @notice Delay after the first good-faith CORRUPTED flag before the named attacker may claim,
+ /// giving the moderator a bounded window to correct a mis-named attacker.
+ uint256 public constant BOUNTY_CLAIM_DELAY = 1 days;
function claimAttackerBounty() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
if (bountyClaimed == bountyEntitlement) revert BountyAlreadyClaimed();
if (!goodFaith) revert InvalidGoodFaithParams();
if (msg.sender != attacker) revert NotAttacker();
+ if (block.timestamp < _firstGoodFaithCorruptedAt + BOUNTY_CLAIM_DELAY) revert BountyClaimNotYetOpen();
if (block.timestamp > corruptedClaimDeadline) revert ClaimWindowExpired();
```