Pool Launch — 集成指南
Pool Launch 在单笔交易内把税币直接上线到 V2 DEX 池。没有联合曲线阶段:部署代币、注入并锁定池子、处理 dev 余量、可选的创建者首买,全部原子完成。
目录
- 一图看懂 Pool Launch
- 已部署地址(BNB Chain 测试网)
- 已部署地址(BNB Chain 主网)
- Pool Launch 做了什么
- Pool Launch 中的毕业
- 面向 Terminal 与 Bot
- 面向 Mini App 开发者
- 面向 Vault 开发者
一图看懂 Pool Launch
没有曲线:直接进入真实池子
联合曲线发射固定了总量和曲线;代币要等曲线被买满才会上 DEX。Pool Launch 跳过曲线,由创建者决定:
- 总供应量。 100 到 10¹⁵ 枚(18 位小数)之间任意取值 —— 不是协议固定的总量。
- 多少代币注入池子(
poolToken)以及配多少 quote(poolQuote)。两者共同决定开盘价poolQuote / poolToken。
LP 在同一笔交易内销毁,池子无法被撤走。代币从第一个区块起就在 DEX 上交易。
毕业:发射时只检查一次
Pool Launch 代币从第一个区块起就在 DEX 上,没有迁移步骤。当前 V2 kind 的「毕业」指:发射时为池子注入了足够的 quote。
- 检查只在发射交易内执行一次,紧接在池子注入之后。
- 达到门槛 → 发出
FlapTokenGraduated,终端可以把代币显示为已毕业。 - 未达到 → 不发出事件。当前 kind 下,代币留在新币栏目,之后不会补发。
公式见 Pool Launch 中的毕业。
Pool Launch 本身看起来很简单:注入池子、销毁 LP,就结束了。真正让它不止于此的是新的 Pool Launch Vault。它为直接构建在发射之上的大量新应用打开了大门 —— 挖矿、空投,以及更复杂的机制。
既处理收入、也分发代币的 Vault
Pool Launch Vault 做两件事:
- 收入。 和联合曲线 Vault 一样,接收代币交易税的一部分(
VAULTfee slot)。 - 预留代币。 还可以在发射交易内直接接收未注入池子的代币,并按写进合约的规则分发 —— 线性释放、空投、质押奖励、回购等。factory 会在 vault 创建之前校验整个发射,所以这些规则属于审计范围。
这样创建者可以围绕预留代币构建机制,而不必把它放在一个随时可以砸盘的钱包里。
示例 Vault:注入流动性,再逐步收回成本
这只是一种可能的 Vault,而非规范本身。以 BNB 为报价币的税币为例:创建者把 12.5 BNB 与 20% 的代币注入池子。Vault 把 BNB 税收的 20% 发给创建者,直到创建者收到初始资金的 1.5 倍(18.75 BNB),其余存入分红合约给持有者;达到上限后 100% 归持有者。另有 1% 代币在 30 天内线性释放给创建者。下文的 三个示例 Vault 都基于同样的设置。
- 没有 Vault 时: 创建者想收回 12.5 BNB,只能自己囤一部分代币再砸盘。价格崩溃,持有者亏损,创建者转去发下一个币。
- 使用这个 Vault: 80% 的预留代币按固定规则存放在 Vault 中,创建者手里没有可砸的筹码(只有在 30 天内线性释放的 1%)。创建者从交易税中以 BNB 收回成本,最高为初始资金的 1.5 倍。交易越活跃,回本越快,所以创建者的利益在于让代币持续活跃,而不是砸死它。
- 创建者有了新的收益方式: 砸盘不再是唯一的回报途径。Pool Launch 给了创建者第二份收入 —— 通过 Vault、按任何人都能查看的规则发放的交易税,所以让代币持续活跃是有回报的。
可以构建什么:三个示例 Vault
三个示例都沿用上面的设置:总量的 80% 进入 vault,1% 在 30 天内线性释放给创建者,quote 收入的 20% 给创建者直到初始资金的 1.5 倍,其余作为分红给持有者。它们的区别在于剩余 79% 的用法。各自的伪代码见 示例 Vault。
质押挖矿
79% 作为挖矿池,线性释放给质押者。质押者选择锁定期;锁得越久,权重倍数越高。
随市值上涨释放预留代币
Vault 在递增的市值档位挂出固定价格卖单:20 万美元市值卖出总量的 1%,30 万美元再卖 1%,以此类推。只有市场涨到对应档位时才会释放代币。卖单收入作为分红给持有者,并逐步释放。
Vault 作为众多单边池的 hook
Vault 作为 PancakeSwap Infinity 或 Uniswap v4 池的 hook。它用预留代币对多种 quote 开设单边池,价格由 hook 内的自定义曲线决定。开池不需要买入任何 quote,因此代币可以与任意多个 quote 配对。
为什么需要代币驿站
任何预留代币的机制在链上都表现为一个巨大的持有者。交易者看到「78% 在一个地址里」,就会以为要跑路。代币驿站(Token Dock)解决这个问题:各个 Vault 把预留代币托管到同一个公开、可识别的合约里,终端可以把它标记为「预留」,就像锁仓合约让被锁的 LP 一眼可辨一样。
所有 Vault 都通过 TokenDockRouter 存入。尚未验证的 Vault 使用开放驿站;Flap 认证其为低风险后,Portal 把 (vault, token) 加入白名单,余额会自动迁移到已验证驿站。提款永远不受限制。接口见 余量去向。
本指南面向谁
- Terminal、bot 与钱包 —— 发射代币、预览结果、索引发射事件、读取状态。
- Vault 开发者 —— 通过
VAULTfee slot 把自定义 vault 接入发射。
已部署地址(BNB Chain 测试网)
| 合约 | 地址 |
|---|---|
| Portal(代理) | 0x000042108465206185A077Edc3F3fd2b49060000 |
| FlapDirectTaxToken 实现 | 0x7777c57ff7dc441c1715b2e36fa581c48B782222 |
| TaxTokenHelper | 0xd64441e5fcd02d342b8cf6eba10ef6e40d0da90f |
| AllocationStrategy | 0x000051353ef814E4779B692cb2Bb3A6Cb3710000 |
| TokenDockRouter | 0x000048E888aBf09f690a952E1b386b2D031a0000 |
| OpenTokenDock | 0x0000E34C74b9eC7b52F35528249Aa9C4AdE00000 |
| VerifiedTokenDock | 0x00004B06D90Df7a39491264744BF187D90Df0000 |
已部署地址(BNB Chain 主网)
| 合约 | 地址 |
|---|---|
| Portal(代理) | 0x000042108465206185A077Edc3F3fd2b49060000 |
| FlapDirectTaxToken 实现 | 0x7777c57ff7dc441c1715b2e36fa581c48B782222 |
| TaxTokenHelper | 0x53841c73217735F37BC1775538b03b23feFD8346 |
| AllocationStrategy | 0x000051353ef814E4779B692cb2Bb3A6Cb3710000 |
| TokenDockRouter | 0x000048E888aBf09f690a952E1b386b2D031a0000 |
| OpenTokenDock | 0x0000E34C74b9eC7b52F35528249Aa9C4AdE00000 |
| VerifiedTokenDock | 0x00004B06D90Df7a39491264744BF187D90Df0000 |
Pool Launch 做了什么
调用 launchTaxTokenV2 会在同一笔交易内完成整个发射:
- 创建税币、其 dividend tracker 与 tax processor。若发射带有
VAULTfee slot,配置的 vault factory 会在这一步校验发射并创建该代币的 vault。 - 注入 V2 池:
poolToken+poolQuote,LP 销毁到 dead 地址。 - 标记代币
status = DEX,并记录 dev 预留余量。 - 若
distribution.amount > 0,分发余量。 - 若
creatorBuyQuote > 0,执行创建者首买。
交易完成后代币立即可交易。
Pool Launch 中的毕业
当前 DIRECT_V2_POOL_TAX kind 的毕业是发射时的一次性检查:代币已经在 DEX 上,没有迁移步骤。池子 quote 边的 USD 价值达到本次进池比例对应的门槛,就算毕业。
quoteUsd = poolQuote × quotePriceUsd
floorUsd = 1500 × multiplierBps / 10000 × totalSupply / poolToken
createFloorUsd = 750 × multiplierBps / 10000 × totalSupply / poolToken
已毕业 ⇔ quoteUsd ≥ floorUsd
允许创建 ⇔ quoteUsd ≥ createFloorUsd
毕业门槛以 100% 进池时的 $1,500 为基准,按 1 / 进池比例 放大:1× 时,50% → $3,000,20% → $7,500。multiplierBps 默认为 10000(1×),除非 vault factory 调低;同一个倍数也会调低创建门槛(100% 进池时基准 $750):50% → $1,500,20% → $3,750,0.5× 时再减半。低于创建门槛会 revert LaunchLiquidityTooLow;通过创建门槛不代表已经毕业。
达标时,FlapTokenGraduated 在发射交易内发出。当前 kind 不会重复检查;发射时没有事件就意味着不会毕业。发射前用 graduationFloorV2() 同时预览两个门槛 —— 见 发射前预览。旧的 graduationFloor() 仍保留给只关心毕业门槛的调用方。
毕业规则取决于 kind,详见 需要索引的事件。
面向 Terminal 与 Bot
入口
/// @notice 一笔交易完成税币发射:创建代币、注入并锁定 V2 池子、分发 dev 余量,
/// 并执行可选的创建者首买。
/// @dev 原生 quote:`msg.value` 必须等于 `p.poolQuote + p.creatorBuyQuote`。
/// ERC-20 quote:`msg.value` 必须为 0;先给 Portal approve `p.poolQuote + p.creatorBuyQuote`。
/// @param p 创建参数。`poolQuote` / `poolToken` / `creatorBuyQuote` 只从 `p` 读取,
/// 调用者无需重复传入,实际注入的池子也一定与创建代币时提交的数值一致。
/// @param distribution dev 预留余量的交接。`distribution.amount > 0` 时,池子注入后立即把这部分
/// 发给解析出的 strategy。发射之后**没有**单独的分发入口:发射要么把全部供应
/// 注入池子(`poolToken == totalSupply`),要么在这里交出余量。余量策略对**每次**
/// 发射都会执行 —— `pendingRemainder` 超过 1 token 会 revert `ExcessiveRemainder`;
/// 不超过 1 token 的残余作为 dust 销毁到 DEAD。
/// @param creatorBuy 可选。`p.creatorBuyQuote > 0` 时,在池子注入、余量分发**之后**执行创建者首买;
/// `minOutputAmount` / `permitData` 是这次 swap 自己的滑点下限与 permit。
/// `p.creatorBuyQuote == 0` 时忽略。
/// @return token 新部署的税币。
/// @return pool 已注入并永久锁定的 V2 池子。
function launchTaxTokenV2(
CreateTaxTokenParams calldata p,
Distribution calldata distribution,
CreatorBuyParams calldata creatorBuy
) external payable returns (address token, address pool);- 原生 quote(
quoteToken == address(0)):msg.value = poolQuote + creatorBuyQuote。 - ERC-20 quote:
msg.value = 0;先给 PortalapprovepoolQuote + creatorBuyQuote。 - 该
(DIRECT_V2_POOL_TAX, quoteToken)组合必须已被协议启用;否则 revertLaunchQuotePairNotEnabled。
发射参数
CreateTaxTokenParams
/// @notice Pool Launch 的参数(即 `launchTaxTokenV2` 的 `p`)。
/// @dev `poolQuote` / `poolToken` 在创建代币时提交,注入池子时再次校验。创建阶段不拉取任何资金。
struct CreateTaxTokenParams {
string name;
string symbol;
string meta; // 代币元数据(IPFS CID)
uint256 totalSupply; // 由创建者自定义,范围 [100 ether, 1e15 ether],18 位小数
bytes32 salt; // CREATE2 salt;代币地址必须以靓号后缀结尾
address quoteToken; // address(0) = 原生币;池子身份在 initialize() 时写入代币
uint8 dexId; // DEX 提供方
uint256 poolQuote; // 将注入池子的 quote(> 0)
uint256 poolToken; // 将注入池子的代币(> 0 且 <= totalSupply)
uint16 buyTaxRate; // bps
uint16 sellTaxRate; // bps
uint32 taxDuration; // 秒;按协议全局上下限校验
uint32 antiFarmerDuration; // 秒
FeeConfig[] feeConfigs; // 激活槽的 bps 之和必须恰好 10000
// ---- 本版本不支持:保留在 ABI 中,但需传 0 / 0 / [] ----
uint8 antiSniperMode; // 必须为 0
uint32 exemptWindow; // 必须为 0
address[] exemptAddrs; // 必须为空
// ---- 可选的创建者首买 ----
// > 0 时让 launchTaxTokenV2 用这么多 quote 执行创建者首买。
uint256 creatorBuyQuote;
// ---- 本版本不支持:保留在 ABI 中,但传 0 / 0x ----
bytes32 extensionID;
bytes extensionData;
// ---- 可选佣金 ----
// address(0) = 关闭。设置后,佣金 bps 由本次发射的有效税率(max(buyTaxRate, sellTaxRate))推导,
// 写入代币的 tax processor;分发时从协议费份额中支付给该地址。
address commissionReceiver;
}| 字段 | 类型 | 说明 |
|---|---|---|
name、symbol、meta | string | 代币标识。meta 是 IPFS CID。 |
totalSupply | uint256 | 由创建者自定义 —— [100 ether, 1e15 ether] 内任意值(100 到 10¹⁵ 枚,18 位小数)。 |
salt | bytes32 | CREATE2 salt。生成的地址必须以税币靓号后缀(0x7777)结尾。 |
quoteToken | address | address(0) = 原生币。 |
dexId | uint8 | 目标 DEX。所有支持链上都是 0。 |
poolQuote | uint256 | 注入池中的 quote。在 create 时提交,注池时再校验。 |
poolToken | uint256 | 注入池中的代币。必须 > 0 且 <= totalSupply。 |
buyTaxRate、sellTaxRate | uint16 | bps。最大 1000(10%)。 |
taxDuration | uint32 | 秒。 |
antiFarmerDuration | uint32 | 秒。 |
feeConfigs | FeeConfig[] | 最多 4 项。激活槽 bps 之和必须恰好 10000。见 Fee slots。 |
antiSniperMode、exemptWindow、exemptAddrs | uint8、uint32、address[] | 本版本不支持。 传 0、0、[]。 |
creatorBuyQuote | uint256 | 创建者首买金额。0 = 不首买。 |
extensionID、extensionData | bytes32、bytes | 本版本不支持。 传 bytes32(0) / 0x。 |
commissionReceiver | address | 可选佣金接收地址。address(0) = 关闭。 |
创建者首买前的开盘价为:
P0 = poolQuote / poolToken
发射必须把全部供应注入池子(poolToken == totalSupply),或通过 distribution 处理余量。
Salt 锁定(防抢跑)
代币地址是 CREATE2(tokenImplDirectTaxed, salt, Portal)。知道 salt 的人都能提前算出地址并抢先发射,所以创建者可以在发射前用原生币支付 SALT_LOCK_FEE() 预留 salt。锁定后,只有 locker 能用该 salt 发射代币。
function lockSalt(bytes32 salt, uint8 kind) external payable;kind=DIRECT_V2_POOL_TAX(0)。msg.value必须精确等于SALT_LOCK_FEE();费用转给 fee receiver,不退还。- 预测地址必须满足靓号后缀(
0x7777);否则VanityAddressRequirementNotMet。 - 一个 salt 只能锁定一次;重复锁定会 revert。已经部署了代币的 salt 不能锁定。
- 锁定没有过期时间;第一次成功发射后会被消耗,entry 保留在链上作为审计记录。
- 未锁定的 salt 仍然任何人都能发射 —— 锁定是可选操作。
const fee = await portal.SALT_LOCK_FEE();
await portal.lockSalt(salt, 0, { value: fee }); // 0 = DIRECT_V2_POOL_TAX
// 之后由 locker 用同一个 salt 发射
await portal.launchTaxTokenV2({ ...p, salt }, distribution, creatorBuy);查询锁定状态:
const lock = await portal.getSaltLock(salt);
// lock.locker / lock.launchKind / lock.isUsed事件:
event FlapSaltLocked(address locker, bytes32 salt, address tokenAddress, uint8 launchKind, uint256 ts);
event FlapSaltUsed(address locker, bytes32 salt, address tokenAddress, uint8 launchKind, uint256 ts);FlapSaltLocked 在预留时发出;FlapSaltUsed 在被发射消耗时发出。
错误:FeatureDisabled(未开启 salt locking —— fee 为 0)、SaltLockFeeMismatch(expected, provided)、SaltAlreadyLockedByAnotherUser(salt, locker)、SaltAlreadyLockedBySelf(salt)、SaltLockTokenAlreadyDeployed(salt, token)、UnsupportedLaunchKind(kind)、VanityAddressRequirementNotMet(token)。发射时:FlapSaltLockedByAnotherUser(salt, locker)、SaltLockKindMismatch(salt, locked, provided)。
Fee slots
/// @dev `FeeConfigDividend.dividendToken` 哨兵值:分红发放本次发射的代币本身。
address constant MAGIC_DIVIDEND_SELF = 0xfEEDFEEDfeEDFEedFEEdFEEDFeEdfEEdFeEdFEEd;
/// @dev `FeeConfigDividend.dividendToken` 哨兵值:由本次发射的 vault factory 计算分红代币。
address constant MAGIC_DIVIDEND_COMPUTED = 0xC0Dec0dec0DeC0Dec0dEc0DEC0DEC0DEC0DEC0dE;
/// @notice Fee slot 类型;决定 `FeeConfig.configData` 的解码方式。只追加、不改序。
/// NONE → 忽略 configData(空槽)
/// DEFLATION → configData 为空(销毁到 DEAD;无参数)
/// LP_BPS → configData 为空(加入 LP;无参数)
/// MARKETING → configData = abi.encode(FeeConfigMarket)
/// VAULT → configData = abi.encode(FeeConfigVault)
/// DIVIDEND → configData = abi.encode(FeeConfigDividend)
enum FeeSlotType {
NONE, // 0
DEFLATION, // 1: 销毁到 DEAD
LP_BPS, // 2: 加入 LP
MARKETING, // 3: 发给营销钱包
VAULT, // 4: 发给本次发射的 vault
DIVIDEND // 5: 作为分红分发
}
/// @notice 一个 fee slot:类型标签、bps 份额,以及按 `feeType` 解码的不透明数据。
/// @dev 新增 fee 类型不会改变此结构体 —— 只需给 `FeeSlotType` 加一个值并定义新的 `FeeConfigXxx`
/// 解码目标。所有非 NONE 槽的 bps 之和必须恰好为 10000。
struct FeeConfig {
FeeSlotType feeType;
uint16 bps;
bytes configData;
}
/// @notice `FeeSlotType.MARKETING` 的 `FeeConfig.configData` 内容。
struct FeeConfigMarket {
address marketingAddress;
}
/// @notice `FeeSlotType.VAULT` 的 `FeeConfig.configData` 内容。
/// @param vaultFactory 校验发射并创建 vault 的 `IDirectLaunchVaultFactory`。
/// @param vaultData factory 自定义参数,布局由 `vaultDataSchema()` 描述。
struct FeeConfigVault {
address vaultFactory;
bytes vaultData;
}
/// @notice `FeeSlotType.DIVIDEND` 的 `FeeConfig.configData` 内容。
/// @dev `dividendToken`:0 = quote,MAGIC_DIVIDEND_SELF = 本次发射的代币,MAGIC_DIVIDEND_COMPUTED = 由
/// 本次发射的 vault factory 计算(需要 VAULT 槽),否则为具体的 ERC-20(quote、本次发射的代币,
/// 或 swap registry 支持的资产)。
/// `minimumShareBalance` 必须在本次发射 totalSupply 的 [0.001%, 0.1%] 区间内。
struct FeeConfigDividend {
address dividendToken;
uint256 minimumShareBalance;
}configData 编码:
| 槽 | configData |
|---|---|
MARKETING | abi.encode(address marketingAddress) |
VAULT | abi.encode(address vaultFactory, bytes vaultData) |
DIVIDEND | abi.encode(address dividendToken, uint256 minimumShareBalance) |
DEFLATION / LP_BPS | 空 |
规则:
- 所有激活(非
NONE)槽的bps之和必须恰好10000。 feeConfigs最多 4 项。每种类型(DEFLATION、LP_BPS、DIVIDEND、MARKETING、VAULT)最多出现一次。MARKETING与VAULT互斥。DIVIDEND的minimumShareBalance必须在totalSupply的[0.001%, 0.1%]区间内。VAULT的地址不由调用者提供。Portal 通过vaultFactory解析,并把它放在本次发射的 marketing-or-vault 槽。见 面向 Vault 开发者。
Dev 余量
/// @notice 选择本次发射 dev 预留余量的去向。
/// NONE - 不分发(余量策略仍会执行;`amount` 必须为 0)。
/// EXPLICIT - 直接使用 `Distribution.strategy`(调用者指定的 IStrategy,必须非零)。
/// VAULT - 发给**本次**发射的 provenance vault;`strategy` 字段被忽略,
/// 并被解析出的 vault 覆盖。
enum DistributionTarget {
NONE, // 0
EXPLICIT, // 1
VAULT // 2
}
/// @notice 一条分发指令:使用哪个目标 / strategy、分发多少代币,以及 strategy 自定义数据。
/// @param target 余量去向(见 `DistributionTarget`)。
/// @param strategy EXPLICIT 时调用的 `IStrategy`;NONE 与 VAULT 时忽略。
/// @param amount 交给 strategy 的代币数量。不得超过记录的 `pendingRemainder`。
/// @param configData strategy 自定义的不透明参数,原样传给 `initializeDistribution`。
struct Distribution {
DistributionTarget target;
address strategy;
uint128 amount;
bytes configData;
}NONE:不分发。amount必须为0。EXPLICIT:把amount发给调用者指定的strategy(必须非零)。VAULT:把amount发给本次发射的 provenance vault。strategy被忽略。amount是 dev 预留余量,通常是totalSupply - poolToken。
余量策略在每次发射都会执行,无论是否指定 distribution:
pendingRemainder == 0:无需处理。pendingRemainder在(0, 1 token]:作为 dust 销毁到 DEAD。pendingRemainder > 1 token:发射 revertExcessiveRemainder。
创建者首买
/// @notice 可选的创建者首买 swap 参数。仅在 `p.creatorBuyQuote > 0` 时执行。
/// 此结构体只携带 swap **自身**的参数 ——
/// 不含数量(== creatorBuyQuote),也不含方向(隐含为 quote → 刚发射的代币)。
/// @param minOutputAmount 最少获得的代币数量(滑点下限)。
/// @param permitData ERC-20 quote 的可选 EIP-2612 permit;为空则依赖事先的 approve。
struct CreatorBuyParams {
uint256 minOutputAmount;
bytes permitData;
}仅在 creatorBuyQuote > 0 时使用。池子注入且余量分发完成后,Portal 会用 creatorBuyQuote 的 quote 执行一次普通买入,输出发给调用者。minOutputAmount 是滑点下限;permitData 是 ERC-20 quote 的可选 EIP-2612 permit。
发射前预览
新集成调用 graduationFloorV2 同时预览创建门槛和毕业门槛。旧的 graduationFloor 仍只返回原来的四个毕业字段。
const preview = await portal.graduationFloorV2({
quoteToken, // address(0) = 原生币
quotePriceUsd, // 18 位 WAD
poolQuote,
poolToken,
totalSupply,
vaultFactory, // 没有 VAULT 槽时传 address(0)
});
// preview.meetsCreateFloor, preview.createFloorUsdWad
// preview.meetsFloor, preview.floorUsdWad
// preview.quoteUsdWad, preview.multiplierBps/// @notice 输入与旧版预览完全相同。
struct GraduationFloorParams {
address quoteToken; // address(0) = 原生币
uint256 quotePriceUsd; // quote 代币 USD 价格,18 位 WAD
uint256 poolQuote; // 注入 V2 池子的 quote
uint256 poolToken; // 注入 V2 池子的代币
uint256 totalSupply; // 发射总供应量
address vaultFactory; // 本次发射 VAULT 槽的 factory(address(0) = 无)
}
/// @notice V2 结果:原有四个字段 + 创建门槛字段。所有 USD 数值都是 18 位 WAD。
struct GraduationFloorResultV2 {
bool meetsFloor; // quoteUsdWad >= floorUsdWad
uint256 floorUsdWad; // 毕业所需 pool quote USD(向上取整)
uint256 quoteUsdWad; // 池子 quote 的 USD 价值 = toWad(poolQuote) * quotePriceUsd
uint16 multiplierBps; // 实际倍数(10000 = 1x)
bool meetsCreateFloor; // quoteUsdWad >= createFloorUsdWad
uint256 createFloorUsdWad; // 创建所需 pool quote USD(向上取整)
}
function graduationFloorV2(GraduationFloorParams calldata params)
external
view
returns (GraduationFloorResultV2 memory result);| 字段 | 公式 |
|---|---|
quoteUsdWad | toWad(poolQuote) * quotePriceUsd |
createFloorUsdWad | ceil(750 * multiplierBps / 10000 * totalSupply / poolToken) |
floorUsdWad | ceil(1500 * multiplierBps / 10000 * totalSupply / poolToken) |
meetsCreateFloor | quoteUsdWad >= createFloorUsdWad |
meetsFloor | quoteUsdWad >= floorUsdWad |
meetsCreateFloor == false表示launchTaxTokenV2会 revertLaunchLiquidityTooLow(quoteUsdWad, createFloorUsdWad)。- 位于两个门槛之间是合法状态:允许创建,但不会毕业。
createFloorUsdWad/floorUsdWad是整个池子需要的 quote USD,不是单价。- 距离毕业还差多少:
再按 quote token 的 decimals 转成实际数量。
const graduationShortfallUsd = max(0n, preview.floorUsdWad - preview.quoteUsdWad); const graduationShortfallQuoteWad = graduationShortfallUsd * 10n ** 18n / quotePriceUsd; - 例子:BNB $600、3 BNB、50% 进池 →
quoteUsd = $1,800,createFloor = $1,500,floor = $3,000;允许创建但不毕业,距离毕业还差 $1,200(2 BNB)。
旧的 graduationFloor() 和它的四字段 GraduationFloorResult 保持不变;新前端/终端请调用 graduationFloorV2()。
其余数字都是发射入参的纯四则运算:
| 指标 | 公式 |
|---|---|
| Initial price | poolQuote / poolToken |
| Initial liquidity | poolQuote * 2 |
| Pool allocation | poolToken |
| Reserved (Dev or Vault) | totalSupply - poolToken |
| FDV | totalSupply * initialPrice |
创建者首买的输出:池子是标准 V2 pair。可用目标 DEX 的 LP fee 按 V2 恒定乘积公式计算,或用 creatorBuy.minOutputAmount 做滑点保护。
发射示例
import { ethers } from "ethers";
const portal = new ethers.Contract(PORTAL, PORTAL_ABI, signer);
const feeConfigs = [
{
feeType: 3, // MARKETING
bps: 9000,
configData: ethers.AbiCoder.defaultAbiCoder().encode(["address"], [marketing]),
},
{ feeType: 2, bps: 1000, configData: "0x" }, // LP_BPS
];
const p = {
name: "My Token",
symbol: "MTK",
meta: "bafkrei...",
totalSupply: 1_000_000_000n * 10n ** 18n,
salt,
quoteToken: ethers.ZeroAddress, // 原生币
dexId: 0,
poolQuote: ethers.parseEther("20"),
poolToken: ethers.parseEther("800000000"),
buyTaxRate: 500,
sellTaxRate: 500,
taxDuration: 86400n,
antiFarmerDuration: 0n,
feeConfigs,
antiSniperMode: 0,
exemptWindow: 0,
exemptAddrs: [],
creatorBuyQuote: 0n,
extensionID: ethers.ZeroHash,
extensionData: "0x",
commissionReceiver: ethers.ZeroAddress,
};
// 无余量:全部注入池子。
const distribution = {
target: 0, // NONE
strategy: ethers.ZeroAddress,
amount: 0n,
configData: "0x",
};
const creatorBuy = { minOutputAmount: 0n, permitData: "0x" };
const tx = await portal.launchTaxTokenV2(p, distribution, creatorBuy, {
value: p.poolQuote + p.creatorBuyQuote,
});
await tx.wait();把 dev 余量交给本次发射的 vault:
const distribution = {
target: 2, // VAULT
strategy: ethers.ZeroAddress,
amount: totalSupply - poolToken,
configData: "0x",
};ERC-20 quote:把 quoteToken 设为该代币,不发送 value,并先给 Portal approve poolQuote + creatorBuyQuote。
读取发射状态
const state = await portal.getLaunchToken(token);
// kind → 0 (DIRECT_V2_POOL_TAX)
// tokenVersion → 0 (TOKEN_DIRECT_TAXED)
// externalVenue → migrator 类型
// dexId, quoteTokenType, quoteTokenAddress
// status → 6 = Created, 4 = DEX
// circulatingSupply, pendingRemainder
// pool, poolType, venueData
// totalSupplylaunchQuoteConfig(kind, quoteToken) 返回 allowlist 开关与配置的 quote 美元价:
const [enabled, quotePriceUsd] = await portal.launchQuoteConfig(0, quoteToken);/// @notice 扁平化的发射状态 = LaunchState + LaunchDexPool + 若干代币信息,一次调用取回。
/// @dev 枚举字段以 uint8 返回,这样即使调用方 ABI 还不认识新的枚举值,读取也不会 revert。
struct LaunchTokenState {
// ── 不可变的发射身份 ──
uint8 kind; // LaunchKind(0 = DIRECT_V2_POOL_TAX)
uint8 tokenVersion; // TokenVersion(0 = TOKEN_DIRECT_TAXED)
uint8 externalVenue; // 代币上架的 MigratorType
// ── 不可变的发射参数 ──
uint8 dexId; // DEX 提供方 id
uint8 quoteTokenType; // QuoteTokenType(原生币或 ERC-20)
address quoteTokenAddress; // quote 代币(原生币为 address(0))
// ── 可变的运行时状态 ──
uint8 status; // TokenStatus(池子创建前为 Created,之后为 DEX)
uint256 circulatingSupply; // 流通量
uint256 pendingRemainder; // 仍可通过分发交出的余量;Created 状态下为 0
// ── 外部池子 ──
address pool; // 池子地址
uint8 poolType; // PoolType 场所类型
bytes venueData; // 场所参数(V2 为空)
// ── 代币信息(实时读取)──
uint256 totalSupply; // 代币的 ERC-20 totalSupply()
}
/// @notice 代币的完整发射状态。若 `token` 不是通过 Pool Launch 发射的,revert `LaunchTokenNotFound`。
function getLaunchToken(address token) external view returns (LaunchTokenState memory state);
/// @notice 读取某个 (kind, quote token) 组合的发射配置。
/// @return enabled 该组合的发射白名单开关。
/// @return quotePriceUsd quote 代币 USD 价格,18 位 WAD。
function launchQuoteConfig(LaunchKind kind, address quoteToken)
external
view
returns (bool enabled, uint256 quotePriceUsd);
/// @notice 发射 VAULT fee slot 的 vault provenance。若发射没有 VAULT 槽(或 `token` 不是发射代币),
/// 两个返回值都是 address(0)。
function getLaunchVault(address token) external view returns (address vault, address vaultFactory);
/// @notice vault provenance 及其认证元数据。
struct LaunchVaultInfo {
address vault;
address vaultFactory;
bool isOfficial;
uint8 riskLevel;
uint8 category;
}
/// @notice 代币 vault provenance 与元数据的兼容视图(见 `LaunchVaultInfo`)。
function getLaunchVaultInfo(address token) external view returns (LaunchVaultInfo memory info);需要索引的事件
/// @notice 本发射路径下 `FlapDirectLaunched.data` 的 schema id。
/// @dev `kind` 表示**哪种**发射形态(稳定的业务分类);`schemaId` 表示 `data` 中打包的是**哪种**结构体
/// 布局,二者可独立演进。形态新增字段不会改变 `kind`,但会升级 schema id(_V1 → _V2)。
/// 索引器从解码后的日志读取 `kind`、按 `schemaId` 选择解码器;遇到未知的 schema id,说明 `data` 使用了比当前实现更新的布局。
bytes32 constant V2_POOL_LAUNCH_SCHEMA_V1 = keccak256("DirectLaunchV2(DirectLaunchV2Data)");
/// @notice 所有 Pool Launch 唯一的发射事件。携带代币、形态(`kind`)、创建者、布局 id(`schemaId`)
/// 以及 abi 编码的形态结构体(`data`)。
/// @dev topic0 = keccak256("FlapDirectLaunched(address,uint8,address,bytes32,bytes)")。
/// V2_POOL_LAUNCH_SCHEMA_V1 → abi.decode(data, (DirectLaunchV2Data))。
/// @param token 发射的代币(关联键)。
/// @param kind 发射形态(DIRECT_V2_POOL_TAX)。
/// @param creator 发射调用者(msg.sender)。
/// @param schemaId 编码结构体规范标签的 keccak256。
/// @param data abi.encode(<schemaId 对应的形态结构体>)。
event FlapDirectLaunched(
address token,
LaunchKind kind,
address creator,
bytes32 schemaId,
bytes data
);
/// @notice 发射达到毕业门槛时,在发射交易内发出一次。之后永不发出;发射时未达标的代币永远不会毕业。
event FlapTokenGraduated(address token);
/// @notice 通过 `lockSalt` 预留 CREATE2 salt 时发出。
event FlapSaltLocked(address locker, bytes32 salt, address tokenAddress, uint8 launchKind, uint256 ts);
/// @notice 被锁定的 salt 被成功发射消耗时发出。
event FlapSaltUsed(address locker, bytes32 salt, address tokenAddress, uint8 launchKind, uint256 ts);
/// @notice 发射为其 VAULT 槽解析出 vault 与 provenance 时发出。
/// @dev 不是分发标记:发射可以有 VAULT 槽,但不把余量交给它。用 `FlapDistributionExecuted`
/// (target == VAULT)判断是否真的分发了余量。
event FlapLaunchVaultResolved(address token, address vault, address vaultFactory);
/// @notice 代币的 dev 余量通过 strategy 分发时发出。
/// @param token 被分发的代币。
/// @param target 调用者意图(`NONE` / `EXPLICIT` / `VAULT`)。`VAULT` 表示本次发射把余量交给了
/// provenance vault。
/// @param strategy 实际拉取代币的已解析 strategy(`VAULT` 时就是该 vault)。
/// @param amount 分发数量。
event FlapDistributionExecuted(
address token,
DistributionTarget target,
address strategy,
uint256 amount
);
/// @notice 与 ERC-20 `Transfer` 相同,另附双方变动后的 `balanceOf`,索引器无需对每笔转账单独查余额。
event FlapTokenBalanceChanged(
address from,
address to,
uint256 value,
uint256 fromBalance,
uint256 toBalance
);FlapDirectLaunched是标准发射记录;schemaId决定data的布局。- 本路径
schemaId = V2_POOL_LAUNCH_SCHEMA_V1;data按DirectLaunchV2Data解码。 FlapTokenGraduated表示毕业。DIRECT_V2_POOL_TAX在发射交易内、达到门槛时发出;注意下方的kind说明。FlapSaltLocked在 salt 预留时发出;FlapSaltUsed在发射消耗它时发出。FlapLaunchVaultResolved在创建阶段解析出VAULT槽时发出;是索引 vault provenance 最快的入口。它不是分发标记 —— 是否真的把余量交给 vault,要看FlapDistributionExecuted(target == VAULT)。FlapDistributionExecuted在 dev 余量交给 strategy 时发出;target == VAULT表示本次发射的 provenance vault 收到了余量。FlapTokenBalanceChanged在代币每次余额变动时发出,携带变动后的双边余额(address(0)= mint/burn)。它不是 indexed —— 在应用层按地址过滤。
毕业规则取决于
kind。 当前仅支持DIRECT_V2_POOL_TAX,其毕业检查在发射交易内执行一次;若发射事件中没有FlapTokenGraduated,则该代币在此 kind 下未毕业。其他 kind 可能采用不同规则:未来的 kind 可以引入进度机制,仅在进度达到阈值后发出FlapTokenGraduated。索引时请以(token, kind)为键(kind可从FlapDirectLaunched或getLaunchToken获取),并在发射交易结束后继续监听该事件。
DirectLaunchV2Data:
/// @notice DIRECT_V2_POOL_TAX 的完整 `FlapDirectLaunched.data` 内容(schema V2_POOL_LAUNCH_SCHEMA_V1)。
/// @param nonce 全局创建 nonce(与联合曲线发射的代币同一序列)。
/// @param ts 创建时的区块时间戳。
/// @param tokenVersion 代币实现(TOKEN_DIRECT_TAXED)。
/// @param quoteToken 池子的 quote 侧(原生币为 address(0))。
/// @param migratorType 代币上架的外部场所 / 池子类型(此路径恒为 V2_MIGRATOR)。
/// @param initialPriceWad 池子开盘价,1e18 定点的 quote-per-token(已按精度归一化)。
/// @param name 代币名称。
/// @param symbol 代币符号。
/// @param meta 代币元数据 URI。
/// @param pool V2 池子地址(mainPool)。
/// @param totalSupply 代币总供应量(18 位小数)。
/// @param antiSniperMode 恒为 0(本版本不支持反狙击)。
/// @param maxBuyBps 恒为 0(没有按 tx.origin 的买入上限)。
struct DirectLaunchV2Data {
uint256 nonce;
uint256 ts;
TokenVersion tokenVersion;
address quoteToken;
MigratorType migratorType;
uint256 initialPriceWad;
string name;
string symbol;
string meta;
address pool;
uint256 totalSupply;
uint8 antiSniperMode;
uint16 maxBuyBps;
}| 字段 | 含义 |
|---|---|
nonce | 全局发射 nonce。 |
ts | 发射区块时间戳。 |
tokenVersion | TOKEN_DIRECT_TAXED(0)。 |
quoteToken | quote 侧(address(0) = 原生币)。 |
migratorType | 恒为 V2_MIGRATOR。 |
initialPriceWad | 开盘价,18 位 quote-per-token。 |
name、symbol、meta | 代币标识。 |
pool | V2 pool 地址。 |
totalSupply | 总供应量,18 位小数。 |
antiSniperMode | 本版本恒为 0。 |
maxBuyBps | 本路径恒为 0。 |
面向 Mini App 开发者
游戏发售、Mint、预售等机制需要先完成前置流程,方可上池。Pool Launch 通过 Preparation 支持此类场景,涉及两个合约:
- PreparationFactory:由 Mini App 提供,须先被设置为低风险。Portal 调用该合约部署 Preparation,并获取该 Preparation 的
multiplier。 - Preparation:承载前置流程的合约,用户直接与其交互。条件满足后,由其以自身地址调用 Portal 的
launchTaxTokenV2完成发射。每个 Preparation 仅可发射一次。
multiplier 同时决定创建门槛与毕业门槛,取值范围为 1 至 10000 bps,10000 表示不下调。该值由 PreparationFactory 返回,须经 Flap 确认后方可使用。
没有前置步骤
不涉及前置步骤的场景,建议以 Vault 的方式接入,详见 面向 Vault 开发者。
涉及前置步骤的场景,请使用 Preparation,详见下文。
Preparation
Preparation 是执行 Mini App 自有机制、并完成一次发射的合约,不是 ERC20。
Portal 不提供具体实现:机制由 Mini App 自行编写,由 factory 负责部署,并由 Flap 将该 factory 设置为低风险。官方提供两个基类用于处理通用接线逻辑,详见 基类。
机制完全位于 Preparation 合约内:游戏内的发售与认购、Mint、预售、白名单、份额、价格及领取状态均由其自行管理。Portal 不读取这些状态,仅要求 Preparation 在条件满足后以自身地址调用 launchTaxTokenV2。
Portal 为每个 Preparation 仅存储一条记录:
struct PreparationRecord {
address preFactory; // 创建它的低风险 factory
uint16 multiplierBps; // 1..10000,按比例缩放创建门槛与毕业门槛
}/// @notice 通过已设置为低风险的 factory 部署 Preparation,并在 Portal 中记录。
/// @dev 可支付:`msg.value` 会转给 factory。
function newPreparation(address preFactory, bytes calldata data)
external payable returns (address preparation, uint16 multiplierBps);
function getPreparation(address preparation) external view returns (PreparationRecord memory);Portal 仅调用 factory 的以下函数:
/// @dev 仅允许 Portal 调用。返回 Preparation 及其 multiplier(1..10000,10000 表示不下调)。
function newPreparation(bytes calldata data) external payable returns (address preparation, uint16 multiplierBps);data 的格式由 Mini App 自行定义:factory 通过 abi.decode 解析参数并据此配置 Preparation,协议不要求特定的初始化函数。factory 只需返回已部署的 Preparation 及其 multiplier,部署、克隆与注资方式由 factory 自行决定。
条件满足后,Preparation 通过 Portal 完成发射:
interface IPreparationPortal {
function launchTaxTokenV2(
CreateTaxTokenParams calldata p,
Distribution calldata distribution,
CreatorBuyParams calldata creatorBuy
) external payable returns (address token, address pool);
}调用流程:
Portal.setTrustedPreparationFactory(factory, true):由 Flap 执行,将 factory 设置为低风险,每个 factory 执行一次(需 admin 或 auditor 角色)。Portal.newPreparation{value}(factory, data):由 Mini App 针对每场销售调用一次。Portal 将msg.value转发给factory.newPreparation{value}(data),校验返回地址为合约且 multiplier 合法,写入{preFactory, multiplierBps}并发出FlapPreparationCreated。- 用户与 Preparation 交互:游戏内认购、Mint、预售买入、白名单领取。条件、价格与状态均由 Mini App 定义。
Preparation调用Portal.launchTaxTokenV2{value}(p, distribution, creatorBuy),仅可调用一次。Portal 将该 Preparation 标记为已发射(再次调用会 revert),并记录 token 到 Preparation 的溯源关系。
事件:FlapPreparationCreated、FlapPreparationFactorySet,所有字段均未设置 indexed。
| 规则 | 行为 |
|---|---|
| 每个 Preparation 只能发射一次 | 重复发射将 revert PreparationAlreadyLaunched。每场销售应部署新的 Preparation。 |
| Preparation multiplier 优先 | 调用方为 Preparation 时,门槛计算使用其 multiplierBps,而非 vault factory 的 multiplier。 |
| 取消低风险设置不会锁死发射 | factory 被取消低风险设置后不能再创建 Preparation,已创建的仍可发射,但门槛改按 vault factory 的 multiplier 计算。 |
| salt 须由 Preparation 自行锁定 | lockSalt 以 msg.sender 作为锁定方;若由 factory 执行锁定,Preparation 的发射将失败。 |
基类
两个基类均不存储状态,仅处理通用接线,机制仍由 Mini App 自行实现。源码:src/vaults/PreparationFactoryBase.sol、src/vaults/PreparationBase.sol;接口:src/interfaces/IPreparation.sol。
Factory
Portal 仅调用 IPreparationFactory 中的以下函数:
/// @title IPreparationFactory
/// @notice Interface a trusted PreparationFactory exposes to the Portal. `newPreparation` is called by
/// the Portal (which records the returned address + multiplier); a factory must only accept
/// calls from its configured Portal.
interface IPreparationFactory {
/// @notice Deploy a new Preparation (or mechanism/fundraising contract) and return it with its
/// floor multiplier. `msg.value` is forwarded by the Portal, so a factory can use it for
/// deployment, salt locking, or initial funding.
/// @dev If the value is used to lock a launch salt, the lock MUST be executed by the Preparation
/// itself: `Portal.lockSalt` records `msg.sender` as the locker, and the Preparation's later
/// launch requires `locker == Preparation`. A lock executed by the factory records the factory
/// as locker and makes the Preparation's launch revert with FlapSaltLockedByAnotherUser.
/// @param data Mechanism-specific payload. Its layout and use are entirely up to the factory; the
/// Portal never decodes it. There is no required initializer function.
/// @return preparation The deployed Preparation (must be a contract).
/// @return multiplierBps Floor multiplier in bps (1..10_000; 10_000 = no reduction).
function newPreparation(bytes calldata data) external payable returns (address preparation, uint16 multiplierBps);
/// @notice The factory-side spec revision this implementation conforms to. MUST return `"plp-v1"`
/// (pool launch preparation spec, v1).
/// @return The version string.
function factorySpecVersion() external pure returns (string memory);
}PreparationFactoryBase 在 newPreparation 上施加 onlyHub 限制,仅 Pool Launch Portal 可以创建 Preparation。factory 需实现 _newPreparation(data):按自定义格式解析 data,部署(或克隆)Preparation,并返回该 Preparation 及其 multiplier。factorySpecVersion() 返回 plp-v1。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {IPreparationFactory} from "src/interfaces/IPreparation.sol";
import {LaunchVaultAddresses} from "src/libraries/LaunchVaultAddresses.sol";
/// @title PreparationFactoryBase
/// @author The Flap Team
/// @notice Recommended (not mandatory) abstract base for Preparation factories.
///
/// A Preparation setup is two contracts:
/// 1. the Preparation — the app's mechanism (game sales, mint, presale, whitelist, ...);
/// 2. the PreparationFactory — deploys that Preparation and returns its floor multiplier.
///
/// This base provides ONLY the caller check every factory needs:
/// - `onlyHub`: the caller is the Pool Launch Portal of this chain;
/// - the per-chain getter behind it (`internal view virtual`).
///
/// `newPreparation` is implemented here and applies `onlyHub` before delegating to the
/// factory's `_newPreparation`, so a factory cannot forget the check. Everything else
/// (deployment / clone model, multiplier, salt locking, initial funding) is the factory's
/// own design. This base is deliberately storage-free.
///
/// @dev ── WHAT A PREPARATION FACTORY IS ──────────────────────────────────────
///
/// The Portal calls `IPreparationFactory(preFactory).newPreparation{value: msg.value}(data)` from
/// `PortalPreparation.newPreparation` — see `Portal.newPreparation(preFactory, data)`. The caller is
/// therefore ALWAYS the Portal proxy (the "Hub"), and never a wallet, an EOA or another contract.
/// Gate the entry point with `onlyHub` (this base does) so nobody can create a Preparation outside
/// the Portal: an unrecorded Preparation has no multiplier and the Portal does not know it.
///
/// `msg.value` is whatever the app attached to `Portal.newPreparation`. The factory may forward it
/// to the Preparation (initial funding) or spend it on deployment.
///
/// ── SALT LOCKING ───────────────────────────────────────────────────────────
///
/// If the value is used to lock a launch salt, the Preparation MUST execute the lock itself.
/// `Portal.lockSalt` records `msg.sender` as the locker, and a Preparation's later launch only
/// accepts a lock whose locker is the Preparation. A lock taken by the factory records the factory
/// and makes the Preparation's launch revert with `FlapSaltLockedByAnotherUser`.
///
/// ── HOW TO IMPLEMENT ───────────────────────────────────────────────────────
///
/// 1. Inherit `PreparationFactoryBase`.
/// 2. Implement `_newPreparation(data)`: `abi.decode` `data` with your own layout, deploy (or clone)
/// the Preparation, initialize it with the Portal address and the decoded parameters, and return
/// it with its floor multiplier — for example:
///
/// ```solidity
/// function _newPreparation(bytes calldata data)
/// internal
/// override
/// returns (address preparation, uint16 bps)
/// {
/// (address creator, uint256 seed) = abi.decode(data, (address, uint256));
/// preparation = address(new MyPreparation(_getHub(), creator, seed));
/// bps = 5_000; // 50% of the create and graduation floors
/// }
/// ```
///
/// The Portal records the returned multiplier per Preparation and rejects `0` or `> 10000`.
///
/// Factories are immutable by convention: to change the mechanism, deploy a new factory and ask Flap
/// to trust it (`Portal.setTrustedPreparationFactory`).
abstract contract PreparationFactoryBase is IPreparationFactory {
/// @notice The caller is not the Pool Launch Portal of this chain.
error NotHub(address caller);
// ─────────────────────────────────────────────────────────────────────────
// Modifiers
// ─────────────────────────────────────────────────────────────────────────
/// @notice Restrict a function to the Pool Launch Portal of this chain.
/// @dev Already applied to `newPreparation` below. Use it on any other Portal-facing entry you
/// add (for example a factory-side `initialize` the Portal calls separately).
modifier onlyHub() {
if (msg.sender != _getHub()) revert NotHub(msg.sender);
_;
}
// ─────────────────────────────────────────────────────────────────────────
// Chain addresses
// ─────────────────────────────────────────────────────────────────────────
/// @notice The Pool Launch Portal of this chain (see `LaunchVaultAddresses`). `virtual`.
function _getHub() internal view virtual returns (address) {
return LaunchVaultAddresses.hub();
}
// ─────────────────────────────────────────────────────────────────────────
// IPreparationFactory
// ─────────────────────────────────────────────────────────────────────────
/// @notice The factory-side spec revision this implementation conforms to. MUST return `"plp-v1"`.
/// @return The version string.
function factorySpecVersion() public pure virtual override returns (string memory) {
return "plp-v1";
}
/// @inheritdoc IPreparationFactory
/// @dev Portal-only: `onlyHub` cannot be bypassed by a subclass.
function newPreparation(bytes calldata data)
external
payable
override
onlyHub
returns (address preparation, uint16 bps)
{
return _newPreparation(data);
}
// ─────────────────────────────────────────────────────────────────────────
// Factory-specific
// ─────────────────────────────────────────────────────────────────────────
/// @notice Deploy the Preparation and return it with its floor multiplier.
/// @dev Called by `newPreparation` after the Hub check. `msg.value` is available as usual, so a
/// factory can fund or initialize the Preparation with it.
///
/// `data` is opaque to the Portal: it is forwarded unchanged and never decoded there. Decode
/// it here with your own layout and use it to configure the Preparation (creator, sale
/// parameters, funding, ...). Keep the layout stable — it is the factory's public ABI.
/// @param data Mechanism-specific payload from `Portal.newPreparation`; never decoded by the Portal.
/// @return preparation The deployed Preparation (must be a contract).
/// @return bps Floor multiplier in bps (1..10_000; 10_000 = no reduction).
function _newPreparation(bytes calldata data) internal virtual returns (address preparation, uint16 bps);
}Preparation
IPreparationPortal 是 Preparation 调用的发射入口:
/// @title IPreparationPortal
/// @notice The Portal entry point a Preparation calls once its pre-DEX conditions are met. Mirrors
/// PortalLaunch.launchTaxTokenV2 (declared locally to keep this interface dependency-light).
interface IPreparationPortal {
function launchTaxTokenV2(
IDirectPoolLaunch.CreateTaxTokenParams calldata p,
Distribution calldata distribution,
IDirectPoolLaunch.CreatorBuyParams calldata creatorBuy
) external payable returns (address token, address pool);
}PreparationBase 对其做了封装。_getHub() 返回发射所用的 Portal;_launchOnHub(...) 默认转发当前调用的 msg.value(原生 quote),也可显式指定 value。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {IDirectPoolLaunch} from "src/interfaces/IDirectPoolLaunch.sol";
import {Distribution} from "src/types/Distribution.sol";
import {IPreparationPortal} from "src/interfaces/IPreparation.sol";
/// @title PreparationBase
/// @author The Flap Team
/// @notice Recommended (not mandatory) abstract helper base for Preparation contracts.
///
/// It provides ONLY:
/// - `_getHub()`, the Pool Launch Portal this Preparation launches on (`internal view virtual`);
/// - `_launchOnHub(...)`, a typed wrapper around `Portal.launchTaxTokenV2`.
///
/// It has NO storage and no initializer, so it imposes no storage layout or deployment
/// pattern. The mechanism — game sales and purchases, mint, presale, whitelist, allocations, pricing,
/// claim state — is entirely the Preparation's own design, and the Portal never reads it.
///
/// @dev ── WHAT A PREPARATION IS ──────────────────────────────────────────────
///
/// The Preparation is the pre-DEX contract an app deploys through its trusted factory. Users interact
/// with it directly (deposit / mint / buy / claim). Once its conditions are met, the Preparation calls
/// `launchTaxTokenV2` on the Portal FROM ITS OWN ADDRESS: the Portal records the token -> Preparation
/// provenance, applies the Preparation's floor multiplier, and allows exactly one launch per
/// Preparation (`PreparationAlreadyLaunched` on a second call). A failed launch reverts the whole
/// mechanism call, so the Preparation stays unlaunched.
///
/// A Preparation is NOT an ERC20 and does not implement any launch logic itself: the token, pool and
/// vault are created by the Portal in that single call.
///
/// ── SALT LOCKING ───────────────────────────────────────────────────────────
///
/// If the launch salt is locked, the Preparation must have locked it itself: `Portal.lockSalt` records
/// `msg.sender` as the locker and the launch requires `locker == Preparation`. A lock taken by the
/// factory records the factory and makes this launch revert.
///
/// ── HOW TO IMPLEMENT ───────────────────────────────────────────────────────
///
/// 1. Inherit `PreparationBase`.
/// 2. Store the Portal (and your factory) in your own storage during initialization, and return it
/// from `_getHub()`.
/// 3. Write the mechanism. When its conditions are met, call `_launchOnHub(p, distribution,
/// creatorBuy)` with the launch parameters.
abstract contract PreparationBase {
/// @notice The Pool Launch Portal (Hub) this Preparation launches on.
/// @dev Implement it as a stored value (set during initialization) or a constructor argument for a
/// non-cloned deployment.
function _getHub() internal view virtual returns (address);
/// @notice Launch through the Portal, forwarding this call's `msg.value` as the launch value.
/// @dev Native quote: `msg.value` must cover `p.poolQuote + p.creatorBuyQuote`.
/// ERC-20 quote: the Preparation must hold the quote and approve the Portal instead, so pass
/// `value == 0` with `_launchOnHub(..., 0)`.
function _launchOnHub(
IDirectPoolLaunch.CreateTaxTokenParams calldata p,
Distribution calldata distribution,
IDirectPoolLaunch.CreatorBuyParams calldata creatorBuy
) internal returns (address token, address pool) {
return _launchOnHub(p, distribution, creatorBuy, msg.value);
}
/// @notice Launch through the Portal with an explicit native value.
/// @param value Native quote sent with the launch (`0` for an ERC-20 quote).
function _launchOnHub(
IDirectPoolLaunch.CreateTaxTokenParams calldata p,
Distribution calldata distribution,
IDirectPoolLaunch.CreatorBuyParams calldata creatorBuy,
uint256 value
) internal returns (address token, address pool) {
return IPreparationPortal(_getHub()).launchTaxTokenV2{value: value}(p, distribution, creatorBuy);
}
}常见报错
| 报错 | 原因 |
|---|---|
UntrustedPreparationFactory | newPreparation 传入了未设置为低风险的 factory。 |
InvalidPreparationFactory | 被设置为低风险的地址不是合约。 |
InvalidPreparation | factory 返回了 EOA 或 EIP-7702 代理地址。 |
InvalidPreparationMultiplierBps | multiplier 为 0 或大于 10000。 |
PreparationAlreadyExists | 返回的 Preparation 已被记录。 |
PreparationAlreadyLaunched | 同一 Preparation 重复发射。 |
面向 Vault 开发者
Vault 是可选的、按代币部署的合约,接收该代币 quote 收益的一部分,也可以接收 dev 预留的代币余量。
Vault 如何挂到发射上
在 feeConfigs 中加入一个 VAULT 槽:
const vaultSlot = {
feeType: 4, // VAULT
bps: 8000,
configData: ethers.AbiCoder.defaultAbiCoder().encode(
["address", "bytes"],
[vaultFactory, vaultData],
),
};剩余 bps 只能来自 DEFLATION、LP_BPS 或 DIVIDEND;MARKETING 不能与 VAULT 同时出现。
在 launchTaxTokenV2 期间:
-
Portal 校验发射,并调用:
/// @notice 校验发射并创建其 vault。由 Portal 在创建阶段调用。 /// @param token 发射代币的**预测**地址(尚未部署)。 /// @param creator 发射调用者。 /// @param launchKind `LaunchKind` 的 `uint8` 值(DIRECT_V2_POOL_TAX = 0)。 /// @param launchParams `abi.encode(CreateTaxTokenParams, Distribution)`。 /// @return vault 放入本次发射 VAULT fee slot 的 vault。 function newVault( address token, address creator, uint8 launchKind, bytes calldata launchParams ) external returns (address vault); -
launchParams = abi.encode(CreateTaxTokenParams, Distribution)。Factory 从该 payload 读取原始quoteToken(address(0)= 原生币)以及自己的vaultData。 -
返回的 Vault 会放入本次发射的
VAULTfee slot,并从那时起接收该槽 bps 份额的代币 quote 收益。 -
Portal 记录 provenance:
getLaunchVault(token)/getLaunchVaultInfo(token)。
newVault 在代币部署之前执行。Factory 可以 revert 或返回地址,但不得调用代币。
Factory 接口
/// @title IDirectLaunchVaultFactory
/// @notice Pool Launch 的 vault factory 接口。
/// @dev `newVault` 接收**完整**的发射输入(创建参数 + Distribution),因此一次调用内既创建 vault
/// 也校验发射。没有单独的 `validateLaunch`。
interface IDirectLaunchVaultFactory {
/// @notice 校验发射并创建其 vault。由 Portal 在创建阶段调用。
/// @param token 发射代币的**预测**地址(尚未部署)。
/// @param creator 发射调用者。
/// @param launchKind `LaunchKind` 的 `uint8` 值。
/// @param launchParams `abi.encode(<该 kind 的创建参数>, Distribution)`。
/// @return vault 放入本次发射 VAULT fee slot 的 vault。
function newVault(address token, address creator, uint8 launchKind, bytes calldata launchParams)
external
returns (address vault);
/// @notice 该 factory 的 vault 是否支持 `quoteToken`(`address(0)` = 原生币)。供 UI 使用。
function isQuoteTokenSupported(address quoteToken) external view returns (bool);
/// @notice factory 规范版本。必须以 `pl` 开头;当前为 `"pl-v1"`。
function factorySpecVersion() external pure returns (string memory);
/// @notice 该 factory 的 `FeeConfigVault.vaultData` 的 schema,用于 UI 生成表单。
function vaultDataSchema() external pure returns (VaultDataSchema memory);
/// @notice 与 `newVault` 内检查一致的 UI 提示。仅供参考。
function tokenCreationPolicies() external view returns (FactoryPolicy[] memory);
/// @notice 升级该 factory 部署的所有 vault 的实现。
/// @dev 要获得低风险徽章,此函数必须只允许 Guardian 调用。
function upgradeVaultImplementation(address newImplementation) external;
}
/// @title IVaultFactoryDividend
/// @notice 可选的 vault factory 扩展:计算本次发射的分红资产。
/// @dev 仅当 `FeeConfigDividend.dividendToken == MAGIC_DIVIDEND_COMPUTED` 且发射带有 VAULT 槽时调用。
/// 未实现该接口的 factory 会被拒绝。
interface IVaultFactoryDividend {
function resolveDividendToken(address predictedToken, address quoteToken, address creator, bytes calldata vaultData)
external
view
returns (address dividendToken);
}| 方法 | 要求 |
|---|---|
newVault | 限制为 Portal 调用(onlyHub)。拒绝不支持的 launchKind。部署前校验发射。在同一次调用内完成部署与初始化。返回非零且有代码的地址。不要调用 token。 |
isQuoteTokenSupported | 必须真实,且与 newVault 一致。无法记账的 quote 返回 false。 |
factorySpecVersion | 返回 "pl-v1"(必须以 pl 开头)。 |
vaultDataSchema | 必须与 newVault 解码的 vaultData 布局一致。 |
tokenCreationPolicies | 面向 UI 的提示信息;与 newVault 的检查保持一致。 |
upgradeVaultImplementation | 限制为 Guardian 调用(onlyGuardian)。Factory 本身保持不可变;要改行为就部署新 factory。 |
所有基于 Pool Launch 的 vault factory 都必须返回以 pl 开头的 factorySpecVersion(当前为 "pl-v1"),以便集成方识别它是 Pool Launch 专属 factory。基类 DirectLaunchVaultFactoryBase 默认返回 "pl-v1",子类不要覆盖成其他值。
Factory 基类
DirectLaunchVaultFactoryBase 是推荐(非强制)的基类。它只提供 onlyHub / onlyGuardian、其背后的各链地址,以及两个解码辅助函数;IDirectLaunchVaultFactory 的所有方法都保持 abstract。完整源码(原文,未翻译),src/vaults/DirectLaunchVaultFactoryBase.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {IDirectLaunchVaultFactory} from "src/interfaces/ILaunchVaultSpec.sol";
import {IDirectPoolLaunch} from "src/interfaces/IDirectPoolLaunch.sol";
import {IPortalTypes} from "src/interfaces/IPortal.sol";
import {Distribution} from "src/types/Distribution.sol";
import {VaultDataSchema, FactoryPolicy} from "src/interfaces/IVaultSchemas.sol";
import {LaunchVaultAddresses} from "src/libraries/LaunchVaultAddresses.sol";
/// @title DirectLaunchVaultFactoryBase
/// @author The Flap Team
/// @notice Recommended (not mandatory) abstract base for Pool Launch vault factories.
///
/// It provides ONLY the caller checks and the per-chain addresses every factory needs in
/// exactly the same way:
/// - `onlyHub`: the caller is the Direct Launch Portal (Hub) of this chain;
/// - `onlyGuardian`: the caller is the Flap Guardian of this chain;
/// - the two per-chain getters behind them (`internal view virtual`).
///
/// Every `IDirectLaunchVaultFactory` method is left abstract. The base enforces no implementation
/// convention: how a vault is deployed, initialized, validated or upgraded is the factory's
/// own design. The natspec on each method states what the Hub, the UI and the low-risk audit
/// expect. `SplitVaultFactory` / `StakingVaultFactory` are worked examples.
///
/// @dev ── WHAT A LAUNCHER VAULT FACTORY IS ─────────────────────────────────
///
/// The Hub's create facets resolve a launch's `FeeSlotType.VAULT` slot by calling
/// `IDirectLaunchVaultFactory(vaultFactory).newVault(predictedToken, creator, uint8(kind), launchParams)` — see
/// `PortalLauncherBase._parseAndValidateFeeConfigs`. `launchParams` is
/// `abi.encode(<that kind's create params>, Distribution)`: the factory reads the RAW `p.quoteToken`
/// (`address(0)` = native, no WBNB normalization) and its own VAULT-slot `vaultData` from it. The
/// returned vault address lands in a marketing slot and receives that slot's bps share of the token's
/// quote revenue from then on. There is no on-chain registration requirement to *deploy* a factory;
/// however, only factories registered via `VaultLaunch.registerVaultFactory` get their vaults stamped
/// `official`/`riskLevel`/`category` at launch, so production factories SHOULD be registered.
///
/// `newVault` runs BEFORE the token is deployed (`predictedToken` is a CREATE2 prediction), so a
/// factory can revert or return a bogus address but cannot re-enter the Hub: the token is not cloned
/// yet and no launch state has been written. Gate `newVault` with `onlyHub` to stop a third party
/// pre-creating a deterministic clone (a squat that would make the real launch revert on the CREATE2
/// collision) — see `SplitVaultFactory` for the reference.
///
///
/// ── HOW TO IMPLEMENT ───────────────────────────────────────────────────────
///
/// 1. Inherit `DirectLaunchVaultFactoryBase`.
/// 2. Implement `newVault(...)` and gate it with `onlyHub`: validate the launch, deploy (or clone)
/// your vault, initialize it with `p.quoteToken` (raw; `address(0)` = native), and return a
/// non-zero address with code. Never call `token` (it has no code yet).
/// 3. Implement `isQuoteTokenSupported(quoteToken)` truthfully. Misdeclaring support strands users'
/// tax revenue in a vault that ignores it.
/// 4. Implement `vaultDataSchema()` / `factorySpecVersion()` / `tokenCreationPolicies()`.
///
/// NOTE ON UPGRADEABILITY: per Flap verification conventions a FACTORY should be immutable — to change
/// one, deploy a new factory and register it. The VAULTS it deploys SHOULD be upgradeable via a
/// solady `UpgradeableBeacon` + `LibClone.deployDeterministicERC1967BeaconProxy` (the launcher's own
/// per-token pattern — see `PortalBase.dividendBeacon` / `PortalDirectLauncherCreate._setupDividend`),
/// so one beacon upgrade retargets the logic of every already-launched vault at once. Give the vault
/// implementation a `version()` marker and bump it on each beacon upgrade (mirrors `Dividend.version`).
/// `StakingVaultFactory` is a complete worked example. This base itself is deliberately storage-free.
abstract contract DirectLaunchVaultFactoryBase is IDirectLaunchVaultFactory {
/// @notice The caller is not the Direct Launch Portal (Hub) of this chain.
error NotHub(address caller);
/// @notice The caller is not the Guardian of this chain.
error NotGuardian(address caller);
// ─────────────────────────────────────────────────────────────────────────
// Modifiers
// ─────────────────────────────────────────────────────────────────────────
/// @notice Restrict a function to the Direct Launch Portal (Hub) of this chain.
/// @dev Put it on `newVault`. Without it, anyone could deploy a vault at a deterministic address
/// before the launch and make the real launch revert on the address collision.
modifier onlyHub() {
if (msg.sender != _getHub()) revert NotHub(msg.sender);
_;
}
/// @notice Restrict a function to the Flap Guardian of this chain.
/// @dev Put it on the upgrade path. For the low-risk badge the Guardian MUST be the only upgrade
/// authority.
modifier onlyGuardian() {
if (msg.sender != _getGuardian()) revert NotGuardian(msg.sender);
_;
}
// ─────────────────────────────────────────────────────────────────────────
// Chain addresses
// ─────────────────────────────────────────────────────────────────────────
/// @notice The Direct Launch Portal (Hub) of this chain (see `LaunchVaultAddresses`). `virtual`.
function _getHub() internal view virtual returns (address) {
return LaunchVaultAddresses.hub();
}
/// @notice The Flap Guardian of this chain (see `LaunchVaultAddresses`). `virtual`.
function _getGuardian() internal view virtual returns (address) {
return LaunchVaultAddresses.guardian();
}
// ─────────────────────────────────────────────────────────────────────────
// IDirectLaunchVaultFactory: all abstract
// ─────────────────────────────────────────────────────────────────────────
/// @notice Validate the launch and create its vault, in one call. Called by the Hub during the
/// create step, before the token is deployed.
/// @dev `launchParams` by `launchKind` (`IPortalTypes.LaunchKind`):
/// | kind | launchParams |
/// |------------------------|------------------------------------------------------------------|
/// | DIRECT_V2_POOL_TAX (0) | abi.encode(IDirectPoolLaunch.CreateTaxTokenParams, Distribution) |
/// The params are the RAW launch input: `p.quoteToken == address(0)` means native (no WBNB
/// normalization). The factory's own `vaultData` is in `p.feeConfigs`: the VAULT slot whose
/// `FeeConfigVault.vaultFactory == address(this)`.
///
/// MUST: gate the caller with `onlyHub`; validate the launch (the reserved share, who the
/// strategy is, and any other rule its economics need) BEFORE deploying; revert with a clear
/// reason unless the launch is acceptable; initialize the vault in this same call and return
/// a non-zero address with code; NEVER call `token` (it has no code yet). SHOULD deploy
/// deterministically from `token` (one vault per token) behind an upgradeable proxy.
/// @param token The launch token's predicted address.
/// @param creator The launch caller (`msg.sender` of the launch).
/// @param launchKind `IPortalTypes.LaunchKind` as `uint8`.
/// @param launchParams `abi.encode(<create params of that kind>, Distribution)`.
/// @return vault The vault, placed in the launch's VAULT fee slot.
function newVault(address token, address creator, uint8 launchKind, bytes calldata launchParams)
external
virtual
override
returns (address vault);
/// @notice Whether this factory's vault can account for revenue in `quoteToken`.
/// @dev For UIs (to grey out unsupported quotes). MUST be truthful and consistent with `newVault`,
/// which MUST revert for a quote this returns false for.
/// @param quoteToken Raw quote (`address(0)` = native).
/// @return Whether the quote is supported.
function isQuoteTokenSupported(address quoteToken) public view virtual override returns (bool);
/// @notice Schema describing the `vaultData` bytes `newVault` expects, for UI form generation.
/// @dev Field order and types MUST match how `newVault` decodes `vaultData`.
/// @return The schema (`IVaultSchemas.VaultDataSchema`).
function vaultDataSchema() public pure virtual override returns (VaultDataSchema memory);
/// @notice The factory-side spec revision this implementation conforms to. Distinct from the
/// vault-side `StakingVault.vaultSpecVersion()`. MUST return a Pool Launch factory
/// version starting with `pl` (currently `"pl-v1"`).
/// @return The version string.
function factorySpecVersion() public pure virtual override returns (string memory) {
return "pl-v1";
}
/// @notice Informational constraints this factory places on launch parameters, for proactive UI
/// hints. Informational only; `newVault` enforces. Keep the two consistent.
/// @return The policies (empty if none).
function tokenCreationPolicies() public view virtual override returns (FactoryPolicy[] memory);
/// @notice Upgrade the implementation of every vault this factory has deployed.
/// @dev For the low-risk badge this MUST be callable by the Guardian only: use `onlyGuardian`, and
/// leave no other upgrade path (no owner, proxy admin, upgrader role or deployer EOA). A
/// factory that wants no upgrades at all may revert unconditionally.
/// @param newImplementation The new vault implementation.
function upgradeVaultImplementation(address newImplementation) external virtual override;
// ─────────────────────────────────────────────────────────────────────────
// Decode helpers (optional convenience)
// ─────────────────────────────────────────────────────────────────────────
/// @notice Thrown by `_vaultDataFor` when the launch's feeConfigs carry no VAULT slot for this
/// factory.
error VaultDataNotFound(address factory);
/// @notice `newVault` was called with a `launchKind` this factory does not support.
error UnsupportedLaunchKind(uint8 launchKind);
/// @dev Decode the launch payload passed to `newVault`, keyed by `launchKind` so each kind decodes
/// its own params struct. Only `DIRECT_V2_POOL_TAX` is supported today; any other kind reverts
/// (`UnsupportedLaunchKind`) rather than being mis-decoded, so a future kind — with different
/// create params — is opt-in and never breaks this one.
function _decodeLaunchParams(uint8 launchKind, bytes calldata launchParams)
internal
pure
returns (IDirectPoolLaunch.CreateTaxTokenParams memory p, Distribution memory d)
{
if (launchKind != uint8(IPortalTypes.LaunchKind.DIRECT_V2_POOL_TAX)) {
revert UnsupportedLaunchKind(launchKind);
}
return abi.decode(launchParams, (IDirectPoolLaunch.CreateTaxTokenParams, Distribution));
}
/// @dev Extract this factory's own VAULT-slot `vaultData` from the launch's feeConfigs.
function _vaultDataFor(address factory, IDirectPoolLaunch.FeeConfig[] memory feeConfigs)
internal
pure
returns (bytes memory vaultData)
{
for (uint256 i = 0; i < feeConfigs.length; ++i) {
if (feeConfigs[i].feeType != IDirectPoolLaunch.FeeSlotType.VAULT) continue;
IDirectPoolLaunch.FeeConfigVault memory v =
abi.decode(feeConfigs[i].configData, (IDirectPoolLaunch.FeeConfigVault));
if (v.vaultFactory == factory) return v.vaultData;
}
revert VaultDataNotFound(factory);
}
}Vault 接口
唯一必需的 Vault 接口是:
/// @title IDirectLaunchVault
/// @notice Pool Launch vault **唯一**必须实现的接口。
/// @dev 若 vault 还要接收本次发射的 dev 预留余量,需额外实现 `IStrategy.initializeDistribution`,
/// 因为 Portal 会调用它。除此之外没有其他要求。
interface IDirectLaunchVault {
/// @notice vault 的动态、可读状态。
/// @dev 必须披露费用 / 佣金,以及所有获得收益或预留代币的参与方。
function description() external view returns (string memory);
/// @notice vault 面向用户方法的链上 UI 描述(`IVaultSchemas`,与联合曲线规范相同)。
function vaultUISchema() external pure returns (VaultUISchema memory);
}description()—— terminal 与 Vault UI 展示的可读状态。必须披露费用、佣金,以及所有获得收益或预留代币的参与方。vaultUISchema()—— 描述 Vault 面向用户的 view / write 方法,让通用 UI 可以渲染。
Vault 基类
DirectLaunchVaultBase 是推荐(非强制)的基类。它没有存储、也没有 initializer;提供 onlyHub / onlyGuardian 以及代币驿站入口 _getTokenDockRouter(),description() / vaultUISchema() 保持 abstract。完整源码(原文,未翻译),src/vaults/DirectLaunchVaultBase.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {VaultUISchema} from "src/interfaces/IVaultSchemas.sol";
import {IDirectLaunchVault} from "src/interfaces/ILaunchVaultSpec.sol";
import {LaunchVaultAddresses} from "src/libraries/LaunchVaultAddresses.sol";
/// @title DirectLaunchVaultBase
/// @author The Flap Team
/// @notice Recommended (not mandatory) abstract helper base for Pool Launch vaults.
///
/// It provides ONLY:
/// - `onlyHub` and `onlyGuardian` modifiers (per-chain addresses);
/// - the Token Dock entry `_getTokenDockRouter()`, behind which routing (verified/open) and
/// migration live — a vault just calls `deposit` / `withdraw` / `balanceOf` on it.
/// The two mandatory `IDirectLaunchVault` methods, `description()` and `vaultUISchema()`, are
/// left abstract.
///
/// It has NO storage and no initializer, so it imposes no storage layout or deployment
/// pattern. Everything else (launch context, revenue accounting, payouts, distribution,
/// upgrade hygiene) is the vault's own design.
///
/// @dev ── TOKEN DOCK RULES ─────────────────────────────────────────────────────────────────────
///
/// - Never cache the dock address. Every deposit and withdrawal of `token` MUST call
/// `_getTokenDockRouter()` and use it. `TokenDockRouter` selects the labeled `VerifiedTokenDock`
/// once the DLP has whitelisted `(this, token)`, and the unlabeled `OpenTokenDock` before that —
/// and it moves this vault's balance across the first time it changes, so the vault never sees the
/// switch.
/// - Do not move a reserve into a dock until both docks are excluded from the token's dividends;
/// until then keep the reserve in the vault (the DLP excludes the vault).
/// - Until the docks are deployed, `LaunchVaultAddresses.tokenDockRouter()` returns `address(0)` and
/// `_getTokenDockRouter()` reverts `TokenDockNotConfigured()`.
abstract contract DirectLaunchVaultBase is IDirectLaunchVault {
/// @notice The caller is not the Direct Launch Portal of this chain.
error NotHub(address caller);
/// @notice The caller is not the Guardian of this chain.
error NotGuardian(address caller);
/// @notice The Token Dock entry is not configured on this chain yet.
error TokenDockNotConfigured();
// ────────────────────────────────────────────────────────────────────────────────────────────
// Modifiers
// ────────────────────────────────────────────────────────────────────────────────────────────
/// @notice Restrict a function to the Direct Launch Portal of this chain.
/// @dev Put it on `IStrategy.initializeDistribution` if the vault receives the remainder.
modifier onlyHub() {
if (msg.sender != _getHub()) revert NotHub(msg.sender);
_;
}
/// @notice Restrict a function to the Flap Guardian of this chain.
/// @dev For the low-risk badge, every permissioned vault function MUST also accept the Guardian,
/// e.g. `if (msg.sender != creator && msg.sender != _getGuardian()) revert`.
modifier onlyGuardian() {
if (msg.sender != _getGuardian()) revert NotGuardian(msg.sender);
_;
}
/// @notice The Direct Launch Portal of this chain. NOT the Bonding Curve Portal.
/// @dev `virtual` for tests and forks with a freshly deployed DLP.
function _getHub() internal view virtual returns (address) {
return LaunchVaultAddresses.hub();
}
/// @notice The Flap Guardian of this chain (same as the bonding-curve vault Guardian).
/// @dev `virtual` for tests.
function _getGuardian() internal view virtual returns (address) {
return LaunchVaultAddresses.guardian();
}
// ────────────────────────────────────────────────────────────────────────────────────────────
// Token Dock entry
// ────────────────────────────────────────────────────────────────────────────────────────────
/// @notice The `TokenDockRouter` entry this vault uses for every reserve deposit / withdrawal. It
/// routes to the verified/open dock and migrates this vault's balance across on
/// whitelisting, so the vault does not implement any of that.
/// @dev `virtual` for tests and forks with a freshly deployed `TokenDockRouter`.
function _getTokenDockRouter() internal view virtual returns (address dock) {
dock = LaunchVaultAddresses.tokenDockRouter();
if (dock == address(0)) revert TokenDockNotConfigured();
}
// ────────────────────────────────────────────────────────────────────────────────────────────
// IDirectLaunchVault: abstract
// ────────────────────────────────────────────────────────────────────────────────────────────
/// @notice Dynamic, human-readable status of the vault, shown by terminals and the vault UI.
/// @dev MUST disclose fees/commission and every party that receives revenue or reserved tokens,
/// with current figures where useful (e.g. "Creator recouped 3.2 / 25 BNB; then 100% to
/// holders"). It may read state; keep it cheap enough for an `eth_call`.
function description() public view virtual override returns (string memory);
/// @notice On-chain description of the vault's user-facing methods, so a generic UI can render
/// them (`IVaultSchemas.VaultUISchema`, unchanged from the bonding-curve spec).
/// @dev List every method a user or keeper is expected to call, with its inputs and any ERC20
/// approvals it needs.
function vaultUISchema() public pure virtual override returns (VaultUISchema memory);
}两个基类共用的各链地址
src/libraries/LaunchVaultAddresses.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
/// @title LaunchVaultAddresses
/// @author The Flap Team
/// @notice Per-chain addresses that every Pool Launch (Direct Launch) vault and vault factory needs.
/// Shared by `DirectLaunchVaultBase` and `DirectLaunchVaultFactoryBase`, so the two can never
/// drift apart.
///
/// | Getter | BNB Chain (56) | BNB Testnet (97) | Robinhood (4663) |
/// |-----------------------|----------------------|----------------------|------------------|
/// | `hub()` | 0x00004210…0000 | 0x00004210…0000 | TBD → reverts |
/// | `guardian()` | 0x9e27098d…8a4b | 0x00004d58…0000 | 0x00004d58…0000 |
/// | `verifiedTokenDock()` | 0x00004B06…0000 | 0x00004B06…0000 | TBD → reverts |
/// | `openTokenDock()` | 0x0000E34C…0000 | 0x0000E34C…0000 | TBD → reverts |
/// | `tokenDockRouter()` | 0x000048E8…0000 | 0x000048E8…0000 | TBD → reverts |
///
/// - "Direct Launch Portal" (DLP) is the flap-launcher `Portal` proxy. It is NOT the Bonding Curve
/// Portal (BCP). Never call this a bare "portal".
/// - The two Token Docks (and the router) are deployed deterministically: the same salt + initcode
/// land on the same address on every chain that shares the dock logic.
/// - Each dock getter returns the TransparentUpgradeableProxy address (the dock implementation is
/// behind a dedicated ProxyAdmin, owned by the deployer and renounced once verified).
/// - Every consumer reads these through an `internal view virtual` wrapper, so test harnesses (and
/// fork tests with a freshly deployed DLP) can override them.
library LaunchVaultAddresses {
/// @notice The current chain has no Direct Launch Portal.
/// @param chainId The `block.chainid` that was looked up.
error UnsupportedChain(uint256 chainId);
/// @notice The Direct Launch Portal (DLP) on the current chain: the only allowed caller of
/// `IDirectLaunchVaultFactory.newVault` and of `IStrategy.initializeDistribution`.
/// @dev Reverts `UnsupportedChain` on any chain without a DLP (including Robinhood until its DLP
/// address is known), so an unconfigured deployment rejects every caller instead of accepting
/// a zero address.
/// @return The DLP proxy address.
function hub() internal view returns (address) {
uint256 id = block.chainid;
if (id == 56 || id == 97) return 0x000042108465206185A077Edc3F3fd2b49060000;
revert UnsupportedChain(id);
}
/// @notice The Flap Guardian on the current chain. For the low-risk badge it is the ONLY upgrade
/// authority of a factory's vaults, and every permissioned vault function also accepts it.
/// @dev BNB Chain uses the bonding-curve Guardian; every other chain uses `0x00004d58…0000`.
/// @return The Guardian address.
function guardian() internal view returns (address) {
if (block.chainid == 56) return 0x9e27098dcD8844bcc6287a557E0b4D09C86B8a4b; // BNB Chain
return 0x00004d58290854c5E9880F4b5B1993c6Ad8b0000; // every other chain
}
/// @notice The labeled `VerifiedTokenDock` singleton, resolved per chain.
/// @dev Deterministically deployed: the same salt + initcode land on the same address on every chain
/// that shares this dock's logic. A chain that needs different dock logic gets its own branch
/// here, so adding logic on a new chain never affects the others. Reverts `UnsupportedChain`
/// elsewhere.
/// @return The VerifiedTokenDock address.
function verifiedTokenDock() internal view returns (address) {
uint256 id = block.chainid;
if (id == 56 || id == 97) return 0x00004B06D90Df7a39491264744BF187D90Df0000;
revert UnsupportedChain(id); // add a branch per chain with its own dock
}
/// @notice The unlabeled, permissionless `OpenTokenDock` singleton, resolved per chain.
/// @dev See `verifiedTokenDock` for the per-chain / deterministic-deploy notes.
/// @return The OpenTokenDock address.
function openTokenDock() internal view returns (address) {
uint256 id = block.chainid;
if (id == 56 || id == 97) return 0x0000E34C74b9eC7b52F35528249Aa9C4AdE00000;
revert UnsupportedChain(id); // add a branch per chain with its own dock
}
/// @notice The `TokenDockRouter` singleton: the single dock entry a vault uses. It
/// routes to the verified/open dock and migrates on whitelisting.
/// @dev See `verifiedTokenDock` for the per-chain / deterministic-deploy notes.
/// @return The TokenDockRouter address.
function tokenDockRouter() internal view returns (address) {
uint256 id = block.chainid;
if (id == 56 || id == 97) return 0x000048E888aBf09f690a952E1b386b2D031a0000;
revert UnsupportedChain(id); // add a branch per chain with its own dock
}
}收益与 dev 余量
收益。 Vault 接收 VAULT 槽 bps 份额的代币 quote 收益。原生 quote 以原生币到账;ERC-20 quote 以该代币到账。建议的记账方式:
- 保存一个 baseline,只识别
balance - baseline。 - 差值为 0 时视为 no-op。
- 每次流出时减少 baseline。
receive()保持轻量:不 swap、不 revert。若需要物化差值,可加一个无许可的sync()。
Dev 余量。 若要接收余量,实现 IStrategy.initializeDistribution:
/// @title IStrategy
/// @notice 代币分发策略接口,由 Portal 在发射过程中调用。
/// @dev strategy 是独立、可插拔的合约;新的分发机制(Airdrop、Vault 等)以新的 IStrategy 实现接入,
/// 而不是在 Portal 里新增分支。
interface IStrategy {
/// @notice 分发初始化时发出。
/// @dev 由 strategy 自己发出,因此日志的 emitter 地址就是该 strategy —— 没有 `distributor` 字段。
/// @param token 被分发的代币。
/// @param amount 交给本次分发的 `token` 数量。
event FlapDistributionInitialized(address token, uint256 amount);
/// @notice 在该策略下初始化一次代币分发。
/// @dev 由 strategy 负责从 `msg.sender` 拉取 `amount` 的 `token`(Portal 已为 strategy 精确 approve
/// `amount`)。部署或预测确定性 distributor 的实现**必须**把 `salt` 纳入地址推导,以便调用者
/// 区分其他参数完全相同的分发。
/// @param token 被分发的代币。
/// @param amount 要分发的 `token` 数量。
/// @param configData strategy 自定义的任意参数。
/// @param salt 域分隔符,对 Portal 不透明。
function initializeDistribution(
address token,
uint256 amount,
bytes calldata configData,
bytes32 salt
) external;
}- 限制为 Portal 调用(
onlyHub)。 - 要求
token == taxToken。 - 用
transferFrom(msg.sender, address(this), amount)精确拉取amount,并校验余额差值。 - 最多只能调用一次。
- 发出
FlapDistributionInitialized。
发射时使用 distribution = { target: VAULT, amount: totalSupply - poolToken, ... }。Portal 会解析 provenance Vault 并调用它的 initializeDistribution。
如果 Vault 没有实现 initializeDistribution,不要把 distribution.target 设为 VAULT。
在 newVault 中强制分发规则
如果你的 Vault 机制依赖于接收预留代币 —— 例如承诺 80% 的总量进入 Vault 用于挖矿、线性释放或回购 —— 就要在 newVault 中强制校验,不符合时 revert。newVault 在任何东西部署之前就拿到完整的发射输入(CreateTaxTokenParams 与 Distribution),所以不合规的发射根本不会发生。
至少检查:
distribution.target == DistributionTarget.VAULT,确保余量发给本 Vault,而不是调用者指定的其他 strategy。distribution.amount恰好等于你的经济模型所需的数量,例如p.totalSupply * 8000 / 10000。p.poolToken + distribution.amount == p.totalSupply,确保没有其他剩余。- 机制依赖的其他条件:quote 代币、
DIVIDEND槽及其分红代币、VAULT槽的 bps、要记录的创建者地址。
function newVault(address token, address creator, uint8 launchKind, bytes calldata launchParams)
external
override
onlyHub
returns (address vault)
{
(IDirectPoolLaunch.CreateTaxTokenParams memory p, Distribution memory d) =
_decodeLaunchParams(launchKind, launchParams);
// 预留代币必须发给本 vault,且必须恰好是总量的 80%。
if (d.target != DistributionTarget.VAULT) revert WrongDistributionTarget(d.target);
uint256 expected = p.totalSupply * RESERVE_BPS / 10_000; // RESERVE_BPS = 8000
if (d.amount != expected) revert WrongReserveAmount(d.amount, expected);
if (p.poolToken + d.amount != p.totalSupply) revert SupplyMismatch();
// ... 然后部署并初始化 vault。
}如果 Vault 不依赖预留代币,可以跳过这一步;发射可以把余量发往别处(或把全部供应注入池子)。
余量去向:AllocationStrategy 或 Token Dock
dev 余量(totalSupply - poolToken)通常有两种去向。
方案 A —— 交给 AllocationStrategy(不由 Vault 托管)。
如果创建者想把余量分发给接收者,把发射的 distribution 指向共享的 AllocationStrategy,而不是 Vault:
const distribution = {
target: 1, // EXPLICIT
strategy: ALLOCATION_STRATEGY, // 共享单例
amount: totalSupply - poolToken,
configData: ethers.AbiCoder.defaultAbiCoder().encode(["address"], [creator]),
};ALLOCATION_STRATEGY 是共享单例(BNB Chain 测试网:0x000051353ef814E4779B692cb2Bb3A6Cb3710000)。
只有 Portal 能调用 AllocationStrategy.initializeDistribution,所以 Vault 无法把代币转进去。创建者之后自行分发:
await allocationStrategy.batchAllocate(token, recipients, amounts);
const remaining = await allocationStrategy.remainingOf(token);适用于余量需要分发、而不是由 Vault 持有的场景。VAULT 槽里的 Vault 仍然可以获得其 bps 份额的收益,只是不接收余量。
方案 B —— Vault 接收余量,并托管到 Token Dock。
当 distribution.target = VAULT 时,Vault 在 initializeDistribution 中收到余量。把余量托管到 Token Dock(TokenDockRouter),避免它作为一个巨大的持仓地址出现在链上:
function initializeDistribution(address token, uint256 amount, bytes calldata, bytes32)
external
onlyHub
{
require(token == taxToken, "wrong token");
require(!distributionInitialized, "already initialized");
distributionInitialized = true;
token.safeTransferFrom(msg.sender, address(this), amount);
address dock = _getTokenDockRouter();
IERC20(token).safeApprove(dock, amount);
ITokenDock(dock).deposit(token, amount);
emit FlapDistributionInitialized(token, amount);
}之后分发:
address dock = _getTokenDockRouter();
ITokenDock(dock).withdraw(token, amount, recipient);TokenDockRouter 会先把存款放进 open token dock。Vault 通过 Flap 低风险认证、并由 Portal 把 (vault, token) 加入白名单后,余额会自动从 open dock 迁移到 verified token dock。余额同一时间只会存在于其中一个 dock;balanceOf(token, vault) 会同时读取两者,以保证跨迁移边界时结果仍然正确。提款不受限制。
代币驿站接口(TokenDockRouter、OpenTokenDock、VerifiedTokenDock 都实现它;Vault 只与 TokenDockRouter 交互):
/// @title ITokenDock
/// @notice 预留代币的共享托管。两个单例使用同一接口。
interface ITokenDock {
/// @notice 给 `msg.sender` 的余额记入 `amount` 的 `token`。通过 `transferFrom` 拉取并校验余额差值。
/// 除非 `isWhitelisted(msg.sender, token)`,否则 VerifiedTokenDock 会 revert。
function deposit(address token, uint256 amount) external;
/// @notice 扣减 `msg.sender` 的余额并发送给 `to`。**永不**受限。
function withdraw(address token, uint256 amount, address to) external;
/// @notice `owner` 记录在案的 `token` 余额。
function balanceOf(address token, address owner) external view returns (uint256);
/// @notice 为所有 owner 托管的 `token` 总量。
function totalReserved(address token) external view returns (uint256);
/// @notice `vault` 是否可以存入 `token`。Verified:按 (vault, token) 白名单。Open:始终为 true。
function isWhitelisted(address vault, address token) external view returns (bool);
event FlapTokenDockDeposited(address token, address owner, uint256 amount);
event FlapTokenDockWithdrawn(address token, address owner, address to, uint256 amount);
}
/// @title IVerifiedTokenDock
/// @notice 仅 verified(带标记的)驿站才有的额外接口。
interface IVerifiedTokenDock is ITokenDock {
/// @notice 把 `(vault, token)` 加入白名单。**只能**由 Portal 调用。
function addToWhitelist(address vault, address token) external;
event FlapTokenDockWhitelisted(address vault, address token);
}Vault data schema
vaultDataSchema()(factory 的创建表单)与 vaultUISchema()(vault 面向用户的方法)使用与联合曲线 Vault 相同的 IVaultSchemas 结构体 —— Pool Launch 没有任何变化。见官方文档:UI schema reference (IVaultSchemasV1)。
示例 Vault
三个 Pool Launch Vault 的示意。它们是伪代码:每个函数体都是注释,说明需要实现什么。三者的收入设置相同,只在预留代币的用法上不同。各示例的动画见 可以构建什么。
共同设置(三个示例都相同):
- 发射把总量的 80% 发给 vault(
distribution.target = VAULT)。newVault强制校验这一点,否则 revert —— 见 在 newVault 中强制分发规则。 - 总量的 1% 在 30 天内线性释放给开发者。
newVault记录开发者地址(creator)。 - quote 收入: 20% 给开发者,直到开发者收到其注入的 quote(
poolQuote)的 1.5 倍;其余全部存入代币的分红合约,作为持有者的奖励。达到上限后,100% 归持有者。 - 剩余的 79% 是各示例不同的地方。
示例 A —— 质押挖矿
79% 作为挖矿池,线性释放给质押者。质押者选择锁定期;锁得越久,权重倍数越高。
Factory:
contract StakeMineVaultFactory is DirectLaunchVaultFactoryBase {
uint16 constant RESERVE_BPS = 8000; // 总量的 80% 必须进入 vault
uint16 constant DEV_VEST_BPS = 100; // 总量的 1% 线性释放给开发者
// 挖矿 = RESERVE_BPS - DEV_VEST_BPS = 总量的 79%
function newVault(address token, address creator, uint8 launchKind, bytes calldata launchParams)
external
override
onlyHub
returns (address vault)
{
// 1. (p, d) = _decodeLaunchParams(launchKind, launchParams)。
// 2. 强制预留:d.target == VAULT 且
// d.amount == p.totalSupply * RESERVE_BPS / 10_000;否则 revert。
// 3. 强制 quote:isQuoteTokenSupported(p.quoteToken);否则 revert。
// 4. 强制分红:p.feeConfigs 中有 DIVIDEND 槽,且其 dividendToken 就是本 vault
// 要存入的代币(例如 quote);否则 revert。
// 5. 通过 _vaultDataFor(address(this), p.feeConfigs) 读取本 factory 的 vaultData
// (例如质押档位)并校验。
// 6. 以 `token` 为 salt 确定性部署 vault(beacon proxy),并用以下参数初始化:
// token、原始 quote、dev = creator、devCap = p.poolQuote * 3 / 2(初始资金的 1.5 倍)、
// devVest = p.totalSupply * DEV_VEST_BPS / 10_000、vestStart = block.timestamp、
// vestDuration = 30 天。
// 7. 返回 vault。这里不要调用 `token`,它还没有代码。
}
// isQuoteTokenSupported / factorySpecVersion("pl-v1")/ vaultDataSchema /
// tokenCreationPolicies / upgradeVaultImplementation(onlyGuardian):同基类。
}Vault:
contract StakeMineVault is DirectLaunchVaultBase, IStrategy {
/// quote 收入(VAULT 槽分得的交易税)。保持轻量,永不 revert。
receive() external payable {
// 1. newRevenue = balance - accountedQuote(余额差值记账);为 0 则直接返回。
// 2. devShare = newRevenue * 20%,上限为 (devCap - devPaid)。若 devShare > 0:
// devPaid += devShare;把 devShare 发给 `dev`。
// 3. 剩余部分(newRevenue - devShare)存入代币的分红合约(IDividend.deposit),
// 作为持有者的分红奖励。
// 4. 开发者线性释放:vested = devVest * min(now - vestStart, vestDuration) / vestDuration;
// 若 vested > devReleased,把 (vested - devReleased) 个代币直接发给 `dev`
// (经由代币驿站),并更新 devReleased。
}
/// 无许可的触发入口,用于识别没有触发回调就到账的收入(例如 ERC-20 quote)。
function sync() external {
// 与 receive() 的步骤 1-4 相同。
}
/// IStrategy:Portal 在发射交易中把 80% 预留代币交给 vault。
function initializeDistribution(address token, uint256 amount, bytes calldata, bytes32)
external
onlyHub
{
// 要求 token == taxToken、只能调用一次;用 transferFrom 拉取 `amount` 并校验余额差值;
// 存入代币驿站;发出 FlapDistributionInitialized。
}
/// 以 `lockTier` 质押 `amount` 个发射代币
/// (例如 0 = 30 天 x1,1 = 90 天 x1.5,2 = 180 天 x2)。
function stake(uint256 amount, uint8 lockTier) external {
// 结算奖励;从调用者拉取 `amount` 个代币;记录仓位
// (amount、unlockAt、weight = amount * multiplier[lockTier]);累加到 totalWeight。
}
/// 领取目前已挖到的代币。
function claimRewards() external {
// 结算释放:rewardPerWeight += emissionRate * elapsed / totalWeight;
// 从挖矿池(代币驿站提取)向调用者支付待领代币。
}
/// 锁定期结束后取回质押。
function unstake(uint256 positionId) external {
// 要求 now >= unlockAt;领取待领奖励;移除权重;返还代币。
}
/// 只读:`user` 的待领奖励。
function pendingRewards(address user) external view returns (uint256) {
// 与 claimRewards 相同的结算,但不修改状态。
}
/// IDirectLaunchVault
function description() public view override returns (string memory) {
// 披露:quote 收入的 20% 给开发者,直到初始资金的 1.5 倍(显示已付 / 上限),其余作为分红
// 给持有者;开发者 1% 的释放进度(已释放 / 总量);以及其余预留代币的去向。
}
function vaultUISchema() public pure override returns (VaultUISchema memory) {
// 列出上面所有面向用户的方法,以及各自的输入与所需的 approve。
}
}示例 B —— 随市值上涨释放预留代币
79% 不用于挖矿。Vault 在递增的市值档位挂出固定价格卖单:例如在 20 万美元市值卖出总量的 1%,30 万美元再卖 1%,以此类推。只有市场涨到对应档位时才会释放代币,供应随代币表现逐步增加。卖单收到的 quote 作为分红给持有者,并且逐步释放(例如在 7 天内)而不是一次性发放,避免有人在大额成交前突击买入来瓜分这笔收入。
Factory 与示例 A 相同,另外校验 vaultData 中的挂单阶梯(档位严格递增、总量 ≤ 总供应的 79%)。
contract PriceLadderVault is DirectLaunchVaultBase, IStrategy {
struct Order {
uint256 marketCapUsd; // 例如 200_000e18、300_000e18……
uint256 amount; // 例如每档总量的 1%
uint256 sold;
}
/// quote 收入(VAULT 槽分得的交易税)。保持轻量,永不 revert。
receive() external payable {
// 1. newRevenue = balance - accountedQuote(余额差值记账);为 0 则直接返回。
// 2. devShare = newRevenue * 20%,上限为 (devCap - devPaid)。若 devShare > 0:
// devPaid += devShare;把 devShare 发给 `dev`。
// 3. 剩余部分(newRevenue - devShare)存入代币的分红合约(IDividend.deposit),
// 作为持有者的分红奖励。
// 4. 开发者线性释放:vested = devVest * min(now - vestStart, vestDuration) / vestDuration;
// 若 vested > devReleased,把 (vested - devReleased) 个代币直接发给 `dev`
// (经由代币驿站),并更新 devReleased。
}
/// 无许可的触发入口,用于识别没有触发回调就到账的收入(例如 ERC-20 quote)。
function sync() external {
// 与 receive() 的步骤 1-4 相同。
}
/// IStrategy:Portal 在发射交易中把 80% 预留代币交给 vault。
function initializeDistribution(address token, uint256 amount, bytes calldata, bytes32)
external
onlyHub
{
// 要求 token == taxToken、只能调用一次;用 transferFrom 拉取 `amount` 并校验余额差值;
// 存入代币驿站;发出 FlapDistributionInitialized。
}
/// 从已达到市值档位的挂单买入。
function buy(uint256 orderId, uint256 minTokensOut) external payable {
// 要求当前市值(V2 池价格 x 总量)>= 该档位;价格 = order.marketCapUsd / totalSupply;
// 收取 quote,从代币驿站发出代币,更新 order.sold。不要把 quote 一次性存入分红:把它加到
// `streamPending`,并重新计算释放速率(streamRate = (streamPending + 未释放部分) /
// STREAM_DURATION,例如 7 天;streamEnd = now + STREAM_DURATION)。
}
/// 无许可:把卖单收入中到目前为止应释放的部分存入分红。
function dripDividends() public {
// due = streamRate * (min(now, streamEnd) - lastDrip);lastDrip = now;
// 把 `due` 存入分红合约(IDividend.deposit)给持有者。
// receive() / sync() 中也会调用,使释放随日常活动推进。
}
/// 只读:每一档的价位、数量、剩余量,以及当前是否开放。
function orders() external view returns (Order[] memory) {
// 返回整个阶梯。
}
/// IDirectLaunchVault
function description() public view override returns (string memory) {
// 披露:quote 收入的 20% 给开发者,直到初始资金的 1.5 倍(显示已付 / 上限),其余作为分红
// 给持有者;开发者 1% 的释放进度(已释放 / 总量);以及其余预留代币的去向。
}
function vaultUISchema() public pure override returns (VaultUISchema memory) {
// 列出上面所有面向用户的方法,以及各自的输入与所需的 approve。
}
}示例 C —— Vault 作为众多单边池的 hook
Vault 作为 PancakeSwap Infinity 或 Uniswap v4 池的 hook。它用 79% 的预留代币,对多种支持的 quote 代币开设虚拟池 —— 价格由 hook 内的自定义曲线决定。开池不需要买入任何 quote:每个池都是单边的、由预留代币支撑,因此代币可以与任意多个 quote 配对。
Factory 与示例 A 相同,另外校验 vaultData 中的 quote 代币列表与曲线参数。
contract HookPoolsVault is DirectLaunchVaultBase, IStrategy {
/// quote 收入(VAULT 槽分得的交易税)。保持轻量,永不 revert。
receive() external payable {
// 1. newRevenue = balance - accountedQuote(余额差值记账);为 0 则直接返回。
// 2. devShare = newRevenue * 20%,上限为 (devCap - devPaid)。若 devShare > 0:
// devPaid += devShare;把 devShare 发给 `dev`。
// 3. 剩余部分(newRevenue - devShare)存入代币的分红合约(IDividend.deposit),
// 作为持有者的分红奖励。
// 4. 开发者线性释放:vested = devVest * min(now - vestStart, vestDuration) / vestDuration;
// 若 vested > devReleased,把 (vested - devReleased) 个代币直接发给 `dev`
// (经由代币驿站),并更新 devReleased。
}
/// 无许可的触发入口,用于识别没有触发回调就到账的收入(例如 ERC-20 quote)。
function sync() external {
// 与 receive() 的步骤 1-4 相同。
}
/// IStrategy:Portal 在发射交易中把 80% 预留代币交给 vault。
function initializeDistribution(address token, uint256 amount, bytes calldata, bytes32)
external
onlyHub
{
// 要求 token == taxToken、只能调用一次;用 transferFrom 拉取 `amount` 并校验余额差值;
// 存入代币驿站;发出 FlapDistributionInitialized。
}
/// 在 PCS Infinity 或 Uni v4 上开设单边池 TOKEN / `quote`,以本 vault 为 hook。
function openPool(address quote) external {
// 要求 `quote` 受支持且尚未开设;以本合约为 hook、带自定义曲线标志初始化池子;
// 记录该 `quote` 的曲线状态。
}
/// Hook 回调:用自定义曲线而不是 AMM 为每笔 swap 定价。
function beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
external
returns (bytes4, BeforeSwapDelta, uint24)
{
// 只允许 pool manager;按该 quote 的曲线状态计算输出;从预留(代币驿站)结算代币并收取 quote;
// 返回替代 AMM swap 的 delta。
}
/// 这些池子产生的 quote 收入,与 receive() 一样按 20% / 80% 分账。
function collect(address quote) external {
// 取出 hook 为 `quote` 累积的 quote,并执行收入分账。
}
/// IDirectLaunchVault
function description() public view override returns (string memory) {
// 披露:quote 收入的 20% 给开发者,直到初始资金的 1.5 倍(显示已付 / 上限),其余作为分红
// 给持有者;开发者 1% 的释放进度(已释放 / 总量);以及其余预留代币的去向。
}
function vaultUISchema() public pure override returns (VaultUISchema memory) {
// 列出上面所有面向用户的方法,以及各自的输入与所需的 approve。
}
}检查清单
-
newVault限制为 Portal 调用(onlyHub)。 -
newVault拒绝不支持的launchKind。 - 若 Vault 依赖预留代币,
newVault校验distribution.target == VAULT与精确的distribution.amount,否则 revert。 - 在部署 Vault 之前完成发射校验。
- Vault 是确定性的(每个代币一个)且有代码。
-
newVault不调用代币(它还没有代码)。 -
isQuoteTokenSupported与newVault一致。 -
vaultDataSchema与解码的vaultData一致。 -
factorySpecVersion返回"pl-v1"(以pl开头)。 - Factory 没有升级路径;Vault 的升级路径只允许 Guardian。
- Vault 只初始化一次,并在构造函数中禁用 initializer。
- Vault 按余额差值识别收益,而不是直接读原始余额。
-
receive()不会 revert。 - Vault 的特权函数同时接受 Guardian。
-
description()披露所有费用与接收方。 -
vaultUISchema()列出所有面向用户的方法及其 approve 需求。 - 若 Vault 接收余量,
initializeDistribution是onlyHub、单次调用,并校验拉取到的余额。