Root + Impact
Description
-
DESIGN.md §4 and the flagOutcome natspec promise the moderator can "fix a typo'd outcome/attacker before any participant locks in the wrong distribution."
-
But claimAttackerBounty imposes no delay: the instant the moderator flags CORRUPTED, goodFaith=true, attacker=A, address A can claim min(bountyEntitlement, balance) (the whole pool) in the same or next block, which sets claimsStarted and forecloses the corrective re-flag. The single field whose mis-set value hands funds to an untrusted party is exactly the one with no correction latency.
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 (payout > 0) {
corruptedReserve -= payout;
@> if (!claimsStarted) claimsStarted = true;
stakeToken.safeTransfer(attacker, payout);
}
}
Risk
Likelihood:
-
Occurs when the moderator flags good-faith CORRUPTED naming the wrong address (a typo, a stale value, or an address positioned to be named) while the registry is CORRUPTED.
-
The named address can claim before the moderator observes and re-flags, since there is no minimum delay.
Impact:
A single moderator naming error is irreversible: the wrongly-named address takes 100% of the pool (principal + bonus), with zero window to correct it, despite the advertised pre-claim correction window.
Proof of Concept
function test_H06_wrongAttackerDrainsBeforeCorrection() public {
_stake(alice, 100 ether);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
address wrongAttacker = makeAddr("wrongAttacker");
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, wrongAttacker);
vm.prank(wrongAttacker);
pool.claimAttackerBounty();
assertEq(token.balanceOf(wrongAttacker), 100 ether, "wrong attacker drained the pool");
assertTrue(pool.claimsStarted());
vm.prank(moderator);
vm.expectRevert(IConfidencePool.OutcomeAlreadySet.selector);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
}
Run:
forge test --match-test test_H06_wrongAttackerDrainsBeforeCorrection -vv
Result:
Ran 3 tests for test/Metatron_Loop.t.sol:MetatronLoopPoC
[PASS] test_H06_wrongAttackerDrainsBeforeCorrection() (gas: 516934)
Suite result: ok. 3 passed; 0 failed; 0 skipped
Recommended Mitigation
Give the good-faith bounty a short claim delay after the naming flag, so a mistyped attacker can be re-flagged before value moves:
function claimAttackerBounty() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
+ if (block.timestamp < _firstGoodFaithCorruptedAt + CORRECTION_WINDOW) revert ClaimWindowNotExpired();
if (bountyClaimed == bountyEntitlement) revert BountyAlreadyClaimed();