पाठ 12 / 25

ERC-1155, Signatures and Merkle Allowlists

Use multi-token contracts and gas-efficient allowlists.

Multi-tokens and efficient distribution

ERC-1155 is a multi-token standard: one contract manages many token types, each identified by an id, which may be fungible (many copies, like game gold) or non-fungible (supply of one). It supports batch transfers and batch balance queries, saving gas for games and collections with many item types. Metadata uses a single URI template with {id} substitution. For distributing tokens to a known list of addresses, storing every address on chain is expensive. A Merkle tree solves this: compute a tree off chain over all eligible entries, store only the 32-byte Merkle root in the contract, and let each user submit a proof (a list of sibling hashes) showing their entry is in the tree; OpenZeppelin's MerkleProof.verify checks it. Alternatively, a trusted signer can issue signed vouchers (EIP-712 typed data) that users redeem, which is flexible but relies on the signer key. In both cases, track claimed status to prevent double claims and include the contract address and chain id in what is signed or hashed to prevent replay on other deployments.

A Merkle allowlist claim

Only the root is stored on chain; users submit proofs.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract EventBadges is ERC1155 {
    uint256 public constant ATTENDEE = 1;
    uint256 public constant SPEAKER = 2;

    bytes32 public immutable merkleRoot;
    mapping(address => bool) public claimed;

    error AlreadyClaimed();
    error InvalidProof();

    constructor(bytes32 root) ERC1155("ipfs://bafy.../{id}.json") {
        merkleRoot = root;
    }

    /// Leaves are keccak256(bytes.concat(keccak256(abi.encode(account, badgeId))))
    function claim(uint256 badgeId, bytes32[] calldata proof) external {
        if (claimed[msg.sender]) revert AlreadyClaimed();
        bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, badgeId))));
        if (!MerkleProof.verify(proof, merkleRoot, leaf)) revert InvalidProof();
        claimed[msg.sender] = true;
        _mint(msg.sender, badgeId, 1, "");
    }
}
// The double hash matches OpenZeppelin's standard Merkle tree JavaScript library
// and prevents second-preimage attacks on 64-byte leaves.

Use the matching off-chain library

Leaf encoding must match exactly between the script that builds the tree and the contract. OpenZeppelin's merkle-tree JavaScript package produces double-hashed leaves compatible with the pattern above.

त्वरित जाँच: What does a Merkle allowlist store on chain?

  • Only the Merkle root; users provide proofs of inclusion
  • Every eligible address
  • The private keys of users
  • Nothing at all
Answer

Only the Merkle root; users provide proofs of inclusion — A single 32-byte root commits to the whole list.