ConfidencePool permanently records the end of the risk window in riskWindowEnd when it observes a terminal AttackRegistry state such as PRODUCTION.
Once this terminal timestamp has been recorded, new deposits must never be accepted because the upper bound T used by the bonus formula has already been fixed.
However, stake() determines whether staking is open exclusively from the current live state returned by the configured AttackRegistry:
The deposit guard only rejects three live registry states:
It does not check whether the pool has already permanently recorded a terminal state through:
This matters during a supported AttackRegistry replacement or migration.
If the replacement registry temporarily reports a pre-terminal state such as NEW_DEPLOYMENT for an agreement whose pool already observed PRODUCTION, stake() reopens even though riskWindowEnd remains permanently fixed.
A new depositor is then assigned:
which may be greater than the already-recorded bonus upper bound T.
The bonus calculation expands the quadratic score as:
For a single deposit, this is algebraically equivalent to:
Because the difference is squared, an invalid deposit with:
does not receive zero weight.
Instead, it receives a positive score proportional to:
The longer the attacker waits after the actual terminal moment, the larger their bonus score becomes.
The repository already models a benign registry replacement reporting NEW_DEPLOYMENT in:
The withdrawal path correctly uses the persisted riskWindowStart as a one-way latch so a registry rewind cannot reopen withdrawals.
The staking path has no equivalent persistent terminal-state guard.
The root cause is that deposit authorization uses only the current external registry state and does not consult the pool's own persisted terminal marker, riskWindowEnd.
At the same time, the bonus accounting assumes that every accepted deposit has an entry timestamp less than or equal to T, but this invariant is never enforced.
The combination of these two missing checks allows a post-terminal deposit to receive an increasingly large quadratic bonus weight.
An attacker can enter after the actual risk period has already ended, bear no protocol risk, and redirect nearly the entire sponsor-funded bonus pool away from honest stakers.
In the provided PoC:
Alice stakes 100 tokens.
The pool contains a 100 token sponsor-funded bonus.
Alice bears the actual risk window.
The pool observes PRODUCTION and permanently records riskWindowEnd.
A replacement registry temporarily reports NEW_DEPLOYMENT.
Bob stakes only 1 token shortly before expiry, after the terminal timestamp.
The replacement registry later reports PRODUCTION.
The outcome is finalized as SURVIVED.
The resulting payouts are:
Bob deposits only 1% as much principal as Alice and enters after the risk window has ended, but receives approximately 99.9819% of the entire bonus pool.
This causes a direct misallocation of sponsor-funded rewards from honest risk-bearing stakers to an ineligible post-terminal entrant.
The issue requires a specific operational condition: the configured AttackRegistry must be replaced or migrated and the replacement must temporarily report a pre-terminal state for an agreement whose pool previously observed a terminal state.
This condition is not an arbitrary malicious-registry assumption.
The protocol intentionally reads the AttackRegistry address live from SafeHarborRegistry, and the repository already contains a regression test modeling the same benign registry-rewind condition for the withdrawal path.
Once this condition occurs, exploitation is permissionless. Any user may call stake() before expiry and choose the timing and amount of the post-terminal deposit.
The attacker does not need to control the registry migration, compromise the registry owner, or influence the replacement registry. They only need to observe the temporary stale state and submit a permissionless stake before the agreement state is restored.
Likelihood is therefore assessed as Low because a registry migration or temporary stale state is required.
Impact is High because sponsor-funded bonus tokens are directly redirected from honest stakers to the late entrant.
The PoC demonstrates that the attacker can capture virtually the entire bonus pool while risking only a minimal amount of principal.
The attack does not require stealing principal from the pool; it directly corrupts the protocol's reward distribution and transfers bonus funds to an ineligible post-terminal participant.
Add the following test as:
Run the PoC with:
Observed result:
The test was executed against repository commit:
Treat riskWindowEnd as a permanent local terminal-state latch.
Once it is non-zero, both staking and bonus contributions must remain closed regardless of the current state returned by a replacement registry.
For example:
This protects both:
because both functions already call _assertDepositsAllowed.
As defense in depth, add a regression invariant ensuring that no accepted deposit can have an effective entry timestamp greater than the already-recorded riskWindowEnd.
A regression test should:
Open the risk window.
Observe PRODUCTION and seal riskWindowEnd.
Replace the registry with one reporting NEW_DEPLOYMENT.
Verify that both stake() and contributeBonus() revert with StakingClosed.
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.