stake() admits raw token amounts using only deposit-time products, although later lifecycle and claim paths multiply the same stake by larger timestamps and materialize wider quadratic intermediates.
The pool distributes bonus according to the k=2 score amount * (T - entryTime)^2. To avoid storing every deposit, it records the first and second absolute-time moments amount * entryTime and amount * entryTime^2, then reconstructs each score at resolution as:
stake() accepts a deposit whenever its moments at the current entryTime fit in uint256. It does not bound the user's or pool's aggregate raw stake against the latest possible lifecycle timestamp, even though every relevant timestamp is bounded by expiry.
This creates two related latent overflow states:
A pre-risk stake can fit at its entry timestamp but overflow the eager totalEligibleStake * riskWindowStart^2 reset after time advances. Every active-risk observation then reverts. Because claimExpired() observes the live registry before resolving, it also reverts at maturity while the agreement remains active-risk.
A post-risk stake can fit all stored moments but overflow the expanded positive claim term T^2 * snapshotTotalStaked + snapshotSumStakeTimeSq. The true score after cancellation can fit comfortably, but Solidity reverts before reaching the subtraction. This global term is evaluated by every claimant, so one large stake blocks even an honest user's small claim.
The claim-time branch is reached whenever the pool has a nonzero observed risk window and a nonzero bonus. Claims revert before principal is transferred, and sweepUnclaimedBonus() treats the same unclaimable principal and bonus as reserved. A resolved pool therefore has no remaining recovery path.
For a deposit at time e and resolution at T > e, the first claim-overflowing amount can be selected as:
Then amount * e^2 fits during stake(), while the later positive term amount * (e^2 + T^2) overflows. The mathematically correct result amount * (T - e)^2 can still be far below type(uint256).max.
At the test suite's timestamp e = 1,750,000,000 and T = e + 1 day, the exact threshold is approximately 1.8904e58 raw units. This is infeasible for a conventional 18-decimal asset, but it is only about 0.019 nominal tokens for a standard ERC-20 with 60 decimals. Neither ERC-20 nor ConfidencePoolFactory's boolean token allowlist constrains decimals, raw supply, or the arithmetic domain accepted by a pool.
Likelihood:
The factory owner must allowlist a token whose raw supply and attacker balance can reach approximately 1e58 units. The owner-controlled allowlist materially reduces exposure, and conventional 18-decimal assets do not approach the threshold.
Permanent pool-wide claim lock additionally requires a nonzero bonus, an observed active-risk window, and a SURVIVED or EXPIRED resolution. All three are ordinary protocol states once a qualifying asset is admitted.
Impact:
One unprivileged stake permanently blocks every remaining SURVIVED or EXPIRED principal-and-bonus claim, including claims from ordinary-sized honest stakers, because all users evaluate the same overflowing global score.
sweepUnclaimedBonus() cannot recover the funds because it correctly reserves all outstanding principal and bonus liabilities. The pool clone has no alternate withdrawal or administrative repair path after resolution.
A different accepted amount can block every active-risk observation and claimExpired() while the registry remains active-risk. A later terminal transition can restore principal access, but the missed risk-window witness causes the full bonus to become sweepable instead of payable to stakers.
The attacker must also lock their own stake, so the primary incentive is griefing rather than direct profit.
Create test/audit/CP002UnboundedStakeOverflow.t.sol with the following contents:
Run the PoC from the repository root:
Execute forge test --offline --match-path test/audit/CP002UnboundedStakeOverflow.t.sol -vv.
Confirm that both tests pass and the suite reports two tests passed with zero failures.
Enforce one aggregate raw-stake invariant against the maximum possible timestamp before recording a measured deposit. Since every entry, risk-window marker, and resolution timestamp is at most expiry, the conservative bound below keeps the eager reset, both positive additions, and both doubled subtraction terms within uint256:
The subtraction-form capacity check avoids overflowing while testing the post-deposit aggregate. Apply the same bound to minStake during initialization, document it as an allowlist requirement, and add exact-boundary plus multi-staker aggregate tests for risk-window observation and both SURVIVED and EXPIRED claims.
If the protocol must support larger raw-unit domains, replace the absolute-timestamp expansion with a proven full-precision implementation that performs the positive terms, doubled terms, and cancellation above 256 bits. Moving only the final bonus multiplication into Math.mulDiv is insufficient because the overflow occurs while constructing userPlus, userMinus, plus, or minus first.
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.