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