创建预售
当前 ABI:版本化源码 c96cd0d。示例使用已激活的 PACKAGE_VERSION 对象;历史非版本化地址需要不同 ABI。详见版本管理及部署参考。
预售在一笔可编程交易中创建,分三个阶段:
- 交易平台模块的
new启动创建,返回PresaleInitializer<Meme, Quote>。 - 初始化器上的设置函数记录创建者的条款。
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 要求的迁移器见证。选定的交易平台之后无法更改。
步骤
跳转到“步骤”-
启动创建。 以类型参数
<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 当前费率的快照,并返回初始化器。 -
必需设置。 均以可变引用接收初始化器:
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)
-
可选设置:
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 合作伙伴。只有此时可以指定。
同一设置函数调用两次时,以最后一次的值为准。
-
完成初始化。
finalize(initializer, package_version, currency)检查各项设置、铸造供应量、消耗TreasuryCap(供应量变为固定或仅可销毁)、发出PresaleCreated,并返回(Presale, PresaleCap)。 -
共享。
share(presale, package_version, start_ms, clock)将开始时间设为max(start_ms, now),因此可提前发布预售。调用发出PresaleOpened。 -
将
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 手续费分配方案。
示例:Bluefin 预售
跳转到“示例:Bluefin 预售”公开预售 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 packageconst PACKAGE_VERSION = "0x…"; // matching activated shared PackageVersionconst FEE_POLICY = "0x…";const CREATION_POLICY = "0x…";const VENUE_CONFIG = "0x…";const TREASURY_CAP = "0x…"; // TreasuryCap<MEME>, zero supplyconst CURRENCY = "0x…"; // Currency<MEME> in the Coin Registryconst 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),后者作为 Bluefincreate_pool接收的原始fee_rate传入。请查阅 Bluefin 文档选择所需档位。超出范围以bluefin::EInvalidTier中止。代币符号最多 32 字节(bluefin::ESymbolTooLong)。 - Cetus 接收
tick_spacing,并记录 Cetus 为其设置的费率。调用还会铸造该代币的 CetusPoolCreationCap,注册该间距的交易对,并将凭证存入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中止。支付代币的冻结风险需另行评估。