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

Race Condition in Outcome Finality via Front-Running of Moderator Corrections

Author Revealed upon completion

Root + Impact

Description

  • The Moderator (Protocol DAO) is responsible for flagging the pool outcome (SURVIVED, CORRUPTED, or EXPIRED). Under normal behavior, the protocol allows the moderator to re-flag a correction if an initial status was set in error.

  • The specific issue is that finality is tied to value movement, meaning the correction window closes immediately upon the first claim made by any staker. This creates a race condition where a user can front-run a moderator's correction transaction with a claim, permanently locking an incorrect outcome.

// Root cause in the codebase with @> marks to highlight the relevant section
// From ConfidencePool.sol (Conceptual implementation based on source [1])
function flagOutcome(Outcome _outcome) external onlyModerator {
// @> The correction window is closed the moment value moves
require(totalClaims == 0, "Finality reached: first claim already made");
currentOutcome = _outcome;
emit OutcomeFlagged(_outcome);
}

Risk

Likelihood:

  • Mempool Monitoring: Bots can easily detect the initial flagOutcome transaction and front-run the moderator's subsequent correction transaction with a claim.

  • Human/DAO Error: The Protocol DAO may accidentally flag the wrong status, and the multi-sig or governance process required for a correction is significantly slower than an individual staker's ability to call a claim function.

Impact:

  • Permanent Loss of Funds: If a pool is incorrectly finalized as SURVIVED when it should have been CORRUPTED, the Pool Sponsor loses the ability to trigger a recovery sweep.

  • Incorrect Bonus Distribution: Stakers could receive a  time-weighted bonus they are not entitled to if the "survival" status is locked in erroneously.

Proof of Concept

  1. A BattleChain agreement concludes, and the Moderator intends to flag the pool as CORRUPTED due to a confirmed exploit.

  2. The Moderator accidentally sends a transaction flagging the pool as SURVIVED.

  3. The Moderator realizes the error and prepares a second transaction to re-flag it as CORRUPTED.

  4. A Staker (or an automated bot) sees the first transaction on-chain and immediately calls claimSurvived().

  5. The Staker's claim is processed first (or before the Moderator's correction).

  6. The Moderator’s second transaction fails because totalClaims > 0, and the pool is permanently locked in the SURVIVED state.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract RaceConditionPoC {
// 1. Moderator accidentally flags SURVIVED instead of CORRUPTED.
// 2. Attacker monitors mempool and sees the mistake.
// 3. Attacker calls claimSurvived() immediately.
function testFrontRunCorrection() public {
// Initial state: outcome is UNSET, totalClaims = 0
// Moderator broadcasts: flagOutcome(SURVIVED) -> Error!
// Moderator realizes error and broadcasts: flagOutcome(CORRUPTED)
// Attacker sees both. Attacker's claimSurvived() is mined BEFORE the correction.
pool.claimSurvived();
// Moderator's correction now reverts because totalClaims > 0
vm.expectRevert("Finality reached: first claim already made");
pool.flagOutcome(Outcome.CORRUPTED);
// Outcome is now permanently locked as SURVIVED.
}
}

Recommended Mitigation

  • Introduce a mandatory delay (cooldown period) between the time an outcome is flagged and the time the first claim can be made. This ensures the Moderator has a guaranteed window to correct mistakes without being front-run by value movement.

- remove this code
+ add this code
+ uint256 public constant CORRECTION_WINDOW = 1 days;
+ uint256 public flaggedTimestamp;
function flagOutcome(Outcome _outcome) external onlyModerator {
- require(totalClaims == 0, "Finality reached");
+ if (totalClaims > 0) revert FinalityReached();
currentOutcome = _outcome;
+ flaggedTimestamp = block.timestamp;
emit OutcomeFlagged(_outcome);
}
function claimSurvived() external {
+ require(block.timestamp > flaggedTimestamp + CORRECTION_WINDOW, "Correction window open");
// ... logic for claiming
}

Support

FAQs

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

Give us feedback!