Root + Impact
Description
The setRecoveryAddress() function validates only that newRecoveryAddress != address(0). It does not check against address(this) (the pool's own address) or address(stakeToken) (the stake token contract). If set to the pool itself, CORRUPTED sweeps transfer tokens back to the pool — effectively locking them permanently, as no function exists to redistribute funds from a post-CORRUPTED pool. This is an owner-operated footgun: the owner can accidentally or maliciously create a token sinkhole.
function setRecoveryAddress(address newRecoveryAddress) external onlyOwner {
@> if (newRecoveryAddress == address(0)) revert InvalidRecoveryAddress();
address oldRecoveryAddress = recoveryAddress;
recoveryAddress = newRecoveryAddress;
emit RecoveryAddressUpdated(oldRecoveryAddress, newRecoveryAddress);
}
Risk
Likelihood: Low — requires the trusted pool owner to set recoveryAddress to the pool itself (accidental fat-finger or malicious rug).
Impact: Low — tokens are locked in the pool, not stolen. The owner cannot recover them either. Under the owner-trust model (DESIGN.md §10), the owner could already steal funds via other means.
Proof of Concept
File: L15-RecoveryAddress-Self.poc.t.sol
pragma solidity 0.8.26;
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
contract L15_RecoveryAddress_Self_POC is BaseConfidencePoolTest {
function testPOC_L15_recoveryAddress_canBePoolItself() external {
pool.setRecoveryAddress(address(pool));
assertEq(pool.recoveryAddress(), address(pool));
_stake(alice, 100 * ONE);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, false, address(0));
uint256 poolBalBefore = token.balanceOf(address(pool));
pool.claimCorrupted();
uint256 poolBalAfter = token.balanceOf(address(pool));
assertEq(poolBalAfter, poolBalBefore, "tokens trapped in pool");
}
function testPOC_L15_recoveryAddress_noGuardAgainstSelf() external {
vm.expectRevert(IConfidencePool.InvalidRecoveryAddress.selector);
pool.setRecoveryAddress(address(0));
pool.setRecoveryAddress(address(pool));
}
}
Run: forge test --match-path 'L15-RecoveryAddress-Self.poc.t.sol' -vv
Recommended Mitigation
Add checks against address(this) and address(stakeToken) in setRecoveryAddress:
- if (newRecoveryAddress == address(0)) revert InvalidRecoveryAddress();
+ if (newRecoveryAddress == address(0) || newRecoveryAddress == address(this)
+ || newRecoveryAddress == address(stakeToken)) revert InvalidRecoveryAddress();