集成
| 链 | Robinhood Chain 主网 4663(RPC https://rpc.mainnet.chain.robinhood.com) |
| 浏览器 | robinhoodchain.blockscout.com |
| 发射项目的曲线和池子 | 以股票报价;池子是共用 hook 上的 Uniswap v4,费率 0.20%,tick spacing 60 |
| 金库的股票腿 | 上架指定的股票/USDG Uniswap V3 池,带 600 秒 TWAP |
| 股票预言机 | Chainlink,RHxxx / USD ÷ USDG 价格源,规则同三种价格 |
| 出块时间 | 约 0.1 秒;所有窗口都按秒计 |
发射代币
一次发射是一个 Request 加 terms,terms 是 predict(request) 返回的哈希。先登记创建者的选择,再报价,再按同样的 terms 发射。
请求
| 字段 | 含义 |
|---|---|
name、symbol | 代币的名称和符号 |
stock | 已上架的股票;否则 predict 回滚且不返回错误原因 |
creator | 永久收取已收税款的 creatorBps,并可否决收款地址提案。用 EOA 或 Safe |
taxBps | 100 到 1500 |
creatorBps | 0 到 5000,按已收税款计(maxCreatorBps)。发射表单显示基础税加一个不超过它的发币人税,发送 taxBps = 基础 + 发币人、creatorBps = 发币人 ÷ 合计 |
tp1Bps、tp2Bps、dipBps、stopBps、lotBps | 规则数字;含义和范围随策略不同 |
bandBpsPerHour | 过期容忍带,0 到 bandCeiling(stock) |
nonce | 区分同一创建者同一符号的两次发射;salt 的一部分 |
maxFee | 发射者愿意支付的发射费上限 |
expectedOpenPriceE18 | 发射者看到的上架开盘价 |
报价之前:创建者的登记
四项登记都以 keccak256(abi.encode(symbol, msg.sender, nonce)) 为键,与 factory 从 (symbol, creator, nonce) 推导的 salt 相同,
其他地址登记的内容不会作用于这次发射。
curveDeployer.setCurveConfig(symbol, nonce, 7931, snipeSeconds) // 窗口 0..180 秒;saleBps 必须是 DEFAULT_SALE_BPS
curveDeployer.setOpeningTaxExemptions(symbol, nonce, recipients) // 最多 40 个;创建者不登记也免加价
treasuryDeployer.setStrategyKind(symbol, nonce, kind) // 1..6;类型 0 不用调
treasuryDeployer.setEngineConfig(symbol, nonce, kind, config) // 只有类型 2、3 和 6 需要
登记在发射前可以修改,不能删除。predict 之后改动任何一项,发射会以 Restated 回滚;将报价时的值重新登记一次即可恢复。
报价与发射
(token, treasury, terms) = factory.predict(request)
id = factory.launch{value: launchFeeAmount}(request, terms)
id = factory.launchWithMetadata{value: launchFeeAmount}(request, terms, metadata)
发布默认值里 launchFeeCurrency 是 Native,launchFeeAmount 是 0.0005 ETH;以 getDefaults() 为准。publicLaunch() 必须为
true,或者调用者是已授权的 launcher(否则 NotOpen)。predict 和 launch 都会执行 deployer 的校验,因此会在 launch 失败的
请求在 predict 时就被具名错误拒绝:BadCurveConfig、BadEngineConfig、StopInsideExecutionFriction、Unseedable。循环
(类型 5)和预留现金的分批止盈(类型 6)是例外:它们的档位下限(tp1Bps 和 dipBps 至少 2 × (maxSlippageBps + 池费率))只在构造函数里检查(类型 6 的 reserveBps 超过 50% 也一样),低于下限的
报价仍能获得 terms,发射时以 TreasuryDeployFailed 回滚。报价前应先套用这个下限。
签名前应向创建者展示上架检查的结论:股票池无法承接的募资永远不能毕业,链上没有任何东西会拒绝这样的发射。
一次 ETH 付款:发射并首买
launchNativeRouter.launchAndBuy{value: launchFeeAmount + b.amountIn}(request, terms, metadata, b, path)
b 是 (amountIn, minStockReceived, minFinalOut, deadline, allowPartialFill);path 是 WETH 到股票的 V3 路线(通常是
WETH → USDG → 股票)。router 将费用支付给 factory,把剩余的 ETH 包装后沿路线换成股票,再替 request.creator 在新曲线上买入,所以
创建者的开盘加价免除生效,基础税照缴。报价过期、路线错误、买入失败或费用转账失败,整笔一起回滚。owner 须先用
setLauncher(router, true) 授权这个 router。不要用 v1 的 HedgeFunLaunchRouter 发射 v2:它的首买直接进入 v4。
资料和图标
launchWithMetadata 和 launchAndBuy 在发射交易里写入代币的第一份资料,所以代币上线时资料不会是空的。资料是
(logo, description, (twitter, telegram, discord, website, farcaster), extraURI):每个链接最多 256 字节,介绍最多 1,024 字节
(否则 TooLong)。
网站只显示托管在本站的 logo,即 https://hedgefun.trade/media/icons/<sha256>.png。指向其他地方的 logo 会留在链上,但不显示:
第三方主机能看到每个访客,也能在发射审核之后换掉图片。社交链接只在是对应平台自己域名的 https 链接时显示(x.com 或
twitter.com、t.me、discord.gg 或 discord.com、farcaster.xyz 或 warpcast.com)。
获取托管图标有两种方式:
-
手动: 在 hedgefun.trade/icons 上传,复制得到的链接。不连接钱包。
-
脚本: 向
POST /api/icons发送 PNG,用一个在这条链上持有不少于发射费 ETH 的钱包签名。签名的文字是Hedgefun icon uploadHost: hedgefun.tradeSHA-256: <PNG 原始字节的 SHA-256,小写十六进制>Issued: <Unix 秒,与服务器时间相差不超过 300 秒>
import { readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { privateKeyToAccount } from 'viem/accounts';
const png = await readFile('icon.png');
const sha256 = createHash('sha256').update(png).digest('hex');
const issued = Math.floor(Date.now() / 1000);
const creator = privateKeyToAccount(process.env.CREATOR_KEY as `0x${string}`);
const signature = await creator.signMessage({
message: `Hedgefun icon upload\nHost: hedgefun.trade\nSHA-256: ${sha256}\nIssued: ${issued}`,
});
const response = await fetch('https://hedgefun.trade/api/icons', {
method: 'POST',
headers: { 'content-type': 'image/png', 'x-icon-issued': String(issued), 'x-icon-signature': signature },
body: png,
});
const { url, error } = await response.json(); // url: https://hedgefun.trade/media/icons/<sha256>.png
PNG 必须是 8 位 RGB 或 RGBA、非隔行、最大 512 × 512 像素和 512 KiB,不含文字、色彩配置或动画块。
sharp(input).resize(512, 512, { fit: 'inside', withoutEnlargement: true }).ensureAlpha().png() 生成的文件即符合要求。
只接受 EOA 签名。同一张图上传两次,得到同一个链接。
发射之后,创建者(代币的 deployer)可以用 token.setMetadata(logo, description, socials, extraURI) 整份替换资料,也可以用
setEditor(editor) 指定另一个地址代为修改(传 address(0) 撤销),适合替创建者发射的机器人。lock() 永久冻结资料;指定了
editor 时会被拒绝,因此应先撤销 editor、核对页面,再锁定。代币页上有给创建者用的编辑器,用钱包做同样的事。
实时值的展示
募资额、LP 份额和金库份额来自上架的 openPriceE18、curve deployer 的 DEFAULT_SALE_BPS()、供应量、treasury deployer 的
lpBps(stock) 和预言机价格。目标在发射时以股票单位固定;美元数字只是估计。
买入与卖出
任意支付资产用 HedgeFunV2TradeRouter,ETH 用 HedgeFunV2NativeRouter,持有股票时可以直接调用曲线和池子。
trade router
buy(params, path) 和 sell(params, path) 接收一个 TradeParams 和一个 Hop[];buyFor(params, path, to) 把代币交给另一个
地址。
| 字段 | 含义 |
|---|---|
id | 这个 factory 上的发射 id |
asset | 买入:付款 ERC20;卖出:期望的产出 ERC20 |
amountIn | 买入:付款资产的原始单位;卖出:策略代币数量 |
minStockReceived | 买入:付款路线至少应换出的股票,包括之后退回的股票;卖出时忽略 |
minFinalOut | 经过所有交换和税之后的最低最终产出,部分成交时不会按比例缩小 |
deadline | 允许执行的最后时间戳 |
expectedStage | 0 Active 或 2 Graduated,资金移动前检查 |
allowPartialFill | 接受买入时的股票退款,或卖出时的代币退款 |
Hop.pool、Hop.tokenOut | 下一跳标准 V3 池及其产出资产;最多 3 跳;直接用股票交易时留空 |
买入路线是 付款 → V3 跳 → 股票 → 曲线或 v4 池 → 代币,卖出反向,以选定的产出资产结束。路线必须连续、资产不重复、池子必须由
factory 指定的 V3 factory 创建;策略代币不能出现在转换路线里。每一跳 V3 都必须完全成交。转账收费和弹性供应的资产被拒绝。任何
失败都回滚整条路线。
退款是股票。 触顶的最后一笔曲线买入、整数取整或 v4 价格限制都可能留下未用完的输入;此时付款已经换成股票,所以未用部分以
股票退回,记在 Bought 事件的 stockRefund 里。即使只是粉尘也需要 allowPartialFill。一笔买入完成毕业后,不会自动在新池
中继续买入。部分成交的 v4 卖出把未卖出的代币记在 tokenRefund 里退回。
买入时两个下限均应设置。minFinalOut 保护代币产出;minStockReceived 保护付款到股票这一腿,因为上游汇率变差时可能仅缩小退款,
而代币产出不变。minStockReceived 为 0 即不保护这一腿。
native router
HedgeFunV2NativeRouter.buy{value: amountIn}(params, path) 把 msg.value 全额包装,把代币转给调用者;params.asset 和路线
里写的是包装后的 ERC20。sell 解包报价产出,未卖出的代币另行退回。股票退款仍是股票。原生币付出被拒绝时整笔交易回滚。
直接交易
- 曲线:先将股票或代币授权给曲线,再调用
buy(stockIn, minTokensOut, recipient, deadline)或sell(tokenIn, minStockOut, recipient, deadline)。 - 池子:用金库保存的 pool key(
poolKey()),通过任何 v4 router 做交换。用最低产出限定价格。在sqrtPriceLimitX96处停止的 指定输入买入会回滚;指定输出的卖出被拒绝。细节在 Uniswap v4 池。
获取报价
曲线阶段
curve.quoteBuy(maxStockIn) → (stockSpent, tokensOut, taxTokens)
curve.quoteBuyFor(maxStockIn, to) → 同上,针对可能免加价的收款人
curve.quoteSell(tokenIn) → (stockOut, taxStock)
curve.buyRateBps(), buyRateBpsFor(to) → 实时买入费率,含开盘加价
stockSpent 是含基础税的总付款;taxTokens 只是开盘加价的销毁量。基础股票费为 floor(stockSpent × taxBps / 10000)。基础费
和加价销毁要分两行展示。会越过毕业目标的买入有上限:stockSpent 小于 maxStockIn,差额以股票退回。在 Active 曲线上获取的
报价,若交易落地时曲线已毕业,则以 StageChanged 回滚;需重新报价。
池子阶段
hook 在交换前收取买入税,因此仅凭池价无法给出报价。用实际的 router 调用做模拟(从已注资、已授权的调用者,带预期的 deadline、
阶段和输入做 eth_call),展示它返回的产出。买家在毕业池上的成本是税(1% 到 15%)加上按净额计的 0.20% 池子手续费,都以
股票计;最低税率时约 1.2%。毕业池没有开盘窗口,也没有卖出尖峰;hook.buyRateBps(poolId) 和 hook.sellRateBps(poolId) 都返回
固定税率。
经路由报价
多跳路线先在链下找出 V3 池,再带预期路线模拟 router 调用。展示净产出、两个下限、每项税费,以及退款的币种(股票)。模拟里的 退款金额是估计,不保证最终未用完的输入数量。
换算成 USDG
池子里的价格是代币以股票计的价格。要显示美元价格或估值,乘以股票的预言机价:
price_usdg = price_in_stock × oracle_usdg(stock)
FDV = price_usdg × supply 说明用的是哪个供应量:已发行,或已发行减已销毁
代币顺序从池子 key 读(vault.poolKey(),或 treasury.stockIsCurrency0InTokenPool()),这条链上各个池的顺序并不一致。开盘
估值以股票计:供应量 × openPriceE18。未读取到的数字显示“未获取”,永远不显示 0。
读取状态
发射项目
factory.strategyCount() → 发射总数
factory.strategies(id) → (token, treasury, hook, stock, creator)
factory.curves(id) → 曲线
curve.status() → 0 Active · 1 Ready(瞬时)· 2 Graduated
curve.realStockReserve() / curve.terminalStock() → 毕业进度
curve.tokenReserve(), curve.minTokenReserve() → 未售库存与销售终点
hook.poolOfTreasury(treasury) → 毕业后的 v4 池 id
hook.liquidityVaultOf(poolId) → vault
treasuryDeployer.strategyKindOf(salt) → 类型;lpBpsOfTreasury(treasury) 是冻结的 LP 份额
毕业进度是 realStockReserve / terminalStock。目标在发射时以原始股票单位固定(CurveLaunched 带 saleBps 和 virtualStock)。
池子
vault.poolKey() 给出 key;vault.seeded()、surplusLiquidity()、surplusTickLower() 和 surplusTickUpper() 描述两个头寸;
vault.lockedSeedTokens() 是取整剩余。一个池子的股票储备是 vault 的两个头寸加未领取的手续费,从池子自身状态读;
balanceOf(PoolManager) 是这条链上所有池的总和。hook.rates(poolId) 返回冻结的费率;hook.accrued(poolId) 和
owedProtocol、owedCreator、owedTreasury 是等待 sweep 的费用。
金库
| 视图 | 类型 | 含义 |
|---|---|---|
health() | 全部 | (ok, price):股票腿现在能否交易。毕业前为 (false, 0) |
unbookedStock() | 全部 | 已到账、尚未记账的股票;任何人可调 book() |
buybackStock() | 全部 | 等着回购代币的股票 |
totalBurned()、totalStockSpentOnBuybacks() | 全部 | 回购记录 |
params() | 全部 | 冻结的规则数字和边界 |
lotCount()、lots(id)、reserveUsdg()、lastSalePrice()、lastStopAt() | 分批止盈、按比例回购、循环 | lot 账本和回调参考价 |
protectedGraduationStock() | 回购销毁 | 闲置的毕业资本 |
engineConfig()、preview()、avgCost() | 固定仓位、再平衡 | 冻结的字段、下一次 execute() 会做什么、库存平均成本 |
riskLimits() | 再平衡 | 实时可交易价值、单次上限、当日上限和剩余额度 |
implementation()、upgradeConfigHash()、treasuryUpgradeController() | 全部 | 代理当前的逻辑和身份 |
controller.proposals(treasury) | 全部 | 待执行的升级:实现、epoch、代码哈希、数据哈希、readyAt |
把一个发射项目显示为可交易之前先展示 health()。代币页上要显示待执行的升级提案,这是已发射项目中唯一可能变化的部分。
keeper 循环
for id in 0 .. factory.strategyCount()-1:
(token, treasury, hook, stock, creator) = factory.strategies(id)
if factory.curves(id).status() != 2: continue # 曲线阶段只有 claimFees 可做
(ok, p) = treasury.health()
分批止盈一族: 模拟 treasury.execute();不回滚 NotDue 就发送
回购销毁: 模拟 treasury.buyback()
固定仓位、再平衡:(due, action, amountIn) = treasury.preview();模拟 execute()
所有类型: buybackStock() > 0 且冷却已过时,模拟 treasury.buyback()
hook.accrued(poolId) 非零时调 hook.sweep(poolId);手续费有增长时调 vault.collectFees()
NotDue 是正常答复,eth_call 不产生费用。execute() 运行时会顺带记账待处理股票,以 NotDue 结束时会将记账回滚,因此有股票待记账、
而没有动作到期时,应单独调用 book()。报酬是动作产出的 0.1%,以产出的资产支付;sweep 和 collectFees 没有报酬。
领取费用与 sweep
费用不会自动移动。以下三个调用负责移动费用,任何人均可发起。
| 调用 | 合约 | 移动什么 | 给谁 | 调用者报酬 | 事件 |
|---|---|---|---|---|---|
claimFees(recipient) | 曲线 | 该收款方累计的税,以股票计 | 只给该收款方 | 没有 | FeesClaimed(recipient, amount) |
sweep(poolId) | hook | 这个池子买方和卖方的全部费用 claim,以股票计 | 协议、创建者、金库 | 没有(sweepTipBps 为 0) | Swept(id, tokenBurned, stockToTreasury, stockToProtocol, stockToCreator) |
collectFees() | vault | 两个头寸累计的池子手续费 | 股票进金库的 buybackStock;代币销毁 | 没有 | FeesCollected(stockToTreasury, tokenBurned) |
claimFees
曲线阶段和毕业后都可用。claimable(recipient) 给出待领金额。被股票发行方拦截的收款方不会阻碍另外两方的领取,也不会阻碍曲线交易;
其 claim 保持待领状态。
sweep
hook 把费用记为 ERC-6909 股票 claim,每个池一本账,所有池在同一个地址,所以金额来自 accrued(poolId),从不来自余额。每种货币
各走自己的一腿;一腿无法结算时不会阻碍另一腿。vault 池上 tokenBurned 为 0:从来没有费用以代币形式持有。sweep 不记账金库的
份额;book() 或下一次 execute() 才记账。
collectFees
对两个头寸执行零流动性变动的操作,无法动用本金。股票转给金库被发行方拒绝时,vault 把股票留作待交付,creditPendingStock() 或
下一次 collectFees() 重试;代币销毁照常完成。v4 的 take 内部被拒绝的股票转账会让这次领取回滚。
金库的调用
book() 按当时价格给收入记账;execute() 和 buyback() 向调用者支付产出的 0.1%。keeper 循环在读取状态。