跳转到内容

创建预售

当前 ABI:版本化源码 c96cd0d。示例使用已激活的 PACKAGE_VERSION 对象;历史非版本化地址需要不同 ABI。详见版本管理及部署参考。

预售在一笔可编程交易中创建,分三个阶段:

  1. 交易平台模块的 new 启动创建,返回 PresaleInitializer<Meme, Quote>。
  2. 初始化器上的设置函数记录创建者的条款。
  3. finalize 一并检查所有设置、铸造供应量,并返回 (Presale, PresaleCap)。随后由 share 将预售置为开放状态。

初始化器采用 hot potato 模式:不具备任何能力,只有 finalize 可以消耗它。Presale 仅具备 key,必须在同一笔交易中共享。缺少任一步骤,交易都会失败。

  • 一个全新代币类型 Meme,其 TreasuryCap 尚未铸造任何代币,以及它在代币注册表中的共享 Currency<Meme>。注册状态必须明确为 Unregulated;受监管币和旧版 Unknown 状态均不接受。
  • Blast 的 CreationPolicy 允许的支付代币类型及交易平台。否则 new 以 EQuoteNotAllowed 或 EMigrationWitnessNotAllowed 中止。
  • 共享 FeePolicy、CreationPolicy 和 VenueConfig 的 ID;Cetus 预售还需 Cetus 的 GlobalConfig 和 Pools。详见部署参考。

创建者始终调用交易平台模块的 new(cetus::new、bluefin::new 或 Aftermath 的专用流程),该模块持有 blast_presale::new 要求的迁移器见证。选定的交易平台之后无法更改。

  1. 启动创建。 以类型参数 <Meme, Quote> 调用交易平台的 new:

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

    调用记录池档位,检查 CreationPolicy、预售代币与支付代币是否不同、TreasuryCap 是否尚未铸币,保存 Blast 当前费率的快照,并返回初始化器。

  2. 必需设置。 均以可变引用接收初始化器:

    • set_supply(initializer, package_version, token_supply, burnable)
    • set_token_allocations(initializer, package_version, token_allocation_bps, liquidity_token_allocation_bps, recipient_allocations)
    • set_raise(initializer, package_version, raise_target, raise_allocations)
    • set_windows(initializer, package_version, duration_ms, liquidity_window_ms)
    • set_eligibility(initializer, package_version, eligibility)
    • set_fee_split(initializer, package_version, fee_split)
  3. 可选设置:

    • set_participant_vesting(initializer, package_version, terms):买家代币从该买家领取时开始分期解锁。
    • set_uncapped_phase(initializer, package_version, phase):从指定阶段起取消单钱包上限。
    • set_hard_cap(initializer, package_version, starts_after_ms, closes_when_filled):从指定时刻起将认购限制在募资目标内,见预售模式。
    • set_partner(initializer, package_version, creation_policy, partner, clock):指定 CreationPolicy 当前接受的 Blast 合作伙伴。只有此时可以指定。

    同一设置函数调用两次时,以最后一次的值为准。

  4. 完成初始化。 finalize(initializer, package_version, currency) 检查各项设置、铸造供应量、消耗 TreasuryCap(供应量变为固定或仅可销毁)、发出 PresaleCreated,并返回 (Presale, PresaleCap)。

  5. 共享。 share(presale, package_version, start_ms, clock) 将开始时间设为 max(start_ms, now),因此可提前发布预售。调用发出 PresaleOpened。

  6. 将 PresaleCap 转给创建者。

finalize 与 share 之间的 Pending 预售只接受 share 及 Blast 的 set_protocol_fees。后者需要在同一笔交易中通过共享 Blast ACL 登录取得的 AdminWitness<FeeAdmin>。详见权限。

通过 Move 调用构造以下值,再将结果传给设置函数:

  • bps::new(value) 构造费率,10,000 表示 100%。
  • vesting_terms(cliff_ms, period_ms, periods) 构造分期解锁条款。要求 period_ms > 0、periods > 0、period_ms × periods ≤ 2^63,且 cliff_ms ≤ period_ms × periods。这些是合约整数边界,不是推荐锁定时长。
  • recipient_share(recipient, bps) 或 vested_recipient_share(recipient, bps, terms) 构造单个份额。使用 MakeMoveVec 将份额组合成代币、募资和手续费接收方列表。任一列表均可为空。
  • open_eligibility(wallet_cap, max_participants, min_commitment) 或 allowlisted_eligibility(root, leaf_count) 构造参与资格规则。
  • uncapped_for_eligible(starts_after_ms) 或 uncapped_for_anyone(starts_after_ms, max_participants, min_commitment) 构造无单钱包上限阶段。
  • fee_split(fee_recipients, holder_rewards_bps) 构造 LP 手续费分配方案。

公开预售 10 亿枚代币(9 位小数),在两天内募集 10,000 SUI,项目财库份额分期解锁。此示例假设 Blast 的代币费率为零,因为买家、流动性、Blast 和接收方的供应量份额必须恰好合计 10,000 基点。

import { Transaction } from "@mysten/sui/transactions";
import { SUI_TYPE_ARG } from "@mysten/sui/utils";
const PKG = "0x…"; // current versioned blast_presale package
const PACKAGE_VERSION = "0x…"; // matching activated shared PackageVersion
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 = SUI_TYPE_ARG;
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(PACKAGE_VERSION),
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.object(PACKAGE_VERSION), tx.pure.u64(1_000_000_000n * 10n ** 9n), tx.pure.bool(false)]);
sale("set_token_allocations", [initializer, tx.object(PACKAGE_VERSION), bps(4_000), bps(3_000), tokenRecipients]);
sale("set_raise", [initializer, tx.object(PACKAGE_VERSION), tx.pure.u64(10_000n * 10n ** 9n), raiseRecipients]);
sale("set_windows", [initializer, tx.object(PACKAGE_VERSION), tx.pure.u64(2 * DAY), tx.pure.u64(7 * DAY)]);
sale("set_eligibility", [initializer, tx.object(PACKAGE_VERSION), eligibility]);
sale("set_fee_split", [initializer, tx.object(PACKAGE_VERSION), feeSplit]);
// 3. Finalize, share, and hand over the cap.
const [presale, presaleCap] = sale("finalize", [initializer, tx.object(PACKAGE_VERSION), tx.object(CURRENCY)]);
sale("share", [presale, tx.object(PACKAGE_VERSION), tx.pure.u64(Date.parse("2026-11-01T15:00:00Z")), tx.object.clock()]);
tx.transferObjects([presaleCap], creator);

此预售未设硬顶,整个窗口内均接受超过募资目标的认购,并按统一成交比例分配。

若使用 Cetus,将步骤 1 替换为 cetus::new(treasury_cap, package_version, creation_policy, fee_policy, venue_config, cetus_config, pools, tick_spacing)。该调用不接收 Currency 或费率,其余步骤相同。

池档位属于交易平台配置,Presale 和 PresaleCreated 均不携带该信息。

  • Bluefin 接收 tick_spacing(1 至 400)及 pool_fee_rate(1 至 20,000),后者作为 Bluefin create_pool 接收的原始 fee_rate 传入。请查阅 Bluefin 文档选择所需档位。超出范围以 bluefin::EInvalidTier 中止。代币符号最多 32 字节(bluefin::ESymbolTooLong)。
  • Cetus 接收 tick_spacing,并记录 Cetus 为其设置的费率。调用还会铸造该代币的 Cetus PoolCreationCap,注册该间距的交易对,并将凭证存入 VenueConfig。支付代币必须在该间距的 Cetus 许可交易对白名单中。

两者均在共享 VenueConfig 中,以 venue_config::PoolTierKey<Meme> 为键记录 venue_config::PoolTier { tick_spacing, pool_fee_rate },并在 PresaleCreated 之前发出 venue_config::PoolTierReserved<Meme, Quote, Venue>;其中 Venue 为 cetus::Venue 或 bluefin::Venue。迁移时会读取该档位,因此 cetus::migrate 和 bluefin::migrate 均接收 VenueConfig。

两个可选设置决定认购方式:

  • 无单钱包上限阶段(set_uncapped_phase)从窗口开始后的 starts_after_ms 起取消钱包上限,仅改变这一限制。
  • 募资硬顶(set_hard_cap)从窗口开始后的 starts_after_ms 起将认购限制在募资目标内,先到先得。每次仅接受尚缺金额,其余退回;此前可超额认购。硬顶不会取消钱包上限。
模式 设置
整个窗口允许超额认购,按比例分配 不设硬顶
保留单钱包上限,先到先得 硬顶从 0 起生效
无单钱包上限,先到先得 无单钱包上限阶段和硬顶均从 0 起生效
先在单钱包上限内超额认购,再进入无单钱包上限的先到先得阶段 两者使用同一时间偏移
// First come, first served within wallet caps, closing as soon as the raise is full.
sale("set_hard_cap", [initializer, tx.object(PACKAGE_VERSION), 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, tx.object(PACKAGE_VERSION), phase]);
sale("set_hard_cap", [initializer, tx.object(PACKAGE_VERSION), tx.pure.u64(DAY), tx.pure.bool(false)]);

启用 closes_when_filled 后,只要硬顶生效且募资达标,任何人即可结算,无需等到原定窗口结束。流动性部署期限从实际结束时间起算。不启用时,即使募满也持续至原定结束时间,以便按宣布的时间开放池。不设硬顶的预售不会提前结束。

设置函数 参数 规则
set_supply token_supply 以原子单位表示的总供应量,大于 0。
burnable true 表示仅可销毁,false 表示固定供应量。
set_token_allocations token_allocation_bps 买家供应量份额,大于 0。
liquidity_token_allocation_bps 池供应量份额,大于 0。
recipient_allocations 最多 64 个不同的非零地址。买家 + 流动性 + Blast 代币费率 + 接收方 = 10,000。
set_participant_vesting terms VestingTerms。每位买家的解锁从其领取时开始。
set_raise raise_target 以原子单位表示的支付代币募资目标,大于 0。
raise_allocations 最多 64 个接收方,与 Blast 募资费率之和必须低于 10,000,池获得剩余部分。
set_windows duration_ms 认购窗口时长,大于 0。
liquidity_window_ms 窗口结束后,达标预售在可被取消前的期限;至少 7 天。
set_eligibility eligibility 公开:0 < min_commitment ≤ wallet_cap、min × max_participants ≥ raise_target、1 至 65,536 个钱包。白名单:32 字节根、1 至 65,536 个叶节点。
set_uncapped_phase phase starts_after_ms 小于 duration_ms。向所有人开放时,容量至少覆盖原规则的钱包数、最多 65,536,且按最低认购额仍可达到募资目标。
set_hard_cap starts_after_ms 小于 duration_ms(EInvalidHardCap)。
closes_when_filled 是否允许募满即结算。
set_fee_split fee_split 最多 16 个不分期解锁、互不重复的手续费接收方,加上持币奖励份额,合计 10,000。

向所有人开放无单钱包上限阶段时,设 N 为该阶段总人数上限、m 为该阶段最低认购额。公开预售必须满足 m × N ≥ raise_target;白名单预售有 L 个叶节点时,必须满足 m × (N − L) ≥ raise_target。白名单钱包可认购任意正数,因此仅新增名额计入最低额容量。N 至少覆盖原人数上限且不超过 65,536。例如有 100 个白名单叶节点时,N = 100 无法满足任何正数募资目标。

即使 BPS 总和正确,原子单位分配也必须为买家、池及每个代币接收方各留下正数;募资分配必须为池和每个募资接收方各留下正数。供应量或目标过小时,某份额取整为零仍会失败。金额和时间输入须在 u64 范围内。

未设置的数值保持为零,因此供应量、募资目标或窗口未设置时,会在各自检查中失败(EInvalidTokenAllocation、EInvalidRaiseTarget、EInvalidWindow)。缺少参与资格或手续费分配设置时,以 EMissingSetting 中止。

若池中每原子单位预售代币对应的支付代币原子单位数,超出两平台都能定价的范围(约 2⁻⁶² 至 2⁶²),finalize 还会以 EUnseedablePrice 中止。极高募资配极少池代币,或反之,都会触发此检查。

  • 全部步骤必须在同一笔交易完成。交易结束前必须消耗初始化器完成初始化,并共享预售。
  • 只能在 finalize 之前,通过初始化器指定合作伙伴。
  • 创建前合理设置公开规则:min_commitment × max_participants 必须达到募资目标。
  • 预售代币必须是明确的 Unregulated 币种。仅检查 is_regulated == false 不足以排除旧版 Unknown 状态;不符合要求时 finalize 以 ESaleTokenNotUnregulated 中止。支付代币的冻结风险需另行评估。