ConfidencePool normally locks its pool-local scope on the first interaction that observes the BattleChain registry outside the pre-attack staging states NOT_DEPLOYED and NEW_DEPLOYMENT. After this lock, the sponsor should no longer be able to replace the scope that existing stakers used when deciding to deposit.
ATTACK_REQUESTED is outside NOT_DEPLOYED / NEW_DEPLOYMENT, and successful pool interactions in that state persist scopeLocked. However, the permissionless pokeRiskWindow() path reverts in ATTACK_REQUESTED after _observePoolState() sets scopeLocked = true, because ATTACK_REQUESTED does not set riskWindowStart or riskWindowEnd. The revert rolls back the scope lock. When the upstream attack request is later rejected back to NOT_DEPLOYED, the sponsor can replace the pool scope even though the agreement already left pre-attack staging once.
Likelihood:
This occurs during an ATTACK_REQUESTED phase where a caller uses the permissionless pokeRiskWindow() observation path instead of a successful stake, withdrawal, bonus contribution, or moderator action.
This occurs after the upstream registry rejects the attack request back to NOT_DEPLOYED, because setPoolScope() then observes a staging state and leaves the rolled-back scopeLocked value as false.
Impact:
Existing stakers remain committed to a pool whose scope can be replaced after the agreement already left pre-attack staging, breaking the documented pool-local scope commitment.
The PoC shows Alice's 100 * 1e18 stake swept to recoveryAddress after the sponsor replaces the scope, the registry later reaches UNDER_ATTACK / CORRUPTED, and the moderator flags bad-faith CORRUPTED.
Fork PoC file:
The fork PoC deploys the pool system locally into a pinned BattleChain testnet fork and uses the live testnet SafeHarborRegistry, demo agreement, agreement owner, and in-scope account at block 16000. It mocks registry lifecycle reads at the live AttackRegistry address to avoid live state mutation. The pinned demo agreement has only one live in-scope account, so the replacement account's isContractInScope() response is mocked only for the replacement-scope branch.
Run:
Result:
Fork PoC code:
This preserves the existing RiskWindowNotReached behavior when no relevant state was sealed, while allowing an ATTACK_REQUESTED observation to persist the scope lock.
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.