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

pokeRiskWindow is permissionless, letting any address seal riskWindowStart and steer the claimExpired auto-resolution branch (CORRUPTED sweep vs EXPIRED refund) when the moderator is absent

Author Revealed upon completion

Root + Impact

Description

riskWindowStart is the on-chain marker that the pool observed the agreement in an active-risk state. It is sealed lazily: any entrypoint that observes the pool during UNDER_ATTACK or PROMOTION_REQUESTED calls _markRiskWindowStart. Whether it is sealed later decides which branch claimExpired takes for a terminally CORRUPTED agreement that the moderator has not flagged: state == CORRUPTED && riskWindowStart != 0 auto-resolves CORRUPTED (whole pool sweeps to recovery), otherwise it falls through to EXPIRED (stakers refunded).

pokeRiskWindow (ConfidencePool.sol:672) carries no access-control modifier, so any address, not just a staker or the moderator, can seal riskWindowStart at a moment of its choosing while the pool is in an active-risk state. It also runs while the pool is paused. When the moderator is slow or absent, an unprivileged caller sealing (or declining to seal) the window is what selects the staker-favorable versus recovery-favorable resolution, so a resolution-affecting variable is under the control of arbitrary callers rather than a privileged role.

@> function pokeRiskWindow() external { // :672 no modifier, no pause gate
_observePoolState(); // seals riskWindowStart during active-risk states
}
function claimExpired() external nonReentrant {
...
@> if (state == PoolStates...CORRUPTED && riskWindowStart != 0) { // :550 branch keyed on the poked flag
// auto-CORRUPTED: whole pool sweeps to recoveryAddress
} else {
// EXPIRED: stakers refunded
}
}

Risk

Likelihood:

  • Any address can call pokeRiskWindow while the pool is in UNDER_ATTACK / PROMOTION_REQUESTED, since the function has no access-control modifier and no pause gate (ConfidencePool.sol:672).

  • The branch it influences is reached whenever the agreement reaches terminal CORRUPTED and the moderator has not yet flagged, which is the same window the pool is designed to backstop.

Impact:

  • An unprivileged caller, not a privileged role, effectively selects whether a post-expiry CORRUPTED pool auto-resolves to a whole-pool recovery sweep or to a staker refund. The value moved is real (the full pool), even though the caller gains nothing directly.

  • Bounded to Low because the recovery destination is the sponsor address (no attacker profit) and the harmful outcome additionally requires an absent moderator, which sits inside the protocol's trusted-moderator assumption.

Proof of Concept

The test shows pokeRiskWindow is callable by an arbitrary address and even while paused, and that the sealed vs unsealed riskWindowStart selects the CORRUPTED-sweep vs EXPIRED-refund branch of claimExpired.

// test/unit/ChainEscalate.t.sol
function testPokeIsPermissionlessAndNotPauseGated() external { /* random caller pokes, seals window */ }
function testClaimExpiredForeclosesInScopeCorruptedNoRiskWindow() external { /* unsealed -> EXPIRED refund path */ }
function testControlRiskWindowSetKeepsBackstop() external { /* sealed -> CORRUPTED backstop holds */ }

Output (forge test --match-contract ChainEscalate -vv, forge 1.7.1):

[PASS] testClaimExpiredForeclosesInScopeCorruptedNoRiskWindow() (gas: 422852)
[PASS] testControlRiskWindowSetKeepsBackstop() (gas: 532186)
[PASS] testPokeIsPermissionlessAndNotPauseGated() (gas: 350822)
Suite result: ok. 3 passed; 0 failed; 0 skipped

Recommended Mitigation

Either restrict pokeRiskWindow to the moderator (or the pool owner), or fold the observation into the state-changing entrypoints that already run it so there is no standalone unprivileged sealing lever, so the moment riskWindowStart seals is not selectable by an arbitrary third party.

- function pokeRiskWindow() external {
+ function pokeRiskWindow() external onlyModerator {
_observePoolState();
}

Support

FAQs

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

Give us feedback!