Normally, stakers who keep their capital in the pool through the agreement's risk period and survive are paid back their principal plus a share of the bonus pool, per the protocol's stated purpose ("stakers … are rewarded from the bonus pool if they [survive]"). The bonus is split by the k=2 time-weighting keyed on riskWindowStart.
The bonus a survivor receives is entirely gated on riskWindowStart != 0, and riskWindowStart is only set as a side effect of some account interacting with the pool while the registry is in an active-risk state. When the agreement genuinely passes through UNDER_ATTACK/PROMOTION_REQUESTED but no account touches the pool during that interval, riskWindowStart stays 0; every survivor is then paid zero bonus, and sweepUnclaimedBonus transfers the entire bonus pool to the sponsor-controlled recoveryAddress. The moderator's authoritative SURVIVED resolution does not seal riskWindowStart, so it cannot restore the bonus.
The root cause spans two functions in src/ConfidencePool.sol (abridged; @> marks the root-cause lines):
Likelihood:
Occurs whenever the agreement moves through an active-risk state while no account calls stake / withdraw / contributeBonus / pokeRiskWindow during that window — the natural state of any low-activity pool, or one funded by a single up-front deposit that then sits idle until resolution.
The sponsor sets and controls recoveryAddress and directly receives the swept bonus, giving them a standing incentive to leave the window unsealed and to not surface the (non-obvious) pokeRiskWindow() requirement to stakers.
Impact:
Stakers who bore real, on-chain-visible risk and survived the term receive none of the bonus they were promised — the core reward mechanism of the protocol silently fails for them.
The full bonus — which may be funded by an unrelated third party through the permissionless contributeBonus — is captured by the sponsor's recoveryAddress: unjust enrichment of a party that neither funded the bonus nor bore the risk. There is no refund path for the misdirected funds.
The following self-contained test (test/BonusLossPoC.t.sol, using the repo's existing mocks) walks the full loss path end to end: two stakers deposit, a third party funds the bonus, the agreement genuinely transits UNDER_ATTACK → PRODUCTION while the pool sits idle, and the moderator resolves SURVIVED. It then asserts the three facts that make this a loss: riskWindowStart == 0, each survivor ends with exactly their principal (zero bonus), and sweepUnclaimedBonus moves the entire 300e18 bonus to the sponsor-controlled recoveryAddress. Run with forge test --match-path test/BonusLossPoC.t.sol -vv — it passes, confirming the survivors are paid nothing and the bonus is captured by the recovery address.
Save as test/BonusLossPoC.t.sol and run forge test --match-path test/BonusLossPoC.t.sol -vv. It compiles and passes with the repository's own foundry.toml settings — no flags or config changes required.
The fix is to treat a SURVIVED/EXPIRED cohort as owed the bonus even when the risk window was never sealed: with no observed time spread there is nothing to k=2-weight, so fall back to an amount-weighted split among the survivors (mirroring the existing globalScore == 0 fallback) instead of returning zero and letting the sweep route it to recoveryAddress. This requires two coordinated changes — pay the amount-weighted share in _bonusShare, and reserve that same bonus in sweepUnclaimedBonus so it is no longer treated as sweepable excess — after which survivors receive the bonus they were promised and only genuine surplus/donations reach recovery.
Two coordinated edits in src/ConfidencePool.sol, in two separate functions:
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.