contributeBonus:266 is a permissionless, irrevocable deposit entrypoint. README.md names its caller a distinct protocol actor - Actor 5, "Bonus Contributor": "can contribute to the bonus pool permissionlessly via contributeBonus (no claim rights)". There is no withdrawBonus(); contributed funds exit only via staker claims or sweepUnclaimedBonus to recoveryAddress.
Both value-committing entrypoints gate on expiry, but only one latches it:
§10 justifies the latch as protecting reliance "once anyone has deposited against a given deadline", but the implementation reads "once anyone has staked" - narrower than the stated rationale, and excluding Actor 5 entirely. contributeBonus is never mentioned in docs/DESIGN.md, so this is an unconsidered entrypoint rather than an accepted trade-off.
The confiscation. The gap is not limited to re-timing. pause() blocks stake but deliberately not the sweeps, so the sponsor can guarantee no staker ever arrives - and sweepUnclaimedBonus then reserves nothing at all:
Sequence: third party contributes B -> sponsor calls pause() -> no stake is possible -> at expiry, claimExpired resolves EXPIRED with totalEligibleStake == 0 -> sweepUnclaimedBonus computes reserved = 0 and sends the full balance to recoveryAddress
This is outside the sweep's documented purpose. protocol-readme.md describes sweepUnclaimedBonus as recovering "any excess over the remaining stakers' entitlement reserve (k=2 rounding dust, non-claimers' forfeited shares, post-resolution donations)", not an entire advertised bonus. §10's "Sponsor trust surface" likewise enumerates the sponsor's levers for stakers only and never addresses contributors
Not claimed: README Actor 5 states contributors "gain no claim on stake or bonus", so a contributor cannot demand a refund, and with no stakers the bonus reaches recoveryAddress on any honest resolution. The defect is that the sponsor can deterministically force that outcome - converting a third party's gift to stakers into their own revenue - not that the destination is novel
Severity: Low - Low Impact x Low Likelihood.
Likelihood: Low. Reachable only when the bonus contributor is not the sponsor; in the common case the sponsor funds their own bonus, making it self-harm. It is reachable at all because contributeBonus is permissionless and README.md names Actor 5 as a real role. The expiry-move half closes permanently the moment anyone stakes
Impact: Low. A third party's entire contribution is confiscated to the sponsor's recoveryAddress. In the no-stakers case that bonus was destined for recoveryAddress anyway on any honest resolution, so the sponsor gains funds but not value they could not otherwise reach - the defect is the absence of any contributor protection, not a novel extraction. Absent the pause, the milder consequence stands alone: the deadline a committed contributor relied on can be moved anywhere in [block.timestamp + 30 days, type(uint32).max], i.e. out to 2106, with the funds unreachable until resolution
Self-contained against the existing BaseConfidencePoolTest harness, no RPC needed. PASSES:
Lock expiry on the first value commitment from either entrypoint, mirroring stake:229 and matching the rationale §10 already states "once anyone has deposited":
This alone does not stop the pause-and-sweep confiscation. If third-party contribution is intended, the pool also needs either a contributor refund path when a pool resolves with totalEligibleStake == 0, or contributeBonus restricted to onlyOwner, which would make README.md Actor 5 accurate to the code. If it is not intended, restricting the function is the smaller change and the docs should drop Actor 5's permissionless framing
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.