SNARKeling Treasure Hunt

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

`Claimed` event indexes `msg.sender` instead of the recipient that actually received the ETH

Description

claim() emits Claimed(treasureHash, msg.sender) at line 111, but the payout at line 107 goes to recipient. Line 86 explicitly forbids recipient == msg.sender, so these are guaranteed different addresses on every successful claim. The event's indexed parameter is named recipient (line 44) yet contains the submitter. Any off-chain tool filtering Claimed by payee address points at the wrong person.

// line 44
event Claimed(bytes32 indexed treasureHash, address indexed recipient); // param NAMED recipient
// line 107
(bool sent, ) = recipient.call{value: REWARD}(""); // ETH to recipient
// line 111
emit Claimed(treasureHash, msg.sender); // but logs msg.sender

Risk

Likelihood: certain on every successful claim. Impact: indexers, dashboards, and incident-response tooling cannot attribute payouts to the real payee. Gas sponsors or relayers show up in the log instead of the winner. No fund loss.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import {TreasureHunt} from "../src/TreasureHunt.sol";
import {IVerifier} from "../src/Verifier.sol";
contract PoC_L02 is Test {
event Claimed(bytes32 indexed treasureHash, address indexed recipient);
TreasureHunt hunt;
function setUp() public {
address v = address(0xABCDEF);
vm.etch(v, hex"00");
vm.deal(address(0xDEAD), 200 ether);
vm.prank(address(0xDEAD));
hunt = new TreasureHunt{value: 100 ether}(v);
vm.mockCall(v, abi.encodeWithSelector(IVerifier.verify.selector), abi.encode(true));
}
function test_event_logs_submitter_not_recipient() public {
address submitter = address(0x5E11DE8);
address payable realRecipient = payable(address(0xA11CE));
bytes32 h = bytes32(uint256(1));
vm.expectEmit(true, true, false, false);
emit Claimed(h, submitter); // BUG: submitter, not realRecipient
vm.prank(submitter);
hunt.claim(hex"de", h, realRecipient);
assertEq(realRecipient.balance, 10 ether, "ETH goes to realRecipient");
// but the event indexed address was submitter
}
}

Actual trace from the H-01 PoC (ten replays from 0x...BAD):

emit Claimed(treasureHash: 0x0354..., recipient: 0x0000000000000000000000000000000000000Bad)
[x10 — every event shows the attacker, never the real 0xA11CE* recipients that received ETH]

Recommended Mitigation

- emit Claimed(treasureHash, msg.sender);
+ emit Claimed(treasureHash, recipient);
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!