If recoveryAddress is configured to be the pool's own address (address(this)), calls to claimCorrupted() and sweepUnclaimedCorrupted() will self-transfer tokens — the pool's token balance remains unchanged after each sweep. Since both functions are designed to be repeat-callable (to recover post-resolution donations), each invocation emits a successful event but achieves no actual fund movement. The CORRUPTED sweep path becomes an unbounded gas-burning loop until the owner resets recoveryAddress to a valid external address.
While the owner can recover by calling setRecoveryAddress again, the current validation only rejects address(0), omitting the equally invalid address(this) case. This deviates from standard Solidity best practices for address parameter validation.
Severity: LOW
Likelihood: LOW — requires the pool owner to explicitly set the recovery address to the pool itself. This could occur through operational error, especially when the pool address is known in advance (deterministic CREATE2 clone address).
src/ConfidencePool.sol — setRecoveryAddress() (lines 611-618)
src/ConfidencePool.sol — initialize() (line 195)
Normal behavior: When the pool resolves as CORRUPTED, claimCorrupted() transfers the pool's entire token balance to recoveryAddress. For bad-faith CORRUPTED, this sweeps all funds (stake + bonus) to the recovery destination. sweepUnclaimedCorrupted() does the same after the attacker bounty window expires.
Bug: The setRecoveryAddress function validates newRecoveryAddress != address(0) but does not validate newRecoveryAddress != address(this). If set to the pool itself:
claimCorrupted() line 423 executes stakeToken.safeTransfer(address(this), toSweep) — a self-transfer
The pool's token balance remains exactly the same
A ClaimCorrupted event is emitted, creating a false record of successful sweep
Because claimCorrupted is repeat-callable by design (for post-resolution donation recovery), the self-transfer can be triggered indefinitely, each time burning the caller's gas with zero effect
Root cause: Missing address(this) check in both setRecoveryAddress() and initialize():
And in initialize() at line 195:
Likelihood: LOW
The pool owner (sponsor) must explicitly call setRecoveryAddress(address(pool)) or pass the pool address during pool creation
The owner has no economic incentive to do this — it only harms their own pool
However, the pool address is deterministic (CREATE2 via Clones.cloneDeterministic with salt keccak256(abi.encode(agreement, poolIndex))), making it predictable before deployment
Impact: LOW
CORRUPTED-path funds become temporarily trapped in a self-transfer loop
Gas is wasted by callers of claimCorrupted() / sweepUnclaimedCorrupted()
Misleading ClaimCorrupted / UnclaimedCorruptedSwept events are emitted
Recovery is simple: owner calls setRecoveryAddress(validAddress) to restore normal operation
The following Foundry test demonstrates the self-transfer loop and recovery, Create in /test/unit/PoC_RecoveryAddressSelfAssignment.t.sol:
Run:
Add address(this) check in both locations:
This follows the same defensive pattern already used for address(0) validation and is consistent with standard Solidity best practices for setter functions that control fund destinations.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.