池、事件与错误
Uniswap v4 池
每个毕业的发射项目在一个 Uniswap v4 池里交易:代币对它的股票,费率 0.20%,tick spacing 60,运行在 HedgeFunV2Hook 上。一个
hook 实例服务所有池;每个池在它内部有自己的账本。
若已集成过其他 v4 发射 hook,请注意以下三点不同,先核对再作假设:费用一直存在 hook 里,直到有人 sweep;池子有两个锁定头寸,不是 一个;买入税在交换之前收取,所以带价格限制的买入会回滚。
池子
| 货币 | 代币和股票;哪个是 currency0 取决于地址顺序,从 vault 读 poolKey() 或从金库读 stockIsCurrency0InTokenPool() |
| 费率 | lpFee 2000(0.20%),按池冻结;factory 为之后的发射接受 1 到 3000 |
| tick spacing | 60 |
| hook | HedgeFunV2Hook,version() 3,地址标志位 0x28CC |
| 创建 | 毕业时由 factory 创建,按曲线终点价初始化 |
| 注册 | factory 在初始化前用 registerGraduatedWithVault 注册池子和它的 vault;未经 factory 注册的池子不能在这个 hook 上初始化 |
两个锁定头寸
V2LiquidityVault,每个池一个,直接持有两个头寸;没有 LP 代币。
| 头寸 | 内容 | 区间 |
|---|---|---|
| 全区间,salt 0 | 募资 70% 的股票,加参考设置下约 8.20% 供应量的代币 | 可用的两端极值 |
| 单边,salt 1 | 约 12.49% 供应量的代币,没有股票 | 从现价代币一侧的下一个可用 tick 到可用的极端 tick |
vault 没有撤出流动性、任意调用或升级的函数。collectFees() 是它唯一的流动性操作,以零流动性变动运行。取整剩余留在
lockedSeedTokens,从不报告为手续费。价格进入单边头寸的区间后,两个头寸都计入池子深度。
hook 的回调
| 回调 | 作用 |
|---|---|
beforeInitialize | 只对 factory 注册的池子、只对已注册的 vault 放行。任何人都无法在这个 hook 上创建非 factory 发射的池子 |
beforeAddLiquidity | 只放行该池的 vault(否则 NotSeeder)。第三方头寸会成为不缴税卖出的通道 |
beforeRemoveLiquidity | 对所有人回滚 HookNotImplemented。锁定即在此实现 |
beforeSwap | 金库以外任何人的指定输入买入(股票进、代币出):在交换前从股票里收 floor(paid × taxBps / 10000),铸成 hook 的股票 claim,发出 Taxed,让 paid − tax 进入池子。其他情况返回零 delta |
afterSwap | 卖出:从毛股票产出里收税。固定费率下的指定输出买入:从股票输入里收 r / (1 − r)。指定输入买入:核对交换是否刚好成交 paid − tax,否则回滚 PartialFillRefused。金库自己的交换免税通过 |
权限位:BEFORE_INITIALIZE、BEFORE_ADD_LIQUIDITY、BEFORE_SWAP、BEFORE_SWAP_RETURNS_DELTA、AFTER_SWAP、
AFTER_SWAP_RETURNS_DELTA。指定输出的卖出被拒绝(ExactOutputRefused)。
费用的存放
税记为 hook 持有的 ERC-6909 股票 claim,按池分账(accruedStock、owedProtocol、owedCreator、owedTreasury)。没有任何
费用按笔付出。任何人可调的 sweep(poolId) 一次性支付协议、创建者和金库的份额,每种货币各走自己的一腿;sweepTipBps 为 0。
vault 池上从来没有费用以代币形式持有,所以 Swept.tokenBurned 为 0。
池子自己的 0.20% 手续费按每笔交换的输入货币累计在 vault 的头寸上。collectFees() 把股票一侧转入金库的 buybackStock,把
代币一侧销毁。
hook 的职责范围
- 不限制谁能交换。任何人都可以通过任何 v4 router 交易这个池子。
- 不对代币转账收税。
HedgeFunToken是普通 ERC20,hook 只看交换。 - 毕业池上不运行开盘窗口:
registerGraduatedWithVault拒绝窗口,买入费率从第一笔交换起就是固定税率,buyRateBps(poolId)等于sellRateBps(poolId)。 - 回购之后不抬高卖出费率:v2 上
spikeBps为 0。 - 不转换费用。没有以代币计的费用库存,也没有 owner 转换的步骤。
会回滚的指定输入买入
beforeSwap 已按全额付款收取了税,因此在调用者的 sqrtPriceLimitX96 处提前停止的买入,会为未成交的股票缴税。afterSwap
以 PartialFillRefused() 拒绝它,PoolManager 包装为 CustomRevert.WrappedError(hook, afterSwap.selector, PartialFillRefused(), HookCallFailed())。像 HedgeFunV2TradeRouter 那样用最低产出限定价格,即可避免这个回滚。卖出按实际成交收税,可以在限价处
停止。解码方法见错误。
需索引的事件
| 合约 | 事件 | 对索引器的意义 |
|---|---|---|
| factory | Launched(id, symbol, token, treasury, hook, stock, creator) | 新发射 |
| factory | CurveLaunched(id, curve, saleBps, virtualStock) | 曲线及其固定目标 |
| factory | Graduated(id, sqrtPriceX96, liquidity, stockSeeded, tokenSeeded, tokenBurned) | 跨线;v2 上 tokenBurned 为 0 |
| factory | GraduationCapitalSplit(id, lpStock, treasuryStock, treasuryBooked) | 70/30 的金额,以及金库是否当场记账 |
| factory | Listed、ListingGatesSet、DefaultsSet、PublicLaunchSet、LauncherSet、BandCeilingSet | owner 操作 |
| curve | Bought(caller, recipient, stockSpent, tokensOut, burned) | 曲线买入;burned 是开盘加价 |
| curve | Sold(caller, recipient, tokenIn, stockOut, fee) | 曲线卖出 |
| curve | TradeFeesAccrued(buying, fee, protocolFee, creatorFee, treasuryFee) | 一笔交易税款的分配 |
| curve | FeesClaimed(recipient, amount)、Ready()、Released(stockAmount, tokenAmount)、OpeningTaxExemptionsFixed(recipients) | 领取;毕业交接;冻结的免除地址 |
| curve deployer | CurveConfigSet、OpeningTaxExemptionsSet | 创建者的窗口和免除地址 |
| treasury deployer | StrategyKindSet、EngineConfigSet、KindRegistered、EngineKindRegistered、PolicyRegistered、PolicyDisabled、LpBpsSet、TreasuryCodeBound | 创建者的类型选择和注册表 |
| hook | Registered(id, token, stock, treasury, protocol, creator, rates) | 池子冻结的费率 |
| hook | Taxed(id, selling, inToken, moved, tax, rateBps) | 一笔池子交易的税。moved 是交换实际移动的股票;买入时交易者付的是 moved + tax |
| hook | Swept(id, tokenBurned, stockToTreasury, stockToProtocol, stockToCreator) | 一次付出;vault 池上 tokenBurned 为 0 |
| hook | Claimed、CreatorProposed、CreatorVetoed、CreatorChanged、ProtocolChanged | 收款地址管理 |
| vault | Seeded(liquidity, amount0, amount1)、SurplusSeeded(lower, upper, liquidity, tokens)、SeedTokensLocked(amount) | 两个头寸 |
| vault | FeesCollected(stockToTreasury, tokenBurned) | 池子手续费:股票进回购预算,代币销毁 |
| treasury,分批止盈一族 | LotBooked(id, qty, cost, fromTax)、ProfitTaken(qty, cost, price, usdgToReserve, stockToBurn)、Stopped(qty, cost, price)、Buyback(stockSpent, tokenBurned) | lot 规则 |
| treasury,固定仓位和再平衡 | StrategyExecuted、KeeperRewardPaid、GainToBuyback(gain, toBuyback, avgCost, price) | 一次引擎动作、它的报酬,以及送去销毁的收益份额 |
| trade router | Bought(id, buyer, paymentAsset, recipient, paymentIn, tokensOut, stockRefund)、Sold(id, seller, outputAsset, tokensSpent, outputAmount, tokenRefund) | 经 router 的交易 |
| native router | NativeBought(id, buyer, nativeIn, tokensOut, stockRefund)、NativeSold(id, seller, nativeOut, tokenRefund) | 同一笔交易,归到用户名下 |
| launch router | NativeLaunchedAndBought(id, creator, launchFee, nativeBuyIn, tokensOut, stockRefund) | 一次付款的发射 |
| 升级控制器 | UpgradeScheduled(treasury, implementation, codeHash, dataHash, readyAt)、UpgradeExecuted、UpgradeCancelled | 两天公示及其结果 |
曲线、router 和 hook 的事件是同一次执行的不同层,成交量只计一次。原生币交易底层 router 事件里的调用者是 native router,归到
NativeBought.buyer 名下。供应量图表里三种销毁要分开:开盘加价(Bought.burned)、回购(Buyback.tokenBurned)和池子手续费
销毁(FeesCollected.tokenBurned)。毕业时不发生任何销毁,锁定的 LP 也不计为供应量减少。topic 哈希见合约仓库的 abi/SURFACE.md;
请针对实际部署的提交重新生成。
错误
| 错误 | 来源 | 场景 |
|---|---|---|
Restated() | factory | predict 之后默认值、上架、门限或已登记选择有变动 |
NotListed()、NotOpen()、BadRequest() | factory | 股票未上架;公开发射关闭且调用者不是已授权的 launcher;某个字段超出范围 |
Unseedable()、SeedOverspent() | factory | 这组条件会让 v4 注入为空、过小或无法定价;注入超出预算 |
NotReady() | factory | 对不是 Ready 的曲线调 graduate(id) |
BadCurveConfig() | curve deployer | saleBps 不是 7931,或窗口超过 180 秒 |
BadEngineConfig()、StopInsideExecutionFriction()、BadPolicy() | treasury deployer | 引擎字段超出下限;止损落在 滑点 + 池费率 + 报酬 以内;policy 已停用或不匹配 |
TreasuryDeployFailed() | treasury deployer | 构造函数拒绝了注册表无法具名检查的内容(循环的档位下限) |
StageChanged(actual) | router | 报价和调用之间曲线已毕业;重新报价 |
Expired()、TooLittle(got)、TooLittleStock(got) | router、curve | 超过 deadline;低于 minFinalOut;低于 minStockReceived |
PartialFill(spent) | router | 有未用输入且 allowPartialFill 为 false |
BadPath()、WrongPool()、BadAmount() | router | 路线不连续或重复;池子不是 factory 指定的 V3 factory 创建的;金额为零 |
Closed()、Slippage()、Insolvent()、GraduationFailed()、UnsupportedTransfer() | curve | 毕业后交易;低于调用者下限;偿付检查失败;毕业回调失败且买入回滚;转账收费或加收的股票 |
NotSeeder()、WrongPool()、HookNotImplemented()、ExactOutputRefused() | hook | vault 以外的地址添加流动性;未经 factory 注册的池子;撤出流动性;指定输出的卖出 |
PartialFillRefused() | hook,被 v4 包装 | 在价格限制处停止的指定输入买入 |
NotDue()、Unhealthy()、Cooldown()、Slippage() | treasury | 没有待执行的动作;health() 为 false;回购限速;一笔未卖出任何资产的卖出 |
UseBuyback()、UseExecute() | treasury | 回购销毁没有 execute();继承的 v1 选择器在 v2 上关闭 |
AlreadySeeded()、NotSeeded()、Busy()、InexactTransfer() | vault | 第二次注入;注入前领取;重入回调;转账金额不精确 |
NotReady()、InvalidUpgrade()、NotOwner() | 升级控制器 | 未到 readyAt;候选的代码哈希、calldata 哈希、配置哈希或 schema 不匹配;调用者不是 owner |
被 v4 包装的 PartialFillRefused
对于指定输入的买入,hook 在 beforeSwap 已按全额付款收取了税。如果交换随后在调用者的 sqrtPriceLimitX96 处停止,afterSwap
以 PartialFillRefused() 回滚,买家因此不会为未成交的股票缴税。PoolManager 会把它包装一层,调用者看到的回滚是:
CustomRevert.WrappedError(address hook, bytes4 afterSwap.selector, bytes PartialFillRefused(), bytes HookCallFailed())
解码内层原因,或按 WrappedError 选择器加 hook 地址匹配。用最低产出限价的 router(HedgeFunV2TradeRouter 即属此类)不受
影响;在买入时用价格限制的第三方 router 会遇到。卖出按实际成交收税,可以在限价处停止。
每个错误的选择器见合约仓库实际部署提交处的 REFERENCE.md 和 abi/SURFACE.md。