The pool owner is permitted to modify expiry until the first stake is deposited. Once the first stake succeeds, expiryLocked is permanently set to true.
This mechanism is intended to ensure that a staker deposits against a fixed and immutable deadline. However, stake() does not let the caller specify the expiry they agreed to, and it does not protect against an expiry change between transaction signing and execution.
The mutable expiry is only locked inside stake():
Before the first stake, the owner can set any expiry that is at least 30 days in the future and no greater than type(uint32).max:
There is no upper-bound duration other than the uint32 timestamp ceiling, approximately February 2106.
The following attack is therefore possible:
A pool advertises a reasonable expiry, such as 31 days.
The agreement is in UNDER_ATTACK, where staking is deliberately allowed.
Alice submits the first stake() transaction after verifying the advertised expiry.
The pool owner observes Alice's pending transaction.
The owner front-runs it with:
Alice's transaction executes successfully because the new expiry is still in the future.
Alice's deposit sets expiryLocked = true, permanently locking the malicious expiry.
Because the stake observed UNDER_ATTACK, riskWindowStart is set and Alice cannot withdraw.
Alice also cannot use claimExpired() until approximately 2106.
The transaction does not revert when the expiry changes. Alice is silently committed to an underwriting period that may be decades longer than the one she approved.
This violates the intended staker reliance guarantee. The documentation states that the sponsor controls expiry only until the first stake and that the lock protects users who deposit against a particular deadline. An off-chain expiry check is insufficient because it is not atomic with transaction execution. :contentReference[oaicite:0]{index=0}
The malicious owner can effectively freeze the first staker's complete principal for approximately 80 years.
Once the transaction executes in UNDER_ATTACK:
withdraw() is permanently disabled;
the expiry is permanently locked at the malicious value;
claimExpired() cannot be used until the modified expiry;
the staker cannot cancel or reverse the successful deposit;
the moderator cannot resolve the pool while the registry remains in a nonterminal active-risk state.
The pool sponsor does not need to steal tokens directly. Extending the deadline immediately before the first stake is enough to create an effectively permanent denial of access to the victim's funds.
The impact is High because the victim's entire principal can become inaccessible for decades.
The likelihood is Medium because exploitation requires:
targeting the first staker;
observing and ordering a transaction before the victim's pending stake;
staking while the agreement is already in UNDER_ATTACK, or transitioning to that state before the victim can withdraw.
Add the following test to a contract inheriting BaseConfidencePoolTest:
Allow the staker to bind their transaction to the pool parameters they reviewed.
The simplest mitigation is to require an expected expiry:
A more complete interface should also include a transaction deadline and an explicit active-risk preference:
The function should revert when:
An alternative is to permanently lock expiry before accepting public deposits, through a separate owner finalization action:
Staking would only become available after configuration has been finalized.
The critical requirement is that a pending stake must revert when the expiry differs from the value the staker authorized.
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.