contributeBonus() is a permissionless, non-refundable entrypoint with no per-contributor accounting. When a pool resolves with riskWindowStart == 0 ("no observable risk"), _bonusShare pays every staker zero and sweepUnclaimedBonus() ships the entire bonus pot, to the sponsor controlled recoveryAddress.
The design docs justify "no observable risk → no bonus" only from the staker's perspective (stakers had a free pre-risk exit, so forfeiting the risk premium is fair). They never address the bonus contributor, who has no exit, no refund, and no accounting.
Worse, a sponsor who also controls the agreement can make this outcome deterministic and undefendable by driving the agreement NEW_DEPLOYMENT → PRODUCTION via the registry's supported attack-skip path (goToProduction()), no active risk state is ever entered, so riskWindowStart can never be sealed and no one can pokeRiskWindow() to defend the bonus. The sponsor then sweeps the third party contributions to their own wallet.
contributeBonus records no per-contributor state and offers no way out:
At resolution, _bonusShare returns zero for every staker whenever the risk window never opened:
And sweepUnclaimedBonus reserves only principal in that case, the whole bonus becomes sweepable to recoveryAddress:
recoveryAddress is sponsor controlled and mutable, so the sponsor can set it to their own wallet.
riskWindowStart only seals when a pool interaction (or pokeRiskWindow()) observes the registry in an active-risk state (UNDER_ATTACK / PROMOTION_REQUESTED), see _observePoolState / _markRiskWindowStart. BattleChain's "Skip Attack Mode" flow drives an agreement straight to PRODUCTION:
The agreement owner calls goToProduction(agreement), taking the registry NOT_DEPLOYED/NEW_DEPLOYMENT → PRODUCTION with no active risk interval.
Because no active risk state is ever reachable, pokeRiskWindow() can never seal riskWindowStart (it would only ever observe a terminal state and seal riskWindowEnd). The mitigation relies on for other timing games, "any counterparty can pokeRiskWindow() the instant the registry transitions", is structurally unavailable here.
Likelihood:
Occurs on every pool built around an agreement that uses BattleChain's supported "Skip Attack Mode" path (goToProduction()), leaving riskWindowStart == 0.
Needs no privileged action, contributeBonus is permissionless, recoveryAddress is sponsor controlled by default, and pokeRiskWindow() cannot defend it since no active risk state ever exist
Impact:
Third party permanently loses 100% of contributed bonus, swept in full to the sponsor's recoveryAddress with no refund path.
Reward mechanism is structurally dead for attack-skipping agreements, every staker earns zero bonus.
The PoC uses the repo's own BaseConfidencePoolTest harness. It stakes principal (alice), has a third party (carol) contribute bonus, drives the mock registry straight to PRODUCTION, resolves SURVIVED, and shows the entire bonus is swept to recoveryAddress while stakers receive zero bonus.
Save as test/poc/ThirdPartyBonusCapture.poc.t.sol and run:
The design already protects stakers symmetrically, they get principal back when no risk materialized (they had a free exit). Extend the same fairness to bonus contributors. When the pool resolves with riskWindowStart == 0, return each contributor's funds instead of sweeping them to the sponsor. That requires per-contributor accounting, which contributeBonus() currently lacks.
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.