Normal behavior: stakers deposit an allowlisted ERC20, the pool opens a risk window when the registry reaches an active-risk state, and stakers later recover principal plus any owed bonus through claimSurvived() or claimExpired().
Normal behavior: the k=2 bonus formula should weight each stake by elapsed risk time, equivalent to stake * (T - entryTime)^2, where T is the resolution timestamp and entryTime is the effective risk-window entry timestamp.
The issue is that the contract stores and expands absolute Unix timestamps before cancellation. With a standard high-decimal ERC20 stake, the intermediate stake * timestamp^2 terms overflow even though the intended elapsed-time score can be small and valid. ERC-20 does not restrict decimals(), and the factory's allowlist does not impose a decimals, supply, or aggregate-stake bound.
The same root cause appears in two places: _markRiskWindowStart() recomputes global stake-time sums using totalEligibleStake * t * t, and _bonusShare() computes the claim score using T * T * stake + stakeTimeSq - 2 * T * stakeTime.
Likelihood:
The factory owner allowlists a standard ERC20 with 60 decimals. This is within the documented ERC20 compatibility: it is neither fee-on-transfer nor rebasing.
A staker deposits 2.835e58 raw units, which is 0.02835 tokens at 60 decimals. The stake succeeds at the production-scale base timestamp, then a one-day risk window makes the claim-time intermediate overflow.
The project documents support for standard ERC20 tokens, but does not define a decimals, total-supply, or per-pool raw stake cap that keeps these timestamp-squared intermediates below uint256.max.
Impact:
Active-risk observation can revert in _markRiskWindowStart(), blocking pokeRiskWindow() and any call path that first observes the active-risk state.
When the pool expires while the registry remains active-risk, claimExpired() can revert before resolving EXPIRED, blocking the intended expiry backstop and principal withdrawal.
With a nonzero bonus and an already observed risk window, claimSurvived() and claimExpired() can revert in _bonusShare().
A huge staker can overflow the global score and block smaller honest stakers from claiming.
Run:
Result:
Use a delta-based accounting representation throughout, or enforce a per-pool aggregate-stake cap derived from the maximum timestamp. A cap must account for both terms in _bonusShare(); capping each individual deposit alone is insufficient because snapshotTotalStaked is aggregate state.
Alternatively, replace the absolute-time polynomial accumulators with accumulators expressed relative to the risk-window start, and apply that representation consistently in stake, _clampUserSums, _markRiskWindowStart, and _bonusShare.
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.