Risk-window global accumulator overflow lets stakers bypass the CORRUPTED backstop
High
Low
ConfidencePool can accept a very large, otherwise standard ERC20 stake at an earlier timestamp, but later overflow when the same stake is evaluated against a later risk-window timestamp. This happens in _markRiskWindowStart() (totalEligibleStake * t * t) and in _bonusShare() (T * T * userEligible / T * T * snapshotTotalStaked).
The stake can fit when recorded at the original deposit timestamp t0, while a later active-risk or terminal timestamp makes stake * t^2 overflow. Depending on where the overflow is hit, either claims are permanently locked or riskWindowStart remains zero and every attempt to observe UNDER_ATTACK / PROMOTION_REQUESTED fails the same way.
This is not the accepted "nobody observed the risk window" residual. Here, observation is attempted but made impossible by accepted pool accounting state.
At stake time, the contract records each deposit with the current wall-clock timestamp:
This only proves the amount fits at t0. It does not prove the same amount fits at a later riskWindowStart, riskWindowEnd, or outcomeFlaggedAt.
When active risk is first observed, _markRiskWindowStart() resets the global accumulators using the later timestamp:
For a stake amount in the range:
the original stake succeeds, but _markRiskWindowStart() always reverts once active risk is observed at t1.
Even when active-risk observation succeeds, _bonusShare() repeats the same future-timestamp assumption:
An amount safe at entry can overflow at claim time once T is later, making every SURVIVED/EXPIRED claim revert.
That stale zero directly changes settlement. claimExpired() only enters the auto-CORRUPTED backstop when riskWindowStart != 0:
So a pool that passed through active risk can later reach CORRUPTED, wait out the moderator grace period, and still resolve as EXPIRED. The staker recovers principal and no corrupted reserve is created.
The same stuck riskWindowStart == 0 also makes survivor/expired bonus shares zero:
Three settlement failures are demonstrated:
SURVIVED/EXPIRED claim lock: riskWindowStart is set successfully, but a later outcomeFlaggedAt makes _bonusShare() overflow. Principal and bonus remain locked, and sweepUnclaimedBonus() cannot sweep because the balance is reserved for the unclaimed staker.
CORRUPTED backstop bypass: after active risk and a later CORRUPTED registry state, claimExpired() resolves EXPIRED instead of bad-faith CORRUPTED, allowing the staker to recover principal that should have been swept.
Survivor bonus suppression: after active risk and later PRODUCTION, _bonusShare() returns zero and the bonus becomes sweepable to recoveryAddress.
Likelihood is low because it requires an allowlisted high-supply/high-decimal standard ERC20 and an extremely large raw stake. The impact is high for any affected pool because terminal settlement can move principal/bonus opposite to the intended risk-window outcome.
Place the PoC at audit-poc/poc2.t.sol and run:
Result:
The first test proves the claim-lock path: risk starts normally, a huge stake is accepted, the outcome is later flagged SURVIVED, and claimSurvived() reverts due score overflow while the pool keeps principal plus bonus.
The second test stakes an amount that fits at t0, proves pokeRiskWindow() reverts at t1, then sets the registry to CORRUPTED after the moderator grace period. Because riskWindowStart is still zero, claimExpired() resolves EXPIRED, pays the staker's full principal, and leaves corruptedReserve == 0.
The third test shows the survivor bonus-suppression side: the same failed risk observation makes the staker receive principal only, while the entire bonus remains in the pool and is swept to recoveryAddress.
Reject any stake that can make the future global square accumulator overflow at the latest possible risk-window timestamp, or use a scaled / 512-bit-safe representation for stake-time scores.
For example, add a cap before increasing totalEligibleStake:
The production patch should implement the bound carefully to avoid overflow in the bound calculation itself.
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.