expiry, permanently preventing the sponsor from using setExpiry() with only a temporary minimum stakeInside ConfidencePool.sol, the pool allows the owner (pool creator) to modify the expiry through setExpiry() function until expiryLocked becomes true.
And the only place where this expiryLocked is set to be true is inside stake function:
If we check setExpiry, then:
The docs/DESIGN.md mentions:
expiry— sponsor-mutable only until the first stake (one-wayexpiryLockedlatch). This protects staker reliance: once anyone has deposited against a given deadline (which feeds the k=2 weighting asTfor the EXPIRED path), the sponsor cannot move it
They are aware of the fact that it should be locked right after the first stake, which makes sense. Soon they said, "This protects staker reliance". Yes, I agree. But what if the latch could simply be triggered, and the people whose reliance is supposedly being protected may not even exist yet?
Let's consider the following case:
Pool gets deployed
A malicious staker stakes the minStake amount immediately, triggering expiryLocked = true
Then he withdraws it right away
The expiry lock now outlives the very stake that justified creating it.
The protocol intentionally provides setExpiry() for a reason, because it supports updating the expiry before it becomes immutable. However, due to the current implementation, this configuration window can be reduced to a single temporary minimum stake by an arbitrary third party.
As a result, the pool creator permanently loses the administrative ability to call setExpiry(), even though the very stake that triggered the lock may no longer exist.
There could be several legitimate reasons why a creator would be willing to extend the expiry before the pool configuration becomes final:
operational delay
governance delay
deployment coordination
launch postponement
...
The point is, setExpiry exists for a reason, not to just simply get bricked right after the pool deployment. Additionally, locking up the expiry in a pre-risk state will always go in the stakers' favour, as this will lead to less time for the whitehats to attack, in case the protocol does want to extend the expiry.
Any user can trigger the lock permissionlessly by staking the minimum amount immediately after pool deployment.
The attacker can immediately recover their funds through withdraw() while the pool is still in a pre-risk state, making the attack inexpensive (primarily gas cost)
The sponsor permanently loses the administrative ability to update the pool expiry, even when no stakers remain in the pool, almost for free.
Operational changes that legitimately require extending the expiry before the pool enters its active underwriting period can no longer be performed, forcing the sponsor to continue with an unintended expiry or abandon the existing pool and migrate participants and bonus liquidity to a replacement pool.
Although I didn't feel like it needed one, we can still check it out by adding the following testSetExpiryGettingBrickedWithNoStakersPresent() function inside test/unit/ConfidencePool.t.sol:
Execution demonstrates:
The owner has been completely stripped of the setExpiry capability with a simple temporary minimum stake, even though there are no remaining stakers in the pool.
Like the docs/DESIGN.md mentioned in the same point:
The latch intentionally does not reset when stake is withdrawn — resetting it would let the sponsor move
expiryduring an all-stakers-exited moment and harm the next cohort.
This is obviously true and should be preserved.
The actual issue is that the expiry becomes immutable while the pool is still in a completely risk-free state, allowing an arbitrary third party whose stake can immediately be withdrawn to permanently revoke an administrative capability from the sponsor.
Instead, the expiry should only become immutable once participants become economically committed in a way that cannot be trivially reverted — for example, once the pool enters the active risk window, where withdrawals are already disabled.
Therefore, we can definitely move this code block (the one setting up the expiryLocked to be true) to _markRiskWindowStart as withdrawals are already disabled from that point onward.
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.