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