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.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.