SNARKeling Treasure Hunt

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

Claimed event emits msg.sender instead of recipient, systematically misattributing every payout in on-chain event logs

Description

Normal behavior: After a successful claim(), the Claimed event should emit (treasureHash, recipient) where recipient is the address that actually received the ETH reward. Off-chain indexers, explorers, and monitoring dashboards use this event to attribute payouts.

Specific issue: The event is declared with a recipient parameter but the emission uses msg.sender. Because claim() explicitly requires recipient != msg.sender at L86, the emitted "recipient" is guaranteed to be wrong on every successful claim — it's always the caller (e.g. an EOA that submitted the claim), never the actual payee.

// contracts/src/TreasureHunt.sol
// Declaration (L44):
event Claimed(bytes32 indexed treasureHash, address indexed recipient);
// Emission (L111), inside claim():
function claim(bytes calldata proof, bytes32 treasureHash, address payable recipient) external nonReentrant() {
...
if (recipient == address(0) || recipient == address(this) || recipient == owner || recipient == msg.sender) revert InvalidRecipient(); //@> L86: forces recipient != msg.sender
...
(bool sent, ) = recipient.call{value: REWARD}(""); //@> L107: pays `recipient`
require(sent, "ETH_TRANSFER_FAILED");
emit Claimed(treasureHash, msg.sender); //@> BUG: logs `msg.sender` instead of `recipient`
}

Risk

Likelihood: HIGH

Reason 1: Every successful claim triggers this bug — the event is always wrong because recipient != msg.sender is enforced.

Reason 2: Consumers of the event (subgraph indexers, analytics dashboards, monitoring tools) have no way to detect the discrepancy without cross-referencing the ETH transfer trace.

Impact: LOW

Impact 1: Off-chain data integrity is compromised. Every "who got paid" record in any external service that watches Claimed events is incorrect.

Impact 2: Accounting and dispute resolution become unreliable. If the hunt organizer needs to publish a list of winners for tax reporting, transparency, or dispute handling, the event log is systematically wrong and must be reconstructed from the ETH transfers.

No funds are misrouted — the ETH transfer at L107 goes to the correct address. Hence Low severity.

Proof of Concept

function test_claimed_event_misattributes_recipient() public {
// Setup: a legitimate claim.
address payable actualRecipient = payable(address(0xB055));
address caller = address(0x51EA1E4);
bytes32 h = bytes32(uint256(42));
bytes memory proof = hex"deadbeef";
vm.recordLogs();
vm.prank(caller);
hunt.claim(proof, h, actualRecipient);
Vm.Log[] memory logs = vm.getRecordedLogs();
// Find the Claimed event.
bytes32 claimedTopic = keccak256("Claimed(bytes32,address)");
for (uint256 i = 0; i < logs.length; i++) {
if (logs[i].topics[0] == claimedTopic) {
address emittedRecipient = address(uint160(uint256(logs[i].topics[2])));
// Event's "recipient" topic is msg.sender == caller, NOT actualRecipient:
assertEq(emittedRecipient, caller, "event logs msg.sender");
assertTrue(emittedRecipient != actualRecipient, "event recipient != actual recipient");
break;
}
}
// ETH was sent correctly:
assertEq(actualRecipient.balance, hunt.REWARD());
assertEq(caller.balance, 0); // caller received no ETH
}

Recommended Mitigation

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

The one-line fix makes the emitted recipient topic match the actual payee.

If the protocol also wants to track the caller (for audit trail purposes), extend the event with a caller field:

-event Claimed(bytes32 indexed treasureHash, address indexed recipient);
+event Claimed(bytes32 indexed treasureHash, address indexed recipient, address indexed caller);
...
- 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!