The pool's central incentive is a k=2 time-weighted bonus meant to "crush late entrants within the observed risk window": a small, long-bearing staker should out-earn a large, last-minute one. docs/DESIGN.md Section 3 relies on this as a security property (deposits during UNDER_ATTACK are "crushed to ~zero by the k=2 weighting"), backed by the test testQuadraticCrushesInsiderLastMinuteStaker.
This protection is bypassed whenever the active-risk window is not promptly observed. riskWindowStart is sealed lazily on the FIRST observation (_observePoolState -> _markRiskWindowStart) and records the OBSERVATION time, not when the registry actually entered active-risk:
If nobody pokes/stakes/withdraws while the registry is in UNDER_ATTACK/PROMOTION_REQUESTED, a late whale whose OWN stake is that first observation seals riskWindowStart at a late instant. At claim time _clampUserSums floors EVERY earlier staker's effective entry to that same late instant, so all deposits share the identical factor (T - riskWindowStart)^2. In share = mulDiv(userScore, snapshotTotalBonus, globalScore) with userScore = sum of amount*(T - entry)^2, that common factor cancels, leaving share proportional to eligibleStake. The k=2 time-weighting collapses to amount-weighting: a whale with 1000x the stake and ~zero real at-risk time takes ~1000x the bonus, draining it from the long-term stakers it was meant to reward.
Section 3 argues the joiner's score (T - entry)^2 ~= 0, but that is the ABSOLUTE score; the payout is a RELATIVE split, and when every score carries the same small (T - riskWindowStart)^2 the whale's share is simply its stake fraction. Section 7 calls pre-seal amount-weighting intentional because "pre-risk calendar time is not at-risk time" - but that premise is FALSE here: the interval collapsed into the cohort is genuine at-risk time (the registry was in UNDER_ATTACK). Every existing test in this area pokes promptly so the seal tracks risk onset; none models an unobserved window followed by a late whale.
Likelihood: Medium
Observation is lazy/permissionless and passive stakers do not proactively poke, so an unobserved active-risk window is the realistic default. An attacker can monitor and SELECT pools already in this state (getAgreementState == UNDER_ATTACK while riskWindowStart == 0) and strike late.
The seal is one-way, so once the whale seals it late the outcome is irreversible.
Impact: Medium
The protocol's core "reward early confidence / crush late entrants" property (Section 3) is defeated; the bonus (the primary staker incentive per the README) is redirected from long-term honest stakers to an opportunistic late whale.
Works on both the EXPIRED auto-resolution path (cheapest - the whale's principal is exposed only minutes before expiry) and the moderator SURVIVED path.
Drop this into test/unit/Poc.t.sol and run forge test --match-path test/unit/Poc.t.sol -vv. Both tests PASS against the UNMODIFIED contracts. alice stakes 100 and bears ~30 days of real at-risk time; bob stakes 100,000 (1000x) during UNDER_ATTACK shortly before resolution, with no prior observation. The control test proves a single prompt pokeRiskWindow() restores k=2 and crushes the whale, isolating delayed observation as the trigger. (The moderator SURVIVED path behaves identically.)
Output (unmodified contracts):
The exploit test is the direct inverse of the team's testQuadraticCrushesInsiderLastMinuteStaker, differing only in whether the risk window is promptly poked.
Root cause: riskWindowStart records OBSERVATION time, not risk-onset time, so honest at-risk time is discarded when observation lags.
Make a deposit that itself first seals the risk window ineligible for bonus (score 0) rather than amount-weighted, preserving the Section 3 guarantee even when observation lagged.
Or gate resolution on a minimum observed risk duration / require the window to be sealed within a bounded delay of the registry transition (incentivized keeper), so riskWindowStart cannot be deferred to collapse the time-weighting.
At minimum, document that stakers MUST promptly pokeRiskWindow() when the registry enters active-risk, or forfeit their time-weighted bonus.
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.