Skip to content

Create a sale

A sale is created in one programmable transaction, in three stages:

  1. A venue module’s new starts the sale and returns a PresaleInitializer<Meme, Quote>.
  2. Setters on the initializer record the creator’s terms.
  3. finalize checks every setting together, mints the supply, and returns (Presale, PresaleCap). share then opens the sale.

The initializer is a hot potato: it has no abilities, and finalize is the only call that consumes it. Presale has key only, so it must be shared in the same transaction. A transaction that skips either step fails.

  • A new coin type Meme whose TreasuryCap has minted nothing, and its shared Coin Registry Currency<Meme>.
  • A quote type and a venue that Blast’s CreationPolicy allows. new aborts with EQuoteNotAllowed or EMigrationWitnessNotAllowed otherwise.
  • The IDs of the shared FeePolicy, CreationPolicy, and VenueConfig, plus Cetus’s GlobalConfig and Pools for a Cetus sale. See Addresses.

Creators always call a venue module’s new (cetus::new or bluefin::new), which holds the migrator witness blast_presale::new requires. The venue is fixed for good.

  1. Start. Call the venue’s new, with type arguments <Meme, Quote>:

    • cetus::new(treasury_cap, creation_policy, fee_policy, venue_config, cetus_config, pools, tick_spacing)
    • bluefin::new(treasury_cap, currency, creation_policy, fee_policy, venue_config, tick_spacing, pool_fee_rate)

    It records the pool tier (see Pool tier), checks the CreationPolicy, that the sale coin differs from the quote, and that the TreasuryCap has minted nothing, snapshots Blast’s current rates, and returns the initializer.

  2. Required setters. Each takes the initializer by mutable reference:

    • set_supply(initializer, token_supply, burnable)
    • set_token_allocations(initializer, token_allocation_bps, liquidity_token_allocation_bps, recipient_allocations)
    • set_raise(initializer, raise_target, raise_allocations)
    • set_windows(initializer, duration_ms, liquidity_window_ms)
    • set_eligibility(initializer, eligibility)
    • set_fee_split(initializer, fee_split)
  3. Optional setters:

    • set_participant_vesting(initializer, terms): each buyer’s tokens vest from that buyer’s claim.
    • set_uncapped_phase(initializer, phase): lifts wallet caps from the phase’s start.
    • set_hard_cap(initializer, starts_after_ms, closes_when_filled): holds the sale to its raise from that point. See Sale shapes.
    • set_partner(initializer, creation_policy, partner, clock): names a Blast partner the CreationPolicy currently accepts. This is the only time a partner can be named.

    A setter called twice keeps its last value.

  4. Finalize. finalize(initializer, currency) checks every setting, mints the supply, consumes the TreasuryCap (the supply becomes fixed or burn-only), emits PresaleCreated, and returns (Presale, PresaleCap).

  5. Share. share(presale, start_ms, clock) opens the sale at max(start_ms, now), so you can publish ahead of the start. It emits PresaleOpened.

  6. Transfer the PresaleCap to the creator.

Between finalize and share, the pending Presale accepts only share and Blast’s set_protocol_fees, which needs an AdminWitness<FeeAdmin> obtained by signing in through the shared Blast ACL in the same transaction. See Authority.

Build these with Move calls and pass the results to the setters:

  • bps::new(value) builds each rate, where 10,000 is 100%.
  • vesting_terms(cliff_ms, period_ms, periods) builds vesting terms.
  • recipient_share(recipient, bps) or vested_recipient_share(recipient, bps, terms) builds one share. Gather shares with MakeMoveVec into token, raise, and fee recipients. Any list may be empty.
  • open_eligibility(wallet_cap, max_participants, min_commitment) or allowlisted_eligibility(root, leaf_count) builds the eligibility policy.
  • uncapped_for_eligible(starts_after_ms) or uncapped_for_anyone(starts_after_ms, max_participants, min_commitment) builds a no-cap phase.
  • fee_split(fee_recipients, holder_rewards_bps) builds the LP-fee split.

An open sale of 1 billion tokens (9 decimals) raising 10,000 SUI over two days, with a vested treasury share. The supply split assumes Blast’s token rate is zero, since buyers, liquidity, Blast, and recipients must total exactly 10,000 basis points.

import { Transaction } from "@mysten/sui/transactions";
const PKG = "0x…"; // blast_presale package
const FEE_POLICY = "0x…";
const CREATION_POLICY = "0x…";
const VENUE_CONFIG = "0x…";
const TREASURY_CAP = "0x…"; // TreasuryCap<MEME>, zero supply
const CURRENCY = "0x…"; // Currency<MEME> in the Coin Registry
const creator = "0x…";
const treasury = "0x…";
const MEME = "0x…::meme::MEME";
const SUI = "0x2::sui::SUI";
const DAY = 86_400_000;
const tx = new Transaction();
type Args = Parameters<typeof tx.moveCall>[0]["arguments"];
const bps = (value: number) =>
tx.moveCall({ target: `${PKG}::bps::new`, arguments: [tx.pure.u64(value)] });
// Value constructors take no type arguments; the sale's own calls take <MEME, SUI>.
const value = (fn: string, args: Args = []) =>
tx.moveCall({ target: `${PKG}::blast_presale::${fn}`, arguments: args });
const sale = (fn: string, args: Args = []) =>
tx.moveCall({ target: `${PKG}::blast_presale::${fn}`, typeArguments: [MEME, SUI], arguments: args });
// Treasury: 30% of supply, vesting in 12 thirty-day steps; nothing unlocks before day 90.
const treasuryVesting = value("vesting_terms", [
tx.pure.u64(90 * DAY), tx.pure.u64(30 * DAY), tx.pure.u64(12),
]);
const tokenRecipients = tx.makeMoveVec({
type: `${PKG}::blast_presale::RecipientShare`,
elements: [value("vested_recipient_share", [tx.pure.address(treasury), bps(3_000), treasuryVesting])],
});
// Treasury: 30% of the raise, paid at migration. The pool keeps the rest after Blast's fee.
const raiseRecipients = tx.makeMoveVec({
type: `${PKG}::blast_presale::RecipientShare`,
elements: [value("recipient_share", [tx.pure.address(treasury), bps(3_000)])],
});
// After Blast's LP fees: 50% to the creator, 50% to the holder allocation.
// Holder Quote funds rounds; holder Meme is paid to the governed treasury.
const feeSplit = value("fee_split", [
tx.makeMoveVec({
type: `${PKG}::blast_presale::RecipientShare`,
elements: [value("recipient_share", [tx.pure.address(creator), bps(5_000)])],
}),
bps(5_000),
]);
// Open sale: up to 2,000 wallets, 10–1,000 SUI each (SUI has 9 decimals).
const eligibility = value("open_eligibility", [
tx.pure.u64(1_000n * 10n ** 9n), tx.pure.u64(2_000), tx.pure.u64(10n * 10n ** 9n),
]);
// 1. Start: record the Bluefin tier and take the initializer.
const initializer = tx.moveCall({
target: `${PKG}::bluefin::new`,
typeArguments: [MEME, SUI],
arguments: [
tx.object(TREASURY_CAP),
tx.object(CURRENCY),
tx.object(CREATION_POLICY),
tx.object(FEE_POLICY),
tx.object(VENUE_CONFIG),
tx.pure.u32(60), // tick_spacing
tx.pure.u64(3_000), // pool_fee_rate: the raw fee_rate Bluefin's create_pool takes
],
});
// 2. Terms.
sale("set_supply", [initializer, tx.pure.u64(1_000_000_000n * 10n ** 9n), tx.pure.bool(false)]);
sale("set_token_allocations", [initializer, bps(4_000), bps(3_000), tokenRecipients]);
sale("set_raise", [initializer, tx.pure.u64(10_000n * 10n ** 9n), raiseRecipients]);
sale("set_windows", [initializer, tx.pure.u64(2 * DAY), tx.pure.u64(7 * DAY)]);
sale("set_eligibility", [initializer, eligibility]);
sale("set_fee_split", [initializer, feeSplit]);
// 3. Finalize, share, and hand over the cap.
const [presale, presaleCap] = sale("finalize", [initializer, tx.object(CURRENCY)]);
sale("share", [presale, tx.pure.u64(Date.parse("2026-11-01T15:00:00Z")), tx.object.clock()]);
tx.transferObjects([presaleCap], creator);

With no hard cap, this sale overflows: it takes commitments above the raise for its whole window and fills every wallet at one rate.

For Cetus, replace step 1 with cetus::new(treasury_cap, creation_policy, fee_policy, venue_config, cetus_config, pools, tick_spacing). It takes no Currency and no fee rate. The rest is the same.

The pool tier belongs to the venue, not the sale. Presale and PresaleCreated carry none.

  • Bluefin takes tick_spacing (1 to 400) and pool_fee_rate (1 to 20,000), passed as the raw fee_rate Bluefin’s create_pool takes. Check Bluefin’s docs for the tier you want. Out of range aborts with bluefin::EInvalidTier. The coin’s symbol must be at most 32 bytes (bluefin::ESymbolTooLong).
  • Cetus takes tick_spacing and records the fee rate Cetus sets for it. It also mints the coin’s Cetus PoolCreationCap, registers the pair at that spacing, and stores the cap in VenueConfig. The quote must be in Cetus’s permission-pair allowlist at that spacing.

Both record a venue_config::PoolTier { tick_spacing, pool_fee_rate } in the shared VenueConfig under venue_config::PoolTierKey<Meme>, and emit venue_config::PoolTierReserved<Meme, Quote, Venue>, where Venue is cetus::Venue or bluefin::Venue, before PresaleCreated. Migration reads the tier back, so cetus::migrate and bluefin::migrate both take the VenueConfig.

Two optional settings decide how a sale fills:

  • A no-cap phase (set_uncapped_phase) lifts wallet caps from starts_after_ms into the window. It does nothing else.
  • A hard cap (set_hard_cap) holds commitments to the raise, first come, first served, from starts_after_ms into the window. Each commit takes only what is left of the raise and returns the excess. Before that point the sale overflows. A hard cap does not lift wallet caps.
Shape Settings
Overflow for the whole window, filled pro rata No hard cap
First come, first served within wallet caps Hard cap at 0
First come, first served without caps No-cap phase and hard cap both at 0
A capped overflow round, then a first-come no-cap round No-cap phase and hard cap at the same offset
// First come, first served within wallet caps, closing as soon as the raise is full.
sale("set_hard_cap", [initializer, tx.pure.u64(0), tx.pure.bool(true)]);
// Or: one day of capped overflow, then a first-come round open to anyone.
const phase = value("uncapped_for_anyone", [
tx.pure.u64(DAY), tx.pure.u64(5_000), tx.pure.u64(10n * 10n ** 9n),
]);
sale("set_uncapped_phase", [initializer, phase]);
sale("set_hard_cap", [initializer, tx.pure.u64(DAY), tx.pure.bool(false)]);

With closes_when_filled, anyone may settle the sale as soon as its raise is full under the hard cap, without waiting for the window’s end. The liquidity deadline then counts from that close. Without it, a full sale runs to its end, so the pool can open at an announced time. A sale without a hard cap never closes early.

Setter Argument Rule
set_supply token_supply Whole supply in atomic units, above 0.
burnable true for burn-only supply, false for fixed.
set_token_allocations token_allocation_bps Buyers’ share of supply, above 0.
liquidity_token_allocation_bps Pool’s share of supply, above 0.
recipient_allocations Up to 64 distinct, nonzero addresses. Buyers + liquidity + Blast’s token rate + recipients = 10,000.
set_participant_vesting terms VestingTerms. Each buyer’s schedule starts at their claim.
set_raise raise_target Quote to raise, atomic units, above 0.
raise_allocations Up to 64 recipients. With Blast’s raise rate they must stay below 10,000; the pool keeps the rest.
set_windows duration_ms Commitment window, above 0.
liquidity_window_ms Time after the window before a funded sale can be cancelled. At least 7 days.
set_eligibility eligibility Open: 0 < min_commitment ≤ wallet_cap, min × max_participants ≥ raise_target, 1 to 65,536 wallets. Allowlist: 32-byte root, 1 to 65,536 leaves.
set_uncapped_phase phase starts_after_ms below duration_ms. A phase for anyone admits at least the policy’s wallets and at most 65,536, and must still be able to reach the raise at its minimum.
set_hard_cap starts_after_ms Below duration_ms (EInvalidHardCap).
closes_when_filled Whether the sale may settle as soon as its raise is full.
set_fee_split fee_split Up to 16 unvested, distinct fee recipients plus the holder share, totalling 10,000.

A setting never set stays zero, so an unset supply, raise, or window fails its own check (EInvalidTokenAllocation, EInvalidRaiseTarget, EInvalidWindow). A missing eligibility policy or fee split aborts with EMissingSetting.

finalize also aborts with EUnseedablePrice if the pool’s quote per pool token, in atomic units, falls outside the range both venues can price, roughly 2⁻⁶² to 2⁶². A huge raise against a tiny pool allocation, or the reverse, fails this check.

  • Do everything in one transaction. The initializer must be finalized and the sale shared before it ends.
  • A partner can be named only on the initializer, before finalize.
  • Size an open policy before creating. min_commitment × max_participants must reach the raise target.
  • If the coin is regulated, its issuer can block participants’ claims after migration. Blast vets this before listing a sale.