The bonus is split by a k=2 time-weighted formula whose entire purpose is to reward stakers in
proportion to how long their capital sat at risk. Effective entry time is floored at
riskWindowStart, and riskWindowStart is meant to mark the start of the at-risk period so that a
staker locked through a long UNDER_ATTACK window out-earns a latecomer.
riskWindowStart does not record the registry's true UNDER_ATTACK transition. It records the first
block in which some pool call happens to observe the active-risk state, and _clampUserSums
retroactively floors every earlier deposit UP to that instant. When the whole active-risk interval
elapses with no pool interaction, the first staker to touch the pool, even one block before expiry,
seals riskWindowStart at that late timestamp and drags every earlier staker's effective entry down
to it. All deposits collapse into one floored cohort and the bonus is split amount-weighted, so a
whale who bore one second of observed risk captures the pool.
The withdraw escape hatch does not save the victim. withdraw() reverts from UNDER_ATTACK onward, so
the early staker is locked through the real active-risk window, and a reverted withdraw rolls back its
own observation, so it never seals the window either.
Likelihood:
The upstream registry holds UNDER_ATTACK indefinitely once approveAttack runs. In
AttackRegistry._getAgreementState the attackApproved branch returns UNDER_ATTACK before the
deadline auto-promotion branch is reached, so the unobserved active-risk interval is naturally long
(29+ days in the PoC) and the setup arises on any pool whose stakers deposit and wait.
The attacker controls the timing and acts last. They wait until one block before expiry, read that
riskWindowStart is still 0, and stake, so they only move when the precondition already holds. A
reactive poke by the victim at that point seals just as late and does not help. The only defense is
a proactive poke made at the true transition, which passive deposit-and-wait stakers do not make.
Impact:
The early staker locked for the entire real active-risk window receives under 1% of the sponsor
bonus while a whale who bore one observed second takes over 99%. In the PoC a 100-token staker
locked 29 days gets 9.90 of a 1,000 bonus and a 10,000-token one-second whale gets 990.10.
The k=2 time-weighting, the protocol's core reward mechanism, is fully defeated and collapses to
pure amount-weighting, so bearing risk longer earns no premium over a last-second entrant.
The sponsor bonus is redirected away from the risk-bearing stakers it was posted to reward and to a
non-risk-bearing latecomer. Principal is not lost.
test/unit/RiskWindowSealingLead.t.sol (4 passing). test_dilution_expiredPath logs:
Flow (test_dilution_expiredPath):
Registry is ATTACK_REQUESTED. Alice stakes 100. Sponsor contributes a 1,000 bonus.
Registry enters UNDER_ATTACK with 30 days left. Nobody interacts with the pool. riskWindowStart
stays 0. Alice's withdraw() reverts (locked), and the reverted call does not seal the window.
One second before expiry, Bob stakes 10,000. This seals riskWindowStart = expiry - 1.
Both claim after expiry. Bob receives 990.10, Alice 9.90.
test_dilution_survivedPath shows the same on the SURVIVED path. test_proactivePokeDefends shows a
poke at the transition preserves Alice's bonus, and test_reactivePokeIsWorthless shows a poke made
once Bob appears does not, which is the asymmetry that distinguishes this from the accepted timing
residual in DESIGN.md section 7 (that residual is a symmetric race to observe the same terminal event
that any counterparty collapses to ~zero, no principal effect, no third-party loss; here the
defender's window closes at the unobserved transition and a whale takes ~99% of the bonus).
The registry exposes no canonical transition timestamp, so riskWindowStart cannot be anchored to
the true transition on-chain. The direct fix is to stop a late deposit from joining the floored
cohort. Block deposits while the registry is UNDER_ATTACK, matching the existing PROMOTION_REQUESTED
block:
This removes the whale-dilution lever. If UNDER_ATTACK deposits must stay open (DESIGN.md section 3),
instead give the window-opening deposit a strictly post-observation entry time so it cannot share the
floored cohort with pre-existing stakers, and require the moderator to supply the true active-risk
start at resolution so genuinely early stakers are not floored to a late seal.
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.