expiry is meant to become immutable the moment value is committed against it. stake() enforces this by latching the one-way expiryLocked flag on the first deposit, after which setExpiry() reverts. DESIGN §10 documents the guarantee: "once anyone has deposited against a given deadline (which feeds the k=2 weighting as T for the EXPIRED path), the sponsor cannot move it." The deadline is load-bearing: it is the EXPIRED-path T, and it gates the only path (claimExpired, hard-gated on block.timestamp >= expiry) that ever releases a bonus contributor's funds.
contributeBonus() is the other value-committing deposit entrypoint — same outcome == UNRESOLVED, block.timestamp < expiry, and _assertDepositsAllowed gating as stake() — but it never sets expiryLocked. So a pool that receives a bonus before its first stake leaves expiry fully owner-mutable even though real capital is already committed against that deadline. The owner can then call setExpiry() to push the deadline out arbitrarily (each call only needs newExpiry >= now + 30 days, with no upper bound below the 2106 uint32 ceiling), indefinitely deferring claimExpired — and the contributed bonus can be neither withdrawn (withdraw() only returns eligibleStake, never totalBonus) nor otherwise reclaimed by the contributor.
Fires whenever contributeBonus is called on a pool that has no stakes yet — a stakerless or newly-created pool bootstrapping its bonus pot before stakers arrive, which is an ordinary lifecycle.
Requires no special state: the expiryLocked flag is simply never written on the bonus path, so any later setExpiry by the owner succeeds until the first stake eventually latches it (which may never happen for a bonus-only pool).
The contributed bonus is bound to a deadline the contributor never consented to: the owner can extend expiry indefinitely (deferring claimExpired, the contributor's only release path) or shorten it, changing the effective lock-up term of committed capital after the fact.
The deadline-immutability guarantee DESIGN §10 states ("once anyone has deposited … the sponsor cannot move it") is broken for the bonus-first ordering, because a bonus contribution is a deposit yet does not freeze expiry.
Add to a test inheriting the project's BaseConfidencePoolTest and run with forge test. Passes green:
Latch expiryLocked on the first funded deposit of either kind, mirroring stake(), so the deadline freezes as soon as any capital (stake or bonus) is committed against it.
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.