ConfidencePool gates scope changes on a one-way scopeLocked latch. The latch has exactly one write site - it is only ever set to true, and no code path ever clears it - inside _observePoolState, on the first interaction that observes the registry in any state other than NOT_DEPLOYED / NEW_DEPLOYMENT:
setPoolScope is blocked by that latch:
The pool's documented contract to the sponsor is explicit that scope is mutable exactly while the registry is in a pre-attack state (docs/DESIGN.md §8):
"The sponsor can update scope freely while the registry is in
NOT_DEPLOYED/NEW_DEPLOYMENT(pre-attack staging). Scope locks permanently on the first interaction observing any other state."
The bug is a state-machine modeling defect: the pool treats "past pre-attack staging" as a monotonic, forward-only condition and therefore latches irreversibly. But the external BattleChain AttackRegistry - which the pool explicitly reads as its source of truth — is not forward-only. Its registry-moderator can move an agreement backward to a fully fresh, unregistered state via rejectAttackRequest, which deletes the agreement record and returns it to NOT_DEPLOYED:
rejectAttackRequest is a legitimate, expected, non-adversarial registry operation - the standard "the DAO reviewed this attack request and declined it" path. It carries no malicious intent and is not an error. Yet its normal use, combined with any prior pool interaction that observed ATTACK_REQUESTED (staking, a pokeRiskWindow, or any other call - an entirely ordinary occurrence), leaves the pool permanently inconsistent: the registry reports the agreement as NOT_DEPLOYED — the exact state in which the documentation guarantees scope is editable - while scopeLocked stays true forever, so every setPoolScope call reverts ScopePostLockImmutable.
The affected party is the pool sponsor, who is a legitimate protocol participant. The sponsor is entitled, by the protocol's own documented state rules, to reconfigure scope while the agreement sits in pre-attack staging (e.g. to realign the pool with a re-requested attack whose BattleChain scope was adjusted after the rejection). The protocol advertises that capability as a function of the current on-chain state, and the on-chain state grants it - but the code denies it. A documented protocol function is thus rendered permanently inoperable under a legitimate flow, and there is no on-chain remedy: no function can clear scopeLocked, so the condition is irreversible for the life of that pool.
A sponsor creates a pool against an agreement and commits to scope {ContractA}. Stakers deposit.
The sponsor calls requestUnderAttack; the registry enters ATTACK_REQUESTED. An ordinary pool interaction (a stake, a pokeRiskWindow, or any observing call) latches scopeLocked = true.
The DAO (registry moderator) reviews the request and legitimately declines it via rejectAttackRequest, which deletes the agreement record and returns the registry to NOT_DEPLOYED.
The registry is now in NOT_DEPLOYED — the state in which docs/DESIGN.md §8 guarantees the sponsor can update scope. The sponsor calls setPoolScope to adjust coverage.
The call reverts ScopePostLockImmutable, because scopeLocked was never cleared. The sponsor cannot reconfigure the pool for its now-fresh agreement, and no on-chain action can restore the capability; the pool is permanently stuck with its pre-reset scope.
A documented protocol function (setPoolScope, mutable while pre-attack per §8) is permanently disabled for the affected pool, under a legitimate, non-adversarial flow (a normal pool observation plus a routine DAO rejection). No admin error and no malice are involved.
The pool sponsor - a legitimate protocol participant - is denied a capability the current on-chain registry state explicitly grants, with no on-chain recovery (the latch is irreversible).
Pool coverage can permanently diverge from the underlying agreement after a reject-and-re-register cycle, leaving the pool committed to a scope the sponsor can no longer bring back into alignment.
Create a test/fork/ScopeLockLatchIgnoresRegistryReset.fork.t.sol file
Put the code below in the file
Run BATTLECHAIN_TESTNET_RPC=https://testnet.battlechain.com forge test --match-test ScopeLatchStaysLockedAfterRegistryResetBlockingSetPoolScopeForever -vv in the terminal
Make the latch bidirectional: have _observePoolState clear scopeLocked when it observes a return to NOT_DEPLOYED / NEW_DEPLOYMENT, keeping it in lock-step with the registry's actual (non-monotonic) state machine.
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.