When a Confidence Pool is created (or its expiry is later updated before the first stake), expiry is only validated against a lower bound but there is no upper bound.
As a result, the pool owner can set expiry anywhere up to type(uint32).max = 4294967295 = 2106-02-07T06:28:15Z, i.e. roughly 80 years from the time of creation. The term is not literally infinite, but for any practical purpose the pool is unexpirable.
This matters because expiry is the pool's terminal backstop. Once risk has materialised, withdraw() is permanently disabled and the only unconditional exit is claimExpired(), which requires block.timestamp >= expiry. If the underlying agreement never reaches a terminal registry state, staker principal has no on-chain exit until expiry, which, with no ceiling, can be pushed ~80 years out.
expiry is validated only against a lower bound (_MIN_EXPIRY_LEAD = 30 days) and a uint32 storage-fit check, there is no upper bound. Both setters accept any value up to type(uint32).max (2106-02-07, ~80 years out):
The factory checks only the floor as well, so pools minted via createPool() share the gap (ConfidencePoolFactory.sol:78).
expiry is the staker's terminal backstop, so an unreasonable value has teeth. Once risk materialises, withdraw() is permanently latched off (riskWindowStart != 0), and the only unconditional exit is claimExpired(), gated on block.timestamp >= expiry.
The early-exit paths (claimSurvived/claimCorrupted) need a terminal outcome, which before expiry only flagOutcome() can set and flagOutcome() reverts unless the registry is already PRODUCTION/CORRUPTED. So if the agreement never terminally resolves, claimExpired() at expiry is the sole remaining exit and with no ceiling, that can be ~2106.
Likelihood:
The likelihood is low because a typical pool creator is unlikely to configure it this way, although it is still a possible scenario.
Impact:
The impact is high because the funds can become permanently locked. Staker principal can be locked with no on-chain exit until an arbitrarily distant expiry (up to ~80 years), whenever risk latches (withdraw() disabled) and the agreement never terminally resolves.
The previous explanation clearly demonstrates the problem.
Introduce an explicit maximum term and enforce it everywhere expiry is set, mirroring the existing _MIN_EXPIRY_LEAD floor with a _MAX_EXPIRY_LEAD ceiling (both expressed as a duration relative to block.timestamp, so the cap is a bounded lead rather than an absolute timestamp).
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.