Skip to content
⌘K

Contracts

Launchpad contract

The launchpad Move package: create a collection with its own token type, set mint phases, mint sequentially or at random, reveal and pay out.

The launchpad package launches NFT collections on Sui. Each collection gets its own token type, its own TransferPolicy and Display, mint phases in any coin, optional allowlists, sequential or fair random mints, a one-time reveal and verified traits. It is a separate package from the marketplace and depends only on the Sui framework and Mysten's kiosk rules package.

Not yet live on mainnet The package id appears here once the package is published.

On this page, PACKAGE_ID is the latest launchpad package id, C is a collection's marker type and T = PACKAGE_ID::nft::Nft<C> is its token type. Amounts are in base units of the payment coin (MIST for SUI, 10^-6 for USDC).

Model

  • One type per collection. The creator publishes a one-module marker package (a template, renamed per collection, for example ghosts::GHOSTS) and keeps the Publisher its init claims. collection::create<C> checks that Publisher against C and binds C to the new collection in the Registry, so each marker launches once. The marker package depends only on the Sui framework and never needs upgrading.
  • Tokens are Nft<C>: a distinct type per collection, minted only by launchpad code, so supply caps hold.
  • Policy and display. The launchpad creates TransferPolicy<T> (shared) and Display<T> for each collection. The TransferPolicyCap stays inside the collection so royalties can always be paid out. The creator can take it out with release_policy_cap.
  • Marketplace fit. T is a plain key + store type with a standard policy, so the marketplace trades it like any other collection.

Modules

ModuleWhat it does
collectionCreating collections, phases, stock, reveal, royalties, payouts, traits, views.
mintSequential and random mints, airdrops and reserve mints.
nftThe Nft<C> token.
platformThe shared Config: fee, treasury, pause switch and version.
registryMarker to collection index, and featured collection entries.
merkleAllowlist and token metadata hashing.
eventsEvery event the package emits.

Shared objects and caps

ObjectSharingKey fields
platform::Configshared, onetreasury, fee_bps (default 10 = 0.1%), queued, paused, version
platform::AdminCapownedFee and treasury queue, global pause, collection block, featured approvals. Handed to a multisig after publishing.
registry::Registryshared, onekinds: Table<TypeName, ID> (marker to collection) and featured entries
collection::Collection<C>shared by sharename, symbol, creator, max_supply (0 for an open edition), ends_ms, reserve, minted, random, phases, payees, policy, royalty_recipient, revealed, paused, blocked, version. Proceeds per coin sit in dynamic fields.
collection::CreatorCap<C>owned, transferableAuthority over one collection.
collection::Phasevaluename, start_ms (inclusive), end_ms (exclusive), price, coin, per_wallet (0 for no limit), cap (0 for no limit), minted, root (allowlist)
nft::Nft<C>owned or in a kioskcollection, n (1-based token number), attributes: VecMap<String, String> (empty until attached with a proof)
TransferPolicy<T>sharedRules chosen at create: royalty_rule (bps, min), kiosk_lock_rule, personal_kiosk_rule.
Display<T>in the collection until reveal, then frozenname = "{collection name} #{n}", description, image_url, thumbnail_url, project_url, attributes

Rules every call follows

  • Gates. Creating collections and minting take &Config and abort with 101 while the platform is paused or 102 when this package version is retired. Mints also abort with 210 while the creator paused the collection or the platform blocked it.
  • Never gated. withdraw, claim_royalties, release_policy_cap and the views ignore every pause and version switch.
  • Exact payments. Mints split price * qty from your &mut Coin<Pay> and you keep the rest, so a failed mint never holds funds.
  • Destination follows the policy. With a kiosk lock rule, tokens are locked into a kiosk: yours (mint_into) or a new one created for you (mint_fresh, personal when the policy has the personal kiosk rule). Wallet mints of such a collection abort (250). Without the lock rule, tokens go to your wallet, or are placed (not locked) in a kiosk when you pass one.
  • Text that goes into Display templates (name, description, project URL, image URIs and extension) must not contain {, } or \ (204).
  • Type names in events are type_name::with_original_ids, in JSON without the 0x prefix.

Creating a collection

Terms

These are pure constructors. Pass their results to create.

Move
public fun capped(max: u64, reserve: u64, random: bool): Supply
public fun open_edition(ends_ms: u64, reserve: u64): Supply
public fun revealed_art(base_uri: String, ext: String, metadata_root: Option<vector<u8>>): Art
public fun hidden_art(placeholder: String, provenance: Option<vector<u8>>): Art
public fun royalty(bps: u16, min: u64, recipient: address, lock: bool, personal: bool): Royalty
  • capped: max > 0 and reserve <= max (202). Public mints stop at max - reserve. The reserve is for the creator only.
  • open_edition: no cap. Every mint, reserve included, must happen before ends_ms (219). Always sequential.
  • revealed_art: art is live from the start at {base_uri}/{n}.{ext} and the Display is frozen at create. A metadata root commits every token's traits up front.
  • hidden_art: placeholder image until reveal. provenance = blake2b256(metadata_root) binds the reveal to that root.
  • royalty: bps <= 10000. bps = min = 0 adds no royalty rule. lock adds the kiosk lock rule and personal the personal kiosk rule.

create

Move
public fun create<C>(
    reg: &mut Registry,
    cfg: &Config,
    publisher: &Publisher,
    name: String,
    symbol: String,
    description: String,
    project_url: String,
    supply: Supply,
    art: Art,
    royalty: Royalty,
    payees: vector<address>,
    shares: vector<u16>,
    ctx: &mut TxContext,
): (Collection<C>, CreatorCap<C>)

Creates the collection, shares its TransferPolicy and returns the collection unshared together with the creator cap. Add phases (and stock, for random collections) in the same PTB, then call share<C>, the only way to consume a collection. Aborts: 201 the publisher is not from C's module, 151 C was launched before, 203 payees must be 1 to 10 nonzero addresses with nonzero shares summing to 10000, 205 roots must be 32 bytes.

Phases and setup

Move
public fun add_phase<C, Pay>(cap: &CreatorCap<C>, col: &mut Collection<C>, name: String, start_ms: u64, end_ms: u64, price: u64, per_wallet: u64, phase_cap: u64, root: Option<vector<u8>>): u64
public fun edit_phase<C, Pay>(cap: &CreatorCap<C>, col: &mut Collection<C>, index: u64, name: String, start_ms: u64, end_ms: u64, price: u64, per_wallet: u64, phase_cap: u64, root: Option<vector<u8>>)
public fun stock<C>(cap: &CreatorCap<C>, col: &mut Collection<C>, count: u64)
public fun share<C>(col: Collection<C>)
  • add_phase returns the phase index. Pay is the coin type, for example 0x2::sui::SUI or USDC. start_ms < end_ms (209), at most 16 phases (208).
  • edit_phase replaces the terms and keeps the phase's minted and per-wallet counts. Close a phase by moving its end. Buyers are protected from price changes by the max_price they pass to mint.
  • stock is for random collections only (223). It loads up to 16,384 slots per call (226 when full). Random mints need stocked == max_supply (224). Stocking costs about 0.005 SUI per 64 items in storage, which is rebated to minters as the pool empties.

Other creator calls, all checked against the cap (207): set_paused, set_payees (only before the first public mint, 241), reveal, set_royalty_recipient, set_royalty (0, 0 removes the rule, 240 after the policy cap was released) and release_policy_cap.

Move
public fun reveal<C>(cap: &CreatorCap<C>, col: &mut Collection<C>, base_uri: String, ext: String, metadata_root: Option<vector<u8>>)

Runs once (230). With a provenance commitment the root must hash to it (231). Sets image_url = {base_uri}/{n}.{ext}, bumps the Display version and freezes it.

Minting

Sequential collections use the public functions. Random collections use the entry functions (223 otherwise). Paid mints share these arguments:

  • phase is the phase index and qty is 1 to 100 (211).
  • allowance and proof are your allowlist entry and its Merkle proof. Pass 0 and [] for open phases.
  • pay: &mut Coin<Pay> must be the phase's coin type (215). price * qty is split off it.
  • max_price is the highest unit price you accept (216).

Checks run in this order: gates (101, 102, 210), the policy is the collection's (225), quantity, random pool stocked (224), phase exists (212), start_ms <= now < end_ms (213, 214), coin, price, phase cap (217), supply (218 sold out, 219 open edition ended), allowlist proof (220) and wallet limit (221). In allowlist phases the limit is your allowance, or per_wallet if that is lower. In open phases it is per_wallet. Every check runs before any randomness is drawn.

Sequential mints

Move
public fun mint<C, Pay>(
    col: &mut Collection<C>,
    cfg: &Config,
    policy: &TransferPolicy<Nft<C>>,
    phase: u64,
    qty: u64,
    allowance: u64,
    proof: vector<vector<u8>>,
    pay: &mut Coin<Pay>,
    max_price: u64,
    clock: &Clock,
    ctx: &mut TxContext,
): vector<ID>

Mints to your wallet and returns the token ids. Aborts with 250 when the policy has a lock rule.

Move
public fun mint_into<C, Pay>(
    col: &mut Collection<C>,
    cfg: &Config,
    policy: &TransferPolicy<Nft<C>>,
    kiosk: &mut Kiosk,
    kiosk_cap: &KioskOwnerCap,
    phase: u64,
    qty: u64,
    allowance: u64,
    proof: vector<vector<u8>>,
    pay: &mut Coin<Pay>,
    max_price: u64,
    clock: &Clock,
    ctx: &mut TxContext,
): vector<ID>

Mints into a kiosk you control: locked when the policy has a lock rule, else placed.

Move
public fun mint_fresh<C, Pay>(col: &mut Collection<C>, cfg: &Config, policy: &TransferPolicy<Nft<C>>, phase: u64, qty: u64, allowance: u64, proof: vector<vector<u8>>, pay: &mut Coin<Pay>, max_price: u64, clock: &Clock, ctx: &mut TxContext): ID

Mints into a new shared kiosk for you (personal when the policy needs it) and returns the kiosk id.

Random mints

Move
entry fun mint_random<C, Pay>(col, cfg, policy, phase, qty, allowance, proof, pay, max_price, r: &Random, clock: &Clock, ctx)
entry fun mint_random_into<C, Pay>(col, cfg, policy, kiosk, kiosk_cap, phase, qty, allowance, proof, pay, max_price, r: &Random, clock: &Clock, ctx)
entry fun mint_random_fresh<C, Pay>(col, cfg, policy, phase, qty, allowance, proof, pay, max_price, r: &Random, clock: &Clock, ctx)

Same arguments as the sequential versions, plus the Random object 0x8 before the clock. Token numbers are drawn uniformly from those not yet minted. After a random mint, only TransferObjects and MergeCoins may follow in the PTB, so you cannot create and share a kiosk after the call: use an existing kiosk (mint_random_into) or let the call create one (mint_random_fresh). mint_random_into with a wrong kiosk cap aborts before the draw (252).

Random mints are built so a minter cannot pick outcomes:

  • They are private entry functions, so other Move code cannot call them, and Sui rejects a PTB where anything other than TransferObjects or MergeCoins follows them. A transaction cannot inspect its draw and abort.
  • Every check that can abort runs before the draw.
  • Every outcome costs the same gas, so a buyer cannot pick a gas budget that only fits the outcomes they want. Gas varies with how many items are left, which is known before the draw.

Creator mints

airdrop<C>(cap, col, cfg, policy, recipients, clock) sends one reserve token per recipient (222 past the reserve, 251 with no recipients). With a lock rule each recipient gets a new kiosk, else the token goes to the wallet. reserve_into<C>(cap, col, cfg, policy, kiosk, kiosk_cap, qty, clock) mints reserve tokens into the creator's kiosk. airdrop_random and reserve_random_into are the entry versions for random collections. Reserve mints and airdrops are free.

Payouts, royalties and traits

Move
public fun withdraw<C, Pay>(col: &mut Collection<C>, cfg: &Config, ctx: &mut TxContext)
public fun claim_royalties<C>(col: &Collection<C>, policy: &mut TransferPolicy<Nft<C>>, ctx: &mut TxContext): u64
public fun attach<C>(col: &Collection<C>, nft: &mut Nft<C>, keys: vector<String>, values: vector<String>, proof: vector<vector<u8>>)
public fun attach_in_kiosk<C>(col: &Collection<C>, kiosk: &mut Kiosk, kiosk_cap: &KioskOwnerCap, item: ID, keys: vector<String>, values: vector<String>, proof: vector<vector<u8>>)
  • withdraw can be called by anyone. It pays everything collected in coin Pay: each payee gets gross * share / 10000 (the last payee takes the rounding remainder) and the platform fee goes to the treasury. It does nothing when nothing is held.
  • claim_royalties can be called by anyone. It sends the policy's royalty balance to the royalty recipient. After release_policy_cap it aborts (240) and the cap holder withdraws royalties directly.
  • attach writes verified traits onto a token. The leaf for token n must be in metadata_root (232 no root, 233 bad proof, 234 token of another collection). attach_in_kiosk does the same for a token in your kiosk (locked is fine, listed is not).

Views include name, symbol, creator, max_supply, ends_ms, reserve, reserved, minted, is_random, stocked, policy, royalty_recipient, is_revealed, base_uri, metadata_root, provenance, is_paused, is_blocked, phase_count, phase(i), minted_by(phase, who), till<C, Pay>() (proceeds and fees held) and payees().

Merkle formats

  • Hash: blake2b-256.
  • Leaf: H(0x00 || payload). Node: H(0x01 || min(a, b) || max(a, b)) in lexicographic byte order. The proof is the sibling hashes from leaf to root, with no position bits. On a level with an odd count, the lone node moves up unchanged and adds nothing to the proof.
  • Allowlist payload: bcs(address) || bcs(u64 allowance).
  • Token payload: bcs(u64 n) || bcs(vector<String> keys) || bcs(vector<String> values), with keys and values in display order.

merkle::allow_leaf, merkle::token_leaf, merkle::join and merkle::verify compute the same thing on chain.

Collections with their own package (for example Rarelings) list themselves in the Registry with their package's Publisher, and the platform approves them.

Move
public fun propose<T: key>(reg: &mut Registry, publisher: &Publisher, name: String, url: String)
public fun amend<T: key>(reg: &mut Registry, publisher: &Publisher, name: String, url: String)
public fun withdraw<T: key>(reg: &mut Registry, publisher: &Publisher)

T is the item type the site lists. propose needs a publisher from T's package (152) and a type not listed yet (153). The entry starts pending. amend updates it and puts it back to pending. Views: collection_of<C>(), is_featured<T>(), feature<T>(). Approval and removal are admin only.

Fees and royalties

The platform fee is fee_bps of mint proceeds, 10 bps (0.1%) by default, capped at 500 bps (103). It is taken out of what the minter pays, not added on top, and is charged at the rate in force when the mint happens. It rounds down, so a mint total under 1,000 base units pays no fee. Reserve mints and airdrops are free. Fee and treasury changes go through a 48 hour timelock.

Royalties are set by the creator with royalty(bps, min, recipient, lock, personal) and enforced by the collection's TransferPolicy on every kiosk sale. The creator can change them later with set_royalty.

Events

All events are in PACKAGE_ID::events. Field order never changes.

EventFields
CollectionCreatedcollection, kind: TypeName (Nft<C>), creator, name, symbol, max_supply, ends_ms, reserve, random, policy, royalty_bps: u16, royalty_min, royalty_recipient, lock, personal, revealed, metadata_root: Option<vector<u8>>, provenance: Option<vector<u8>>, payees: vector<address>, shares: vector<u64>
PhaseSetcollection, index, name, start_ms, end_ms, price, coin: TypeName, per_wallet, cap, root: Option<vector<u8>> (from add_phase and edit_phase)
Stockedcollection, stocked, max_supply
Mintedcollection, item, n, to, kiosk: Option<ID>, phase: Option<u64>, price, coin: Option<TypeName>. One per token. kiosk is where it was locked or placed (none: sent to to). Reserve mints have no phase or coin and price 0.
Revealedcollection, base_uri, ext, metadata_root: Option<vector<u8>>
TraitsAttachedcollection, item, n
Withdrawncollection, coin: TypeName, gross, fee, treasury, payees: vector<address>, amounts: vector<u64>
RoyaltySetcollection, bps: Option<u16>, min: Option<u64>, recipient (none: only the recipient changed)
RoyaltiesClaimedcollection, amount, recipient
PolicyCapReleasedcollection
CollectionPausedcollection, paused, by_admin
PayeesSetcollection, payees, shares
FeatureProposedkind: TypeName, proposer, name, url, collection: Option<ID>
FeatureApproved / FeatureRemovedkind / kind, by_admin
ConfigQueued / ConfigAppliedconfig, ready_ms, treasury, fee_bps: u16 / config, treasury, fee_bps: u16
ConfigDiscarded / ConfigPaused / ConfigMigratedconfig / config, paused / config, version

The framework also emits TransferPolicyCreated<T> and DisplayCreated<T> at create, and VersionUpdated<T> at create and at reveal. A collection's item list is its Minted stream. Traits come from TraitsAttached plus the token object.

Error codes

RangeMeaning
101 to 106platform: paused, outdated version, fee ceiling, nothing queued, too early, not newer
151 to 155registry: marker already launched, not the type owner, already listed, not listed, already approved
201 to 209setup: not the marker's publisher, bad supply, bad payees or royalty bps, template syntax in text, root not 32 bytes, bad art, wrong creator cap, too many phases, empty window
210 to 226minting: paused, quantity, no phase, not started, phase over, wrong coin, price above max, phase full, sold out, open edition ended, not on the allowlist, wallet limit, reserve spent, wrong mode, pool not stocked, wrong policy, pool already full
230 to 234reveal and traits: already revealed, provenance mismatch, no metadata root, bad proof, token of another collection
240 to 241royalty cap released, payees locked
250 to 252mint: kiosk required, no recipients, not the kiosk's owner

Examples

These use @mysten/sui with the Transaction builder.

Mint into a new kiosk

TypeScript
import { Transaction } from "@mysten/sui/transactions";

const PACKAGE_ID = "0x..."; // latest launchpad package id
const CONFIG_ID = "0x...";  // shared launchpad::platform::Config

export function mintFresh(
  tx: Transaction,
  m: {
    marker: string;       // C, for example "0xabc::ghosts::GHOSTS"
    collectionId: string;
    policyId: string;     // the collection's TransferPolicy<Nft<C>>
    phase: number;
    qty: number;
    price: bigint;        // phase price per token, in MIST
    allowance?: bigint;   // allowlist phases only
    proof?: number[][];   // allowlist phases only
  },
) {
  const SUI = "0x2::sui::SUI";
  const [pay] = tx.splitCoins(tx.gas, [m.price * BigInt(m.qty)]);
  tx.moveCall({
    target: `${PACKAGE_ID}::mint::mint_fresh`,
    typeArguments: [m.marker, SUI],
    arguments: [
      tx.object(m.collectionId),
      tx.object(CONFIG_ID),
      tx.object(m.policyId),
      tx.pure.u64(m.phase),
      tx.pure.u64(m.qty),
      tx.pure.u64(m.allowance ?? 0n),
      tx.pure.vector("vector<u8>", m.proof ?? []),
      pay,
      tx.pure.u64(m.price), // max_price: refuse a higher unit price
      tx.object("0x6"),
    ],
  });
  tx.mergeCoins(tx.gas, [pay]);
}

For a random collection, call mint_random_fresh with tx.object("0x8") (Random) before the clock. Nothing but TransferObjects or MergeCoins may follow it in the PTB. For a USDC phase, pass the USDC coin type as Pay and a USDC coin as pay.

Mint into an existing kiosk

TypeScript
const [pay] = tx.splitCoins(tx.gas, [price * BigInt(qty)]);
tx.moveCall({
  target: `${PACKAGE_ID}::mint::mint_into`,
  typeArguments: [marker, "0x2::sui::SUI"],
  arguments: [
    tx.object(collectionId),
    tx.object(CONFIG_ID),
    tx.object(policyId),
    tx.object(kioskId),
    tx.object(kioskCapId),
    tx.pure.u64(phase),
    tx.pure.u64(qty),
    tx.pure.u64(0),
    tx.pure.vector("vector<u8>", []),
    pay,
    tx.pure.u64(price),
    tx.object("0x6"),
  ],
});
tx.mergeCoins(tx.gas, [pay]);

Launch a collection

After publishing the marker package and keeping its Publisher, the creator launches in one PTB:

TypeScript
const REGISTRY_ID = "0x..."; // shared launchpad::registry::Registry
const SUI = "0x2::sui::SUI";
const C = `${markerPackageId}::ghosts::GHOSTS`;

const supply = tx.moveCall({
  target: `${PACKAGE_ID}::collection::capped`,
  arguments: [tx.pure.u64(10_000), tx.pure.u64(100), tx.pure.bool(true)], // max, reserve, random
});
const art = tx.moveCall({
  target: `${PACKAGE_ID}::collection::hidden_art`,
  arguments: [tx.pure.string("ipfs://<cid>/egg.png"), tx.pure.option("vector<u8>", provenance)],
});
const royalty = tx.moveCall({
  target: `${PACKAGE_ID}::collection::royalty`,
  arguments: [tx.pure.u16(500), tx.pure.u64(0), tx.pure.address(creator), tx.pure.bool(true), tx.pure.bool(false)],
});
const [col, cap] = tx.moveCall({
  target: `${PACKAGE_ID}::collection::create`,
  typeArguments: [C],
  arguments: [
    tx.object(REGISTRY_ID),
    tx.object(CONFIG_ID),
    tx.object(publisherId),
    tx.pure.string("Ghosts"),
    tx.pure.string("GHOST"),
    tx.pure.string("A collection of ghosts."),
    tx.pure.string("https://example.com"),
    supply,
    art,
    royalty,
    tx.pure.vector("address", [creator]),
    tx.pure.vector("u16", [10_000]),
  ],
});
tx.moveCall({
  target: `${PACKAGE_ID}::collection::stock`, // random collections only, up to 16,384 per call
  typeArguments: [C],
  arguments: [cap, col, tx.pure.u64(10_000)],
});
tx.moveCall({
  target: `${PACKAGE_ID}::collection::add_phase`,
  typeArguments: [C, SUI],
  arguments: [
    cap, col, tx.pure.string("public"),
    tx.pure.u64(startMs), tx.pure.u64(endMs),
    tx.pure.u64(1_000_000_000n), // 1 SUI
    tx.pure.u64(5), tx.pure.u64(0), // per_wallet, phase cap
    tx.pure.option("vector<u8>", null), // no allowlist
  ],
});
tx.moveCall({ target: `${PACKAGE_ID}::collection::share`, typeArguments: [C], arguments: [col] });
tx.transferObjects([cap], creator);

Collections over 16,384 items call stock again in later transactions before minting opens.

Pay out

TypeScript
tx.moveCall({
  target: `${PACKAGE_ID}::collection::withdraw`,
  typeArguments: [marker, "0x2::sui::SUI"],
  arguments: [tx.object(collectionId), tx.object(CONFIG_ID)],
});
tx.moveCall({
  target: `${PACKAGE_ID}::collection::claim_royalties`,
  typeArguments: [marker],
  arguments: [tx.object(collectionId), tx.object(policyId)],
});

Anyone can send this. Proceeds always go to the payees and royalties to the royalty recipient.

--
TPS:--