Normal behavior:
When a pool is created, the sponsor sets an initial scope (accounts[]) of BattleChain contracts the pool covers. Stakers deposit against that scope. The scope is mutable by the sponsor until the registry leaves NOT_DEPLOYED/NEW_DEPLOYMENT staging, at which point scopeLocked is permanently set to true.
The issue:
stake() locks expiry after the first deposit (via expiryLocked) to protect stakers from the sponsor moving the deadline. No equivalent latch exists for scope. A sponsor can call setPoolScope at any point during staging — including after stakers have deposited — and replace the pool scope with a superset that includes contracts stakers never evaluated or consented to cover. If those contracts are subsequently attacked and the pool is flagged CORRUPTED, stakers lose their full principal.
DESIGN.md §8 states: "stakers' exposure is bounded by what they signed up for at deposit time." This claim is not enforced by code. The accurate invariant the code enforces is: stakers' exposure is bounded by the pool scope at the time scope locks (first non-staging observation) — which can be days after deposit.
Likelihood:
The sponsor (pool owner) must actively call setPoolScope after stakers enter — this requires intentional or negligent action by a trusted party
Stakers must fail to observe the ScopeUpdated event and withdraw before the registry advances to UNDER_ATTACK
Impact:
Stakers lose 100% of their principal to the recoveryAddress via claimCorrupted()
Stakers had no on-chain protection preventing this only an off-chain monitoring requirement
Place this test in test/unit/ alongside the existing scope tests. It uses the same MockAttackRegistry and MockAgreement infrastructure already present in the test suite.
Forge output (confirmed passing):
Mirror the expiryLocked pattern already applied to setExpiry:
This ensures scope is frozen at the same moment expiry is frozen — after the first stake — making the DESIGN.md §8 guarantee ("stakers' exposure is bounded by what they signed up for at deposit time") true in code, not just in documentation.
Alternatively, correct DESIGN.md §8 line 220 to accurately describe the actual invariant: "stakers' exposure is bounded by the pool scope at the time scope locks (first non-staging observation)."
The behavior is intentional — test/unit/ConfidencePool.scope.t.sol:155 explicitly asserts scope stays mutable during staging even after staking.
DESIGN.md §10 lists scope as a sponsor trust surface and tells stakers to verify on-chain state before depositing.
The ScopeUpdated event IS emitted on scope change, giving stakers an observable signal.
The escape hatch (withdraw() during staging) is real but depends on the staker acting before the DAO advances the registry — a race the staker cannot guarantee winning.
The finding is a documentation inconsistency + centralization risk from a trusted party, not an unintended code bug.
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.