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

Recovery address can be changed after CORRUPTED resolution, enabling fund theft

Author Revealed upon completion

Root + Impact

Description

  • ConfidencePool.sol allows the pool owner (sponsor) to change the recoveryAddress via setRecoveryAddress() at any time. This function has no guard checking whether the pool outcome has already been resolved.

    After a moderator flags the pool as CORRUPTED, the sponsor can change recoveryAddress to an address they control, then call claimCorrupted() or sweepUnclaimedCorrupted() to drain the entire pool balance (stake + bonus + attacker bounty) to their own wallet instead of the intended recovery destination.

@> The setRecoveryAddress function in ConfidencePool.sol lacks a check on the pool's outcome state. There is no require(outcome == PoolStates.Outcome.UNRESOLVED) guard, so the owner can call it even after the pool has been resolved as CORRUPTED.
@> function setRecoveryAddress(address newRecoveryAddress) external onlyOwner {
if (newRecoveryAddress == address(0)) revert InvalidRecoveryAddress();
address oldRecoveryAddress = recoveryAddress;
recoveryAddress = newRecoveryAddress;
emit RecoveryAddressUpdated(oldRecoveryAddress, newRecoveryAddress);
}

Risk

Likelihood:

  • Reason 1 — The sponsor calls setRecoveryAddress after the moderator flags the pool as CORRUPTED, with no additional conditions required.

  • Reason 2 — The exploit executes in a single transaction with no dependencies on external state or user actions.

Impact: High

  • Impact 1 — The entire pool balance (all staker deposits plus bonus contributions) is redirected to the sponsor's controlled address instead of the intended recovery destination.

  • Impact 2 — In a good-faith CORRUPTED scenario, the whitehat attacker's bounty entitlement is also stolen, as the sponsor can sweep the unclaimed remainder after the claim window expires.


Proof of Concept

// Scenario: Bad-faith CORRUPTED (no named attacker)
// Step 1: Moderator flags CORRUPTED, bad-faith
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, false, address(0));
// Step 2: Sponsor changes recovery address to their own wallet
vm.prank(sponsor);
pool.setRecoveryAddress(sponsorControlledWallet);
// Step 3: Sponsor immediately drains the ENTIRE pool
vm.prank(sponsor);
pool.claimCorrupted(); // Transfers ALL funds to sponsorControlledWallet

Recommended Mitigation

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

Support

FAQs

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

Give us feedback!