SNARKeling Treasure Hunt

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

[M-01] Proof statement omits chain/deployment context and allows cross-chain replay

Author Revealed upon completion

Root + Impact

Description

In multi-deployment setups, the normal behavior is that a proof should be valid only for one intended deployment context.

The issue is that public inputs include only treasureHash and recipient. Without block.chainid and address(this) in the verified statement, the same proof can verify on another deployment that uses compatible circuit/verifier artifacts.

// contracts/src/TreasureHunt.sol
bytes32[] memory publicInputs = new bytes32[](2); // @> no chainId, no contract address
publicInputs[0] = treasureHash;
publicInputs[1] = bytes32(uint256(uint160(address(recipient))));
bool ok = verifier.verify(proof, publicInputs);

Risk

Likelihood:

  • Replay is straightforward once an additional deployment with compatible verifier/circuit artifacts exists.

  • The proof payload and public inputs are reusable without additional secrets.

Impact:

  • One successful finder can claim corresponding rewards on multiple deployments.

  • Any multi-chain rollout inherits duplicated payout risk from one proof.

Proof of Concept

A minimal cross-chain replay flow:

// Minimal replay flow across two deployments
bytes memory proof = validProofForTreasureAndRecipient;
bytes32 hash = HASH_1;
address payable r = payable(address(0x1234));
// Chain A deployment
huntA.claim(proof, hash, r); // succeeds
// Chain B deployment with same verifier/circuit statement
huntB.claim(proof, hash, r); // also succeeds because statement omits chain/deployment binding

Written explanation: verification binds only (treasureHash, recipient), not deployment identity. So the same statement can be replayed on another compatible deployment. This is not exploitable on a single isolated deployment.

Recommended Mitigation

Use function claim(bytes calldata proof, bytes32 treasureHash, address payable recipient) and circuit main(...).

- bytes32[] memory publicInputs = new bytes32[](2);
- publicInputs[0] = treasureHash;
- publicInputs[1] = bytes32(uint256(uint160(address(recipient))));
+ bytes32[] memory publicInputs = new bytes32[](4);
+ publicInputs[0] = bytes32(block.chainid);
+ publicInputs[1] = bytes32(uint256(uint160(address(this))));
+ publicInputs[2] = treasureHash;
+ publicInputs[3] = bytes32(uint256(uint160(address(recipient))));

Update Noir main public inputs accordingly and regenerate verifier artifacts.

Support

FAQs

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

Give us feedback!