The sponsor is allowed to change the pool expiry until the first stake is accepted. The first successful stake then locks the expiry permanently.
The sponsor and the staker may act independently. For example, the sponsor may submit a legitimate expiry extension without knowing that the first stake is already pending. Currently, stake() accepts only an amount, so the staker cannot require the expiry to remain equal to the value they reviewed.
Both transactions remain valid when they are included in the same block and the expiry update executes first:
The pending stake still succeeds because it checks only the new live expiry. It then locks that new value:
For example, Alice sees a pool that expires in 31 days and submits the first stake. At around the same time, the sponsor independently submits setExpiry(block.timestamp + 365 days). Both transactions are included in the same block, and normal transaction ordering happens to execute the expiry update first. Alice's stake still succeeds and permanently locks the one-year expiry even though she submitted it based on the original 31-day term.
This does not require front-running or malicious transaction ordering. It is a Time-of-check to time-of-use race condition between two valid actions that were prepared using different pool terms.
The result is worse when the Agreement is already UNDER_ATTACK. Alice's stake observes the active risk state and starts the risk window. Withdrawals are then disabled immediately, so Alice cannot exit after noticing the changed expiry. At the original 31-day deadline, claimExpired() also reverts because the pool now expires after one year.
Likelihood:
The first stake must be pending at the same time as a legitimate sponsor expiry update.
Normal block ordering must execute the expiry update before the stake.
Immediate loss of the withdrawal path occurs when the Agreement is already UNDER_ATTACK.
Impact:
The staker accepts a materially different term from the one they reviewed.
The stake can remain locked until the sponsor-selected expiry.
No tokens are stolen, and the principal remains claimable after the new expiry, so the impact is Low.
Add the following test to test/unit/StakeExpirySlippage.t.sol and run:
The test passes and shows that Alice cannot withdraw and cannot claim at the expiry she originally observed.
Let every staker bind the transaction to an acceptable expiry. The simplest option is an exact check:
A maxExpiry parameter can be used instead when the protocol wants to allow small changes:
This keeps the sponsor's existing ability to update the pool before the first stake while allowing the staker to reject an unexpected term.- remove this code
+ add this code
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.