The ConfidencePool uses a k=2 time-weighted formula score = stake × (T − entry)² to distribute the bonus pool, where T = riskWindowEnd. The quadratic weighting is designed to crush late entrants: the closer a staker enters to T, the smaller their score. The withdraw() function is hardened against upstream registry rewinds using the pool's own one-way riskWindowStart != 0 latch, with an explicit code comment: "gate on it so an upstream registry rewind cannot re-open withdrawals."
The symmetric riskWindowEnd != 0 latch is not applied to stake(). The _assertDepositsAllowed function is pure and gates exclusively on the live upstream registry state. When a benign registry rewind (e.g., DAO migration, registry repointing) reverts the live state from PRODUCTION back to UNDER_ATTACK or NEW_DEPLOYMENT, staking is silently re-enabled despite riskWindowEnd already being sealed. A late staker enters with entry > T, and because the O(1) algebraic expansion T²·a − 2T·(a·t) + (a·t²) computes a perfect square that is always non-negative, the "negative" distance past T produces a positive and growing score — inverting the penalty into a reward that allows the late staker to steal the majority of the bonus pool.
Likelihood:
The protocol already explicitly defends withdraw() against registry rewinds using the one-way riskWindowStart latch, establishing that rewinds are a considered scenario — not a purely theoretical concern. The asymmetric absence of the equivalent riskWindowEnd guard on stake() is an oversight in an otherwise consistent defense pattern.
Registry rewinds occur as routine DAO administrative actions: registry migration, bug-fix repointing, or temporary state corrections. These are benign governance operations that produce the exact state sequence the exploit requires. The attacker is a third-party opportunist monitoring for such events, not the DAO itself.
Impact:
The late staker captures the majority of the bonus pool from honest at-risk stakers while bearing zero risk. With entry = T + 10 days and an honest staker at entry = T − 5 days, the attacker's score is (10d)² = 100d² vs (5d)² = 25d² — 4× the honest staker's share. The further past T the attacker enters, the larger their share grows (quadratic scaling).
Once entry > 2T − riskWindowStart, the attacker's score strictly dominates even the earliest possible staker (one who entered at riskWindowStart), enabling near-total drainage of the bonus pool. The attacker can stake and resolve in the same block after expiry, making the attack risk-free.
Mirror the riskWindowStart latch on withdraw() by adding a riskWindowEnd check to _assertDepositsAllowed, permanently closing deposits once a terminal state has been observed:
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.