SNARKeling Treasure Hunt

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

Unconstrained Public Input (Recipient Malleability)

Author Revealed upon completion

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);
}

Support

FAQs

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

Give us feedback!