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

`Pausable` implementation intentionally omits withdrawal and claim paths, functioning only as an inflow block

Author Revealed upon completion

Description

Normal Behavior

Standard protocol architecture utilizing OpenZeppelin's Pausable typically gates both inflows (deposits/stakes) and outflows (withdrawals/claims) to completely freeze contract state in the event of an emergency.

Specific Issue

In ConfidencePool, the whenPoolNotPaused modifier is exclusively applied to stake and contributeBonus (as well as flagOutcome). Outflow functions like withdraw, claimExpired, claimSurvived, and claimCorrupted are missing the modifier, meaning users can continue withdrawing funds even if the sponsor specifically attempts to "pause" the pool.

// src/ConfidencePool.sol
// @> Root cause: modifier whenPoolNotPaused applies to stake...
function stake(uint256 amount) external nonReentrant whenPoolNotPaused {
// @> ...but is explicitly missing from withdraw and claims
function withdraw() external nonReentrant {
if (outcome != PoolStates.Outcome.UNRESOLVED) revert OutcomeAlreadySet();

Risk

Likelihood:

  • The sponsor or DAO identifies a potential logic flaw during active deployment.

  • The sponsor triggers the pause() function expecting a complete freeze of all token movement to prevent exploitation.

Impact:

  • The pause mechanism does not halt outflows, leaving the pool partially exposed.

  • While explicitly designed this way (to guarantee user access to liquidity and prevent the sponsor from trapping funds), the discrepancy from standard OpenZeppelin patterns creates a UX friction for admins relying on traditional pause security.

Proof of Concept

Create a new test file test/unit/POCTest.t.sol inheriting from BaseConfidencePoolTest, paste the following code inside the contract body, and run it using the command forge test --match-test test_POC_PausableDoesNotBlockOutflows -vvv.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {Test, console} from "forge-std/Test.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {ConfidencePoolFactory} from "src/ConfidencePoolFactory.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
import {MockAgreement} from "test/mocks/MockAgreement.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {IAgreement} from "@battlechain/interface/IAgreement.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract POCTest is BaseConfidencePoolTest {
function test_POC_PausableDoesNotBlockOutflows() external {
console.log("--- POC: Pausable Implementation Omits Outflows ---");
_stake(alice, 100 * ONE);
// Owner pauses the pool
vm.prank(pool.owner());
pool.pause();
assertTrue(pool.paused(), "Pool should be paused");
console.log("Pool is paused by owner.");
// Staking is blocked
vm.expectRevert(); // Reverts with EnforcedPause()
vm.prank(bob);
pool.stake(50 * ONE);
console.log("Staking is correctly blocked while paused.");
// Withdrawals are NOT blocked
uint256 balBefore = token.balanceOf(alice);
vm.prank(alice);
pool.withdraw(); // should succeed
uint256 balAfter = token.balanceOf(alice);
assertEq(balAfter - balBefore, 100 * ONE);
console.log("Alice successfully withdrew 100 tokens while pool was paused.");
console.log("This proves the pause mechanism only stops inflows, not outflows.");
}
}

Recommended Mitigation

While intentionally designed to fail-safe and allow user liquidity withdrawals even during an emergency, the deviation from standard Pausable behavior should be explicitly documented via NatSpec on the pause() function. This prevents sponsors from falsely assuming they can freeze malicious outflows.

// In documentation, clarify the exact limitations of the pause function
+ /// @notice Pausing the pool ONLY stops new inflows (stakes/bonuses) and moderator resolutions.
+ /// It does NOT prevent users from withdrawing or claiming their existing funds.
function pause() external onlyOwner {
_pause();
}

Support

FAQs

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

Give us feedback!