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

After auto-SURVIVED resolution via claimExpired, other stakers cannot use claimExpired and get a confusing revert

Author Revealed upon completion

Description

When claimExpired() auto-resolves the pool to SURVIVED (because registry is PRODUCTION at expiry), the first caller claims normally. But every subsequent staker who calls claimExpired() gets InvalidOutcome and has to figure out they need claimSurvived() instead. This doesn't happen with the EXPIRED path — that one works for everyone.

Root Cause

Line 514 of ConfidencePool.sol:

if (outcome != PoolStates.Outcome.UNRESOLVED && outcome != PoolStates.Outcome.EXPIRED) {
revert InvalidOutcome();
}

After the first caller auto-resolves the pool and sets outcome = SURVIVED, this gate blocks all further claimExpired() calls because SURVIVED is not in the allowed set. There is no event, getter, or error message telling subsequent stakers which function to use instead. Compare with auto-EXPIRED where outcome = EXPIRED passes the gate, so everyone can keep calling claimExpired().

Risk

Likelihood: High — this happens every time a pool auto-resolves to SURVIVED via claimExpired and has more than one staker.

Impact: Low — no loss of funds. Stakers can always claim via claimSurvived(). The impact is UX confusion from an opaque revert.

Proof of Concept

function testSecondCallerRevertsOnClaimExpiredAfterAutoSurvived() external {
_stake(alice, 100 * ONE);
_stake(bob, 50 * ONE);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
vm.warp(pool.expiry());
vm.prank(alice);
pool.claimExpired();
vm.prank(bob);
vm.expectRevert(IConfidencePool.InvalidOutcome.selector);
pool.claimExpired();
uint256 bobBefore = token.balanceOf(bob);
vm.prank(bob);
pool.claimSurvived();
assertGt(token.balanceOf(bob) - bobBefore, 0);
}

Recommended Mitigation

Add SURVIVED to the allowed outcomes in claimExpired:

if (outcome != PoolStates.Outcome.UNRESOLVED
&& outcome != PoolStates.Outcome.EXPIRED
&& outcome != PoolStates.Outcome.SURVIVED) {
revert InvalidOutcome();
}

Or use a more descriptive custom error like UseClaimSurvived() when outcome == SURVIVED.

Support

FAQs

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

Give us feedback!