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

Codebase protocol modifier using inconsistencies in pause/unpause methods between ConfidencePoolFactory and ConfidencePool contracts difficults code readability and shows different behavior(reverts) between contracts

Author Revealed upon completion

Description

There are functions modifier's using and implementation inconsistencies between:

  • ConfidencePoolFactory::unpause and ConfidencePool::unpause and

  • ConfidencePoolFactory::pause and ConfidencePool::pause
    This difficults code readability between contracts and different behavior (reverts signatures) in this methods

Unlike ConfidencePool::unpause
https://github.com/CodeHawks-Contests/2026-07-bc-confidence-pools/blob/58e8ba4ce3f3277866e4926f3140e597f9554a1e/src/ConfidencePool.sol#L668

contract ConfidencePool is Initializable, Ownable2Step, ReentrancyGuard, Pausable, IConfidencePool {
modifier whenPoolPaused() {
if (!paused()) revert PoolNotPaused();
_;
}
@> function unpause() external onlyOwner whenPoolPaused {
_unpause();
}

ConfidencePoolFactory::unpause does not implement a pause status modifier check
https://github.com/CodeHawks-Contests/2026-07-bc-confidence-pools/blob/58e8ba4ce3f3277866e4926f3140e597f9554a1e/src/ConfidencePoolFactory.sol#L169

@> function unpause() external onlyOwner {
_unpause();
}

This leads to codebase protocol modifier implementation inconsistencies due to ConfidencePool::pause has a custom revert in whenPoolNotPaused modifier:

contract ConfidencePool is Initializable, Ownable2Step, ReentrancyGuard, Pausable, IConfidencePool {
//...
@> modifier whenPoolNotPaused() {
if (paused()) revert PoolPaused();
_;
}
//...
@> function pause() external onlyOwner whenPoolNotPaused {
_pause();
}

But ConfidencePoolFactory::pause not:

contract ConfidencePoolFactory is
Initializable,
Ownable2StepUpgradeable,
UUPSUpgradeable,
PausableUpgradeable,
IConfidencePoolFactory
{
//...
@> function pause() external onlyOwner {
_pause();
}

This difficults code readability between contracts and shows distinct behavior for the same method with same intended functionality between contracts
The same happens between ConfidencePool::unpause and ConfidencePoolFactory::unpause

Risk

Likelihood:
High, this distinct behavior (custom / inherited revert) will be present each time a call to ConfidencePool::unpause or ConfidencePoolFactory::unpause fails

Impact:
Low, the difference between implementation (modifier usage) ConfidencePool and ConfidencePoolFactory pause methods, difficults code readability between contracts and different behavior

Proof of Concept

To show the difference between reverts add the following test in test/unit/ConfidencePoolFactory.t.sol

function testRevert_pause_whenPaused() external {
factory.pause();
vm.expectRevert();
factory.pause();
}

And exec test:

forge test --mt 'testRevert_pause_whenPaused' -vvvv | fgrep '[Revert]'

Output

│ │ └─ ← [Revert] EnforcedPause()
│ └─ ← [Revert] EnforcedPause()

For ConfidencePool
Exec test

forge test --mt 'testRevert_pause_whenAlreadyPaused' -vvvv | fgrep '[Revert]'

Observe output

│ │ └─ ← [Revert] PoolPaused()
│ └─ ← [Revert] PoolPaused()

As it is shown, one method (ConfidencePool::pause) reverts with a clearly custom message but the other one (ConfidencePoolFactory::pause) reverts with a generic non descriptive OZ library message

Recommended Mitigation

Implement a similar ConfidencePool whenPoolPaused modifier to ConfidencePoolFactory::unpause and ConfidencePoolFactory::pause functions

@> modifier whenFactoryNotPaused() {
if (paused()) revert FactoryPaused();
_;
}
//...
@> function pause() external onlyOwner whenFactoryNotPaused {
_pause();
}

Support

FAQs

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

Give us feedback!