Every other sponsor-controlled lever that determines fund destiny is locked once it becomes security-relevant: expiry locks permanently on the first stake (expiryLocked), and pool scope locks permanently once the registry leaves pre-attack staging (scopeLocked) — both documented and individually justified in the project's own docs/DESIGN.md §8/§10.
recoveryAddress — the address that receives the entire pool, including stakers' principal, under bad-faith CORRUPTED — has no equivalent lock anywhere in its lifecycle. The Sponsor (onlyOwner) can call setRecoveryAddress at any point, including after CORRUPTED is already flagged and public on-chain, and the permissionless sweep functions (claimCorrupted, sweepUnclaimedCorrupted) read it live at transfer time rather than from the snapshot flagOutcome takes of every other resolution-relevant value.
docs/DESIGN.md §10 titles the Sponsor's powers a "trust surface" — enumerated, bounded trust, not blanket exemption — distinct from §11's "out of the adversarial model" language reserved only for registry/moderator compromise. Every other lever on that same trust surface gets a matching limitation; recoveryAddress is the one left without it. The contest's own README.md shows the identical pattern independently: expiry and scope each get a paired LIMITATIONS bullet; recoveryAddress gets only the RESPONSIBILITIES bullet, with no counterpart anywhere in the same actor list.
Likelihood:
The Sponsor already holds onlyOwner legitimately by design — redirecting the sweep destination requires only one additional call, at any point up to and including the moment funds are swept, with no extra access, no race against another party, and no capital beyond gas.
Both sweep functions are permissionless, so once recoveryAddress is redirected, any third party (not necessarily the Sponsor) can immediately trigger the transfer.
Impact:
100% of the pool's staked principal is redirected to an address the Sponsor was never committed to (confirmed in PoC 1, bad-faith path).
A named whitehat's entire earned bounty — the full pool, per the project's own bounty mechanics (§12) — is redirected away from them if unclaimed within the 180-day window (confirmed in PoC 2, good-faith path).
Three independent Foundry tests confirm the exploit end-to-end: PoC 1 shows the Sponsor redirecting the sweep destination immediately after a bad-faith CORRUPTED flag; PoC 2 shows the same redirection stealing a named whitehat's bounty in the good-faith path after the 180-day claim window; PoC 3 confirms why the recommended fix below is outcome-gated rather than an unconditional freeze.
Gate setRecoveryAddress on outcome == UNRESOLVED, mirroring the exact lock pattern already used for expiryLocked and scopeLocked elsewhere in the contract. This closes both PoC 1 and PoC 2: once flagOutcome(CORRUPTED, ...) succeeds, outcome is no longer UNRESOLVED, so any subsequent setRecoveryAddress call reverts — for both the immediate bad-faith sweep and the 180-day-later good-faith sweep — while still preserving the Sponsor's legitimate ability to correct recoveryAddress during normal, unresolved operation. The guard is outcome-gated rather than an unconditional freeze specifically because of PoC 3: a full freeze would remove the only currently-working rescue path if stakeToken supports issuer-side blacklisting and recoveryAddress is later blacklisted independently of the Sponsor.
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.