FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: high
Likelihood: high

Attacker can permanently bypass CORRUPTED slashing penalty by front-running moderator with claimExpired() when riskWindowStart is 0

Author Revealed upon completion

Description

Normally, when an agreement breaches, the registry transitions to CORRUPTED, and the moderator slashes stakes via flagOutcome(CORRUPTED).
However, if the registry reaches CORRUPTED without a prior UNDER_ATTACK observation (riskWindowStart == 0), claimExpired() fails to auto-slash. An attacker can wait until expiry and front-run the moderator with claimExpired(). This resolves the pool to EXPIRED (returning principal to attackers) and permanently locks out the moderator.

Risk

Likelihood:

  • The registry state can transition to CORRUPTED rapidly without an intermediate pokeRiskWindow() call.

  • MEV bots actively monitor the mempool and will easily front-run the moderator to prevent their stake from being slashed.

Impact:

  • The protocol completely loses its insurance protection. Capital is returned to the attackers instead of being slashed to the recovery address.

Proof of Concept

The vulnerability exists because the auto-slash fallback logic requires riskWindowStart to be non-zero. If it is zero, the logic incorrectly falls through to the EXPIRED state, allowing the attacker to bypass the slash.

function claimExpired() external nonReentrant {
// ...
if (outcome == PoolStates.Outcome.UNRESOLVED) {
IAttackRegistry.ContractState state = _observePoolState();
// @> Vulnerability: requires riskWindowStart != 0. If 0, falls through to EXPIRED.
if (state == IAttackRegistry.ContractState.CORRUPTED && riskWindowStart != 0) {
// ...
} else {
// @> State falls through to EXPIRED, completely bypassing the slash
outcome = PoolStates.Outcome.EXPIRED;
}
// @> claimsStarted permanently locks out the moderator
claimsStarted = true;
}

Recommended Mitigation

To fix this, remove the riskWindowStart != 0 condition. The mechanical resolution should prioritize the terminal CORRUPTED state to ensure the pool is correctly slashed, regardless of whether a risk window was previously observed.

- if (state == IAttackRegistry.ContractState.CORRUPTED && riskWindowStart != 0) {
+ if (state == IAttackRegistry.ContractState.CORRUPTED) {

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!