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

`pokeRiskWindow()` Silently No-Ops After Resolution

Author Revealed upon completion

Root + Impact

Description

After the pool is resolved (outcome != UNRESOLVED), pokeRiskWindow() silently returns without reverting or emitting any event. Callers invoking this function to eagerly seal risk markers receive no indication their call had no effect. The function checks resolution state and returns early at line 652 before reaching the _observePoolState() call that would normally seal markers or emit events.

// ConfidencePool.sol — pokeRiskWindow() lines 649-658
function pokeRiskWindow() external {
@> if (outcome != PoolStates.Outcome.UNRESOLVED) return; // <-- silent return
_observePoolState();
if (riskWindowStart == 0 && riskWindowEnd == 0) revert RiskWindowNotReached();
}

Risk

Likelihood: Low — only affects callers who invoke pokeRiskWindow() after the pool is already resolved. By that point, the risk window markers are irrelevant.

Impact: Low — no funds at risk. Callers receive no feedback that their transaction was a no-op, which could cause off-chain systems to incorrectly assume risk markers were modified.

Proof of Concept

File: L6-PokeRiskWindow-SilentNoop.poc.t.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
/// @notice PoC for L-6: pokeRiskWindow silently no-ops after resolution
contract L6_PokeRiskWindow_SilentNoop_POC is BaseConfidencePoolTest {
function testPOC_L6_pokeRiskWindow_silentNoopAfterResolution() external {
// Pre-resolution: pokeRiskWindow on pre-attack state reverts
attackRegistry.setAgreementState(IAttackRegistry.ContractState.NOT_DEPLOYED);
vm.expectRevert(); // RiskWindowNotReached — clear feedback
pool.pokeRiskWindow();
// Resolve the pool
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
// Post-resolution: pokeRiskWindow silently returns — no revert, no event
// Caller has no indication their call was a no-op
pool.pokeRiskWindow(); // silent return, no feedback
// If this reverted or emitted, the caller would know. It does neither.
}
function testPOC_L6_pokeRiskWindow_noEventOnNoop() external {
// Resolve the pool
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
// After resolution, pokeRiskWindow returns silently
// No RiskWindowStarted, RiskWindowEnded, or error — caller gets nothing
// After resolution, pokeRiskWindow silently returns with no revert and no side effects.
// The outcome is already set, so the function returns at line 652 without emitting events.
// This is confirmed by the function's line 652: `if (outcome != PoolStates.Outcome.UNRESOLVED) return;`
pool.pokeRiskWindow();
// No revert, no event — caller has no indication their call was a no-op.
}
}

Run: forge test --match-path 'L6-PokeRiskWindow-SilentNoop.poc.t.sol' -vv

Recommended Mitigation

Either revert with a descriptive error, or document the silent return behavior more prominently in NatSpec:

- if (outcome != PoolStates.Outcome.UNRESOLVED) return;
+ if (outcome != PoolStates.Outcome.UNRESOLVED) revert PoolAlreadyResolved();

Support

FAQs

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

Give us feedback!