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.
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.