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

Non-Staker Auto-Resolve via ​claimExpired​ Permanently Locks Outcome Against Moderator Correction

Author Revealed upon completion

Root + Impact

Description

  • claimExpired() allows any caller — even one with zero stake — to mechanically auto-resolve the pool post-expiry. The function sets claimsStarted = true unconditionally on all resolution branches (line 664), which permanently blocks the moderator's flagOutcome re-flag window.

  • A non-staker (or bot) calling claimExpired immediately at expiry locks in the auto-determined outcome before the moderator can review it.

// ConfidencePool.sol — claimExpired()
if (outcome == PoolStates.Outcome.UNRESOLVED) {
// ... auto-resolution logic ...
@> claimsStarted = true; // ← set even when caller has zero stake
}
if (hasClaimed[msg.sender]) revert InvalidAmount();
uint256 userEligible = eligibleStake[msg.sender];
if (userEligible == 0) {
@> return; // ← soft-success: no payout but outcome is now terminal
}

Risk

Likelihood:

  • This occurs every time claimExpired is the first post-expiry call. MEV bots or monitoring scripts calling claimExpired at the exact expiry block are common in DeFi.

Impact:

  • The moderator loses the ability to correct a mechanically-determined outcome that may not reflect the true scope assessment.

  • No direct fund loss — the auto-resolution follows the registry state faithfully.

Proof of Concept

function test_nonStakerAutoResolve_locksOutcome() external {
_stake(alice, 100 * ONE);
vm.warp(pool.expiry());
address resolver = makeAddr("resolver");
vm.prank(resolver);
pool.claimExpired();
assertEq(uint256(pool.outcome()), uint256(PoolStates.Outcome.EXPIRED));
assertTrue(pool.claimsStarted());
vm.prank(moderator);
vm.expectRevert(IConfidencePool.OutcomeAlreadySet.selector);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
}

Recommended Mitigation

This is documented as intended behavior (DESIGN.md §4, §13). The defense-in-depth finality is uniform across all resolution branches. If the moderator needs guaranteed priority, consider adding a brief moderator-only grace window before permissionless resolution:

if (block.timestamp < expiry + MODERATOR_RESOLUTION_GRACE) {
revert AwaitingModeratorResolution();
}

Support

FAQs

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

Give us feedback!