When a pool expires while the registry remains in a pre-deployment state, claimExpired() finalizes the outcome without locking scope. The owner can subsequently replace the scope even though settlement is final, causing the canonical getters to publish a different coverage commitment from the one that existed when the outcome was resolved.
The pool scope is the public commitment used to determine which BattleChain accounts the pool covers. Although the moderator performs the in-scope breach assessment off-chain, the stored scope is intended to remain the binding audit trail for that assessment and the resulting pool outcome.
Scope normally becomes immutable when _observePoolState() sees a registry state beyond NOT_DEPLOYED or NEW_DEPLOYMENT. However, a pool can reach expiry while the registry is still in either pre-deployment state. In that case, claimExpired() resolves the pool as EXPIRED and sets claimsStarted = true, but _observePoolState() deliberately leaves scopeLocked == false.
setPoolScope() checks only scopeLocked; it does not reject calls after outcome or claimsStarted has become final. The owner can therefore replace _scopeAccounts after settlement and after claims have begun. getScopeAccounts() and isAccountInScope then expose the replacement as the pool's current scope, rather than the scope committed when the outcome was resolved.
Historical ScopeUpdated events allow a careful indexer to reconstruct the former scope, and the rewrite does not change snapshotted balances or claim rights. The defect nevertheless breaks outcome finality for the protocol's canonical scope state and can cause explorers, indexers, auditors, or governance processes that read the current getters to attribute a settled outcome to the wrong covered contracts.
Likelihood:
The pool reaches expiry while its agreement still reports NOT_DEPLOYED or NEW_DEPLOYMENT, so mechanical resolution does not set scopeLocked.
The pool owner submits a later scope update containing accounts that remain valid in the underlying agreement. No registry rewind or unprivileged exploit is required.
Impact:
The latest getScopeAccounts() and isAccountInScope values can describe a different coverage commitment from the one present when the final outcome and accounting snapshots were established.
Historical consumers may misattribute a settled outcome to the wrong contracts. Funds and claim calculations are unchanged, and event reconstruction remains possible, limiting the issue to Low severity.
Create test/audit/CP045ResolvedScopeRewrite.t.sol with the following contents:
Run the PoC from the repository root:
Execute forge test --offline --match-path test/audit/CP045ResolvedScopeRewrite.t.sol -vv.
Confirm that test_ResolvedPoolCanRewriteItsHistoricalScope() passes: the pool is already EXPIRED with claimsStarted == true, yet the owner replaces the original scope and the canonical getters expose only the replacement.
Reject scope replacement after any outcome has been resolved. This preserves the settlement-time scope even when the registry never left pre-deployment staging. Optionally introduce a separate scopeFinalized flag or set scopeLocked during resolution so the public lifecycle state also reflects finality.
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.