Under normal behavior, _replaceScope() validates each account by calling IAgreement(agreement).isContractInScope(account) and reverts if the agreement rejects the address. This correctly delegates scope validation to the upstream agreement contract. The pool itself adds no local address(0) guard, relying entirely on the external agreement to reject the zero address.
The specific problem is that if the upstream IAgreement contract is misconfigured, has a default-true fallback, or is later upgraded by the DAO to return true for uninitialized addresses, address(0) will be accepted without any local sanitisation and stored permanently in _scopeAccounts.
Once scopeLocked is set (triggered by the first interaction that observes the registry past NEW_DEPLOYMENT), setPoolScope permanently reverts with ScopePostLockImmutable. Before that transition window closes, the sponsor can call setPoolScope to repair the scope. After it closes, the corrupted state is irreversible.
Likelihood:
A sponsor's agreement contract defaults to true for isContractInScope on uninitialized mappings (a common Solidity default), or the DAO later upgrades the agreement in a way that makes isContractInScope(address(0)) return true.
Because _replaceScope is also called from initialize (at pool creation), the error can occur at deployment before the sponsor has any opportunity to inspect the resulting scope state.
Impact:
address(0) is permanently stored in _scopeAccounts and isAccountInScope[address(0)] = true. This is an anomalous state not intended by the protocol's design invariants.
Off-chain tooling and integrations reading getScopeAccounts() or isAccountInScope(address(0)) will receive misleading data, potentially affecting moderator decisions about whether a breach targeted an in-scope contract.
If any future upgrade to the protocol adds scope-membership checks to financial logic paths, the pre-existing address(0) entry would become an exploitable foothold.
Note: in the current codebase, isAccountInScope and _scopeAccounts are not read by any financial operation (stake, withdraw, flagOutcome, or claims), so there is no direct fund-loss risk today.
Steps to Reproduce:
Create test/ZeroAddressScope.t.sol and paste the code below.
Run: forge test --match-test test_QA_ZeroAddressInScope -vvv
The test passes, confirming address(0) is permanently stored in the pool's scope with no revert.
Add a single local zero-address guard at the top of the per-account loop in _replaceScope. This protects the pool's state independently of whatever the upstream agreement returns:
ZeroAddress() is already defined in IConfidencePool.sol (line 60) and used in initialize(), making this a one-line addition consistent with existing validation patterns across the contract.
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.