The scopeLocked latch is designed to become permanent when the pool first observes the registry in any state outside NOT_DEPLOYED or NEW_DEPLOYMENT, fixing the pool's BattleChain scope after the agreement leaves pre-attack staging. The public maintenance paths that observe this transition write scopeLocked = true inside _observePoolState() but then revert in the same transaction, rolling back the latch. When setPoolScope() calls _observePoolState() in ATTACK_REQUESTED state, the function sets the latch but then immediately reverts with ScopePostLockImmutable(). When pokeRiskWindow() observes ATTACK_REQUESTED, it sets scopeLocked but reverts with RiskWindowNotReached() because no risk window markers exist in that state. If the registry returns to NOT_DEPLOYED via rejectAttackRequest and no successful pool interaction persisted scopeLocked during the ATTACK_REQUESTED interval, the pool owner can call setPoolScope() in NOT_DEPLOYED and replace the scope even though the agreement already moved past pre-attack staging.
Likelihood:
Requires a valid upstream transition to ATTACK_REQUESTED followed by a valid return to NOT_DEPLOYED, such as the documented rejectAttackRequest path
No successful pool interaction persists scopeLocked during the ATTACK_REQUESTED interval because pokeRiskWindow() reverts and rolls back the lock
The decisive scope replacement is owner-only via setPoolScope(), so exploitation requires the sponsor/pool owner to abuse a boundary that should already have closed
The sponsor can pause inflows and passive stakers may not withdraw merely to preserve scope, making the condition plausible but not routine
Impact:
Existing stakers remain in a pool whose scope can be replaced after the agreement crossed the documented lock boundary
A malicious or conflicted sponsor can swap in a broader or riskier subset of the agreement scope, increasing the chance that a later in-scope CORRUPTED outcome causes stakers to lose principal and bonus under coverage they did not originally accept
Indexers and users relying on ScopeLocked as the commitment point can be misled because attempted observations leave no durable on-chain trace
Make pokeRiskWindow() treat a newly-set scopeLocked as useful work and not revert in ATTACK_REQUESTED, even when no risk window markers exist:
Refactor setPoolScope() so that if it observes a post-staging state it persists the lock and returns without replacing scope, rather than reverting after the write:
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.