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

recoveryAddress remains mutable after users lose the ability to withdraw, allowing the owner to redirect CORRUPTED recoveries.

Author Revealed upon completion

Root + Impact

Description

  • The protocol freezes several sponsor-controlled parameters after user reliance begins. For example, expiry becomes immutable after the first stake and the pool scope permanently locks once the risk window is observed. These restrictions prevent the sponsor from changing economically significant pool parameters after users have committed capital.


  • However, recoveryAddress remains mutable for the entire lifetime of the pool. The owner can update it at any time through setRecoveryAddress(), while all recovery functions read the current storage value when transferring funds. As a result, the destination of CORRUPTED recoveries can be changed even after withdrawals have become permanently unavailable and after the outcome has been determined.

function setRecoveryAddress(address newRecoveryAddress) external onlyOwner {
if (newRecoveryAddress == address(0)) revert InvalidRecoveryAddress();
address oldRecoveryAddress = recoveryAddress;
recoveryAddress = newRecoveryAddress;
//@> Recovery destination remains mutable throughout the pool lifecycle.
emit RecoveryAddressUpdated(oldRecoveryAddress, newRecoveryAddress);
}
// also claimCorrupted()
stakeToken.safeTransfer(recoveryAddress, amount);
//@> Uses the current recoveryAddress, not a committed snapshot.
//(The same applies to sweepUnclaimedCorrupted() and sweepUnclaimedBonus().)

Risk

Likelihood:

  • The owner retains permission to update recoveryAddress throughout the pool lifecycle, including after withdrawals are permanently disabled.

  • Recovery transfers resolve the destination using the live storage value instead of a value committed before users became locked into the pool.

Impact:

  • The destination of CORRUPTED recovery funds can change after users have irreversibly committed to the pool.

  • This leaves recoveryAddress as the only economically significant sponsor-controlled parameter that remains mutable after the protocol otherwise narrows sponsor authority.

Proof of Concept

//1. Owner deploys a pool with recoveryAddress = TreasuryA.
//2. Users review the pool and stake.
//3. The risk window opens and withdrawals become permanently disabled.
//4. The pool is resolved as CORRUPTED.
//5. Before claimCorrupted() or sweepUnclaimedCorrupted() executes,
// the owner calls: setRecoveryAddress(TreasuryB)
//6. claimCorrupted() transfers funds to TreasuryB because the function
// reads the current storage value instead of a committed value.

Recommended Mitigation

Freeze recoveryAddress once users can no longer meaningfully revise their participation,
consistent with the protocol's treatment of other sponsor-controlled parameters.
Alternatively, snapshot the recoveryAddress when the recovery destination becomes
economically bining (for example, when the risk window begins or when the outcome is
finalized),so subsequent updates cannot alter the destination of pending recovery
transfers.

Support

FAQs

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

Give us feedback!