src/ConfidencePool.sol — the withdraw() gate and the bonus-eligibility premise are inconsistent for the direct-to-PRODUCTION path.
DESIGN.md §9 justifies "no observed risk -> no bonus" on the premise that a staker only forfeits the exit option once risk materializes (riskWindowStart == 0 is assumed to imply the staker could always have exited). That is false for goToProduction: the agreement owner (== the pool sponsor, per createPool) can send the agreement NEW_DEPLOYMENT -> PRODUCTION, skipping the attack phase. UNDER_ATTACK is never observed, so riskWindowStart stays 0, yet withdraw() is disabled once the state leaves the pre-attack set. Stakers are locked in, _bonusShare returns 0 for all of them, and the whole bonus pool is swept to recoveryAddress (the sponsor).
Impact: every staker loses their entire bonus share (the yield they staked to earn) even though the protocol survived; the bonus is diverted to the sponsor. A malicious sponsor can trigger this deliberately: seed a bonus to attract stakers, then call goToProduction to claw it back.
withdraw gate blocks every non pre-attack state, independent of riskWindowStart:
@> PRODUCTION is not allowed, so withdraw reverts once PRODUCTION is reached, even though riskWindowStart is still 0.
bonus share zeroes out when no risk window opened:
@> every staker's bonus share is 0, so sweepUnclaimedBonus sends the full bonus to recoveryAddress.
Sequence:
Agreement NEW_DEPLOYMENT. Bonus contributor funds the pool; stakers deposit, expecting principal + bonus if the protocol survives.
Sponsor (agreement owner) calls goToProduction: NEW_DEPLOYMENT -> PRODUCTION, skipping UNDER_ATTACK. _observePoolState never opens riskWindowStart.
withdraw now reverts (PRODUCTION not in pre-attack set): stakers cannot exit.
Outcome resolves SURVIVED (moderator or claimExpired). claimSurvived pays principal + _bonusShare == 0.
sweepUnclaimedBonus transfers the entire bonus to the sponsor's recoveryAddress.
§9's fairness argument ("staker could exit") does not hold: the exit is revoked without risk ever materializing, yet the staker is denied the bonus.
Likelihood: Medium — goToProduction is a documented, sponsor-reachable registry path for protocols that skip the attack phase; no special timing/front-run needed.
Impact: Medium — total loss of the bonus/yield for all stakers, redirected to the sponsor.
forge test --match-contract GoToProductionBonusTheftTest -vv passes against the in-scope contracts:
Align the bonus premise with the withdraw gate: when the pool resolves SURVIVED/EXPIRED with riskWindowStart == 0 but withdraw was already disabled (state advanced to PRODUCTION), distribute the bonus amount-weighted to stakers (the fallback already used when globalScore == 0) instead of returning 0 and sweeping to recovery. Alternatively, treat reaching PRODUCTION as opening-and-closing the risk window in one observation (riskWindowStart = riskWindowEnd = block.timestamp) so stakers earn the bonus for the term they were locked in.
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.