ConfidencePool.withdraw() is documented and designed as the staker exit hatch until attack risk actually materializes, but the implementation instead closes withdrawals as soon as the upstream AttackRegistry auto-promotes an unapproved ATTACK_REQUESTED agreement to PRODUCTION after 14 days. Because pool expiry is at least 30 days and can be much longer, a staker can become unable to withdraw, unable to call claimExpired(), and still have riskWindowStart == 0, leaving principal trapped in an unresolved pool until the moderator intervenes or the pool finally expires.
The core mistake is that withdraw() closes on any registry state after ATTACK_REQUESTED, while the live BattleChain integration can move an agreement from ATTACK_REQUESTED to PRODUCTION without ever visiting UNDER_ATTACK. The repo documents the opposite trust model: DESIGN.md says the withdraw escape hatch stays open for the entire pre-attack window and that stakers can freely exit until risk materializes, and README.md says stakers can fully exit while the registry is in any pre-attack state. In production, that assumption is false.
The pool never treats terminal PRODUCTION as “risk materialized.” _observePoolState() only seals riskWindowStart for UNDER_ATTACK or PROMOTION_REQUESTED, while terminal states only touch riskWindowEnd. So when BattleChain reaches PRODUCTION directly, the pool closes withdrawals without ever opening the local risk window the docs say should justify that closure.
The upstream registry path is real, intentional, and sponsor-reachable. requestUnderAttackForUnverifiedContracts() registers the agreement in ATTACK_REQUESTED, _registerAgreement() stores deadlineTimestamp = block.timestamp + PROMOTION_WINDOW, and _getAgreementState() returns PRODUCTION once that deadline passes, before falling back to attackRequested. The same mismatch is even stronger on the verified path because goToProduction() skips the attack phase entirely.
The issue is reachable under the following conditions:
The pool has at least one remaining staker and an expiry materially later than the BattleChain promotion timer.
The agreement owner can register the agreement through BattleChain’s normal ATTACK_REQUESTED path, or use the verified goToProduction() shortcut.
No DAO approval moves the agreement through UNDER_ATTACK before the 14-day timer elapses, or the verified owner skips the timer entirely with goToProduction().
The moderator does not immediately flag SURVIVED before the staker tries to exit.
The exploit path is:
Stakers deposit into a long-dated pool while the agreement is still pre-risk.
The agreement owner registers the agreement into BattleChain’s ATTACK_REQUESTED review flow.
At least one staker remains in the pool after that registration.
BattleChain reaches PRODUCTION without ever entering UNDER_ATTACK, either after the 14-day auto-promotion timer or immediately through goToProduction().
Because the pool only treats UNDER_ATTACK or PROMOTION_REQUESTED as active risk, riskWindowStart is still zero even though the upstream state is now terminal.
The remaining staker calls withdraw() and hits WithdrawsDisabled, but claimSurvived() still reverts until SURVIVED has been flagged and claimExpired() still reverts until the pool’s own expiry`.
The staker’s principal is frozen in an unresolved pool until moderator intervention or expiry, despite no observed active-risk phase ever opening locally.
This is not just documentation drift. The live sink is a sponsor-triggerable rights change: the staker loses the unilateral pre-risk exit path and becomes dependent on moderator responsiveness or eventual expiry, even though the pool never recorded the active-risk phase that is supposed to justify that loss of control.
Likelihood:
Reason 1 // The freeze appears at the end of BattleChain's 14-day promotion window, when an unresolved ATTACK_REQUESTED agreement auto-advances to PRODUCTION without any observed active-risk state.
Reason 2 // The trapping condition persists throughout the remainder of a long-dated pool term, because withdraw() is already closed while both claim paths remain unavailable until moderator action or expiry.
The agreement owner gains an undocumented principal-lock lever over existing stakers. By moving the agreement into BattleChain’s pre-risk review flow and letting it auto-promote to PRODUCTION, they can close withdraw() before any observed active-risk state exists and before the staker can call either claim path. On long-dated pools this freezes principal for months and strips the user of the exact pre-risk exit guarantee they deposited under.
Run command:
Observed output:
Keep withdrawals open whenever riskWindowStart == 0, or explicitly treat BattleChain’s direct ATTACK_REQUESTED -> PRODUCTION and goToProduction() paths as non-freezing pre-risk states until the pool is resolved.
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.