setPoolScope() (L636-641) calls _observePoolState() before checking the scopeLocked guard:
_observePoolState() (L784-799) sets scopeLocked = true as a side effect the first time it observes the registry past pre-attack staging (NOT_DEPLOYED / NEW_DEPLOYMENT). Because the guard is checked after this side effect, the Sponsor's own call can be the interaction that seals the lock, then revert against the lock it just set — in the same transaction.
DESIGN.md §8 documents that "scope locks permanently on the first interaction observing any other state" as intentional, but does not address this specific consequence: the Sponsor's dedicated update entrypoint can never succeed if its own call is the first post-staging observation.
Impact: the Sponsor loses the ability to make any successful scope update once the registry leaves pre-attack staging, if their own setPoolScope() call is the first post-staging interaction with the pool.
Likelihood reasons:
Triggers on the first setPoolScope() call made after the registry leaves pre-attack staging — an ordinary Sponsor action, not an edge case.
Deterministic: no attacker, no race condition required.
Masked in the common case where scope was already locked by a prior staker interaction, which is likely why it went unnoticed — but nothing guarantees that happens first.
Impact points:
Permanently forecloses the Sponsor's ability to update scope during the window the design intends it to still be open.
Self-inflicted by the protocol's own code path — the Sponsor cannot avoid it if their first post-staging interaction is a scope update.
Low severity: no fund loss, no third-party impact; general scope-locking behavior otherwise works as intended.
Run: forge test --match-test test_L1_SetPoolScopeSelfLocking -vvv
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.