riskWindowStartConfidencePool records lifecycle latches in _observePoolState(), but withdraw() and setPoolScope() can revert after calling it. If the first UNDER_ATTACK observation happens inside one of those calls, the EVM rolls back riskWindowStart.
The pool then behaves as if the attack window never started. A later successful call can become the first persisted observation even though risk started much earlier.
This is not just a cosmetic state desync. riskWindowStart is later used by the k=2 time-weighted bonus logic to decide how much of the bonus pool a staker deserves. Once the first persisted observation is delayed, the bonus formula starts measuring from the wrong timestamp.
A late sponsor/sybil stake can redefine the effective risk window and receive a large bonus share. This breaks the intended k=2 time-weighted bonus formula because late entrants are no longer crushed by the real elapsed risk time.
Example: Alice stakes before risk starts. The registry enters UNDER_ATTACK. The sponsor calls setPoolScope(), _observePoolState() sets riskWindowStart, then the call reverts with ScopePostLockImmutable. Bob stakes 20 days later while still under attack. Bob's stake becomes the first successful observation and receives about half the bonus after SURVIVED.
The same root issue also affects other paths that depend on the first persisted lifecycle observation. For example, a reverted first terminal observation can similarly delay riskWindowEnd, and a pool can momentarily behave as if no active-risk period was ever seen even though the registry already moved to an attack state.
Bob receives principal plus a large bonus despite entering long after the real risk start.
PoC flow:
Alice is the honest early staker.
The registry already moved into UNDER_ATTACK.
The sponsor uses setPoolScope() as the first call that observes this state, but that call reverts after _observePoolState() runs.
Because the entire call frame reverts, riskWindowStart returns to zero.
Bob then stakes much later while the pool is still in the same attack period.
Bob's later stake becomes the first persisted observation and is rewarded as if he entered near the start of the window.
Do not persist lifecycle latches inside calls that can revert after observation. Split observation into read/commit phases, or commit the first active-risk observation through a non-reverting path.
For example, _observePoolState() can be split into a pure/view classification step plus a commit step that is only reached from non-reverting paths:
The important property is that withdraw() and setPoolScope() must not be able to observe UNDER_ATTACK, revert, and silently erase the first risk observation.
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.