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

Pool pause does not gate `withdraw()`

Author Revealed upon completion

Root + Impact

Description

whenPoolNotPaused gates both stake() and contributeBonus(), blocking new
inflows during an emergency pause. withdraw() has no such modifier, so
principal exits remain possible in any withdraw-allowed registry state even
while the pool is paused.

// stake() / contributeBonus() — src/ConfidencePool.sol:222,266
function stake(uint256 amount) external nonReentrant whenPoolNotPaused { ... }
function contributeBonus(uint256 amount) external nonReentrant whenPoolNotPaused { ... }
// withdraw() — src/ConfidencePool.sol:288
@> function withdraw() external nonReentrant { // no whenPoolNotPaused
if (outcome != PoolStates.Outcome.UNRESOLVED) revert OutcomeAlreadySet();
IAttackRegistry.ContractState state = _observePoolState();
...
stakeToken.safeTransfer(msg.sender, amount);
}

Risk

Likelihood:

  • Only surfaces during an active pause combined with a withdraw-eligible
    registry state — a narrow, admin-triggered window, not attacker-chosen.

  • Note: many protocols pause entries but deliberately leave exits open to
    avoid trapping user funds, so this asymmetry may be intentional rather than
    an oversight — worth confirming with the team.

Impact:

  • If pause is meant to freeze all pool activity during an incident (e.g. a
    suspected accounting bug or malicious registry), stakers can still exit
    and change totalEligibleStake mid-incident, undermining the pause's
    purpose.

  • No funds are stolen or misdirected — worst case is the pause control being
    less comprehensive than its sibling checks suggest.

Proof of Concept

This shows a paused pool still allowing a full withdrawal, since withdraw()
never checks the paused state that blocks stake() and contributeBonus().

// Pool paused by admin; registry in ATTACK_REQUESTED (withdraw-allowed)
pool.pause();
pool.withdraw(); // succeeds — no whenPoolNotPaused check

Recommended Mitigation

- function withdraw() external nonReentrant {
+ function withdraw() external nonReentrant whenPoolNotPaused {
if (outcome != PoolStates.Outcome.UNRESOLVED) revert OutcomeAlreadySet();

Note: only apply if pause is intended to halt all pool activity. If exits
should remain open during pause by design, this is not a bug — worth a quick
confirmation before submitting.

Support

FAQs

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

Give us feedback!