ConfidencePool stores the first and second absolute-time moments of every stake and later expands
amount * (T - entry)^2 as:
All three moment terms are ordinary checked uint256 multiplications. A stake can be accepted at
its earlier entry time, and the global accumulator can still be reset successfully when the risk
window opens, yet T^2 * snapshotTotalStaked can overflow at the later terminal time. Once a
nonzero bonus exists, every SURVIVED claim evaluates this denominator and reverts. The clone is
non-upgradeable, PRODUCTION is terminal, and sweepUnclaimedBonus() reserves all unclaimed
principal and bonus, so the funds remain permanently locked.
At stake time, stake()
records:
When risk is first observed, _markRiskWindowStart()
resets the global moments:
Finally, every bonus-bearing claim reaches _bonusShare():
For an entry timestamp t_entry and later terminal timestamp T, the nonempty interval
contains amounts which stake() accepts but which later overflow in _bonusShare(). The PoC also
chooses the amount so the intermediate risk-window reset remains safe.
This is within the published token compatibility. The repository supports standard ERC20 tokens
and excludes only fee-on-transfer and rebasing behavior. ERC-20 defines decimals() as uint8
and totalSupply() as uint256; it does not impose an 18-decimal or smaller-supply bound. The PoC
uses an otherwise standard OpenZeppelin ERC20 with 60 decimals:
configured minimum stake: 1e55 raw = 0.00001 token;
attacker stake: approximately 3.7694e58 raw = 0.037694 token;
honest stake: approximately 5.5869e55 raw = 0.00005587 token;
bonus: 1e60 raw = 1 token.
The token must be allowlisted, but no administrator needs to violate the published compatibility
assumptions: the asset has no transfer fee, rebasing, callback, blacklist, or nonstandard transfer
behavior.
Likelihood: Low. Exploitation requires an allowlisted ERC20 with unusually high decimals or supply, but no privileged action or non-standard transfer behavior once the token is allowlisted.
Impact: High.
An unprivileged staker can permanently deny all SURVIVED payouts for one pool. The attacker also
locks their own stake, so this is griefing rather than theft, but the PoC locks a one-token bonus
with roughly 0.0377 token of attacker principal, in addition to every honest principal position.
The condition snapshotTotalBonus != 0 is not a barrier: contributeBonus() is permissionless and
even one raw unit bypasses the early return in _bonusShare().
The lock is permanent in the demonstrated state:
every honest claim uses the same overflowing global denominator;
the attacker's own claim also overflows;
the terminal PRODUCTION state cannot be reflagged CORRUPTED;
pool clones are non-upgradeable;
sweepUnclaimedBonus() computes the whole balance as reserved and reverts NothingToSweep.
Medium is appropriate because the impact is permanent loss of availability for all funds in the
affected pool, while exploitation depends on a peculiar but explicitly supported high-decimal or
high-supply ERC20.
Save the following as test/audit/AccumulatorOverflow.t.sol and run:
Expected result:
The full local PoC also demonstrates the earlier liveness manifestation where a stake accepted at
t_entry later overflows _markRiskWindowStart(). That secondary case is not required for the
permanent SURVIVED lock above.
Do not retain or expand absolute Unix-timestamp moments without an explicit numerical bound.
Possible fixes are:
Express entry and terminal times relative to a pool-local origin, then maintain the first and
second moments of those bounded deltas.
Enforce, at every stake, a documented upper bound on totalEligibleStake derived from the
worst-case expiry and every intermediate addition, not only the final Math.mulDiv.
Add boundary tests at floor(type(uint256).max / T^2) and immediately above it for both
_markRiskWindowStart() and _bonusShare().
Using Math.mulDiv only for the final bonus multiplication is insufficient because the moment
expansion has already overflowed before Math.mulDiv is reached.
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.