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.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.