ConfidencePool accepts deposits whose timestamp-weighted accumulator terms each fit in uint256, but later adds two such terms in _bonusShare(). That addition can
overflow and make every claim revert with Solidity panic 0x11.
The factory allowlist approves only token addresses; it has no aggregate-stake or precision limit. An ERC20-compatible token implementing decimals() = 60 is sufficient. Once such a token is allowlisted, any token holder can trigger the lock without privileged access.
stake() and _markRiskWindowStart() store individual weighted terms:
At claim time, _bonusShare() reconstructs the global k=2 score:
Let S be total stake, s be riskWindowStart, and T be the terminal timestamp. The attacker selects:
Then both persisted terms remain valid:
but the later addition does not:
Thus deposits and outcome resolution succeed; the claim path alone panics.
For a pool with an observed risk window and a SURVIVED or EXPIRED outcome:
every staker's claim reverts with panic 0x11;
principal and bonus remain locked in the pool; and
sweepUnclaimedBonus() cannot release them, because it reserves the unclaimed principal and bonus and reverts NothingToSweep.
At timestamp 1,750,000,000, the required stake is approximately 1.89048e58 base
units. With a 60-decimal ERC20 this equals approximately 0.0189 tokens.
The factory owner must first allowlist the token. However, the pool advertises support for standard ERC20 tokens and does not enforce a decimals or accounting-capacity
bound. After allowlisting, exploitation is permissionless and requires no moderator, owner, or registry action.
The executable PoC below uses the repository's existing OpenZeppelin MockERC20, which has the default 18 decimals, and mints the required raw-unit balance to isolate
the arithmetic invariant. Token decimals do not affect the vulnerable calculation because ConfidencePool operates exclusively on raw ERC20 units and never calls decimals(). An otherwise standard ERC20 overriding decimals() to 60 makes the same raw-unit threshold economically equal to approximately 0.0189048 whole tokens.
Run:
The test passes and confirms both SURVIVED claims revert with panic 0x11, while the full principal-plus-bonus balance remains in the pool. The EXPIRED path reaches the same failure because claimExpired() invokes the same _bonusShare() calculation at ConfidencePool.sol:591; the executable PoC intentionally demonstrates the SURVIVED branch only.
Enforce a total-stake capacity before changing any timestamp-weighted accumulator. Derive the bound from the maximum permitted timestamp (uint32), not the current timestamp:
This bounds T² × snapshotTotalStaked + snapshotSumStakeTimeSq for every valid pool timestamp. Restricting approved token precision may be a useful operational safeguard,
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.