Bonus accrual is meant to reward stakers for time spent at risk. _bonusShare pays a k=2
time-weighted share to any staker who was locked while the agreement was attackable. DESIGN section 9
justifies the "no observed risk, no bonus" rule by asserting that a staker "only forfeits the exit
option once risk has actually materialized, which is exactly when they begin earning the risk
premium," so forfeiting the exit and earning the premium are presented as the same event.
They are not the same event. withdraw() is disabled by the live registry state from UNDER_ATTACK
onward, independent of riskWindowStart, so the staker forfeits the exit the instant the agreement
is attackable. But _bonusShare returns zero unless riskWindowStart != 0, which only happens if
some pool call observed the active-risk state. When the registry passes through the whole active-risk
window and reaches a terminal state with no pool interaction, riskWindowStart stays zero, the locked
stakers earn zero premium, and the entire bonus sweeps to recoveryAddress.
Likelihood:
The registry reaches a terminal state after an active-risk window that no pool call observed. This
arises whenever a pool of deposit-and-wait stakers sees the agreement go UNDER_ATTACK and then
PRODUCTION while nobody stakes, withdraws, pokes, or claims in between. Nobody is required to poke.
A staker who tries to withdraw during UNDER_ATTACK reverts (locked), and the reverted call rolls
back its own observation, so it does not seal the window either.
Impact:
Stakers locked through the full real active-risk window receive zero bonus; the entire sponsor
bonus sweeps to recoveryAddress. In the PoC a staker locked through UNDER_ATTACK gets 0 of a 500
bonus.
No principal is lost, and any single pokeRiskWindow during the window prevents it, which caps
this at the low end.
test/unit/FableAuditLeads.t.sol:
test_lead_bonusForfeited_despiteLockedRisk (passing): Alice stakes 100, sponsor funds a 500
bonus, registry goes UNDER_ATTACK (Alice's withdraw reverts, proven), survives to PRODUCTION with
zero pool interaction, moderator flags SURVIVED. Alice claims principal + 0 bonus; the full 500
sweeps to recovery.
test_lead_singlePokeSecuresBonus (passing, control): one pokeRiskWindow during UNDER_ATTACK
gives Alice all 500.
Do not gate the premium on whether the window was locally observed when the staker's exit was already
disabled by registry state. Either seal riskWindowStart from moderator-supplied transition data at
resolution, or drop the section 9 exit-forfeiture-equals-premium claim and document that stakers must
poke to earn the premium. The same fix that addresses the risk-window sealing dilution resolves this.
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.