SNARKeling Treasure Hunt

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

Lack of msg.sender proof binding allows Front-Runners to hijack Claimed event log attribution

Root + Impact

Description

  • TheClaimedevent is intended to record who discovered and solved each treasure, serving as an on-chain attribution record for leaderboards, NFT rewards, or reputation systems

  • The event incorrectly emitsmsg.senderinstead ofrecipient, allowing front-runners to steal attribution credit since the ZK proof binds therecipientaddress but NOT themsg.sender

// Root cause in the codebase
function claim(bytes calldata proof, bytes32 treasureHash, address payable recipient) external nonReentrant() {
...
// ZK proof verification binds recipient as a public input
bytes32[] memory publicInputs = new bytes32[](2);
publicInputs[0] = treasureHash;
publicInputs[1] = bytes32(uint256(uint160(recipient))); // @> Proof is bound to recipient
bool isValid = verifier.verify(proof, publicInputs);
...
(bool sent, ) = recipient.call{value: REWARD}(""); // @> ETH correctly goes to recipient
...
emit Claimed(treasureHash, msg.sender); // @> Bug: emits msg.sender instead of recipient
}

Risk

Likelihood: HIGH

  • This will occur whenever a valid proof transaction is visible in the mempool before confirmation

  • MEV bots and front-runners actively monitor for valuable transactions to copy

  • The attacker simply copies the exact calldata and submits with higher gas

Impact: MEDIUM

  • Front-runners steal on-chain attribution for all treasure discoveries without solving any puzzles

  • Legitimate solvers receive their ETH rewards but lose all recognition/credit

  • Corrupts leaderboards, NFT distributions, or reputation systems based on

    Claimed

    events

  • In competitive CTF environments, this allows one attacker to claim credit for all 10 treasures

Proof of Concept

The vulnerability allows front-runners to steal attribution credit while the legitimate solver still receives the ETH reward. This attack is particularly damaging in competitive environments where reputation matters.

/ Setup: Alice solves a treasure and generates a valid ZK proof
address alice = address(0xA11CE);
address aliceColdWallet = address(0xC01D);
bytes memory validProof = generateProof(secretPreimage, treasureHashX, aliceColdWallet);
// Step 1: Alice submits her claim transaction to the mempool
// Transaction data: claim(validProof, treasureHashX, aliceColdWallet)
// Alice expects: 10 ETH to aliceColdWallet, Claimed event crediting alice
// Step 2: Front-runner bot (Bob) monitors mempool and sees Alice's transaction
address bob = address(0xB0B);
// Step 3: Bob copies Alice's exact calldata but submits from his address with higher gas
vm.prank(bob);
hunt.claim(validProof, treasureHashX, payable(aliceColdWallet)); // Same proof & recipient
// Result analysis:
assert(aliceColdWallet.balance == 10 ether); // Alice's wallet got the ETH (correct)
// But check the emitted event:
// Event: Claimed(treasureHashX, bob) // Bob gets the credit!
// NOT: Claimed(treasureHashX, alice) or Claimed(treasureHashX, aliceColdWallet)
// Impact on leaderboard/reputation system:
// Leaderboard query: "Who solved treasureHashX?"
// Answer from events: bob (wrong - Bob just front-ran, didn't solve anything)
// Alice solved the puzzle but gets no recognition
// Attack at scale:
contract FrontRunBot {
function stealAllCredit(TreasureHunt hunt) external {
// Monitor mempool for all claim() transactions
// Copy their calldata exactly
// Submit with higher gas from this contract
// Result: All Claimed events show this contract as solver
// Real solvers get ETH but no credit
}
}

Recommended Mitigation

Emit therecipientaddress instead ofmsg.senderin theClaimedevent, since the recipient is cryptographically bound to the proof:

- 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.

front-running to steal on-chain attribution

The contract’s attribution event records `msg.sender` instead of the actual rewarded recipient. A mempool watcher can copy a legitimate claimant’s transaction and front-run it using the same proof, `treasureHash`, and bound recipient, causing the ETH to still go to the rightful recipient while the on-chain event credits the attacker’s address as the claimer. That means the core economic asset is not stolen, but the protocol’s on-chain attribution layer is, which is a real integrity failure wherever `Claimed` is used as the canonical record of who found a treasure for leaderboards, reputation, prizes, or any other downstream recognition system. In a competitive contest setting, this enables a single bot to systematically preempt honest solvers and capture apparent credit for every discovery without ever finding the treasure.

Support

FAQs

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

Give us feedback!