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