SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Treasure witness domain is limited to a tiny enumerable set

Root + Impact

Description

  • Normal behavior: The private witness should have large entropy so offline guessing cannot recover valid proofs faster than legitimate discovery.

  • Problem: Fixtures and deploy commentary bind treasure to the strings 1 through 10. The private input space is tiny, so an adversary can enumerate candidates offline against the fixed allowlist and produce valid proofs without physical participation.

// Prover.toml.example — treasure column aligned with contest fixtures
treasure = ["1", "2", ... "10"]
// @> circuit only checks H(treasure) == treasure_hash for one Field witness
assert(std::hash::pedersen_hash([treasure]) == treasure_hash);

Risk

Likelihood:

  • Ten candidates fit in a short offline loop once public treasure_hash values are known.

  • The contest README and example inputs document the same small domain.

Impact:

  • The real-world hunt stops being a strong differentiator versus pure off-chain grinding.

  • First mover with automation can claim before honest participants who rely on physical discovery.

Proof of Concept

Explanation: The shipped example binds each row’s private treasure to a single-digit string ("1""10"), so the search space is 10 candidates per treasure, not a large random secret. After build.sh materializes a witness from that row, the same valid proof bytes work with TreasureHunt.claim as in snarkeling-1 (no physical discovery required).

Supporting code — build fixture and run on-chain PoC (from contest repo root):

cd circuits
TREASURE_INDEX=0 ./scripts/build.sh
cd ..
forge test --match-test testPoC_RepeatedClaimDrainsAllRewards -vv

Supporting code — the Foundry test proves one fixture proof pays out repeatedly (same root issue as snarkeling-1; here it demonstrates the small-witness fixture is sufficient to drain the pool):

// contracts/test/TreasureHuntPoC.t.sol
function testPoC_RepeatedClaimDrainsAllRewards() public {
(bytes memory proof, bytes32 treasureHash, address payable recipient) = _fixture();
uint256 recipientBefore = recipient.balance;
vm.startPrank(PARTICIPANT);
for (uint256 i = 0; i < 10; i++) {
hunt.claim(proof, treasureHash, recipient);
}
vm.stopPrank();
assertEq(hunt.claimsCount(), 10);
assertEq(address(hunt).balance, 0);
assertEq(recipient.balance, recipientBefore + (10 * hunt.REWARD()));
}

Offline enumeration (conceptual): For a target treasure_hash, at most 10 treasure strings from Prover.toml.example need to be tried in nargo until pedersen_hash([treasure]) == treasure_hash and is_allowed succeed; then bb prove yields a valid proof.bin for claim.

Recommended Mitigation

Explanation: Demo fixtures that use ten tiny string witnesses make offline brute force trivial. For production, draw high-entropy secrets per treasure, optionally commit to them (hash or Merkle root) before the hunt opens, and keep witnesses out of public repos. Align Prover.toml examples with production entropy policy or label them clearly as non-production.

-// Use single-digit string treasures in fixtures for demos only
+// Production: 256-bit+ random secrets per treasure; publish commitments before the hunt
Updates

Lead Judging Commences

s3mvl4d Lead Judge 18 days ago
Submission Judgement Published
Validated
Assigned finding tags:

secrets can be brute-forced

The protocol’s “secret” is not drawn from a high-entropy space; instead, the treasure values are just small scalar integers, and the circuit proves validity by checking whether the public `treasure_hash` equals `pedersen_hash([treasure])`. An attacker can offline-enumerate all plausible treasure values, compute their Pedersen hashes, and recover the matching secret with negligible effort.

Support

FAQs

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

Give us feedback!