Perpetual Protocol 一: 概览
v1 的池子里一分钱都没有,Amm.sol 只存两个储备量数字,真钱躺在 ClearingHouse 上。这一篇把 v1 实际存在的三个合约摆清楚,顺带说明它和 v2 Curie 为什么不能混着讲。
永续合约交易所最难办的一件事是找对手盘。你要做多 ETH,就得有人愿意在同一个价位做空。中心化交易所拿订单簿撮合,链上撮合太贵,于是 Perpetual Protocol v1 给了一个绕开的答案:不撮合,对手盘的钱也不需要 —— 池子里一分钱都没有,只有两个数字。
这两个数字叫 quoteAssetReserve 和 baseAssetReserve,存在 Amm.sol 里。它们的乘积构成一条恒定乘积曲线,下单时按曲线算出成交价,然后把两个数字改掉。没有任何代币在这条曲线上流动,也没有人往里存过钱。真正的 USDC 躺在另一个合约里,从头到尾没动过。这就是 vAMM(虚拟自动做市商,池子里没有真实资产,只有一条记账用的曲线)。
仓库里没有 Vault.sol,也没有 Exchange.sol
先把一件事挡在前面,因为它是这个话题下最常见的错误。
Perpetual Protocol 有两代,实现完全不同。v1 是 2020 年上线的那套 vAMM,合约在 perpetual-protocol/perpetual-protocol 仓库的 src/ 下。v2 代号 Curie,2021 年 11 月 30 日上线 Optimism,把 vAMM 整个换成了 Uniswap V3 的集中流动性,合约在另一个仓库 perpetual-protocol/perp-curie-contract 的 contracts/ 下。
Vault.sol、Exchange.sol、AccountBalance.sol 这几个名字属于 v2。v1 里它们一个都不存在。中文资料里经常能看到「vAMM + Vault + Exchange」的架构图,那是把 v2 的合约名贴到了 v1 的机制上,两头都对不上源码。
v1 跟交易有关的合约就这些:
ClearingHouse.sol 仓位、保证金、清算 —— 用户的钱也存在这个合约上Amm.sol vAMM:两个储备量,一条恒定乘积曲线InsuranceFund.sol 兜底资金,同时是「哪些 Amm 算数」的注册表ChainlinkPriceFeed.sol 指数价格来源L2PriceFeed.sol 从 L1 桥过来的价格ClearingHouseViewer.sol 只读聚合,给前端用,不参与状态变更AmmReader.sol 同上src/ 下另外还有一批和交易无关的东西 —— PERP 代币的增发、质押、手续费分配。真正在一次开仓里被调用到的,是上面这个清单的前四个。
把 ClearingHouse.sol 的状态变量拉出来看,它依赖谁一目了然:
// only admin Decimal.decimal public initMarginRatio; Decimal.decimal public maintenanceMarginRatio; Decimal.decimal public liquidationFeeRatio;
// key by amm address mapping(address => AmmMap) internal ammMap;
// 合约依赖只有这两行:兜底资金,和手续费的去处 IInsuranceFund public insuranceFund; IMultiTokenRewardRecipient public feePool;没有 vault,没有 exchange。三个保证金率是全站一份,不按市场分开存 —— 这一点第四篇还会再用到。
保证金存在 ClearingHouse 自己身上
既然没有 Vault,用户的 USDC 去哪了?答案是转给了 ClearingHouse 这个合约地址本身。开仓时那一行长这样:
// transfer the actual token between trader and vault if (positionResp.marginToVault.toInt() > 0) { _transferFrom(quoteToken, trader, address(this), positionResp.marginToVault.abs()); } else if (positionResp.marginToVault.toInt() < 0) { withdraw(quoteToken, trader, positionResp.marginToVault.abs()); }address(this) 就是 ClearingHouse。源码注释和变量名里确实出现了 vault 这个词(marginToVault),但它指的是 ClearingHouse 自己的余额,不是某个叫 Vault 的合约。这个命名是 v1 留下的坑,也多半是那批错误架构图的来源之一。
一次开仓依次碰到哪几个合约
用户调 ClearingHouse.openPosition,传进去的第一个参数是要交易哪台 Amm。接下来发生的事按顺序是:
- ClearingHouse 问 InsuranceFund「这台 Amm 注册过吗」,没注册直接 revert。
- 检查杠杆够不够 —— v1 不存一个叫 maxLeverage 的变量,它检查的是「1 除以杠杆」还大不大于
initMarginRatio。 - ClearingHouse 调
Amm.swapInput,Amm 按恒定乘积曲线算出能换到多少基础资产,改掉两个储备量,把结果返回。 - ClearingHouse 把仓位写进
ammMap[amm].positionMap[trader],并把保证金从用户钱包转到自己名下。 - 手续费转给
feePool和 InsuranceFund。
Amm 全程不碰任何真实代币,它只改自己那两个数字。ClearingHouse 全程不算价格,它把定价问给 Amm。定价和记账被切开了,这是 v1 架构里唯一一条真正重要的分工。
Amm.swapInput 上挂着 onlyCounterParty 修饰符,counterParty 就是 ClearingHouse。别人调不动它,vAMM 的储备量不会被外部直接推动。
v2 Curie 换掉了这套机制的核心
Curie 保留了「交易者和协议做对手」的思路,定价换了个来源:v2 部署一批虚拟 ERC20(报价代币符号是 vUSD,基础代币是 vETH、vBTC 这些),把它们放进真实的 Uniswap V3 池子,成交走 Uniswap 的集中流动性。虚拟代币的转账被白名单卡着,只能在协议和池子之间流动。
职责也被拆得更细:
| 干什么 | v1 | v2 Curie |
|---|---|---|
| 定价 / 成交 | Amm.sol 的恒定乘积曲线 |
Uniswap V3 池子,由 Exchange.sol 包一层 |
| 存用户的钱 | ClearingHouse 自己 | Vault.sol |
| 记仓位和盈亏 | ClearingHouse 自己 | AccountBalance.sol |
| 有哪些市场 | InsuranceFund.sol 的 amm 列表 |
MarketRegistry.sol |
| 参数旋钮 | ClearingHouse 上的几个 public 变量 | ClearingHouseConfig.sol |
注意 v2 的 Exchange.sol 不是「多市场管理器」,它干的是执行 Uniswap 换币和结算资金费。注册表是 MarketRegistry。这个误会在第四篇会单独讲。
这个系列讲 v1
接下来四篇拆的都是 v1:Amm.sol 怎么定价和算资金费,ClearingHouse.sol 怎么记仓位和清算,多市场在没有注册中心的情况下怎么管,以及钱在 InsuranceFund 和 ClearingHouse 之间怎么周转。需要和 v2 对照的地方会明确写出版本。
选 v1 是因为它小。三个合约、一条曲线,能一口气读完,而永续合约的那几个核心问题 —— 价格从哪来、谁承担亏损、什么时候清算 —— 一个都没少。代价是 v1 已经不再运行,读它是为了看清机制,不是为了对着线上系统调参。
v1 本身也确实有它的结构性问题。虚拟池子没有真实的套利者把价格拉回现货,只能靠资金费率把它往回拽;「无限流动性」的说法容易误导,储备量是人设的,设小了大单照样滑得很惨。这两件事第二篇会展开。
这篇是 Perpetual Protocol的第 1 篇,后一篇是 Perpetual Protocol 二:VAMM - 虚拟自动做市商的实现。