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.
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.