_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.
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.