pokeRiskWindow() rolls back the permanent scope lock in ATTACK_REQUESTEDSuggested form selections:
Impact: Medium
Likelihood: Low
Resulting severity: Low
Affected file: src/ConfidencePool.sol
A pool's account scope is supposed to become a permanent commitment as soon as the registry leaves NOT_DEPLOYED or NEW_DEPLOYMENT. This bounds what an existing staker has agreed to underwrite.
pokeRiskWindow() does not persist that lock when the first observed post-staging state is ATTACK_REQUESTED:
_observePoolState() correctly sets scopeLocked = true for ATTACK_REQUESTED, since that state is neither NOT_DEPLOYED nor NEW_DEPLOYMENT:
However, ATTACK_REQUESTED is not an active-risk or terminal state, so both risk-window timestamps remain zero. pokeRiskWindow() then reverts. EVM atomicity rolls back both scopeLocked = true and the ScopeLocked event.
This matters because the real AttackRegistry.rejectAttackRequest() explicitly permits the registry to move from ATTACK_REQUESTED back to NOT_DEPLOYED. After that valid rewind, setPoolScope() sees a staging state and succeeds because the attempted lock was erased.
An existing staker can therefore remain deposited while the sponsor replaces the account subset the staker originally underwrote. If a newly added account is corrupted in a later attack cycle, the moderator can correctly flag the pool CORRUPTED under the replacement scope and the staker's pre-existing principal is swept. Under the original scope, that same breach could have been out of scope and resolved SURVIVED.
This contradicts the documented guarantee that scope locks permanently once the registry leaves pre-attack staging. It is not a malicious-registry scenario: ATTACK_REQUESTED -> NOT_DEPLOYED is the normal DAO rejection path in the in-repository dependency.
Relevant source:
Likelihood:
Stake must exist before the request.
No successful stake, bonus contribution, or withdrawal may persist the lock while the request is pending.
The DAO must reject the request, returning the registry to NOT_DEPLOYED.
The sponsor must replace the pool scope before a later attack cycle.
A sponsor can pause stake and bonus entrypoints during the request. The only remaining staker action that persists the lock is withdraw(), which requires the staker to exit the pool entirely. The intended free keeper hook is the path that fails.
These are specific but protocol-supported conditions, so likelihood is low rather than impossible.
Impact:
The pool loses its promised permanent exposure bound.
Existing capital can be made to underwrite a different contract subset without a new deposit or explicit staker consent.
The proof of concept demonstrates the entire pre-existing principal entering a later CORRUPTED sweep under the replacement scope.
Because the fund loss is indirect and stakers retain a withdrawal window if they detect the lifecycle change, Medium impact / Low likelihood (Low severity) is the conservative classification.
Add test/audit/ScopeLockRollback.t.sol with the following test and run:
Observed result:
The trace shows ScopeLocked emitted inside pokeRiskWindow() immediately before RiskWindowNotReached; the transaction rollback leaves scopeLocked() false. The later scope replacement succeeds while Alice's 100-token stake remains, and claimCorrupted() transfers all 100 tokens to the recovery address.
The test mock only stages the exact enum transitions needed for isolation. The backward transition itself is implemented by the real, in-repository AttackRegistry.rejectAttackRequest() linked above; the vulnerable state rollback occurs in the in-scope ConfidencePool.
Treat a newly persisted scope lock as useful work in pokeRiskWindow(), even when neither risk-window timestamp changed:
Add a regression test that moves the registry to ATTACK_REQUESTED, calls pokeRiskWindow() successfully, rewinds the registry to NOT_DEPLOYED, and confirms setPoolScope() still reverts with ScopePostLockImmutable.
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.