A prior large scope makes subsequent setPoolScope calls prohibitively expensive or impossible — the owner cannot update the pool's BattleChain account scope. Scope is informational (not load-bearing for any fund movement), but the denial of a documented owner capability is a permanent impairment of protocol functionality. _replaceScope clears every old-scope account by iterating the entire existing scope array before validating and applying the new scope. Each old entry costs an SSTORE (isAccountInScope[old[i]] = false). The cost scales linearly with the existing scope size with no upper bound.
When the pool owner calls setPoolScope to update the BattleChain accounts the pool monitors, _replaceScope first iterates the entire existing scope array to clear isAccountInScope for every old account, then applies the new scope. This is a valid wholesale-replacement pattern — the owner controls the input scope and is trusted to keep it reasonably sized.
The loop that clears the old scope iterates over _scopeAccounts with no upper bound on its length. Each iteration executes an SSTORE (isAccountInScope[old[i]] = false). If a prior scope contained thousands of accounts, the cumulative gas cost of the clearing loop alone can exceed the block gas limit or become economically prohibitive, permanently preventing any subsequent scope update.
Likelihood:
The owner must have previously set a scope with thousands of accounts for the clearing loop to consume prohibitive gas. Normal scope sizes are expected to be tens to low hundreds of accounts.
The window for scope updates is limited: scopeLocked locks after the registry leaves pre-attack staging (first observation of any other state).
Impact:
Scope is informational only, never read by any fund-moving or bonus-computation logic. Stakers, claims, and sweeps are unaffected.
The owner cannot update the pool's BattleChain account list, impairing a documented owner capability.
The existing scope remains in place and functional; only updates are blocked.
Owner calls setPoolScope with 5,000 accounts during pre-attack staging.
Later, owner calls setPoolScope again with a small updated set.
The _replaceScope function must SSTORE isAccountInScope to false for all 5,000 old accounts before it can write the 10 new ones.
At ~5k gas per non-zero-to-zero SSTORE (net of refunds), the clearing loop alone costs ~25M gas, pushing the transaction close to or beyond the block gas limit.
The call reverts with OOG. The old scope is preserved but cannot be replaced.
Three options, ordered by preference:
Option A (minimal): Remove the old-scope clearing loop. The new scope's accounts are independently set to true in the second loop. Stale accounts from the old scope remain in isAccountInScope as true but do not affect the protocol (scope is not read by any core logic). The _scopeAccounts array is still overwritten by delete + push.
Option B (refund-safe): Move the clear loop AFTER the validation loop so the revert-on-failure atomicity is preserved, and limit per-transaction scope size.
Option C (bound): Keep the current structure but enforce accounts.length <= MAX_SCOPE_SIZE (e.g., 500) in setPoolScope. The owner must batch scope updates within the limit.
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.