[setRecoveryAddress](file:///home/joe/sm-sec/2026-07-bc-confidence-pools/src/ConfidencePool.sol#L611-L618) is guarded only by onlyOwner and a non-zero check. It has no guard against the pool outcome being already resolved. Because every CORRUPTED-path sweep — [claimCorrupted](file:///home/joe/sm-sec/2026-07-bc-confidence-pools/src/ConfidencePool.sol#L423), [sweepUnclaimedCorrupted](file:///home/joe/sm-sec/2026-07-bc-confidence-pools/src/ConfidencePool.sol#L468), and [sweepUnclaimedBonus](file:///home/joe/sm-sec/2026-07-bc-confidence-pools/src/ConfidencePool.sol#L506) — reads recoveryAddress live at call time (never snapshotted at resolution), the pool owner (sponsor) can silently redirect the entire pool balance after a bad-faith CORRUPTED outcome is frozen but before the sweep executes.
setRecoveryAddress lacks an outcome != UNRESOLVED guard, and the sweep paths consume recoveryAddress live instead of from a snapshot taken at resolution time.
Neither [flagOutcome](file:///home/joe/sm-sec/2026-07-bc-confidence-pools/src/ConfidencePool.sol#L322-L379) nor the [claimExpired](file:///home/joe/sm-sec/2026-07-bc-confidence-pools/src/ConfidencePool.sol#L532-L554) auto-CORRUPTED branch snapshots recoveryAddress at resolution. The sweep functions then read the live value:
The design locks other staker-relied-upon parameters once stakers commit capital:
expiry — locked on first stake via the one-way expiryLocked latch (DESIGN.md §10: "This protects staker reliance: once anyone has deposited against a given deadline, the sponsor cannot move it.")
Pool scope — locked once the registry leaves pre-attack staging (DESIGN.md §8)
recoveryAddress has the same staker-reliance property — stakers verify the worst-case sweep destination before depositing — but receives no equivalent lock. Once riskWindowStart seals, withdrawals are permanently disabled, so stakers cannot exit if the address changes.
Under a bad-faith CORRUPTED resolution, the full pool balance — snapshotTotalStaked + snapshotTotalBonus (all staker principal plus contributed bonus) — is swept to recoveryAddress via claimCorrupted. A sponsor who repoints the address after resolution diverts 100% of the TVL. claimCorrupted is permissionless, so the sponsor can trigger it themselves or let any third party do so. Every staker loses all principal with no on-chain recourse.
Under the SURVIVED/EXPIRED paths, only unclaimed bonus/dust is affected (principal is paid directly to stakers via claimSurvived/claimExpired), so the impact there is limited.
Sponsor creates pool via factory with recoveryAddress = honestMultisig. Stakers verify the address and deposit.
Registry transitions through UNDER_ATTACK → CORRUPTED.
Moderator calls flagOutcome(CORRUPTED, false, address(0)). Outcome is frozen; recoveryAddress is not snapshotted.
Sponsor calls setRecoveryAddress(attackerWallet) — succeeds (only onlyOwner + non-zero check).
Anyone calls claimCorrupted() — toSweep = balanceOf(pool), transferred to the repointed attackerWallet.
Precondition: moderator collusion or compromise.
claimExpiredSame deposit setup as Chain A.
Registry reaches CORRUPTED with riskWindowStart != 0.
No moderator action for expiry + MODERATOR_CORRUPTED_GRACE (180 days).
Anyone calls claimExpired() → auto-resolves bad-faith CORRUPTED, sets claimsStarted = true.
Sponsor calls setRecoveryAddress(attackerWallet) — claimsStarted does not block it.
Anyone calls claimCorrupted() — full pool swept to attackerWallet.
Precondition: 180-day moderator absence (no collusion needed).
Place in test/unit/M01_RecoveryAddressRepoint.t.sol and run:
Result: All 4 tests pass, confirming setRecoveryAddress succeeds post-resolution across all affected paths.
Manual review, Foundry
Lock setRecoveryAddress once the outcome is set, consistent with the expiryLocked pattern:
This preserves the sponsor's pre-resolution control documented in DESIGN.md §10 while closing the post-resolution repoint. The outcome-based lock covers all affected sweep paths (claimCorrupted, sweepUnclaimedCorrupted, sweepUnclaimedBonus) because all three are gated on a terminal outcome.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.