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

renounceOwnership() Left Unguarded on ConfidencePool/ConfidencePoolFactory, Allowing Permanent, Irreversible Loss of Owner Privileges (Including UUPS Upgrade Authorization)

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior: Both contracts rely on their owner() staying callable for the protocol's lifetime — the factory's UUPS upgrade path (_authorizeUpgrade) and both contracts' admin functions (allowlist, registry/moderator, pause, recoveryAddress/expiry/scope) all gate on onlyOwner.

  • The issue: Neither ConfidencePool nor ConfidencePoolFactory overrides renounceOwnership(), so both inherit OpenZeppelin's standard version unchanged — callable by the owner at any time, no time-lock, no recovery. One transaction sets owner() = address(0) and permanently kills every onlyOwner function on that contract, including upgrade authorization.

// src/ConfidencePoolFactory.sol — no renounceOwnership override exists here
function _authorizeUpgrade(address) internal override onlyOwner {}
@> // Permanently unreachable the instant owner() == address(0) — no other path
@> // can ever authorize a new implementation again.
// lib/openzeppelin-contracts-upgradeable/.../OwnableUpgradeable.sol — inherited as-is
@> function renounceOwnership() public virtual onlyOwner {
@> _transferOwnership(address(0));
@> }

Risk

Likelihood: Medium — triggered by a single unprotected transaction from the owner (key compromise, phished signature, or a mistaken call), with no legitimate use case for this function in the protocol's design.

Impact: Medium — no funds are ever at risk (existing pools, withdrawals, and claims keep working normally). The damage is permanent loss of future functionality: on the factory, UUPS upgrades die forever and (if paused first) createPool is blocked forever; on a pool, admin controls freeze and (if paused first) no new stakers/bonus can join.

Proof of Concept

Foundry test — pauses the factory, renounces ownership, then shows unpause, upgradeToAndCall, and every other admin function revert permanently:

function test_RenounceOwnershipPermanentlyBricksFactory() public {
factory.pause();
factory.renounceOwnership();
assertEq(factory.owner(), address(0));
vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, owner));
factory.unpause(); // no new pools, ever
address newImpl = address(new ConfidencePoolFactory());
vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, owner));
factory.upgradeToAndCall(newImpl, ""); // no fix ever shippable
}

Recommended Mitigation

Override renounceOwnership() on both contracts to always revert.

// src/ConfidencePoolFactory.sol
function _authorizeUpgrade(address) internal override onlyOwner {}
+
+ function renounceOwnership() public pure override {
+ revert CannotRenounceOwnership();
+ }
// src/ConfidencePool.sol
function pause() external onlyOwner whenPoolNotPaused {
_pause();
}
+
+ function renounceOwnership() public view override onlyOwner {
+ revert CannotRenounceOwnership();
+ }

Add error CannotRenounceOwnership(); to both IConfidencePool.sol and IConfidencePoolFactory.sol.

Support

FAQs

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

Give us feedback!