SURVIVED and EXPIRED claims are intended to distribute the bonus according to each staker's quadratic time-weighted score:
stake() accepts a deposit while each stored absolute-timestamp moment fits in uint256:
Later, _bonusShare() expands the same nonnegative score and adds two large positive terms before subtracting the cancellation term:
At the test suite's realistic base timestamp entry = 1,750,000,000, with a SURVIVED observation one day later (T = 1,750,086,400):
the first raw stake that overflows T^2 * stake + entry^2 * stake is approximately 1.89039e58;
stake() and _markRiskWindowStart() can still accept total stake up to approximately 3.78097e58;
therefore 2e58 is inside a nonempty accepted-but-unclaimable range.
For a standard OpenZeppelin ERC20 overriding the display precision to 60 decimals, 2e58 raw units are only 0.02 token. The published compatibility section supports standard ERC20s and excludes fee-on-transfer and rebasing tokens, but neither the factory allowlist nor the pool constrains decimals, raw supply, or aggregate eligible stake. The reproduction uses the normal owner allowlist and createPool() path and does not require fee behavior, rebasing, callbacks, registry compromise, or moderator compromise.
The final Math.mulDiv only protects the last userScore * snapshotTotalBonus multiplication. It cannot protect the earlier userPlus and global plus additions.
The global expression overflows for every claimant once the large position is included in the snapshot. Consequently, both the attacker and ordinary stakers revert with panic 0x11. After resolution, withdraw() rejects terminal outcomes and sweepUnclaimedBonus() reserves all unclaimed principal plus bonus. Existing pool clones are non-upgradeable and expose no emergency withdrawal, so the entire pool remains permanently frozen.
The same fault is reachable through two independent resolution paths:
the moderator flags SURVIVED after the registry reaches PRODUCTION; and
a non-staker mechanically finalizes EXPIRED while the registry remains in an active-risk state, requiring no moderator or terminal registry transition.
Likelihood:
The factory owner allowlists a standard ERC20 with sufficient raw supply. This is a conditional token configuration, but it is inside the stated compatibility boundary; a peculiar ERC20 is the CodeHawks severity guide's example of Medium likelihood.
A 60-decimal token needs only 0.02 token for the demonstrated attack, and a 58-decimal token needs approximately 1.9 tokens at the reproduced timestamp.
The pool contains a nonzero bonus. Bonus liquidity is described as economically required for rational participation, and any account can activate the vulnerable branch by contributing one raw unit.
minStake does not close the range: every usable pool with minStake at or below the accepted arithmetic ceiling admits an amount in the overflow interval; a higher minStake makes all stakes revert instead.
Impact:
Every staker permanently loses access to their full principal under SURVIVED and bonus-paying EXPIRED outcomes.
The full sponsor-funded bonus is also frozen. The PoC locks a 2-token bonus with a 0.02-token attack position, demonstrating that affected value can be far larger than the attacker's locked stake.
Re-flagging SURVIVED repeats the same broken snapshot math, PRODUCTION cannot be re-flagged CORRUPTED, withdrawal is closed, and the bonus sweep cannot take reserved liabilities.
The result is permanent denial of service and fund lock rather than direct theft. Impact is High while the token precondition makes likelihood Medium/conditional.
Add the following file as test/unit/OverflowPoc.t.sol and run:
Result:
A matched negative control keeps the same standard 60-decimal OpenZeppelin ERC20,
factory allowlist and createPool() path, two stakers, sponsor bonus, observed risk
window, and SURVIVED/EXPIRED resolution paths. Its only material change is reducing
aggregate stake from 3e58 to 1.8e58 raw units, below both reproduced global
addition thresholds. Both control pools settle successfully and return all
principal and bonus, while both overflow cases revert with panic 0x11 for every
staker. The combined suite passes 4/4 tests.
The unchanged positive PoC was also reproduced from a clean checkout: both targeted
tests passed and the full local suite passed 258 tests, with only two RPC-gated tests
skipped. This isolates raw-unit magnitude as the cause and confirms that the result
does not depend on fee-on-transfer behavior, rebasing, callbacks, or altered transfer
semantics.
Reject deposits that would push aggregate eligible stake above a conservative bound derived from the maximum possible T. Since every effective entry and T are at most expiry, this guarantees that the positive and negative expanded moments fit:
Also validate at pool creation that minStake <= maxEligibleStake, and add boundary tests immediately below and above the cap. A more invasive alternative is to store moments relative to a bounded epoch instead of expanding absolute Unix timestamps.
Affected code: ConfidencePool.sol#L696-L719.
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.