A staker is meant to lose the exit and start earning the bonus at the same moment: docs/DESIGN.md §9 says a staker "only forfeits the exit option once risk has actually materialised, which is exactly when they begin earning the risk premium."
In code these are two different signals. withdraw closes on the live UNDER_ATTACK state, but the bonus only accrues once riskWindowStart is sealed, which needs a transaction that succeeds while the registry is active-risk (stake / contributeBonus / pokeRiskWindow). A locked-in staker's own withdraw seals the flag and then reverts on the next line, so the revert rolls the seal back. Result: a staker locked in through a real attack that survives can receive zero bonus, and the whole bonus pool is swept to the sponsor's recoveryAddress.
Likelihood:
Occurs whenever the registry passes through an active-risk state and no stake / contributeBonus / pokeRiskWindow lands during that interval, i.e. the default for a passive pool (locked-in stakers cannot withdraw, and staking more only dilutes their share).
Occurs when the active-risk interval is short, e.g. a DAO instantPromote from UNDER_ATTACK straight to PRODUCTION in one transaction, so a watching staker gets no block in which to poke.
Impact:
Winning stakers who bore the full risk get none of the k=2 bonus (_bonusShare returns 0 for all).
sweepUnclaimedBonus then sends the entire bonus pool to the sponsor's recoveryAddress (with riskWindowStart == 0 only principal is reserved), reassigning participants' yield to the party that funded it.
PoC_LockedInStakerNoBonus.t.sol (in this folder) drops into test/unit/. Two tests share the same path (NEW_DEPLOYMENT -> UNDER_ATTACK -> PRODUCTION), stake, and bonus; a single pokeRiskWindow during the attack is the only difference and it flips the reward between zero and the full bonus.
Make the observation that closes the exit commit the seal instead of discarding it. _observePoolState() already seals riskWindowStart on an active-risk read; just stop withdraw from reverting that seal away.
This preserves the genuine §5 "no observable risk" case (a jump straight to a terminal state never seals riskWindowStart, so it still reverts and pays no bonus) while closing the gap for a demonstrably locked-in staker. WithdrawBlocked(address) is a new event so the caller learns the exit was closed; a plain return also works.
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.