Pool scope must lock permanently when an interaction first observes a registry state other than NOT_DEPLOYED or NEW_DEPLOYMENT.
During ATTACK_REQUESTED, _observePoolState() sets scopeLocked = true, but does not set either risk-window marker because this state is neither active-risk nor terminal. pokeRiskWindow() then reverts and rolls back the scope lock:
The reverted write occurs here:
A rejected BattleChain attack request returns the agreement from ATTACK_REQUESTED to NOT_DEPLOYED. Because the agreement owner is also the pool owner, they can pause the pool, initiate this transition, and replace the scope after rejection while existing stake remains deposited.
setPoolScope() cannot persist the lock either: it calls _observePoolState() and then reverts on scopeLocked, undoing the same write.
Medium. Exploitation requires a rejected request and no successful withdrawal during ATTACK_REQUESTED. The owner can pause stake() and contributeBonus(), while pokeRiskWindow() still reverts. A staker can preserve the lock only by monitoring the transition and fully withdrawing.
High. Existing deposits can be reassigned to a different or riskier scope. A later in-scope CORRUPTED resolution can sweep the complete pool balance, including staker principal and bonus.
This test proves the rollback, scope replacement, and loss of Alice's principal. Add it to test/unit/ConfidencePool.scope.t.sol:
Run:
The critical assertion is assertFalse(pool.scopeLocked()): the pool observed a post-staging state but failed to retain its permanent scope lock.
Treat a newly persisted scope lock as a successful observation and avoid reverting after setPoolScope() discovers the lock:
This preserves the one-way scope commitment without treating ATTACK_REQUESTED as the start of the risk window.
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.