Microsales
Microsales 没有自己的合约。它们与 pools 和 Melt Pools 共用同一个 launchpad 地址——每条链一个地址、一份 ABI,三种上线方式共用。
| Network | Chain ID | Launchpad |
|---|---|---|
| BNB Chain (mainnet) | 56 | 0xD9CcE715619fA9082d21375B6B37A3a0d99138F4 |
| BNB Chain (testnet) | 97 | 0x0AA21d47b600B04527c09BbD9A30aF32cb64f6cF |
| Robinhood Chain | 4663 | 0x7E0752B42f9db92F12E221E10fFfa10bfE8668C6 |
单个 microsale 由它的 fundManager 地址标识,你会把它作为参数传给下面这些调用。它是每次
销售对应的一个句柄,而不是一个你去调用的合约。
Microsale 是一种固定价格的代币销售。你选择一个 config 模板,launchpad 铸造一个全新的代币, 贡献者持续注入资金直到达到 hard cap,finalize 时 launchpad 通过向 DEX 添加流动性来上线代币。 每个 microsale 只创建一个代币,且一个代币只能通过 microsale 上线一次。
本页介绍完整的生命周期:选择模板、创建、贡献、finalize、领取以及处理退款。带 vault 的上线有 独立的页面 vaults.md,但 create 调用在这里也会做简要说明。
目录
- 组成部分
- 销售状态
- Config 模板
- 允许的 quote token
- 代币类型与创建路径
- 创建一个 microsale
- 贡献
- Finalizing
- 领取
- 退款与退出
- 读取销售状态
- 靓号代币地址
- 错误参考
组成部分
- Config(模板)。 平台发布可复用的模板,固定了经济模型:多少代币进入 presale、多少
进入流动性、hard cap,以及销售持续时间。你通过
configId针对某个模板发起上线。 - 销售(sale)。 从模板创建出的一个实例,以其
fundManager地址标识。 - 代币。 在创建时铸造。总供应量固定为 1,000,000,000(18 位小数下为 1e27),按模板 在 presale 与 LP 之间分配。
- Fund manager。 每次销售对应一个地址,在 finalize 之前持有募资资金和代币。你不需要 直接调用它。它是你在几乎所有 microsale 函数中传入的句柄,关键事件也按它索引。
销售状态
一个 microsale 始终处于以下三种状态之一:
| State | Value | Meaning |
|---|---|---|
Active | 0 | 开放贡献 |
Finalized | 1 | 已达到 hard cap,已添加流动性,代币可领取 |
Cancelled | 2 | 已取消;贡献者可提现 |
一个关键细节:只有当募资金额精确达到 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,
});
}
相关的读取方法:
| Function | Returns |
|---|---|
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 地址。
| Function | Token | Tax | Raise currency | Fee receiver |
|---|---|---|---|---|
createMicroSaleNoTaxToken | CheeseToken | 无 | 模板绑定的原生或 ERC20 | 不适用 |
createMicroSaleTaxTokenV2 | CheeseTaxTokenV2 | 买入/卖出 tax | 模板绑定的原生或 ERC20 | 你指定的钱包 |
createMicroSaleTaxTokenWithVaultV2 | CheeseTaxTokenV2 | 买入/卖出 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 内):
| Field | Type | Notes |
|---|---|---|
configId | uint256 | 要发起上线的模板;必须处于活跃状态 |
name | string | 代币名称 |
symbol | string | 代币符号 |
poolDetails | string | 自由格式元数据(JSON、URL,或你的 UI 存储的任何内容) |
maxContribution | uint256 | 单地址累计贡献上限;0 表示无限制 |
salt | bytes32 | 由 CheesePad API 返回的全新 CREATE2 salt |
lockDuration | uint256 | LP 锁定时长(秒);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 smallrevert。
由于贡献不能超过 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
编辑一次销售
创建之后,有两个字段仍可由销售所有者(或平台运营方)编辑:
| Function | Updates |
|---|---|
updateMicroSaleDetails(fundManager, poolDetails) | 自由格式元数据;所有者只能在销售处于 Active 时调用,运营方随时可调用 |
setMicroSaleMaxContribution(fundManager, maxContribution) | 单地址上限;0 表示无限制 |
两者都会触发 MicroSaleUpdated(index, fundManager, timestamp)。
读取销售状态
搭建仪表盘所需的一切都可通过 view 调用获取,且对任何人开放。
查询一次销售
| Function | Use |
|---|---|
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
| Message | Cause |
|---|---|
Config not found / Config not active | configId 无效或已停用 |
Invalid lock duration | lockDuration 不为 0 且超出允许范围 |
Invalid total supply | 模板的 presale + LP 代币之和不等于 1e9 |
Insufficient creation fee | msg.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 allocation | tax allocation 各份额之和不等于 10000 |
Fee receiver required | 钱包-receiver 路径中 feeReceiver 为零地址 |
Config not bound to this quote | taxConfig.quoteToken 与模板绑定不匹配 |
Dex not enabled | dexKey 不是一个已启用的 DEX 条目 |
Contribute
| Message | Cause |
|---|---|
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 transfer | fee-on-transfer 类型的 quote token |
Finalize / claim / withdraw
| Message | Cause |
|---|---|
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 | 没有任何贡献却尝试提现 |