Skip to content

Allowlist proofs

An allowlisted sale stores a 32-byte Merkle root and a leaf count. Each leaf is one wallet with its index and cap. A wallet commits by presenting its index, cap, and proof.

leaf = blake2b256( 0x00 || "blast-presale-v1" // the byte 0x00, then ASCII
|| bcs(index: u64) // 8 bytes, little-endian
|| bcs(wallet: address) // 32 bytes
|| bcs(wallet_cap: u64) ) // 8 bytes, little-endian
node(left, right) = blake2b256( 0x01 || left || right )

The contract walks up from the leaf with i = index. For each sibling in order, it computes node(digest, sibling) when i is even and node(sibling, digest) when i is odd, then halves i. The result must equal the root.

  • The proof must have exactly ceil(log2(leaf_count)) siblings, each 32 bytes. A single-leaf tree has an empty proof, and its root is the leaf.
  • index must be below leaf_count, and wallet_cap must be positive.
  • The leaf’s wallet must be the transaction sender.
  • The contract takes each sibling as given, so the tree builder chooses how to handle an unpaired node. The builder below pairs it with itself.

This builder uses @noble/hashes and the BCS encoder from @mysten/sui.

import { blake2b } from "@noble/hashes/blake2.js";
import { bcs } from "@mysten/sui/bcs";
export type Entry = { wallet: string; cap: bigint };
const LEAF_PREFIX = new Uint8Array([0x00, ...new TextEncoder().encode("blast-presale-v1")]);
const hash = (...parts: Uint8Array[]) => {
const bytes = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
let offset = 0;
for (const p of parts) {
bytes.set(p, offset);
offset += p.length;
}
return blake2b(bytes, { dkLen: 32 });
};
export const leaf = (index: number, { wallet, cap }: Entry) =>
hash(
LEAF_PREFIX,
bcs.u64().serialize(index).toBytes(),
bcs.Address.serialize(wallet).toBytes(),
bcs.u64().serialize(cap).toBytes(),
);
const node = (left: Uint8Array, right: Uint8Array) => hash(new Uint8Array([0x01]), left, right);
/** Builds every level of the tree. An unpaired node is paired with itself. */
export function buildTree(entries: Entry[]): Uint8Array[][] {
const levels: Uint8Array[][] = [entries.map((entry, i) => leaf(i, entry))];
while (levels.at(-1)!.length > 1) {
const below = levels.at(-1)!;
const above: Uint8Array[] = [];
for (let i = 0; i < below.length; i += 2) above.push(node(below[i], below[i + 1] ?? below[i]));
levels.push(above);
}
return levels;
}
export const root = (levels: Uint8Array[][]) => levels.at(-1)![0];
/** The siblings from the leaf up, in the order the contract consumes them. */
export function proof(levels: Uint8Array[][], index: number): Uint8Array[] {
const siblings: Uint8Array[] = [];
for (let level = 0, i = index; level < levels.length - 1; level++, i = Math.floor(i / 2)) {
const row = levels[level];
siblings.push(row[i ^ 1] ?? row[i]);
}
return siblings;
}

Publish the root and the leaf count with allowlisted_eligibility(root, leaf_count) when you create the sale, and serve each wallet its index, cap, and proof so your app can commit.

Caps may be equal, tiered, or custom. Allowlisted wallets have no minimum commitment.