SNARKeling Treasure Hunt

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

Treasure secrets have ~3.3 bits of entropy and are plaintext-disclosed in `Deploy.s.sol`, so anyone can generate valid proofs without physically finding any treasure

Author Revealed upon completion

Scope: contracts/scripts/Deploy.s.sol, circuits/src/main.nr

Root + Impact

Description

The Noir circuit proves knowledge of a preimage treasure for one of
10 allowed Pedersen hashes. This hides which treasure the prover
found, but only if the secret space itself is hard to brute-force and
is not publicly disclosed.

Two independent problems break that security assumption:

(a) The secrets are tiny-integer plaintext and are published in the
in-scope
Deploy.s.sol.

// contracts/scripts/Deploy.s.sol (lines 14–26)
// Secret Treasures for the snorkeling hunt (not revealed to the public):
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
// Treasures' hashes (revealed to the public, used as public inputs for the proof generation):
// 1505662313093145631275418581390771847921541863527840230091007112166041775502,
// ...

The deploy script is in scope per the contest README, and the comments
literally enumerate all 10 secret values and their Pedersen hashes.
Anyone who reads the public GitHub repo already has all preimages.

(b) Even if the disclosure were redacted, the secret space is
enumerable.
Prover.toml.example and tests.nr confirm the secrets
are small ASCII-decimal integers (1..10). The total preimage space
tried during brute-force is effectively just single-digit integers,
which a laptop computes in milliseconds:

# pseudocode
for candidate in range(0, 1_000_000):
h = pedersen_hash([candidate])
if h in ALLOWED_TREASURE_HASHES:
# Found a secret without leaving the shore.

So even without the leak in the comment, the entropy is ~3.3 bits
(log2(10)) — trivially brute-forceable. main.nr checks knowledge of
the preimage but does not incorporate any location-bound or high-entropy
salt, so no physical treasure-finding effort is required to produce a
valid proof.

Risk

Likelihood: HIGH

  • The disclosure exists as of the contest snapshot and is part of the
    in-scope repo; no adversarial reconnaissance is required.

  • Independently of the disclosure, a ~3.3-bit search is trivial.

Impact: HIGH

  • An attacker can generate valid ZK proofs for all 10 treasures
    without any physical discovery and, combined with Finding 1 (the
    double-claim bug), drain the entire 100 ETH reward pool in seconds
    after deployment. Even without Finding 1 in play, the attacker
    harvests 9 × 10 ETH = 90 ETH honestly, while the hunt organizer
    loses the entire advertised reward budget and the physical
    snorkelers get nothing.

  • The protocol's core economic and game-design guarantee — "only
    people who find the treasure in the ocean can claim"
    — is
    invalidated end-to-end.

Proof of Concept

  1. Read contracts/scripts/Deploy.s.sol lines 14–15:

    // Secret Treasures for the snorkeling hunt (not revealed to the public):
    // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

    Attacker now has every secret.

  2. For each secret s ∈ {1..10}, run the circuit's normal proof
    generation path (off-chain):

    cd circuits/scripts
    TREASURE_INDEX=0 ./build.sh # generates fixture for secret = 1
    # ... repeat for TREASURE_INDEX = 1..9 (with recipient = attacker's EOA)

    All proofs are valid.

  3. Submit each proof on-chain to claim() (choosing any attacker-
    controlled recipient that is ≠ owner, ≠ address(0),
    address(this), ≠ msg.sender). Total take ≤ 9 × 10 = 90 ETH
    (or 100 ETH when combined with Finding 1).

Brute-force-only variant (assuming the Deploy.s.sol comment were
removed in a future deployment): iterate candidate preimages
0..10⁶ through pedersen_hash([·]), match against the public
ALLOWED_TREASURE_HASHES. Runtime: on the order of seconds on a
laptop. Same on-chain consequence.

Recommended Mitigation

  1. Remove the plaintext-secret comment from Deploy.s.sol. Even
    for a demo, the comment teaches the wrong pattern and is directly
    exploitable if the script is deployed as-is on mainnet.

  2. Raise secret entropy to the field order. Use cryptographically
    random 254-bit Field values (e.g. via
    openssl rand -hex 32 mod BN254 field prime) as secrets. Store
    secrets off-chain with the physical treasures (e.g. waterproof
    QR-code cards); never commit them to the repo.

  3. Bind secrets to the physical location. If the goal is "prover
    was physically present at a specific GPS point at a specific time",
    the proof must bind something only physical presence can reveal
    (e.g. a hash of a beacon broadcast, a QR-code signature, a
    rolling code). Pure pedersen_hash([random_secret]) still fails
    to proof-of-location; it only proves knowledge. At minimum,
    use a high-entropy secret so the proof is meaningful.

  4. Consider adding an immutable salt to the circuit so that the same
    10 preimages can't be reused across redeployments — each hunt has a
    per-instance salt baked into its verifier. Combine with chain-ID /
    contract-address domain separation.

Disclosure

This finding was identified and written up with the assistance of an
autonomous AI auditor (Anthropic Claude). Additional LLM cross-checks
used to rule out false positives.

Support

FAQs

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

Give us feedback!