The protocol intentionally supports two materially different staking conditions:
In a pre-risk state, the staker can deposit and later recover their principal through withdraw().
In UNDER_ATTACK, the staker can deposit, but the deposit is immediately committed to settlement and cannot be withdrawn.
A staker should only enter the second condition when they explicitly agree to accept that additional risk.
However, stake() does not let the caller bind the transaction to the registry state they observed when signing it. The function accepts the transaction even if the registry changes from a withdrawable state to UNDER_ATTACK before execution.
The transaction first observes the current registry state:
_assertDepositsAllowed() deliberately does not reject UNDER_ATTACK:
When the state is UNDER_ATTACK, _observePoolState() permanently opens the pool's risk window:
The deposit is then credited normally. Once it has succeeded, withdraw() checks the persistent risk-window latch and rejects the user even if the registry later returns to a pre-risk state:
This creates the following transaction-ordering attack:
Alice reads the registry in NEW_DEPLOYMENT or ATTACK_REQUESTED.
Alice submits stake(), expecting that the withdrawal escape hatch remains available.
The normal registry transaction that changes the state to UNDER_ATTACK is placed before Alice's transaction.
Alice's transaction does not revert because staking during UNDER_ATTACK is allowed.
The transaction opens riskWindowStart and transfers Alice's tokens into the pool.
Alice can no longer withdraw.
If the agreement is later marked CORRUPTED, Alice's entire principal can be paid to the attacker or swept to recoveryAddress.
This can occur through ordinary transaction ordering around the public transition to UNDER_ATTACK. The state-changing transaction does not need to be malicious. A sequencer or block builder only needs to place Alice's pending stake after it.
The design describes an UNDER_ATTACK deposit as visible and voluntary and states that a staker can inspect the live registry before depositing. An off-chain inspection is not atomic with transaction execution, so it does not protect against this ordering change. :contentReference[oaicite:0]{index=0}
A user who intended to enter while withdrawals were available can instead be committed to the full corruption-resolution path.
The consequences are:
the user's principal becomes non-withdrawable immediately;
the user may be locked until expiry or moderator resolution;
if the agreement becomes corrupted, the user's complete principal may be transferred to the named attacker or recoveryAddress;
the transaction succeeds rather than reverting, so the user receives no on-chain indication that the risk condition changed;
the attack requires no compromise of the pool contract.
The impact is High because the affected user's complete principal can be lost.
The likelihood is Medium because it applies to pending stake transactions around the normal transition from a pre-risk state to UNDER_ATTACK.
Add the following test to a contract inheriting BaseConfidencePoolTest:
Allow callers to bind a stake transaction to an acceptable risk condition.
One option is to add an expected-state parameter:
A more flexible interface can let callers specify whether they permit entering after the risk window opens:
The check must occur inside stake() before transferring tokens. An off-chain registry read or frontend warning is insufficient because the state can change before transaction inclusion.
The same protection should be considered for integrations that submit stake transactions through relayers or delayed execution systems.
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.