The pool is designed so that deposits are only accepted while the risk window is live, and rewards are weighted by how early a user entered relative to the sealed end of the risk window, riskWindowEnd (T). Once T is sealed (set to non-zero), it is intended to represent the final, immutable end of the reward-accruing period — no valid deposit should ever be able to occur after it.
The issue is that deposit eligibility and reward-window finality are checked against two different, non-synchronized sources of truth. stake() only checks the live state of the external AttackRegistry, while the bonus calculation relies on the permanently sealed riskWindowEnd. If the registry performs a benign rewind from a terminal state (e.g. PRODUCTION) back to an active state (UNDER_ATTACK) — which the protocol's own threat model acknowledges as normal, recurring behavior — stake() reopens even though riskWindowEnd is already sealed and can never be reset. This allows entryTime > T, and because the bonus formula is evaluated through its polynomial expansion, the resulting negative (T - entryTime) is squared into a large positive score instead of reverting.
Likelihood: Medium
This will occur any time the upstream AttackRegistry performs a benign rewind from a terminal state (PRODUCTION) back to an active state (UNDER_ATTACK) after riskWindowEnd has already been sealed once — a scenario the protocol's own design documentation acknowledges as a normal, recurring part of the threat model (e.g., false-positive attack flags being cleared and re-triggered by moderators over the life of the pool).
This will occur without any privileged access or coordination: once the registry is in the rewound UNDER_ATTACK state, any unprivileged user observing this on-chain can call stake() and profit, since _assertDepositsAllowed only reads the live registry state.
This will occur repeatedly and permanently once triggered: because riskWindowEnd has no reset path anywhere in the contract, the exploitable window remains open for the entire remaining lifetime of the pool after the first benign rewind, not just a single transaction.
Impact: High
Honest, risk-bearing stakers who deposited before or during the actual risk window have their bonus share diluted or almost entirely wiped out, since the quadratic weighting grows unbounded the further after T a malicious deposit occurs.
An attacker with zero risk exposure (depositing only after the window has effectively closed) can capture a disproportionate majority of the bonus pool — demonstrated in the PoC as ~90% of the pool with only a 15-day post-seal delay — directly inverting the documented purpose of the quadratic weighting mechanism.
Add the following test to test/unit/ConfidencePool.k2Bonus.t.sol. (Ensure import {console} from "forge-std/console.sol"; is present at the top of the file).
Add regression tests confirming stake() reverts with DepositsClosedAfterRiskWindowSealed after riskWindowEnd is sealed, including across multiple PRODUCTION ↔ UNDER_ATTACK rewind cycles, and a fuzz test asserting riskWindowEnd never changes after its first non-zero assignment.
Impact - Medium Bonus pool only, principal always returns under SURVIVED/EXPIRED, but the quadratic advantage is unbounded and a tiny late stake can take nearly all of it. Likelihood - Low It needs a DAO registry migration to land inside the window where a pool is unresolved with riskWindowEnd already sealed, and the attacker still has to be watching. No attacker action can create the precondition.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.