SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
Submission Details
Impact: low
Likelihood: high

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

Author Revealed upon completion

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);

Support

FAQs

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

Give us feedback!