After a pool resolves to SURVIVED or bonus-paying EXPIRED, every staker should be able to recover their principal and their k=2 time-weighted share of the bonus pool.
_bonusShare() calculates the mathematically equivalent score
through an expansion over absolute Unix timestamps:
Although the final centered score can fit in uint256, the positive and negative intermediate terms can overflow before cancellation. In particular, the global positive term is evaluated with checked arithmetic:
At a production-like timestamp t ~= 1.75e9, the expanded addition can overflow at approximately:
This raw threshold must not be interpreted as 1.89e40 economically meaningful tokens by assuming 18 decimals. The documented compatibility includes standard ERC20 tokens and imposes no decimal or raw-supply bound. ERC-20 defines decimals() as optional uint8 display metadata; it does not constrain token arithmetic. For a standard token returning 60 decimals, 1.89e58 raw units represent only approximately 0.0189 displayed tokens.
Once such a token has been allowlisted and used by a pool, an unprivileged staker can push totalEligibleStake over the arithmetic boundary and permissionlessly contribute one raw bonus unit. The nonzero bonus bypasses _bonusShare()'s early return. After resolution, the same overflowing global term is evaluated for every claimant, so no staker can recover principal through the intended SURVIVED or EXPIRED payout.
The factory owner allowlists an ERC20-compliant token with 60 decimals. This is unusual, but it does not use fee-on-transfer or rebasing behavior and does not violate the documented standard-ERC20 compatibility.
An honest staker deposits 99% of type(uint256).max / (2*t^2) + 1 raw units. A griefer deposits the remaining 1% and contributes one raw bonus unit.
Each deposit succeeds because the stored entry-time-square accumulator is still approximately half of type(uint256).max.
The risk window opens and the pool later resolves to SURVIVED (the same payout calculation is used by bonus-paying EXPIRED).
claimSurvived() calls _bonusShare() before transferring principal. T*T*snapshotTotalStaked + snapshotSumStakeTimeSq overflows with Panic(0x11).
The claim transaction rolls back. Repeating the claim from either the victim or the griefer reaches the same global overflow. Withdrawals are already disabled and the pool has no principal-rescue path, leaving the stakers' intended payout locked.
The attack requires a standard ERC20 whose raw supply can reach approximately 1.89e58, and that token must first be allowlisted by the trusted factory owner. High-decimal tokens at this scale are unusual, so the precondition is not expected for every pool.
However, the condition is computationally and economically feasible: minting and transferring the raw balance is constant-cost EVM arithmetic, and with 60 decimals the PoC's total stake is approximately 0.0189 displayed tokens. After allowlisting, the overflow is triggered entirely through permissionless stake() and contributeBonus() calls.
All SURVIVED claims and bonus-paying EXPIRED payouts route through the overflowing global score before principal is transferred. A successful trigger therefore freezes the principal and bonus entitlement of every staker in the affected pool. This directly affects user funds and breaks a core resolution path; there is no per-user escape or emergency principal withdrawal after the risk window opens.
The PoC uses the repository's existing Foundry harness. First, make the mock exercise the supported high-decimal ERC20 scale; this changes display metadata only and does not introduce fee-on-transfer, rebasing, hooks, or nonstandard transfer behavior:
Add the following test as test/audit/AbsoluteTimestampOverflow.t.sol:
Run:
Expected result:
The minimal complete mitigation is to cap total eligible stake using the maximum possible timestamp (expiry) before updating the k=2 accumulators. The factor of two protects both expanded timestamp-squared-equivalent terms:
Alternatively, store the k=2 accumulators using timestamps centered on riskWindowStart and calculate each score from (T - entry) deltas. This removes the large absolute-timestamp terms but requires consistently rebasing the existing global and lazy per-user accumulators.
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.