SNARKeling Treasure Hunt

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

Unconstrained Public Input (Recipient Malleability)

Root + Impact

Description

In a Noir circuit, pub only defines the interface (how the Solidity contract sends data to the Verifier). It does not define the integrity of that data. To ensure a public input is part of the proof, it must be part of the constraint graph.

In the current main function, the recipient is effectively a "dead" variable.

// Root cause in the Noir code
fn main(treasure: Field, treasure_hash: pub Field, recipient: pub Field) {
assert(is_allowed(treasure_hash));
assert(std::hash::pedersen_hash([treasure]) == treasure_hash);
// @> THE RECIPIENT IS NEVER USED.
// This proof is valid for ANY recipient address in the world.
}

Risk

Likelihood: High

  • The proof is "malleable." An attacker doesn't need to change the proof bytes; they only need to change the publicInputs[1] value in the Solidity call.

Impact: High

  • Direct theft of the 10 ETH reward. The person who actually found the treasure gets nothing, while the attacker (the one who changed the recipient input) gets the payout.

Proof of Concept

Recommended Mitigation,

You must force the circuit to "notice" the recipient. The best way to do this is to include the recipient in the hash that defines the treasure discovery. This creates a cryptographic link: "This proof is only valid for Treasure X IF the reward goes to Recipient Y."

fn main(treasure: Field, treasure_hash: pub Field, recipient: pub Field) {
assert(is_allowed(treasure_hash));
- assert(std::hash::pedersen_hash([treasure]) == treasure_hash);
+ // Use both treasure and recipient to create a unique commitment
+ let commitment = std::hash::pedersen_hash([treasure, recipient]);
+ assert(commitment == treasure_hash);
}
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'.

Support

FAQs

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

Give us feedback!