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

Missing Event for Auto‑Resolved CORRUPTED

Author Revealed upon completion

Root + Impact

Description

  • The contract emits OutcomeFlagged for all outcome resolutions, including auto‑resolution from claimExpired

  • In the auto‑CORRUPTED branch, the event is emitted with flagger = address(0), goodFaith = false, attacker = address(0). This is identical to a moderator manually calling flagOutcome(CORRUPTED, false, 0x0). Off‑chain indexers cannot distinguish between a moderator action and a system‑triggered backstop, hindering monitoring and analytic

function claimExpired() external nonReentrant {
if (block.timestamp < expiry) revert PoolNotExpired();
if (outcome != PoolStates.Outcome.UNRESOLVED && outcome != PoolStates.Outcome.EXPIRED) {
revert InvalidOutcome();
}
if (outcome == PoolStates.Outcome.UNRESOLVED) {
IAttackRegistry.ContractState state = _observePoolState();
snapshotTotalStaked = totalEligibleStake;
snapshotTotalBonus = totalBonus;
snapshotSumStakeTime = sumStakeTime;
snapshotSumStakeTimeSq = sumStakeTimeSq;
if (state == IAttackRegistry.ContractState.CORRUPTED && riskWindowStart != 0) {
if (block.timestamp < expiry + MODERATOR_CORRUPTED_GRACE) {
revert AgreementCorruptedAwaitingModerator();
}
outcome = PoolStates.Outcome.CORRUPTED;
outcomeFlaggedAt = riskWindowEnd;
corruptedReserve = snapshotTotalStaked + snapshotTotalBonus;
claimsStarted = true;
// @> LOW FINDING: This event is identical to moderator-initiated bad-faith CORRUPTED
// @> There is no dedicated event to signal this was a system backstop (auto-resolution),
// @> causing ambiguity for off-chain indexers, UIs, and monitoring tools.
@> emit OutcomeFlagged(address(0), PoolStates.Outcome.CORRUPTED, false, address(0));
return;
}
if (state == IAttackRegistry.ContractState.PRODUCTION) {
outcome = PoolStates.Outcome.SURVIVED;
outcomeFlaggedAt = riskWindowEnd;
emit OutcomeFlagged(address(0), PoolStates.Outcome.SURVIVED, false, address(0));
} else {
outcome = PoolStates.Outcome.EXPIRED;
outcomeFlaggedAt = expiry;
emit OutcomeFlagged(address(0), PoolStates.Outcome.EXPIRED, false, address(0));
}
claimsStarted = true;
}
if (hasClaimed[msg.sender]) revert InvalidAmount();
uint256 userEligible = eligibleStake[msg.sender];
if (userEligible == 0) return;
_clampUserSums(msg.sender);
hasClaimed[msg.sender] = true;
uint256 bonusShare = _bonusShare(msg.sender, userEligible);
uint256 payout = userEligible + bonusShare;
totalEligibleStake -= userEligible;
claimedBonus += bonusShare;
delete eligibleStake[msg.sender];
delete userSumStakeTime[msg.sender];
delete userSumStakeTimeSq[msg.sender];
if (!claimsStarted) claimsStarted = true;
stakeToken.safeTransfer(msg.sender, payout);
if (outcome == PoolStates.Outcome.SURVIVED) {
emit ClaimSurvived(msg.sender, userEligible, bonusShare);
} else {
emit ClaimExpired(msg.sender, userEligible, bonusShare);
}
}

Risk

Likelihood: High

  • always occurs when auto‑CORRUPTED fires


Impact: low

  • no funds at risk, but operational visibility reduced). UIs may misattribute the action, and alerts for moderator inactivity may be missed.


Proof of Concept

contract ClaimExpiredRegistryGateTest is BaseConfidencePoolTest{
........
function test_autoCorruptedEventAmbiguity() public {
_stake(alice, 100 * ONE);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.warp(pool.expiry() + pool.MODERATOR_CORRUPTED_GRACE() + 1);
vm.expectEmit(true, true, true, true);
emit IConfidencePool.OutcomeFlagged(address(0), PoolStates.Outcome.CORRUPTED, false, address(0));
// No distinct event to indicate auto-resolution
vm.prank(alice);
pool.claimExpired();
}
}

Recommended Mitigation

add and emit this event to the claimExpired ()
+ event AutoResolvedCorrupted(address indexed resolver, uint256 timestamp, uint256 snapshotTotalStaked, uint256 snapshotTotalBonus);

Support

FAQs

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

Give us feedback!