Root cause: contributeBonus() takes a permissionless, non-refundable donation but, unlike stake(), never sets the one-way expiryLocked latch - so expiry stays sponsor-mutable while the pool already holds bonus funds.
Impact: The sponsor shrinks expiry to the 30-day minimum, forces an EXPIRED resolution before the risk window can open, and sweeps 100% of the donated bonus to the sponsor-controlled recoveryAddress - a total, irreversible loss to the donor.
expiryLocked is a one-way latch that freezes the advertised expiry once funds are committed against it, so the sponsor can't move the deadline out from under participants. The bonus pool rewards stakers who bear risk over that term, and contributeBonus() is permissionless and non-refundable.
The latch is set only in stake() - contributeBonus() never touches it. A bonus deposit therefore leaves expiry fully mutable, and setExpiry() (gated solely on expiryLocked) stays callable, so the sponsor can still shrink the term after the donation arrives.
Likelihood:
A bonus is permissionless and routinely lands before the first stake (the sponsor bootstraps the pool by topping up the bonus, per README) - precisely the window where expiryLocked is still false.
The sponsor alone controls setExpiry and recoveryAddress and needs no counterparty, so the entire pre-first-stake period is a guaranteed shrink-and-sweep window.
Impact:
100% of the donation is transferred to the sponsor-controlled recoveryAddress with no donor reclaim path - a total, irreversible loss.
This exceeds the accepted §5 behavior ("no observable risk → bonus sweeps to recovery"): §5 treats that outcome as circumstantial, but the shrink lets the sponsor force it, killing the pool before riskWindowStart can seal so no staker can ever earn the bonus it was meant to reward.
Save the code to test/unit/Finding_BonusExpiryShrink_PoC.t.sol and run it with the command: forge test --mt test_POC_sponsorRugsThirdPartyBonusViaUnlockedExpiry -vvv
result: [PASS]
Freeze expiry on a bonus contribution just as stake() does - a bonus is a deposit made in reliance on the advertised term and deserves the same protection.
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.