SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Recipient Not Bound in Circuit Enables Proof Replay and Front-Running Theft

Description

The Noir circuit defines recipient as a public input intended to bind a zero-knowledge proof to a specific payout address. However, the recipient variable is never used in any circuit constraint, meaning it is not enforced during proof verification.

In zk-SNARK circuits, public inputs are only meaningful if they participate in constraints. Since recipient is not used anywhere in the circuit logic, it is effectively unconstrained and can be set to any value without affecting proof validity.

The circuit only enforces:

  • The treasure_hash is in the allowed set

  • The treasure_hash equals Pedersen(treasure)

There is no cryptographic link between the recipient and the proof.

As a result, a valid proof can be reused with a different recipient, breaking the intended replay-resistance mechanism and enabling reward redirection.


Risk

This issue breaks the protocol’s core assumption that proofs are bound to a specific recipient.

An attacker can intercept a valid proof submission from the mempool, replace the recipient address with their own, and still pass verification successfully.

Impact:

  • Reward redirection to attacker-controlled addresses

  • Broken replay-resistance guarantees

  • Loss of integrity in proof-to-recipient binding

  • Enables straightforward front-running attacks

This results in fund theft with minimal effort, making the issue Critical severity.


Proof of Concept

Exploit Steps

  1. A valid proof is generated off-chain for a treasure:

treasure = secret
treasure_hash = H(secret)
recipient = Alice
  1. The honest user submits:

claim(proof, treasureHash, Alice);
  1. The attacker copies the same proof and treasureHash from the mempool.

  2. The attacker modifies only the recipient:

claim(proof, treasureHash, attacker_address);

Why this works

Inside the circuit:

pub recipient: Field;

However, recipient is never used in any constraint, meaning it is not enforced by the proof system.

Therefore:

  • The same proof is valid for any recipient

  • Changing the recipient does not invalidate the proof

Step 1 — Honest user generates a valid proof

A user finds a valid treasure and generates a proof:

  • treasure = secret

  • treasure_hash = H(secret)

  • recipient = Alice

They submit:

claim(proof, treasureHash, Alice);

Step 2 — Attacker observes transaction

An attacker monitors the mempool and copies:

  • proof

  • treasureHash


Step 3 — Attacker modifies recipient only

The attacker resubmits the same proof but changes the recipient:

claim(proof, treasureHash, attacker_address);

Step 4 — Result

  • The proof still verifies successfully

  • Because recipient is not enforced in the circuit

  • The contract accepts the modified recipient

  • Reward is transferred to the attacker instead of the original prover


Key Insight

Inside the circuit:

pub recipient: Field;

However, recipient is never used in any constraint, making it completely unbound and non-verifiable.


Recommended Mitigation

The recipient must be explicitly included in circuit constraints so it becomes part of the cryptographic proof.


Option 1 — Direct binding in hash

Bind recipient into the Pedersen hash:

assert(std::hash::pedersen_hash([treasure, recipient]) == treasure_hash);

This ensures the proof is uniquely tied to both the treasure and the recipient.

Requires regeneration of all allowed treasure hashes and circuit artifacts.


Option 2 — Preferred design (clean separation)

Separate treasure identity and recipient binding:

assert(std::hash::pedersen_hash([treasure]) == treasure_hash);
assert(std::hash::pedersen_hash([treasure, recipient]) == binding);

Where:

  • treasure_hash verifies the secret

  • binding enforces recipient-specific validity

This design prevents replay attacks while keeping responsibilities separated.


Result After Fix

After applying the fix:

  • Proofs are cryptographically bound to recipients

  • Proof replay with modified recipients becomes invalid

  • Front-running and reward redirection are prevented

  • The protocol’s intended security guarantees are restored

Updates

Lead Judging Commences

s3mvl4d Lead Judge 18 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

unused "recipient" in circuit

The claim that the proof system is broken because the recipient is not explicitly constrained in the Noir circuit reflects a misunderstanding of how zero-knowledge proofs bind public inputs. Although the circuit does not impose algebraic constraints on recipient, the value is still included in the public input vector, which is cryptographically committed to during proof generation. As a result, the proof is only valid for the exact tuple of public inputs it was created with. Any attempt by an attacker to front-run and substitute a different recipient would alter this tuple, causing the verifier’s check to fail because the proof no longer matches the provided public inputs. Therefore, while unconstrained public inputs do not enforce logical relationships within the circuit, they remain inseparably bound to the proof itself, and this binding is sufficient to prevent tampering or replay with modified values. Run the unit tests 'testClaimInvalidProofFails', 'testFrontRunningClaimFails'.

Appeal created

cybervikink Submitter
18 days ago
s3mvl4d Lead Judge
17 days ago
s3mvl4d Lead Judge 15 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

unused "recipient" in circuit

The claim that the proof system is broken because the recipient is not explicitly constrained in the Noir circuit reflects a misunderstanding of how zero-knowledge proofs bind public inputs. Although the circuit does not impose algebraic constraints on recipient, the value is still included in the public input vector, which is cryptographically committed to during proof generation. As a result, the proof is only valid for the exact tuple of public inputs it was created with. Any attempt by an attacker to front-run and substitute a different recipient would alter this tuple, causing the verifier’s check to fail because the proof no longer matches the provided public inputs. Therefore, while unconstrained public inputs do not enforce logical relationships within the circuit, they remain inseparably bound to the proof itself, and this binding is sufficient to prevent tampering or replay with modified values. Run the unit tests 'testClaimInvalidProofFails', 'testFrontRunningClaimFails'.

Support

FAQs

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

Give us feedback!