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

Owner pause followed by renounceOwnership permanently freezes stake and bonus ingress

Author Revealed upon completion

Owner pause followed by renounceOwnership permanently freezes stake and bonus ingress

Description

The pool sponsor can pause deposits and later unpause them. Resolution, withdrawal, and claim paths remain available while paused so existing capital is not trapped by a temporary pause.

However, pause() and unpause() are both onlyOwner, and the inherited OpenZeppelin Ownable/Ownable2Step path exposes renounceOwnership(). If the owner pauses and then renounces, unpause() becomes permanently unreachable. Future stake() and contributeBonus() calls revert forever.

function pause() external onlyOwner whenPoolNotPaused {
_pause();
}
function unpause() external onlyOwner whenPoolPaused {
// @> Becomes unreachable after renounceOwnership().
_unpause();
}
// OpenZeppelin Ownable
function renounceOwnership() public virtual onlyOwner {
// @> Leaves the contract with no owner.
_transferOwnership(address(0));
}

Risk

Likelihood:

  • Occurs when the pool owner pauses and then renounces ownership, whether accidentally or deliberately.

  • Requires only the owner's normal privileged actions; no external attacker is needed.

Impact:

  • All future stake and bonus contributions are permanently blocked.

  • Existing principal remains recoverable through withdraw or claim paths, so the loss is ingress liveness rather than theft of deposited funds.

Proof of Concept

Setup: append the function below to test/unit/ConfidencePool.t.sol (same ConfidencePoolTest harness / BaseConfidencePoolTest helpers already used by the suite), then run:

forge test --match-test testPauseThenRenouncePermanentlyFreezesIngress -vv
  1. The pool owner calls pause().

  2. The owner then calls renounceOwnership(), leaving owner() == address(0).

  3. Alice's stake() and contributeBonus() both revert with PoolPaused.

  4. Any subsequent unpause() attempt also reverts because there is no owner left to call it.

// PoC: pause() + renounceOwnership() leave unpause() permanently unreachable, freezing
// stake/bonus ingress while withdraw and claims remain available.
function testPauseThenRenouncePermanentlyFreezesIngress() external {
pool.pause();
pool.renounceOwnership();
assertEq(pool.owner(), address(0));
token.mint(alice, 100 * ONE);
vm.startPrank(alice);
token.approve(address(pool), 100 * ONE);
vm.expectRevert(IConfidencePool.PoolPaused.selector);
pool.stake(100 * ONE);
vm.expectRevert(IConfidencePool.PoolPaused.selector);
pool.contributeBonus(100 * ONE);
vm.stopPrank();
// No owner can unpause after renounce.
vm.expectRevert();
pool.unpause();
}

Recommended Mitigation

Disable renounce for the pool, or allow a fallback unpause path that does not depend on the owner.

+ function renounceOwnership() public pure override {
+ revert RenounceDisabled();
+ }

Support

FAQs

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

Give us feedback!