SNARKeling Treasure Hunt

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

Front-running (Proof Theft)

Author Revealed upon completion

Root + Impact

Description

The intended behavior of the claim() function is that only the legitimate user who generated a valid ZK proof should be able to claim the reward associated with a specific treasureHash.
However, all parameters required to execute a valid claim — including proof, treasureHash, and recipient — are submitted in plaintext and visible in the public mempool before the transaction is mined. The contract does not bind the proof to msg.sender, nor does it implement any protection such as a commit-reveal scheme or nullifier.

// Root cause in the codebase.

function claim(bytes calldata proof, bytes32 treasureHash, address payable recipient) external nonReentrant {
// Proof verification does not include msg.sender binding
bool ok = verifier.verify(proof, publicInputs);
if (!ok) revert InvalidProof();
}
Because of this, an attacker observing the mempool can copy a valid transaction submitted by a legitimate user and submit the exact same transaction with a higher gas price, causing their transaction to be mined first.

Risk

Likelihood:

  • Reason 1 // This occurs whenever a user submits a valid claim() transaction, as all inputs are publicly visible in the mempool before confirmation.

  • Reason 2 // Attackers can easily monitor the mempool and prioritize their transaction using higher gas fees

Impact:

  • Impact 1 // Legitimate users can have their rewards stolen before their transaction is mined.

  • Impact 2 //Attackers can consistently capture rewards without generating valid proofs themselves

Proof of Concept

function testFrontRunningPossible() public {

( bytes memory proof,

bytes32 treasureHash,

address payable recipient

) = _loadFixture();

address payable attackerRecipient = payable(address(0xBADBEEF));
// Attacker copies the exact transaction and front-runs
vm.prank(attacker);
hunt.claim(proof, treasureHash, recipient);
// Original user transaction would now fail or lose reward opportunity

// Show that the SAME treasureHash is reused
assertTrue(hunt.claimed(treasureHash));

// Yet it still allows another claim → proves logic bug
hunt.claim(proof, treasureHash, recipient);

// Critical proof: wrong key is being checked
assertFalse(hunt.claimed(bytes32(0))); // the actual checked key

Recommended Mitigation

// Option 1: Bind proof to msg.sender inside the circuit

  • Include msg.sender as part of publicInputs and verify it inside the ZK proof
    // Option 2: Commit-reveal scheme

  • Users first submit a hash(commitment) of their proof

  • Later reveal the full proof to prevent mempool theft
    // Option 3: Use a nullifier

  • Track used proofs or unique identifiers to prevent replay/front-running

Support

FAQs

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

Give us feedback!