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

Permissionless `claimExpired` allows frontrunning the Moderator to lock Bad-Faith outcome

Author Revealed upon completion

Root + Impact

The claimExpired function can be triggered by any caller after the grace period to finalize a CORRUPTED registry state as bad-faith. This mechanical resolution sets claimsStarted to true, which permanently locks the outcome and prevents the moderator from ever flagging a good-faith outcome for a valid whitehat attacker.

Description

The protocol provides a MODERATOR_CORRUPTED_GRACE period after pool expiry. Once this period elapses, claimExpired allows anyone to resolve the pool if the registry is CORRUPTED.

The specific issue is that this mechanical resolution is "scope-blind" and defaults to a bad-faith outcome. By setting claimsStarted = true, it invokes the finality latch of the contract. A malicious or automated actor can frontrun the moderator's attempt to call flagOutcome(CORRUPTED, true, ...) the moment the grace period ends. This results in the entire pool being swept to the recoveryAddress, effectively stealing the bounty from the whitehat who performed the in-scope attack.

if (state == IAttackRegistry.ContractState.CORRUPTED && riskWindowStart != 0) {
if (block.timestamp < expiry + MODERATOR_CORRUPTED_GRACE) {
revert AgreementCorruptedAwaitingModerator();
}
outcome = PoolStates.Outcome.CORRUPTED;
@> claimsStarted = true; // Permanent lock
emit OutcomeFlagged(address(0), PoolStates.Outcome.CORRUPTED, false, address(0));
return;
}

Risk

Likelihood:

  • Occurs precisely at the block when block.timestamp >= expiry + MODERATOR_CORRUPTED_GRACE.

  • Automated bots or the pool sponsor (who controls recoveryAddress) have a direct financial incentive to trigger this before the moderator can name an attacker.

Impact:

  • Total loss of bounty for the whitehat attacker.

  • Permanent lockout of administrative power to correct the outcome to "Good Faith".

Proof of Concept

function test_PoC_ClaimExpiredFrontrunsModerator() public {
_stake(alice, ONE); _contributeBonus(bob, ONE);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
pool.pokeRiskWindow();
vm.warp(pool.expiry() + pool.MODERATOR_CORRUPTED_GRACE() + 1);
vm.prank(dave);
pool.claimExpired(); // Dave locks the pool as bad-faith
vm.prank(moderator);
vm.expectRevert(); // Moderator cannot award bounty anymore
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
}

Recommended Mitigation

Allow the moderator to override a mechanical bad-faith resolution if and only if the current outcome is CORRUPTED and goodFaith is currently false.

Support

FAQs

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

Give us feedback!