Normal behavior: once the BattleChain registry leaves pre-attack staging (NOT_DEPLOYED / NEW_DEPLOYMENT), the pool scope should become immutable so existing stakers are not exposed to a different scope than the one they deposited against.
I analyzed ConfidencePool.sol and observed that ATTACK_REQUESTED attempts to lock scope in _observePoolState(), but the lock is rolled back by reverting caller paths. In particular, pokeRiskWindow() calls _observePoolState(), which sets scopeLocked = true, but then pokeRiskWindow() reverts because ATTACK_REQUESTED does not set riskWindowStart or riskWindowEnd. If the attack request is later rejected back to NOT_DEPLOYED, the sponsor can replace scope while existing stake remains in the pool.
The issue is that scopeLocked = true is only persisted when the outer transaction succeeds. In ATTACK_REQUESTED, the available no-token observer path reverts after setting the lock, so the lock never persists.
Likelihood:
This occurs when the registry reaches ATTACK_REQUESTED, no successful pool call persists scopeLocked, and the attack request is later rejected back to NOT_DEPLOYED.
pokeRiskWindow() is the permissionless observation function, but during ATTACK_REQUESTED it reverts after _observePoolState() attempts to lock scope.
Impact:
A sponsor can replace the pool scope after stakers have already deposited and after the agreement already left pre-attack staging once.
Existing stakers can later lose principal under a replacement scope they did not deposit against, as shown by the corrupted-sweep PoC.
Create this file:
Run:
Observed output:
The PoC proves that:
Existing stake is deposited under the original scope.
ATTACK_REQUESTED cannot be permissionlessly persisted as a scope lock through pokeRiskWindow().
After rejection back to NOT_DEPLOYED, the sponsor replaces scope.
The existing stake remains in the pool under the replacement scope.
The replacement scope later reaches CORRUPTED, and the existing stake is swept to recoveryAddress.
Treat a newly persisted scopeLocked transition as successful work in pokeRiskWindow().
This lets keepers/stakers persist the one-way scope lock during ATTACK_REQUESTED, even when no risk-window start or end is reached yet. It prevents ATTACK_REQUESTED -> NOT_DEPLOYED rejection from reopening sponsor-controlled scope mutation for already-staked pools.
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.