The protocol documents UNDER_ATTACK deposits as near-zero-reward late risk capital. The root cause is that a late entrant can be the first call that observes UNDER_ATTACK; stake() seals riskWindowStart at that entrant's block and resets the existing cohort's time weight, letting the entrant share bonus as if they had borne the missed interval.
The docs make a strong safety claim about this branch. docs/DESIGN.md and protocol-readme.md both justify allowing deposits during UNDER_ATTACK on the basis that the late entrant's bonus share is "crushed to ~zero" and that there is no late-join advantage. The dedicated time-weighting suite encodes that same assumption in testEarlierStakerEarnsLargerBonusShareThanLaterStaker: when riskWindowStart is observed promptly, Alice's earlier at-risk time dominates Bob's late entry.
The real entry path does not preserve that property when the pool misses the beginning of the active-risk interval. stake() calls _observePoolState() before it records the caller's stake. If the registry is already UNDER_ATTACK and riskWindowStart == 0, _observePoolState() immediately calls _markRiskWindowStart(), which sets riskWindowStart = block.timestamp and rewrites the global accumulators so every existing staker is treated as if they only entered at that late block.
The same stake() call then credits the late entrant at that exact same block.timestamp. Earlier stakers are later clamped to stake * riskWindowStart, so they lose all real active-risk time that elapsed before the observation. The newcomer therefore joins the exact same score cohort as the long-standing stakers even though the newcomer only arrived after the agreement had already been attackable for most of the real risk interval.
That is a real payout bug, not just an accepted timestamp residual. docs/DESIGN.md accepts that the terminal moment T is first-observed rather than the true transition block, and says that bias only redistributes bonus "among stakers." This branch is stronger: it lets a new entrant become a staker after most of the real active-risk interval has already elapsed, then inherit equal bonus weight with the earlier cohort because the missed interval is erased from riskWindowStart itself. If the agreement resolves terminally in that same block, _bonusShare falls back to a full amount-weighted split, making the late-join advantage even stronger.
The smallest permissionless exploit sequence is:
Alice stakes early and the sponsor funds a large bonus. The registry then becomes UNDER_ATTACK, but nobody calls pokeRiskWindow() or touches the pool during the first week of real attackability. Bob watches the live registry, waits until that missed week has already passed, then stakes during the still-live UNDER_ATTACK state. Bob's own stake() call becomes the first observation, so riskWindowStart is sealed at Bob's entry block and Alice's earlier week of real risk is discarded from the score. When the agreement later reaches PRODUCTION, Alice and Bob now have identical k=2 scores and split the whole bonus 50/50 even though Bob only joined after most of the real risk interval had already elapsed.
I validated that sequence locally with the temporary regression included below. It shows both branches:
with prompt pokeRiskWindow(), Alice receives essentially the entire bonus and Bob gets almost none, matching the design claim;
with delayed first observation, Bob gets the same 150 payout as Alice after joining a week late;
if terminal resolution happens in the same block as Bob's entry, the amount-weighted fallback also hands Bob a full half of the bonus.
The current suite never checks that invariant. test/unit/ConfidencePool.timeWeightedBonus.t.sol only covers the prompt-observation path where riskWindowStart was already sealed before Bob entered.
An ordinary attacker can wait until the agreement has already spent most of its real time in UNDER_ATTACK, become the first pool interaction that observes that state, and siphon an arbitrary fraction of snapshotTotalBonus away from the earlier stakers who actually bore that missing risk interval. The protocol pays the late entrant as if they had underwritten time they never actually bore.
Do not allow a new UNDER_ATTACK stake to define riskWindowStart for the existing cohort; either anchor the start to an upstream transition timestamp or block staking when the live registry is already in UNDER_ATTACK but the pool has not yet sealed the window.
I validated this locally with a temporary standalone Foundry test.
Run:
Use:
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.