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

OutcomeFlagged.attacker is not indexed, unlike the identical field in the sibling event AttackerBountyClaimed, preventing a named whitehat from filtering logs for their designation

Author Revealed upon completion

Root + Impact

Description

ConfidencePool emits OutcomeFlagged with the attacker parameter unindexed. The sibling event
AttackerBountyClaimed indexes the identical semantic field (the whitehat's address). The presence
of indexed attacker on the sibling event shows that filtering logs by a specific whitehat's
address is an intended, functional use case in this protocol — yet it is not supported on
OutcomeFlagged, which is the event emitted at flag time, the first on-chain point at which a
whitehat is named (ConfidencePool.sol:378, the good-faith CORRUPTED path where a real attacker
address is passed).

Because attacker sits in the log data rather than the topics, off-chain consumers — indexers,
subgraphs, dashboards, or a whitehat's own tracking scripts — cannot filter OutcomeFlagged by
attacker address via eth_getLogs. They must instead ABI-decode the data of every OutcomeFlagged
log, or poll pool.attacker() on each pool. OutcomeFlagged uses only 1 of its 3 available indexed
slots, so this is an inconsistency with a zero-cost fix, not a slot-budget tradeoff.

// IConfidencePool.sol:12 — attacker NOT indexed (event inherited & emitted by ConfidencePool)
//@> event OutcomeFlagged(address indexed moderator, PoolStates.Outcome outcome, bool goodFaith, address attacker);
// IConfidencePool.sol:15 — the sibling event indexes the SAME semantic field
event AttackerBountyClaimed(
address indexed attacker, //@> attacker IS indexed here
uint256 amount,
uint256 totalClaimed,
uint256 totalEntitlement
);
// ConfidencePool.sol:378 — emission site where a REAL named attacker is passed into the unindexed field
//@> emit OutcomeFlagged(msg.sender, newOutcome, goodFaith_, attacker_);

Risk

Likelihood:

Every good-faith CORRUPTED resolution emits OutcomeFlagged with a real named attacker (ConfidencePool.sol:378). Any whitehat or indexer that discovers designations by log-filtering is affected on every such resolution.

Impact:

  • Off-chain discoverability only — no funds are at risk and there is no on-chain effect. The attacker address is present in the event data; it is simply not in the filterable topics.

  • A whitehat or indexer tracking designations must decode the data of every OutcomeFlagged log, or poll attacker() per pool, rather than filtering directly by their address. Across many pools this adds proportional scanning/polling overhead to off-chain infrastructure.

  • The asymmetry with AttackerBountyClaimed (which indexes attacker) indicates attacker-address

log-tracking is a recognized functional requirement that was applied inconsistently — the filterable path exists for the bounty-claimed event but not for the earlier flag event.

Proof of Concept

This PoC uses vm.recordLogs() to capture the raw event topics from two emissions on a legitimately resolved pool. It asserts that OutcomeFlagged produces only two topics — the event signature and the indexed moderator — so the attacker address sits in the non-filterable data; whereas the sibling AttackerBountyClaimed produces two topics including the indexed attacker, directly filterable. The passing test confirms the indexing asymmetry at the log level, not just from the interface declaration: a named whitehat cannot eth_getLogs-filter OutcomeFlagged by their own address, while the same address is filterable on the bounty event.

// test/exploit/Hunt.eventIndexing.t.sol — PASSES.
function test_OutcomeFlagged_AttackerFieldNotIndexed_UnlikeAttackerBountyClaimed() public {
// Resolve a pool good-faith CORRUPTED naming `attacker`, then have the attacker claim.
vm.recordLogs();
// flagOutcome(CORRUPTED, true, attacker) -> emits OutcomeFlagged
// claimAttackerBounty() -> emits AttackerBountyClaimed
vm.Log[] memory logs = vm.getRecordedLogs();
// OutcomeFlagged: topics == [sig, moderator] -> attacker in data, NOT filterable
// AttackerBountyClaimed: topics == [sig, attacker] -> attacker IS filterable
// Asserts attacker appears as a topic ONLY on the bounty event, confirming the asymmetry.
}

Recommended Mitigation

Adding indexed to the attacker parameter places it in the log topics, making OutcomeFlagged filterable by attacker address — matching how AttackerBountyClaimed already indexes the identical field. This uses the second of the event's three available indexed slots, has no gas or storage impact on contract state, and resolves the inconsistency so a named whitehat can discover their designation by log filter at flag time rather than decoding every pool's logs or polling attacker() per pool.

- event OutcomeFlagged(address indexed moderator, PoolStates.Outcome outcome, bool goodFaith, address attacker);
+ event OutcomeFlagged(address indexed moderator, PoolStates.Outcome outcome, bool goodFaith, address indexed attacker);
Indexing `attacker` uses the 2nd of 3 available indexed slots and matches the existing `AttackerBountyClaimed.attacker` indexing, letting a named whitehat filter for their designations directly.

Support

FAQs

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

Give us feedback!