_observePoolState() sets scopeLocked when it sees any post-staging registry state, but setPoolScope() and pokeRiskWindow() can revert after that write. Transaction rollback erases the supposedly permanent observation, so a later registry rewind can let the owner replace scope after the agreement already left staging.
The scope is intended to become immutable on the first pool interaction that observes a registry state other than NOT_DEPLOYED or NEW_DEPLOYMENT. This prevents the sponsor from changing the coverage commitment after the attack lifecycle begins.
The lock is written inside _observePoolState(), but two callers deliberately revert in ATTACK_REQUESTED: setPoolScope() sees the newly set lock and throws ScopePostLockImmutable, while pokeRiskWindow() sees that neither risk marker was set and throws RiskWindowNotReached. EVM atomicity rolls scopeLocked and ScopeLocked back with the rest of either call.
The upstream state machine can legitimately reject an attack request and return the agreement from ATTACK_REQUESTED to NOT_DEPLOYED. After a reverted observation fails to persist the lock, that rewind makes setPoolScope() succeed, replacing the pool's original coverage list. A separate successful pool interaction during ATTACK_REQUESTED would preserve the lock, but neither of the explicit observation routes guarantees it.
Likelihood:
The sequence requires the registry to reach ATTACK_REQUESTED, every relevant pool observation to revert, and the attack request then to be rejected back to NOT_DEPLOYED before a successful lock-persisting interaction.
The pool owner must then exercise the reopened scope setter; all replacements still require agreement-scope validation and are visible on-chain.
Impact:
Existing stakers can have the pool-local coverage commitment changed after the agreement has already left staging, violating the documented immutability boundary.
The rewind happens before active risk, limiting immediate fund exposure, but the changed scope can later affect the moderator's breach-in-scope judgment and therefore pool resolution.
Create test/audit/CP006RevertingObserverScopeLock.t.sol with the following contents:
Run the PoC from the repository root:
Execute forge test --offline --match-path test/audit/CP006RevertingObserverScopeLock.t.sol -vv.
Confirm that both tests pass: the rejected setter and rejected poke each leave scopeLocked == false, and a registry rewind lets the replacement scope succeed.
A transaction cannot both persist a new lock and revert. Let observation-only state changes complete successfully, while clearly signaling that a requested scope update was not applied. Likewise, do not revert pokeRiskWindow() when it successfully sealed scope even though no risk marker changed.
An alternative is a dedicated permissionless sealPoolScope() call that never reverts after a qualifying post-staging observation, combined with explicit documentation that reverted calls do not count as observations.
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.