The pool accepts stake while it is still unresolved and later pays SURVIVED or EXPIRED claims using a k=2 time-weighted bonus formula. The vulnerable path is not direct theft, but a liveness and fund-access failure: a very large stake can be accepted at the entry timestamp and then make the later bonus-share calculation overflow for every claimant.
The issue matters because _bonusShare() evaluates absolute-time polynomial terms using the terminal timestamp T. A stake amount that fits the accounting at deposit time can become too large once T increases, causing honest stakers' principal and bonus claims to revert in the normal payout path.
File: src/ConfidencePool.sol
Lines: 222-260
Function/Type: stake(uint256)
File: src/ConfidencePool.sol
Lines: 382-405
Function/Type: claimSurvived()
File: src/ConfidencePool.sol
Lines: 512-607
Function/Type: claimExpired()
File: src/ConfidencePool.sol
Lines: 696-720
Function/Type: _bonusShare(address,uint256)
A staker controls amount in stake(uint256). The function only performs the current entry-time multiplications:
received * newEntry
received * newEntry * newEntry
Those values can fit at the deposit timestamp. The code does not also check whether the resulting total stake can fit the later claim-time formula for any terminal timestamp up to expiry.
When the outcome is SURVIVED or EXPIRED and a nonzero bonus is present, claimSurvived() and the payout branch of claimExpired() call _bonusShare(). That function computes T * T * snapshotTotalStaked + snapshotSumStakeTimeSq and related terms using outcomeFlaggedAt. Since T can be greater than the original deposit timestamp, the global score can overflow even though the original stake was accepted. The overflow occurs before any payout is transferred, so every claimant can be blocked.
The root cause is that stake admission validates only the current timestamp accounting, while bonus payout later multiplies the accepted stake by a larger terminal timestamp without a conservative cap.
Step 1 A large-balance staker deposits an amount that fits received * block.timestamp * block.timestamp.
Step 2 The pool later observes an active-risk state, then the moderator flags SURVIVED at a later terminal timestamp.
Step 3 An honest small staker calls claimSurvived().
Result: _bonusShare() overflows while computing the global score, so the honest staker cannot claim principal or bonus.
Step 1 A large-balance staker deposits before the terminal payout timestamp.
Step 2 The pool reaches expiry and resolves through the EXPIRED payout branch with a nonzero bonus.
Step 3 A staker calls claimExpired(), which reaches the same _bonusShare() calculation used by SURVIVED claims.
Step 4 The global score overflows before the transfer.
Result: The expiry backstop cannot pay the affected staker while the overflowing stake remains in the snapshot.
stake(uint256) accepts the large deposit and records totalEligibleStake, sumStakeTime, and sumStakeTimeSq.
The pool later snapshots snapshotTotalStaked, snapshotTotalBonus, snapshotSumStakeTime, and snapshotSumStakeTimeSq.
claimSurvived() or claimExpired() loads the caller's eligible stake.
The claim path calls _bonusShare(msg.sender, userEligible).
_bonusShare() evaluates T * T * snapshotTotalStaked + snapshotSumStakeTimeSq.
Solidity 0.8 arithmetic overflow reverts the transaction.
No claimant reaches the token transfer in the affected claim path.
This is a Medium severity liveness issue affecting staker withdrawals in normal SURVIVED/EXPIRED resolution. One accepted large participant can block unrelated stakers from claiming principal and bonus. The attacker does not need to steal funds for the impact to matter: the protocol's core promise is that stakers can recover their capital plus any earned bonus after a surviving term.
The attacker cost is the temporary deposit of a very large allowlisted stake token amount. The protocol cost is that all affected claimants can be forced into reverting claim transactions until the implementation is corrected or funds are otherwise recovered.
Framework used: Foundry / forge
File 1: test/poc/ConfidencePoolOverflowPoC.t.sol
The passing PoC shows that the large stake is accepted, the pool reaches a SURVIVED outcome with snapshotTotalBonus() == 1, and both Alice's small claim and Bob's large claim revert with Solidity arithmetic overflow. Alice's revert proves the issue is not limited to the large staker's own accounting; the global bonus score can block unrelated honest stakers.
assertEq(pool.snapshotTotalBonus(), 1) -> proves a dust bonus is enough to enter the overflowing bonus-share path -> PASS
vm.expectRevert(stdError.arithmeticError) before Alice's claimSurvived() -> proves an honest small staker's claim reverts -> PASS
vm.expectRevert(stdError.arithmeticError) before Bob's claimSurvived() -> proves the overflowing snapshot also prevents the large staker from exiting through the same path -> PASS
Reject stakes that can make future bonus accounting overflow at any timestamp up to expiry. The check should run before mutating stake accounting.
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.