The expiryLocked flag is set inside stake() at line 229 but never inside contributeBonus(). A sponsor can accept bonus contributions while expiryLocked is still false, then extend the deadline via setExpiry() before any staker deposits. The bonus pool becomes locked for longer than the contributor intended.
The expiry parameter defines when staking closes (line 226) and anchors the T upper bound in the k=2 formula for the EXPIRED resolution path (line 569). Once any staker deposits, expiry is locked to prevent the sponsor from moving the goalpost (DESIGN.md §10):
contributeBonus() never touches expiryLocked:
Before any staker deposits, a bonus contributor can send funds. The sponsor sees expiryLocked == false and calls setExpiry() to push the deadline forward — by days, months, or years. The bonus is locked for the extended duration. If the sponsor is malicious or compromised, they can extend expiry up to type(uint32).max, trapping the bonus pool until the new deadline passes. Bonus contributors have no claim rights and cannot withdraw their contribution.
The design intent (DESIGN.md §10) — "protects staker reliance" — is incomplete. It protects stakers but not bonus contributors, even though both deposit funds into the pool.
Likelihood:
A bonus contributor sends funds to the pool before any staker has deposited. The expiryLocked flag is still false.
The pool sponsor calls setExpiry() to extend the deadline after the bonus contribution. This passes all guards — expiryLocked is false and the caller is the owner.
Impact:
The bonus pool is locked for longer than the contributor anticipated. The contributor cannot withdraw their contribution and has no recourse.
Pool resolution is delayed for all participants. If the extension pushes expiry past the moderation grace window, the auto-CORRUPTED backstop in claimExpired() (which requires block.timestamp >= expiry + 180 days) is correspondingly delayed.
The test uses the default pool from BaseConfidencePoolTest. Carol contributes 1000 tokens as bonus before any staker deposits.
Timeline and math:
Carol calls contributeBonus(1000 * ONE). The pool's totalBonus increases to 1000. No staker has deposited yet.
expiryLocked returns false — the lock was never triggered because stake() was never called.
The sponsor calls setExpiry(originalExpiry + 90 days). The call succeeds because expiryLocked is false and the caller is the owner.
pool.expiry() now returns a timestamp 90 days later than originally set.
Carol's 1000-token bonus is locked for an additional 90 days. No staker has deposited, so claimExpired (the only path that pays bonus) cannot be called until the new expiry passes.
Contrast — stake correctly locks expiry: When Alice stakes first, expiryLocked becomes true. The subsequent setExpiry(originalExpiry + 90 days) reverts with ExpiryLocked. This confirms the lock works — but only for stake(), not for contributeBonus().
All three tests pass: expiry is extended after a bonus-only contribution, but correctly locked after any stake.
Set expiryLocked in contributeBonus() identically to how it is set in stake(). Any deposit that creates an economic commitment to the pool's deadline should lock the deadline.
This ensures the first deposit of any kind — stake or bonus — locks the pool's deadline. A sponsor can only set expiry before any funds have been committed.
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.