[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.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.