FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: high
Likelihood: low

`claimAttackerBounty` has no delay, so a moderator-mistyped attacker drains the whole pool before the advertised pre-claim correction window can be used

Author Revealed upon completion

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.

// src/ConfidencePool.sol :: claimAttackerBounty (no time gate between the naming flag and the drain)
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(); // named address only...
@> // ...no `block.timestamp >= flaggedAt + CORRECTION_WINDOW` gate here
...
if (payout > 0) {
corruptedReserve -= payout;
@> if (!claimsStarted) claimsStarted = true; // latches finality, forecloses re-flag
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

// test/Metatron_Loop.t.sol :: MetatronLoopPoC
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); // typo
vm.prank(wrongAttacker);
pool.claimAttackerBounty(); // drains before any correction
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); // correction now impossible
}

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();

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!