Confidence pools are designed to remain resolvable through claimExpired() once their configured expiry is reached, including while the associated agreement remains in an active-risk state such as UNDER_ATTACK.
In this case, the pool should settle as EXPIRED, allowing stakers to recover their principal and applicable bonus.
However, the first interaction that observes an active-risk registry state calls _markRiskWindowStart(), which reconstructs the global time-weighted accumulators using the current aggregate eligible stake and the later risk-window timestamp.
A stake is initially accepted using the deposit timestamp:
Therefore, an amount can satisfy:
when deposited, while later violating:
because riskWindowStart is greater than the original deposit timestamp.
When the agreement remains UNDER_ATTACK until pool expiry, claimExpired() invokes _observePoolState(), which attempts to mark the risk-window start before resolving the pool.
The accumulator calculation then reverts with Solidity arithmetic panic 0x11.
As a result, the pool remains unresolved and the expiry backstop cannot return the affected staker’s principal while the agreement remains in the active-risk state.
This behavior is not one of the intentional active-risk expiry behaviors documented in docs/DESIGN.md. The documented behavior is that expiry remains a mechanical backstop that returns principal and bonus even during active risk.
Likelihood:
This occurs when the pool accepts an ERC-20 stake whose amount fits the squared-time accumulator at the deposit timestamp but no longer fits when the risk window is first observed at a later timestamp.
The agreement must enter UNDER_ATTACK or PROMOTION_REQUESTED after the deposit, and the relevant pool interaction must observe that state at a sufficiently later timestamp.
No fee-on-transfer behavior, rebasing, malicious callback, compromised moderator, or invalid registry transition is required. The proof of concept uses the repository’s standard MockERC20.
The required token quantity is extremely large at current Unix timestamps. Therefore, the practical likelihood is very low.
Impact:
claimExpired() reverts before setting the pool outcome to EXPIRED, disabling the documented expiry-resolution path.
The staker cannot recover their principal while the registry remains in the active-risk state.
Other functions that attempt to observe the same active-risk transition also repeat the overflowing calculation, preventing the risk-window start from being successfully recorded.
Create test/PoC1.t.sol:
Run:
The test passes and the trace shows claimExpired() reverting from _markRiskWindowStart() with arithmetic panic 0x11.
Use timestamps relative to a bounded pool accounting epoch instead of absolute Unix timestamps in the first- and second-moment accumulators.
For example, store the pool creation timestamp:
During initialization:
Use a relative timestamp when adding stake:
Use the same relative timestamp when resetting the accumulators:
As defense in depth, reject stakes that would exceed the accumulator’s maximum capacity at expiry:
Relative-time accounting substantially reduces accumulator magnitudes, while the explicit upper bound ensures that a future risk-window reset cannot overflow.
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.