Normal behavior: The pool keeps its k=2 time-weighted bonus accounting as raw uint256 products of token amounts and absolute Unix timestamps; a deposit is weighted at the current timestamp, and settlement (_markRiskWindowStart, _bonusShare) evaluates those products at the terminal timestamp T before the 512-bit Math.mulDiv.
Specific issue: The accepted amounts are never bounded against the product capacity, and a timestamp squared is already about 1e19 at production scale, so a standard ERC20 with high decimals or a large raw supply lets an amount safe at deposit overflow T·T·amount at settlement — a checked-arithmetic revert on the exact calls that must succeed. Because _bonusShare skips its products only while snapshotTotalBonus == 0, a permissionless one-unit contributeBonus (no stake, no role) forces the overflowing product to run against the honest stakers' aggregate; at its worst, one raw unit of a dust token permanently freezes the entire pool's principal.
Likelihood:
WHEN the factory has allowlisted a standard ERC20 whose decimals/supply let the aggregate stake (or a single stake) push a T*T*amount product across 2^256 at the pool's terminal timestamp, an amount accepted at deposit time overflows at settlement.
WHEN a permissionless one-unit contributeBonus flips snapshotTotalBonus non-zero, the overflowing _bonusShare product runs against the honest stakers' aggregate; the attacker holds no stake and no role.
Impact:
The effect climbs from misrouted bonus (a stuck observation resolves SURVIVED with riskWindowStart == 0, so honest bonus is swept to recovery as unowed) to a full denial of claims, up to a permanent, unrecoverable freeze of all staker principal in a non-upgradeable clone.
The worst case is reachable by an unprivileged attacker for the cost of one raw token unit, once an affected standard ERC20 is allowlisted. No fee-on-transfer, rebasing, hook, or callback behavior is involved.
Both the worst-case permissionless freeze and the observation-time surface, against the in-scope pool. Save as test/unit/K2ScoreOverflow.t.sol and run forge test --match-contract K2ScoreOverflowTest -vv.
Passing result: both tests pass. The mock registry only moves the agreement forward through legal states; the overflow is a pure arithmetic property of accepting an unbounded amount, independent of the registry path.
Bound the accepted amounts against the score capacity at the pool's maximum timestamp on stake() / contributeBonus() (reject amounts where expiry * expiry * (totalEligibleStake + amount) would exceed type(uint256).max), or compute every score product through Math.mulDiv / 512-bit intermediates. Removing the snapshotTotalBonus == 0 short-circuit alone is insufficient — the bound must cover the stake aggregate, not just the presence of 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.