SNARKeling Treasure Hunt

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

Claimed event emits the caller instead of the actual reward recipient

Author Revealed upon completion

Root + Impact

Description

  • The Claimed event is intended to log the address that received the reward, but claim() emits msg.sender instead of recipient.

  • As a result, off-chain indexers, UIs, and monitoring tools will record the wrong payout recipient.

emit Claimed(treasureHash, msg.sender);

Risk

Likelihood:

  • This occurs on every successful claim where msg.sender differs from recipient

  • The contract explicitly requires recipient != msg.sender, so the emitted recipient is always wrong for valid claims.

Impact:

  • The actual ETH transfer still goes to the correct recipient, so funds are not directly lost because of this bug.

  • However, event consumers will see incorrect claim data, which can break frontends, analytics, accounting, and dispute resolution.

Proof of Concept

The PoC performs a valid claim from caller while sending the reward to a separate recipient. The transaction succeeds and ETH is transferred to recipient, but the emitted Claimed event records caller as the recipient.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "forge-std/StdJson.sol";
import {TreasureHunt} from "../src/TreasureHunt.sol";
import {HonkVerifier} from "../src/Verifier.sol";
contract TreasureHuntWrongClaimedEventPoC is Test {
using stdJson for string;
HonkVerifier verifier;
TreasureHunt hunt;
address constant owner = address(0xDEADBEEF);
address constant caller = address(0xBEEF);
uint256 constant INITIAL_OWNER_BALANCE = 200 ether;
uint256 constant INITIAL_FUNDING = 100 ether;
event Claimed(bytes32 indexed treasureHash, address indexed recipient);
function setUp() public {
vm.deal(owner, INITIAL_OWNER_BALANCE);
vm.startPrank(owner);
verifier = new HonkVerifier();
hunt = new TreasureHunt{value: INITIAL_FUNDING}(address(verifier));
vm.stopPrank();
}
function _loadFixture()
internal
view
returns (
bytes memory proof,
bytes32 treasureHash,
address payable recipient
)
{
proof = vm.readFileBinary("contracts/test/fixtures/proof.bin");
string memory json = vm.readFile(
"contracts/test/fixtures/public_inputs.json"
);
bytes memory raw = json.parseRaw(".publicInputs");
bytes32[] memory inputs = abi.decode(raw, (bytes32[]));
treasureHash = inputs[0];
recipient = payable(address(uint160(uint256(inputs[1]))));
}
function testPoC_claimedEventEmitsCallerInsteadOfRecipient() public {
(
bytes memory proof,
bytes32 treasureHash,
address payable recipient
) = _loadFixture();
uint256 reward = hunt.REWARD();
uint256 recipientBalanceBefore = recipient.balance;
uint256 callerBalanceBefore = caller.balance;
vm.prank(caller);
// The reward is paid to `recipient`, but the contract emits `caller`
// as the recipient in the Claimed event.
vm.expectEmit(true, true, false, false);
emit Claimed(treasureHash, caller);
hunt.claim(proof, treasureHash, recipient);
// ETH was transferred to the real recipient.
assertEq(recipient.balance, recipientBalanceBefore + reward);
// The caller did not receive the reward.
assertEq(caller.balance, callerBalanceBefore);
// The event was wrong because it emitted `caller`, not `recipient`.
assertTrue(caller != recipient);
}
}

Recommended Mitigation

Emit the actual reward recipient instead of msg.sender.

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

Support

FAQs

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

Give us feedback!