跳到主要内容

Microsales

合约地址

Microsales 没有自己的合约。它们与 pools 和 Melt Pools 共用同一个 launchpad 地址——每条链一个地址、一份 ABI,三种上线方式共用。

NetworkChain IDLaunchpad
BNB Chain (mainnet)560xD9CcE715619fA9082d21375B6B37A3a0d99138F4
BNB Chain (testnet)970x0AA21d47b600B04527c09BbD9A30aF32cb64f6cF
Robinhood Chain46630x7E0752B42f9db92F12E221E10fFfa10bfE8668C6

单个 microsale 由它的 fundManager 地址标识,你会把它作为参数传给下面这些调用。它是每次 销售对应的一个句柄,而不是一个你去调用的合约。

Microsale 是一种固定价格的代币销售。你选择一个 config 模板,launchpad 铸造一个全新的代币, 贡献者持续注入资金直到达到 hard cap,finalize 时 launchpad 通过向 DEX 添加流动性来上线代币。 每个 microsale 只创建一个代币,且一个代币只能通过 microsale 上线一次。

本页介绍完整的生命周期:选择模板、创建、贡献、finalize、领取以及处理退款。带 vault 的上线有 独立的页面 vaults.md,但 create 调用在这里也会做简要说明。

目录​

组成部分​

  • Config(模板)。 平台发布可复用的模板,固定了经济模型:多少代币进入 presale、多少 进入流动性、hard cap,以及销售持续时间。你通过 configId 针对某个模板发起上线。
  • 销售(sale)。 从模板创建出的一个实例,以其 fundManager 地址标识。
  • 代币。 在创建时铸造。总供应量固定为 1,000,000,000(18 位小数下为 1e27),按模板 在 presale 与 LP 之间分配。
  • Fund manager。 每次销售对应一个地址,在 finalize 之前持有募资资金和代币。你不需要 直接调用它。它是你在几乎所有 microsale 函数中传入的句柄,关键事件也按它索引。

销售状态​

一个 microsale 始终处于以下三种状态之一:

StateValueMeaning
Active0开放贡献
Finalized1已达到 hard cap,已添加流动性,代币可领取
Cancelled2已取消;贡献者可提现

一个关键细节:只有当募资金额精确达到 hard cap 时,microsale 才会 finalize。贡献会被 限制,使 totalRaised 永远不会超过 hardCap,最后填满差额的那笔贡献会设置 filledAt。 如果一个销售到达结束时间时还没填满,它会保持 Active,但会变得可退款(见退款与退出)。

Config 模板​

你不设置销售的经济参数,而是从平台发布的模板中挑选。在展示创建表单之前先发现它们:

// Full template objects (recommended for discovery)
const configs = await launchpad.getActiveMicroSaleConfigs();

for (const c of configs) {
console.log({
configId: c.configId,
presaleTokens: c.presaleTokens,
lpTokens: c.lpTokens,
hardCap: c.hardCap, // in the template's raise currency
duration: c.duration, // seconds
active: c.active,
});
}

相关的读取方法:

FunctionReturns
getActiveMicroSaleConfigs()活跃 MicroSaleConfig 组成的数组
getActiveMicroSaleConfigIds()活跃 configId 值组成的数组
getMicroSaleConfig(configId)一个模板
getMicroSaleConfigsCount()曾经创建过的模板总数

价格由模板推导而来​

你不设置价格。launchpad 会根据模板和 hard cap 计算出两个费率:

  • Presale 费率 = presaleTokens * 1e18 / hardCap。这是贡献者每单位募资货币能获得 的代币单位数:purchased = amount * rate / 1e18。
  • Listing 费率 = lpTokens * 1e18 / (hardCap * 0.95)。这决定了 DEX 上开盘价格。 0.95 是在添加流动性之前从募资金额中扣除的 5% 平台费。

募资货币​

大多数模板以链的原生币(BNB)募资。有些模板则绑定了一个 ERC20 quote token。在创建或 贡献之前先检查绑定情况:

const quote = await launchpad.getMicroSaleConfigQuoteToken(configId);
// address(0) => native (BNB) raise
// otherwise => contributors pay with that ERC20

该绑定适用于 no-tax、wallet-receiver 和 vault 上线。原生募资使用 contributeMicroSale; ERC20 募资使用 contributeMicroSaleQuote。混用两者会 revert。

允许的 quote token​

Microsales 与 Melt Pool 曲线以链的原生币或 launchpad 全局 quote token 允许 列表中的任何代币募资——用 getAllowedQuoteTokens() 实时读取该列表,并用 isQuoteTokenAllowed(token) 检查某个代币。完整的分链列表见 支持的货币。

代币类型与创建路径​

目前有三条创建路径。每条都会返回新销售的 fundManager 地址。

FunctionTokenTaxRaise currencyFee receiver
createMicroSaleNoTaxTokenCheeseToken无模板绑定的原生或 ERC20不适用
createMicroSaleTaxTokenV2CheeseTaxTokenV2买入/卖出 tax模板绑定的原生或 ERC20你指定的钱包
createMicroSaleTaxTokenWithVaultV2CheeseTaxTokenV2买入/卖出 tax模板绑定的原生或 ERC20上线时部署的 vault

Tax-token 路径会铸造一个对买卖收税的代币,并将该收入路由给一个 fee receiver。Plain 路径 铸造的是一个没有 tax、也没有 fee receiver 的普通 ERC20。如果你希望收入流入可编程的分账 逻辑,就选择 vault 路径;参见 vaults.md。

验证代币源码​

每个由 microsale 铸造的代币都是以下两个合约之一:普通 ERC20(CheeseToken)或收税 ERC20(CheeseTaxTokenV2)。它们的源码发布在 cheesepad-ai/cheesepad-contracts。 可下载的 ABI 以及代币专属的 getter 参见已铸造的代币。这同样适用于由 Melt Pools 铸造的代币。

创建一个 microsale​

共享参数​

每个创建调用都接受以下字段(位于 CreateParams 内):

FieldTypeNotes
configIduint256要发起上线的模板;必须处于活跃状态
namestring代币名称
symbolstring代币符号
poolDetailsstring自由格式元数据(JSON、URL,或你的 UI 存储的任何内容)
maxContributionuint256单地址累计贡献上限;0 表示无限制
saltbytes32由 CheesePad API 返回的全新 CREATE2 salt
lockDurationuint256LP 锁定时长(秒);0 表示销毁 LP 而非锁定

lockDuration 规则:要么为 0(LP 在 finalize 时被销毁),要么在链的最小值(两个主网均为 365 天,BNB 测试网为 5 分钟)与 10 年上限之间。其他情况会以 Invalid lock duration revert。

如果平台收取创建费用,请在创建调用中以 msg.value 发送(参见 getting-started.md)。

生成代币 salt​

在每次创建调用之前立即请求一个全新的 salt。响应中还包含 launchpad 将要部署的代币地址。不要在 另一次上线中复用同一个 salt。

type SaltTokenType = "CheeseToken" | "CheeseTaxTokenV2";

async function generateTokenSalt(
tokenType: SaltTokenType,
name: string,
symbol: string,
router: string,
): Promise<{ salt: string; address: string }> {
const { chainId } = await provider.getNetwork();
const query = new URLSearchParams({
tokenType,
launchpadContract: LAUNCHPAD,
chainId: chainId.toString(),
name,
symbol,
router,
});

const response = await fetch(
`https://api.cheesepad.ai/microsale/generate-salt?${query}`,
);
if (!response.ok) {
throw new Error(`Salt generation failed: ${response.status} ${await response.text()}`);
}
return response.json();
}

传入 launchpad 所支持的 router 之一。对于 V2 tax-token 上线,请使用你传给创建函数的同一个 dexKey。该 API 总是返回一个 32 字节的 salt 及其对应的 address。

为 tax token 选择 DEX​

两个 V2 tax-token 调用在其他参数之后接受一个 dexKey。请使用一个 DEX 条目已启用的受支持 router:

const [dexKey] = await launchpad.getSupportedRouters();
const dex = await launchpad.getDex(dexKey);
if (!dex.enabled) throw new Error("DEX is not enabled");

launchpad 会把该 DEX 的场所配置提供给代币。调用方无需自行发现或计算交易对 hash 或费率档位。

No-tax token​

最简单的上线方式。所选模板决定贡献者以原生币还是以某个 ERC20 quote token 支付。

const name = "My Token";
const symbol = "MTK";
const [router] = await launchpad.getSupportedRouters();
if (!router) throw new Error("No supported router");
const { salt, address: predictedToken } = await generateTokenSalt(
"CheeseToken",
name,
symbol,
router,
);

const params = {
configId: 0n,
name,
symbol,
poolDetails: "",
maxContribution: 0n, // unlimited per wallet
salt,
lockDuration: 0n, // burn LP at finalize
};

const creationFee = (await launchpad.getMasterConfig()).microSaleCreationFee;
const tx = await launchpadRW.createMicroSaleNoTaxToken(params, { value: creationFee });
const receipt = await tx.wait();

// The fundManager and token come from the MicroSaleCreated event
const ev = receipt.logs.find((l) => l.eventName === "MicroSaleCreated");
const fundManager = ev.args.fundManager;
const token = ev.args.token;
if (token.toLowerCase() !== predictedToken.toLowerCase()) {
throw new Error("Created token does not match the predicted address");
}

Tax token(钱包 fee receiver)​

结构相同,另外附加一个 taxConfig,描述 tax 及其去向。收入会发送到你指定的一个普通钱包。

const name = "Taxed";
const symbol = "TAX";
const { salt, address: predictedToken } = await generateTokenSalt(
"CheeseTaxTokenV2",
name,
symbol,
dexKey,
);

const taxConfig = {
buyTaxBps: 300n, // 3% on buys; each side max 1000 (10%)
sellTaxBps: 500n, // 5% on sells; at least one side must be > 0
taxDuration: 365n * 24n * 60n * 60n, // required; seconds after listing
antiFarmerDuration: 15n * 60n, // taxes every registered venue during this window
allocation: { // must sum to 10000
feeReceiver: 10000, // 100% to the fee receiver in this example
holders: 0,
burn: 0,
liquidity: 0,
},
feeReceiver: "0xYourRevenueWallet",
quoteToken: ethers.ZeroAddress, // launchpad fills WETH for native templates
minHolderShare: 0n, // required > 0 only when holders > 0
v3Factory: ethers.ZeroAddress, // DEX-derived fields are filled by the launchpad
v2PairInitCodeHash: ethers.ZeroHash,
v3PoolInitCodeHash: ethers.ZeroHash,
v3FeeTiers: [],
};

const params = {
configId: 0n, name, symbol, poolDetails: "",
maxContribution: 0n, salt, lockDuration: 0n,
taxConfig,
};

const tx = await launchpadRW.createMicroSaleTaxTokenV2(params, dexKey, {
value: creationFee,
});

创建时会强制执行的 tax 规则:

  • buyTaxBps 与 sellTaxBps 各自最多为 1000(10%),且至少一个必须大于零。
  • taxDuration 必须大于零且至少等于 antiFarmerDuration;两者都被限制在 150 年以内。
  • allocation.feeReceiver + holders + burn + liquidity 之和必须等于 10000。
  • Fee receiver 只有在 allocation.feeReceiver > 0 时才会获得收益,所以如果你设置了 tax 却把这一份额留空,创建会 revert(否则 receiver 永远不会拿到资金)。
  • 如果 allocation.holders > 0,你必须设置 minHolderShare > 0(钱包获得持有者分红所 需的最低余额)。
  • Tax 清算阈值是代币常量,而非创建参数。

allocation 的分配比例让同一份 tax 可以支付给一个钱包、以分红形式奖励持有者、销毁一部分, 并补充流动性。对于纯钱包路径,大多数人使用 feeReceiver: 10000。持有者/销毁/流动性份额 在 vaults.md 中有详细说明。

带 vault 的 tax token​

fee receiver 是由你指定的一个工厂为你部署的 vault。请将 taxConfig.feeReceiver 设为零地址; launchpad 会绑定所部署的 vault。由于额外的参数和工厂握手流程需要单独说明,完整的操作指南在 vaults.md。函数签名如下:

function createMicroSaleTaxTokenWithVaultV2(
CreateParamsV2 calldata params,
address vaultFactory,
bytes calldata vaultData,
address dexKey
) external payable returns (address fundManager);

为 vault 上线生成一个单独的 salt,并将 fee receiver 设为零地址:

const { salt, address: predictedToken } = await generateTokenSalt(
"CheeseTaxTokenV2",
params.name,
params.symbol,
dexKey,
);
const vaultParams = {
...params,
salt,
taxConfig: { ...params.taxConfig, feeReceiver: ethers.ZeroAddress },
};

await launchpadRW.createMicroSaleTaxTokenWithVaultV2(
vaultParams,
VAULT_FACTORY,
vaultData,
dexKey,
{ value: creationFee },
);

回读你所创建的内容​

每次创建都会触发 MicroSaleCreated(index, fundManager, owner, token, configId, timestamp)。 从中取出 fundManager 和 token,或者事后查询这次销售:

const sale = await launchpad.getMicroSaleByFundManager(fundManager);
// or by the token address, or by index
const sameByToken = await launchpad.getMicroSaleByToken(token);

贡献​

调用哪个函数取决于这次销售的募资货币。读取 sale.currency:address(0) 表示原生募资, 其他值表示 ERC20 募资。

原生(BNB)募资​

await launchpadRW.contributeMicroSale(fundManager, {
value: ethers.parseEther("1"), // 1 BNB
});

ERC20 quote 募资​

先 approve quote token,再贡献一个明确的金额。不需要 msg.value。

const quote = new ethers.Contract(sale.currency, ERC20_ABI, signer);
await quote.approve(LAUNCHPAD, amount);
await launchpadRW.contributeMicroSaleQuote(fundManager, amount);

Fee-on-transfer 类型的 quote token 会被拒绝:launchpad 会检查它实际收到的金额是否恰好 等于 amount,否则会以 Non-standard quote transfer revert。

launchpad 在每次贡献时会检查什么​

  • 销售处于 Active 状态。
  • 当前时间处于 [startTime, endTime) 区间内。
  • totalRaised + amount <= hardCap(不能超出上限)。
  • 如果 maxContribution > 0,你的累计贡献必须在该上限内。
  • 购买数量四舍五入后必须大于零(amount * rate / 1e18 > 0),因此尘埃金额会以 Contribution too small revert。

由于贡献不能超过 hard cap,填满最后一个名额所需的金额恰好是 hardCap - totalRaised。 把这个值展示给你的用户,这样他们的交易就不会 revert。

const sale = await launchpad.getMicroSaleByFundManager(fundManager);
const remaining = sale.hardCap - sale.totalRaised;

let maxForUser = remaining;
if (sale.maxContribution > 0n) {
const me = await launchpad.getMicroSaleContribution(fundManager, myAddress);
const headroom = sale.maxContribution - me.contribution;
maxForUser = headroom < remaining ? headroom : remaining;
}

Finalizing​

当 hard cap 达到时,销售所有者会 finalize 它(平台运营方也可以代为操作,这也是 UI 能够 代表所有者提供 finalize 选项的原因)。Finalize 会扣取 5% 平台费,将募资余额连同 LP 代币 一并加入 DEX,然后锁定 LP(若 lockDuration > 0)或将其销毁。

const sale = await launchpad.getMicroSaleByFundManager(fundManager);
if (sale.totalRaised === sale.hardCap && sale.state === 0n) {
await launchpadRW.finalizeMicroSale(fundManager); // sale owner
}

Finalize 之后,sale.state 变为 1(Finalized),sale.listedAt 是上线时间戳,DEX 交易对随即生效。这是代币开始交易的时刻,如果你在追踪上线情况,应关注这一事件 (MicroSaleFinalized)。

领取​

一旦销售 finalize,每位贡献者都可以领取自己购买的代币。每个地址只能领取一次。

await launchpadRW.claimMicroSale(fundManager); // sends msg.sender their tokens

const me = await launchpad.getMicroSaleContribution(fundManager, myAddress);
console.log("claimed:", me.claimed); // equals `purchased` after a successful claim

如果销售未 finalize(MicroSale not finalized)、你没有购买任何代币 (Nothing to claim),或者你已经领取过(Already claimed),claim 会 revert。

退款与退出​

根据情况不同,有两种取回资金的方式。

全额退款:销售失败或被取消​

withdrawMicroSale 在以下任一情况下退还 100% 的贡献:

  • 销售仍处于 Active 状态,但 endTime 已过且从未达到 hard cap(销售失败);或者
  • 销售被其所有者 Cancelled。
await launchpadRW.withdrawMicroSale(fundManager); // refunds msg.sender in full

这是未能填满的销售的常规路径,没有任何手续费。

在销售仍在进行时提前退出:紧急提现​

emergencyWithdrawMicroSale 允许贡献者在销售仍开放且尚未填满时离场,需支付 10% 的罚金 (罚金归平台所有)。在 endTime 前的最后 15 分钟内该操作会被阻止。

await launchpadRW.emergencyWithdrawMicroSale(fundManager); // returns 90% to msg.sender

条件:销售处于 Active,尚未达到 hard cap,销售未结束,且至少还剩 15 分钟。与全额提现不同, 这个操作会减少销售的 totalRaised,为其他人腾出空间。

取消一次销售​

所有者可以取消一个 Active 的销售,将其状态翻转为 Cancelled,并为所有贡献者开放全额 提现。

await launchpadRW.cancelMicroSale(fundManager); // sale owner

编辑一次销售​

创建之后,有两个字段仍可由销售所有者(或平台运营方)编辑:

FunctionUpdates
updateMicroSaleDetails(fundManager, poolDetails)自由格式元数据;所有者只能在销售处于 Active 时调用,运营方随时可调用
setMicroSaleMaxContribution(fundManager, maxContribution)单地址上限;0 表示无限制

两者都会触发 MicroSaleUpdated(index, fundManager, timestamp)。

读取销售状态​

搭建仪表盘所需的一切都可通过 view 调用获取,且对任何人开放。

查询一次销售​

FunctionUse
getMicroSale(index)按创建索引
getMicroSaleByFundManager(fundManager)按销售句柄
getMicroSaleByToken(token)按代币地址(与销售一一对应)
getMicroSaleByVault(vault)按绑定的 vault,用于 vault 上线
getMicroSalesCount()曾经创建过的销售总数
isQuoteTokenAllowed(token)该 ERC20 是否被接受为 vault 上线的 quote token
getAllowedQuoteTokens()平台接受的所有 ERC20 quote token

返回的 MicroSale 结构体包含完整状态:state、totalRaised、hardCap、startTime、 endTime、filledAt、listedAt、rate、maxContribution、token、currency、 vault 等等。每个字段的说明见 events-and-data.md。

查询贡献者​

// One user's ledger
const info = await launchpad.getMicroSaleContribution(fundManager, user);
// { contribution, purchased, claimed, excessiveRefunded }

// How many contributors
const n = await launchpad.getMicroSaleContributorCount(fundManager);

// A page of contributors (inclusive indexes; endIndex is clamped to the last one)
const page = await launchpad.getMicroSaleContributionInfos(fundManager, 0, 49);
// [{ user, contribution, purchased, claimed }, ...]

一个小的进度辅助函数​

async function progress(fundManager: string) {
const s = await launchpad.getMicroSaleByFundManager(fundManager);
const pct = (Number(s.totalRaised) / Number(s.hardCap)) * 100;
return {
state: ["Active", "Finalized", "Cancelled"][Number(s.state)],
raised: ethers.formatEther(s.totalRaised),
hardCap: ethers.formatEther(s.hardCap),
percent: pct.toFixed(2),
endsIn: Number(s.endTime) - Math.floor(Date.now() / 1000),
};
}

靓号代币地址​

每条创建路径都用 CREATE2 部署代币。在每次上线之前使用 generate-salt API 辅助工具;它会针对当前链上的代币模板进行 搜索,并同时返回一个唯一的 salt 和预测的代币地址。返回的地址以 6666 结尾。将返回的 salt 原封不动地传给创建函数,并使用返回的 address 进行交易前预览或交易后验证。

错误参考​

按调用分组,你最可能遇到的 revert 字符串。

Create

MessageCause
Config not found / Config not activeconfigId 无效或已停用
Invalid lock durationlockDuration 不为 0 且超出允许范围
Invalid total supply模板的 presale + LP 代币之和不等于 1e9
Insufficient creation feemsg.value 低于平台创建费
Buy tax exceeds max / Sell tax exceeds max某一侧的 tax 超过 1000 BPS
Tax must be > 0两侧 tax 都为零
Tax duration required / Tax duration too short持续时间为零或短于 antiFarmerDuration
Invalid allocationtax allocation 各份额之和不等于 10000
Fee receiver required钱包-receiver 路径中 feeReceiver 为零地址
Config not bound to this quotetaxConfig.quoteToken 与模板绑定不匹配
Dex not enableddexKey 不是一个已启用的 DEX 条目

Contribute

MessageCause
Invalid fund manager该地址不是一个 microsale 的 fund manager
Quote sale: use contributeMicroSaleQuote在 ERC20 募资中调用了原生方法
Native sale: use contributeMicroSale在原生募资中调用了 ERC20 方法
MicroSale not active销售已 finalize 或已取消
Contribution outside of start and end time早于开始或晚于结束
Buying amount exceeds hard cap会使 totalRaised 超过上限
Contribute more than allowed超过该销售的 maxContribution
Contribution too small四舍五入后代币数量为零
Non-standard quote transferfee-on-transfer 类型的 quote token

Finalize / claim / withdraw

MessageCause
Unauthorized既不是销售所有者,也不是平台运营方
Hard cap not reached在恰好填满上限之前 finalize
MicroSale not finalized在 finalize 之前 claim
Nothing to claim / Already claimed没有可领取的份额,或已经领取过
Hard cap reached对实际已填满的销售尝试全额提现
Pool is still in progress在销售结束之前进行全额提现
Hard cap already reached在上限已填满之后进行紧急提现
Too close to end time在最后 15 分钟内进行紧急提现
No contribution没有任何贡献却尝试提现