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:
Impact - Low Nothing moves when the scope is swapped, and the state afterward is pre-attack, so any staker who notices can withdraw in full. _replaceScope emits ScopeUpdated, which puts the change on-chain where it can be seen. Real loss needs a restarted attack cycle that breaches one of the substituted accounts with the original staker still in the pool, at which point their principal funds a CORRUPTED payout for coverage they never chose. Likelihood - Medium The step the sponsor cannot force is the moderator rejecting the attack request, though rejections are an ordinary registry operation. The rest they can arrange. Neither observation path can seal the lock in this state, and pausing the pool shuts the deposit routes, leaving only a full-exit withdraw to do it. So reaching the mutable-scope condition is not hard; what holds the severity down is how much has to go wrong afterward for anyone to actually lose money.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.