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

Owner Can Redirect Corrupted Pool Funds by Changing Recovery Address After Outcome

Author Revealed upon completion

Description

The setRecoveryAddress() function has no restrictions on when it can be called relative to the pool's outcome. An owner can flag CORRUPTED, change the recovery address to their own, and sweep all funds to themselves.

Vulnerability Details

ConfidencePool.sol:611-617 allows the owner to change recoveryAddress at any time with only onlyOwner access control:

function setRecoveryAddress(address newRecoveryAddress) external onlyOwner {
if (newRecoveryAddress == address(0)) revert InvalidRecoveryAddress();
address oldRecoveryAddress = recoveryAddress;
recoveryAddress = newRecoveryAddress;
emit RecoveryAddressUpdated(oldRecoveryAddress, newRecoveryAddress);
}

The function lacks:

  • whenPoolNotPaused modifier (unlike claim functions)

  • Outcome state check (can be called after CORRUPTED flagging)

  • Time lock (instant effect)

Impact

The owner can:

  1. Call flagOutcome(CORRUPTED, ...) — pool enters CORRUPTED state

  2. Call setRecoveryAddress(ownerAddress) — redirect recovery to themselves

  3. Call claimCorrupted() — entire pool swept to owner

  4. Optional: change recovery address back to cover tracks

The designated recovery entity receives nothing, and the owner captures the full pool.

Proof of Concept

function testOwnerRedirectRecoveryFunds() public {
address originalRecovery = pool.recoveryAddress();
// Moderator flags CORRUPTED
vm.prank(moderator);
pool.flagOutcome(CORRUPTED, false, address(0));
assertEq(pool.outcome(), CORRUPTED);
// Owner redirects recovery to self
vm.prank(owner);
pool.setRecoveryAddress(owner);
assertEq(pool.recoveryAddress(), owner);
// Owner sweeps — funds go to owner, not originalRecovery
uint256 balanceBefore = token.balanceOf(owner);
vm.prank(owner);
pool.claimCorrupted();
uint256 balanceAfter = token.balanceOf(owner);
assertGt(balanceAfter, balanceBefore); // Owner got the sweep
assertEq(token.balanceOf(originalRecovery), 0); // Original recovery got nothing
}

Recommended Mitigation

Lock recoveryAddress after the first outcome flagging:

function setRecoveryAddress(address newRecoveryAddress) external onlyOwner {
if (outcome != PoolStates.Outcome.UNRESOLVED) revert OutcomeAlreadySet();
if (newRecoveryAddress == address(0)) revert InvalidRecoveryAddress();
recoveryAddress = newRecoveryAddress;
emit RecoveryAddressUpdated(oldRecoveryAddress, newRecoveryAddress);
}

Or add a time-lock delay for recovery address changes after flagging.

References

  • ConfidencePool.sol:611 — setRecoveryAddress

  • ConfidencePool.sol:408 — claimCorrupted (uses recoveryAddress)

Support

FAQs

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

Give us feedback!