ConfidencePool stores stake-time accounting as amount * timestamp and amount * timestamp * timestamp.
There is no maximum stake amount or token supply constraint. A staker can deposit an extremely large,
but still valid standard ERC20 amount that does not overflow at deposit time, then later causes either:
_markRiskWindowStart() to overflow when the active-risk window is first observed; or
_bonusShare() to overflow during post-resolution claims after the risk window was successfully opened.
This makes active-risk observation revert in pokeRiskWindow(), stake(), contributeBonus(), withdraw(),
and claimExpired() while the registry is in UNDER_ATTACK or PROMOTION_REQUESTED. In the claim-time
variant, the pool can resolve as SURVIVED, but honest stakers cannot claim because claimSurvived() reverts.
src/ConfidencePool.sol:252-260
stake() records received * newEntry * newEntry.
src/ConfidencePool.sol:800-815
_markRiskWindowStart() later recomputes global sums as totalEligibleStake * t * t.
src/ConfidencePool.sol:695-718
_bonusShare() recomputes user/global scores as T * T * stake + stakeTimeSq.
src/ConfidencePool.sol:783-797
_observePoolState() calls _markRiskWindowStart() on first active-risk observation.
The amount can be safe for the entry timestamp but unsafe for a later risk-window timestamp.
For current-scale timestamps, the safe maximum is approximately:
Because maxAmount(t) decreases as t increases, an attacker can choose amounts that are safe for
the earlier timestamp but unsafe for a later risk-start or outcome timestamp:
At the repository test base timestamp 1_750_000_000, waiting 31 days leaves a valid attack interval of about:
So the interval is non-empty.
This test was added locally as:
Expected result: stake() succeeds, but the first active-risk observation reverts on arithmetic overflow in
_markRiskWindowStart() when calculating sumStakeTimeSq = totalEligibleStake * t * t.
The same test file also includes a claim-time variant:
Expected result: risk-window start and outcome flagging both succeed, but claimSurvived() reverts when
_bonusShare() calculates T * T * userEligible.
Local verification:
Result:
Regression check:
Result:
While the registry remains in an active-risk state, the pool cannot seal riskWindowStart.
Entrypoints that observe pool state during active risk revert, including pokeRiskWindow(), withdraw(),
stake(), contributeBonus(), and claimExpired() after expiry.
If the active-risk window is opened before the amount becomes unsafe, the pool can still later resolve as
SURVIVED, but claimSurvived() reverts for claimants once _bonusShare() evaluates the larger
outcome timestamp. This blocks distribution of principal and bonus through the normal survivor claim path.
If the registry later reaches a terminal state without a successful active-risk observation, riskWindowStart
stays zero. Under the protocol's documented no-observed-risk rule, stakers recover principal but receive no
bonus; the bonus can be swept to recoveryAddress. This can grief honest stakers' expected bonus distribution.
This depends on a standard ERC20 with a very large supply or denomination being allowlisted by the factory owner.
It does not require a fee-on-transfer, rebasing, callback, or otherwise non-standard token. Because the factory
does not constrain token supply or maximum stake size, the issue is best framed as a low-severity griefing/DoS
finding unless contest rules treat extreme token supplies as out of scope.
Add an upper bound for stake amounts based on the maximum possible expiry timestamp, or compute score terms with
checked Math.mulDiv/bounded arithmetic and reject stake amounts that would make future global score updates overflow.
For example, at stake time:
Using expiry is conservative because _markRiskWindowStart() and _markRiskWindowEnd() cap timestamps at expiry.
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.