SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
View results
Submission Details
Severity: low
Valid

Event `Claimed` emits msg.sender in the `recipient` indexed slot, corrupting off-chain indexer data

Root + Impact

Description

Normal behaviour: when a treasure is claimed, the contract emits a Claimed event whose second indexed field is declared as the recipient — i.e. the address that received the 10 ETH reward. Off-chain indexers (The Graph, Etherscan topic filters, custom dashboards, analytics pipelines) rely on this event to track "who got paid for which treasure".

Specific issue: the emit site passes msg.sender (the claim submitter) instead of the recipient argument. Because msg.sender and recipient are different by design — the code explicitly rejects recipient == msg.sender on line 86 — these are always two distinct addresses on every successful claim. The indexed topic therefore carries the wrong address for every emitted Claimed event.

// contracts/src/TreasureHunt.sol
event Claimed(bytes32 indexed treasureHash, address indexed recipient); // @> 2nd topic documented as "recipient"
function claim(bytes calldata proof, bytes32 treasureHash, address payable recipient) external nonReentrant() {
// ... validations including: require(recipient != msg.sender) ...
(bool sent, ) = recipient.call{value: REWARD}("");
require(sent, "ETH_TRANSFER_FAILED");
emit Claimed(treasureHash, msg.sender); // @> emits the claimer instead of the recipient
}

Risk

Likelihood:

  • Certain — the bug is in the code path itself, so every successful claim() emits the wrong indexed topic.

  • Given the explicit recipient != msg.sender guard, the emitted address never coincides with the declared recipient for any legitimate claim.

Impact:

  • Off-chain indexers and subgraphs that filter Claimed events by the recipient topic will never match the real recipients; they will match claim submitters instead. Dashboards showing "treasure winners", leaderboards, tax reporting tools, or any downstream tooling relying on this topic will display incorrect data.

  • Analytics drift: anyone building on top of this contract (e.g. linking the hunt to loyalty programs, airdrops, or merchandise drops) must audit the event layer themselves and work around the bug — introducing per-consumer inconsistency.

  • No direct loss of funds.

Proof of Concept

Inspect the event declaration at line 44 and the emission at line 111:

event Claimed(bytes32 indexed treasureHash, address indexed recipient); // line 44
// ...
emit Claimed(treasureHash, msg.sender); // line 111

A minimal Foundry observation:

// Pseudo-test — assuming the H-01 guard bug has been fixed so that a valid claim completes once.
function test_eventEmitsWrongAddress() public {
// Setup: player submits a valid claim with recipient = R (R != player).
vm.recordLogs();
vm.prank(player);
hunt.claim(validProof, treasureHash, payable(R));
Vm.Log[] memory entries = vm.getRecordedLogs();
// Claimed(bytes32, address) signature topic
bytes32 sig = keccak256("Claimed(bytes32,address)");
for (uint256 i = 0; i < entries.length; i++) {
if (entries[i].topics[0] == sig) {
address emitted = address(uint160(uint256(entries[i].topics[2])));
assertEq(emitted, player, "event logs claimer, not recipient");
assertNotEq(emitted, R, "event does NOT log the real recipient");
}
}
}

A downstream subgraph subscribing to Claimed(indexed bytes32, indexed address) and filtering by the recipient topic will therefore never see events indexed under the real recipients — it will only see them under the claim submitters.

Recommended Mitigation

Emit the actual recipient to match the event's declared indexed field:

- emit Claimed(treasureHash, msg.sender);
+ emit Claimed(treasureHash, recipient);

If the protocol additionally wants to record the claim submitter (for analytics on "who bothered to relay the proof on-chain"), add a second non-indexed field:

- event Claimed(bytes32 indexed treasureHash, address indexed recipient);
+ event Claimed(bytes32 indexed treasureHash, address indexed recipient, address submitter);
// ...
- emit Claimed(treasureHash, msg.sender);
+ emit Claimed(treasureHash, recipient, msg.sender);
Updates

Lead Judging Commences

s3mvl4d Lead Judge 18 days ago
Submission Judgement Published
Validated
Assigned finding tags:

incorrect event parameter

The event is declared as event `Claimed(bytes32 indexed treasureHash, address indexed recipient);`, which clearly indicates that the second indexed field is meant to represent the reward recipient, but `claim()` emits `Claimed(treasureHash, msg.sender)` instead of `Claimed(treasureHash, recipient)`, even though the ETH transfer is sent to recipient and the proof itself is constructed around the public inputs (treasureHash, recipient). As a standalone finding, this is appropriately low severity because it is fundamentally an event/accounting inconsistency rather than a direct loss-of-funds issue: the core state transition and payout still follow the intended recipient, but off-chain consumers reading the event log will observe incorrect metadata about who was associated with the claim.

Support

FAQs

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

Give us feedback!