SNARKeling Treasure Hunt

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

Recipient is not constrained in the Noir circuit, allowing proof front-running to steal rewards

Author Revealed upon completion

ROOT - circuits/src/main.nr ->Recipient public input is never used in any circuit constraint

Impack: Any valid proof can be submitted with an arbitrary recipient, allowing front-runners to steal rewards from legitimate finders

Description

  • The claim() function accepts a recipient address as a public input and passes it to the verifier. The Noir circuit is designed to bind the proof to a specific recipient to prevent replay attacks.

    The recipient field is declared as a public input in the circuit but is never used in any constraint. A proof generated for one recipient is valid for any other recipient, making the binding completely ineffective.

fn main(treasure: Field, treasure_hash: pub Field, recipient: pub Field) {
assert(is_allowed(treasure_hash));
assert(std::hash::pedersen_hash([treasure]) == treasure_hash);
// @> recipient is never referenced in any constraint
// @> any proof is valid for any recipient
}

Risk

Likelihood:

  • Likelihood:

    • A legitimate participant submits a claim() transaction, it is visible in the public mempool before inclusion, giving attackers the proof bytes and treasure hash

    • MEV bots and searchers routinely monitor the mempool for profitable front-running opportunities with no manual effort required

Impact:

  • An attacker copies the proof from the mempool and resubmits it with their own address as recipient , stealing the 10 ETH reward from the legitimate finder

Every single treasure claim is vulnerable , no participant can safely claim any reward on a live network

Proof of Concept

1. Alice finds a physical treasure and generates a valid ZK proof with recipient = Alice
2. Alice submits claim(proof, treasureHash, Alice) to the network
3. Bob sees the pending transaction in the mempool
4. Bob submits claim(proof, treasureHash, Bob) with higher gas fee
5. Bob's transaction is mined first
6. Verifier accepts the proof because recipient is unconstrained
7. Bob receives 10 ETH, Alice receives nothing

Recommended Mitigation

Include recipient in the pedersen hash preimage so the proof // is cryptographically bound to a specific recipient address. Bind recipient into the proof by hashing it with the treasure.

NOTE: ALLOWED_TREASURE_HASHES and the off-chain hash generation must also be updated to use pedersen_hash([treasure, recipient])

- fn main(treasure: Field, treasure_hash: pub Field, recipient: pub Field) {
- assert(is_allowed(treasure_hash));
- assert(std::hash::pedersen_hash([treasure]) == treasure_hash);
- }
+ fn main(treasure: Field, treasure_hash: pub Field, recipient: pub Field) {
+ assert(is_allowed(treasure_hash));
+ assert(std::hash::pedersen_hash([treasure, recipient]) == treasure_hash);
+ }

Support

FAQs

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

Give us feedback!