永续合约 05 - vAMM 永续合约演进史

vAMM (Virtual AMM) 是 DeFi 永续合约的第一代链上定价方案, 借用 x * y = k 曲线定价但不持有真实代币. 本文梳理从 Perpetual Protocol v1 到 v2 集中流动性再到 Drift 的 vAMM+DLOB 混合方案的演进过程, 分析每一代的改进点和遗留问题.

一、术语表

1.1 vAMM 基础

术语 英文 含义
vAMM Virtual AMM 虚拟自动做市商, 用 AMM 公式定价但不持有真实代币
虚拟储备 Virtual Reserves vAMM 中的 x_v 和 y_v, 仅用于计算价格, 不是实际代币余额
k 值 k (Constant Product) x_v * y_v = k 中的常数, 决定池子的虚拟深度和滑点
保证金池 Margin Vault / Clearing House 存放所有交易者保证金的合约, 负责真实资金的结算
虚拟价格 vAMM Price 由虚拟储备计算的价格, price = y_v / x_v

1.2 协议与架构

术语 英文 含义
Clearing House 清算所 管理保证金、开平仓、清算的核心合约
Insurance Fund 保险基金 用于覆盖穿仓损失的资金池
DLOB Decentralized Limit Order Book 去中心化限价订单簿, Drift 的混合流动性层之一
JIT Just-In-Time Liquidity Drift 中做市商在交易执行前一刻注入的即时流动性
Maker 流动性提供者 Perp v2 中在 vAMM 内提供集中流动性的角色
Taker 交易者 在 vAMM 中执行交易的一方

1.3 风险

术语 英文 含义
Squeeze 挤压/逼仓 大户通过大量单边交易推动虚拟价格到极端, 触发他人清算
脱锚 De-peg / Price Divergence vAMM 价格长时间偏离现货指数价格
k 值治理 k Governance 通过治理调整 k 值, 平衡滑点和价格灵敏度

二、vAMM 概念: 虚拟的做市商

2.1 从真实 AMM 到虚拟 AMM

在 Uniswap (真实 AMM) 中:

  • LP 存入真实的 token0 和 token1, 形成储备 x, y
  • 交易者用一种 token 换另一种, x * y = k 决定价格
  • LP 承担无常损失, 交易者获得真实 token

vAMM 的核心洞察: 永续合约不需要真实的 token 交换, 只需要一个定价机制.

在 vAMM 中:

  • 没有 LP 存入代币, x_v 和 y_v 是协议设定的虚拟数字
  • 交易者开多/开空, 虚拟储备发生变化, 产生一个虚拟价格
  • 所有资金结算在独立的保证金池 (Clearing House) 中进行
真实 AMM vs vAMM: 资金流与定价分离 真实 AMM (Uniswap) Pool x * y = k 真实 ETH + USDC 储备 LP 存入真实 token Trader ETH ↔ USDC 定价 + 资金 在同一个池子 LP 承担无常损失 交易者获得真实 token vAMM (Perpetual Protocol) vAMM (虚拟池) x_v * y_v = k 没有真实代币! 纯数字 Clearing House 保证金池 (真实 USDC) 所有结算在这里发生 仅传递价格 Trader 影响虚拟价格 存入/取出 USDC 无 LP 定价 (vAMM) 与资金 (Vault) 分离 无需 LP, 无无常损失 交易者只持有合约仓位

关键洞察: vAMM 把 AMM 的两个功能拆开了:

  • 定价功能 → vAMM (虚拟池)
  • 资金托管功能 → Clearing House (保证金池)

这样就不需要 LP 存入两种 token, 只要交易者存入 USDC 保证金就够了.

2.2 为什么叫 “虚拟”?

1
2
3
4
5
6
7
8
9
10
11
12
13
真实 AMM (Uniswap):
池子里有 100 ETH + 200,000 USDC
x (base储备) * y (quote储备) = 100 * 200000 = 20,000,000
price (价格) = y/x = 2000 USDC/ETH
→ 这些 ETH 和 USDC 是真实存在的代币

虚拟 AMM:
x_v (虚拟base储备) = 100 (虚拟 ETH 储备)
y_v (虚拟quote储备) = 200,000 (虚拟 USDC 储备)
k (常数乘积) = 100 * 200000 = 20,000,000
price (价格) = y_v / x_v = 2000 USDC/ETH
→ x_v 和 y_v 只是数字, 合约里没有 100 ETH
→ 它们的唯一作用是: 计算交易的成交价格和滑点

三、Perpetual Protocol v1

3.1 历史背景

Perpetual Protocol v1 (2020 年底, xDai/Gnosis Chain) 是第一个将 vAMM 概念落地的永续合约协议. 它证明了一个重要的事情: 链上永续合约不需要订单簿, 也不需要 LP.

3.2 架构

Perpetual Protocol v1 架构 Trader 存入 USDC 保证金 USDC Clearing House • 管理保证金账户 • 计算 PnL • 执行清算 • 结算 funding (真实资金在这里) 查询价格 vAMM x_v * y_v = k • 计算成交价格 • 计算滑点 (没有真实资金) Insurance Fund 覆盖穿仓损失 穿仓时补偿 Chainlink index price funding rate 参考

三个核心组件:

组件 职责 是否持有真实资金
vAMM 定价: 根据交易方向和大小, 计算成交价格
Clearing House 结算: 管理保证金, 计算盈亏, 执行清算
Insurance Fund 兜底: 当清算不足以覆盖亏损时, 由保险基金补偿

3.3 开多/开空在 vAMM 中的运作

核心直觉:

  • 开多 (Long) = 在虚拟池中用 quote token (USDC) 买入 base token (ETH) → 推高虚拟价格
  • 开空 (Short) = 在虚拟池中卖出 base token (ETH) 换取 quote token → 压低虚拟价格
  • 平仓 = 反向操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
示例 1 (合理 k 值): x_v (虚拟base)=10000, y_v (虚拟quote)=20000000, k (常数)=200000000000, price (价格)=2000

Alice 开多, 保证金 10,000 USDC, 杠杆 5x = notional (名义价值) 50,000 USDC

1) 向虚拟池注入 50,000 quote:
y_v' = 20,000,000 + 50,000 = 20,050,000
x_v' = k / y_v' = 200,000,000,000 / 20,050,0009,975.06
Alice 获得虚拟 base = 10,000 - 9,975.06 = 24.94 vETH

2) 虚拟价格变化:
开仓前: price = 20,000,000 / 10,000 = 2,000
开仓后: price = 20,050,000 / 9,975.062,010
slippage (滑点) = (2,010 - 2,000) / 2,000 = 0.5% ← 正常范围

3) 注意: 没有真实 ETH 被转移
Alice 的 10,000 USDC 保证金存入 Clearing House
vAMM 只是记录了 "Alice 有 24.94 vETH 多头仓位"
1
2
3
4
5
6
7
8
9
示例 2 (极端小 k): x_v (虚拟base)=100, y_v (虚拟quote)=200000, k (常数)=20000000, price (价格)=2000

同样的 50,000 USDC 开多:
y_v' = 250,000
x_v' = 20,000,000 / 250,000 = 80
Alice 获得 20 vETH

开仓后: price = 250,000 / 80 = 3,125
slippage (滑点) = (3,125 - 2,000) / 2,000 = 56.25% ← 滑点极大!

对比: 同样的交易, k 值差 10,000 倍, 滑点从 0.5% 飙到 56%.
k 值本质上决定了 vAMM 的 “虚拟深度”. 实际协议会设置很大的 k 来模拟合理的市场深度.
但 k 值设多大, 本身就是一个治理难题 (后面第 6 节讨论).

3.4 k 值的设定

k 值决定了 vAMM 的 “虚拟深度”:

k 值 影响 类比
k 大 滑点小, 交易体验好, 但价格对交易不敏感 像一个深水游泳池, 扔石头几乎没波浪
k 小 滑点大, 价格对交易敏感, 容易被操纵 像一个浅水杯, 滴一滴水就溢出

Perpetual Protocol v1 中, k 值由治理 (governance) 调整, 不是市场自然形成的.

这是 vAMM 最根本的设计缺陷之一: 深度是人为设定的, 不是真实供需决定的.


四、vAMM 的数学

4.1 虚拟储备常数乘积

vAMM 复用了 Uniswap V2 的常数乘积公式 (参见 Uniswap V2 常数乘积公式):

1
2
3
4
5
6
7
8
x_v (虚拟base储备) * y_v (虚拟quote储备) = k (常数乘积)

x_v = 虚拟 base token 储备 (如 vETH)
y_v = 虚拟 quote token 储备 (如 vUSDC)
k = 常数, 由治理设定

当前价格:
P (价格) = y_v / x_v

4.2 开多仓位计算

交易者用 notional_quote 金额开多 (实际支付 notional_quote / leverage 的保证金):

1
2
3
4
5
6
7
8
9
开多: 注入 Δy (quote增量) → 取出 Δx (base增量)

y_v' = y_v + Δy
x_v' = k / y_v' = k / (y_v + Δy)
Δx = x_v - x_v' = x_v - k / (y_v + Δy)

avg_price (平均成交价) = Δy / Δx

new_price (新价格) = y_v' / x_v' = (y_v + Δy)^2 / k

4.3 开空仓位计算

1
2
3
4
5
6
7
开空: 注入 Δx (base增量) → 取出 Δy (quote增量) (但这里没有真实 base token!)

实际操作: 协议用 notional (名义价值) 金额反推虚拟 base 数量

x_v' = x_v + Δx_implied (隐含base增量)
y_v' = k / x_v'
虚拟价格下降: y_v' / x_v' < y_v / x_v

4.4 滑点公式

1
2
3
4
5
6
7
8
9
10
11
12
13
对于开多 (买入 base):

slippage (滑点)(%) = | avg_price (平均成交价) - initial_price (初始价格) | / initial_price
// 注意: 这里用平均成交价计算滑点 (更准确), 前面 §2.3 的示例用的是 spot price 变化率 (更直觉但高估了实际滑点)

avg_price = Δy (quote增量) / Δx (base增量) = Δy / (x_v - k/(y_v + Δy))

简化近似 (Δy << y_v 时):
slippage ≈ Δy / y_v = trade_size (交易规模) / virtual_quote_reserve (虚拟quote储备)

关键关系:
slippage = f(trade_size / k)
k (常数乘积) 越大 → 同样交易量的滑点越小
vAMM 滑点 vs k 值 (开多 50,000 USDC) k 值 (百万) 滑点 (%) 0% 10% 25% 50% 75% 1 20 50 100 200 500 k=1M: 滑点~69% k=20M: 滑点~25% k=100M: 滑点~5% k=500M: 滑点~1% k 太小 → 滑点不可接受 k 越大 → 虚拟深度越大 → 滑点越小

4.5 数值验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
初始: x_v (虚拟base)=1000, y_v (虚拟quote)=2,000,000, k (常数)=2,000,000,000, P (价格)=2000

Case 1: 开多 notional (名义价值) = 100,000 USDC
y_v' = 2,000,000 + 100,000 = 2,100,000
x_v' = 2,000,000,000 / 2,100,000 = 952.381
Δx (base增量) = 1000 - 952.381 = 47.619 vETH
avg_price (平均成交价) = 100,000 / 47.619 = 2,100 USDC/ETH
slippage (滑点) = (2100 - 2000) / 2000 = 5%

Case 2: 开多 notional = 500,000 USDC
y_v' = 2,000,000 + 500,000 = 2,500,000
x_v' = 2,000,000,000 / 2,500,000 = 800
Δx = 1000 - 800 = 200 vETH
avg_price = 500,000 / 200 = 2,500 USDC/ETH
slippage = (2500 - 2000) / 2000 = 25%

→ 交易量增大 5 倍, 滑点增大 5 倍. 基本呈线性关系 (在交易量远小于 k 时).

五、Perpetual Protocol v2 (Curie)

5.1 v1 的问题

Perpetual Protocol v1 上线后暴露了几个关键问题:

  1. k 值治理: 人为设定深度, 调整滞后
  2. 资本效率低: 虚拟深度在全价格范围均匀分布 (和 Uniswap V2 一样的问题)
  3. Maker 无激励: 没有 LP 的角色, 协议无法吸引做市流动性
  4. 滑点竞争力差: 和 CEX 及 Oracle 型协议相比, 滑点偏大

5.2 v2 的方案: 用 Uniswap V3 做 vAMM

Perpetual Protocol v2 (2021, Optimism) 做了一个大胆的决定: 直接复用 Uniswap V3 合约作为 vAMM 引擎.

Perpetual Protocol v1 → v2 架构变化 v1: 自定义 vAMM 自定义 x*y=k 合约 全范围均匀流动性 角色: 仅 Taker 没有 Maker/LP k 由治理设定 资本效率低 (和 V2 AMM 一样) xDai / Gnosis Chain 2020.12 上线 v2 (Curie): Uniswap V3 as vAMM Fork Uniswap V3 池子合约 集中流动性 (Concentrated Liquidity) 角色: Maker + Taker Maker 在 vAMM 中提供集中流动性 流动性由市场决定, 非治理 资本效率提高 (V3 集中流动性) Optimism L2 2021.11 上线

5.3 Maker vs Taker

v2 引入了 Maker 角色 (类比 Uniswap V3 的 LP):

角色 行为 盈利来源 风险
Taker 开多/开空 (交易) 仓位盈利 仓位亏损, 清算
Maker 在 vAMM 的 tick 范围内提供流动性 手续费 + funding 收入 无常损失 (虚拟), 对手方风险

Maker 存入保证金到 Clearing House, 然后在 Uniswap V3 vAMM 的指定价格区间提供虚拟流动性. 这和在 Uniswap V3 做 LP 的体验类似, 但所有流动性都是虚拟的.

5.4 为什么选 Uniswap V3?

  1. 成熟 & 经过审计: Uniswap V3 是最经过实战检验的 AMM 合约
  2. 集中流动性: Maker 可以在当前价格附近集中提供流动性, 大幅减少滑点
  3. Tick 系统: 现成的离散价格点和 bitmap 结构, 适合永续合约的精确定价
  4. 手续费机制: 可以复用 Uniswap V3 的手续费累积逻辑, 激励 Maker

关于 Uniswap V3 的集中流动性机制, 详见 Uniswap V3 集中流动性机制.


六、Drift Protocol (Solana)

6.1 混合流动性模型

Drift Protocol (Solana, 2021) 采用了 vAMM + DLOB + JIT 的三层混合架构, 试图解决纯 vAMM 的流动性问题:

Drift Protocol: 三层流动性架构 优先级 高 ↑ Layer 1: JIT Liquidity (即时流动性, 最优先) 做市商在交易执行前 ~5s 注入即时流动性 (Just-In-Time) 类似 Uniswap 的 JIT LP, 但专为永续设计. 做市商提供最优报价, 交易完成后撤出 JIT 未完全成交的部分 Layer 2: DLOB (去中心化限价订单簿) 链上限价单和止损单, 由 Keeper 网络维护和撮合 Keeper 监控订单条件是否满足, 触发后在链上执行 DLOB 仍未成交的部分 低 ↓ Layer 3: vAMM (兜底流动性) 虚拟 AMM 作为最后的流动性来源, 保证交易一定能成交 类似 Perp Protocol v1, 但 k 值动态调整, 并受 Oracle 价格约束 大部分交易在 Layer 1 (JIT) 完成, vAMM 只是最后兜底

6.2 为什么用混合而不是纯 vAMM?

纯 vAMM Drift 混合模型
所有交易都和虚拟池成交 JIT 做市商提供真实报价, 滑点更小
k 值决定一切 vAMM 只是兜底, 大部分交易在 JIT/DLOB 完成
容易被操纵 多层流动性分散操纵风险
价格可能长期脱锚 DLOB 的限价单提供锚定力

6.3 JIT 做市商 (Just-In-Time Liquidity) 详解

JIT 做市是 Drift 最核心的创新: 做市商不是提前挂单等着, 而是看到交易请求后, 在执行前的瞬间注入流动性.

1
2
3
4
5
6
7
8
9
10
11
12
13
传统做市 (dYdX / CEX):

做市商 → 提前挂大量买卖单 → 等交易者来吃单
问题: 挂单期间价格变动 → 做市商被套 (逆向选择风险)
所以做市商要: 不停撤单/改价, 每秒几十次 → 高频操作

JIT 做市 (Drift):

做市商 → 不提前挂单, 而是监听待执行的交易
→ 看到 "有人要买 100 ETH" → 瞬间注入卖单流动性
→ 交易完成 → 做市商立即撤出

类比: 传统做市 = 开一家店等客人; JIT = 看到客人来了才摆摊

JIT 的完整流程:

JIT 做市流程 (Drift Protocol) 时间 → 1. 用户提交交易 "买 100 ETH, 市价" 交易进入 Solana 的 待执行队列 (~5s 窗口) 2. 做市商监听 看到待执行的买单 计算: 当前 ETH 公允价 决定: 以 $3001 提供流动性 3. JIT 注入 做市商在执行前注入: "卖 100 ETH @ $3001" 和用户的买单在同一 slot 撮合 4. 成交 + 撤出 用户买到 100 ETH @ $3001 做市商卖出, 赚 spread 做市商立即撤出, 不留仓位 传统做市 (dYdX / CEX) 挂单 → 等待 → 被吃单 → 调整 → 循环 风险: 挂单期间价格变动 → 被套利者吃掉 成本: 每秒几十次更新报价 (高频操作) 需要: 极低延迟 + 大量资金长期锁定在订单簿 暴露时间: 长 (挂单 → 成交, 可能几秒到几小时) JIT 做市 (Drift) 看到交易 → 瞬间注入 → 成交 → 撤出 风险: 极低 (只在成交瞬间暴露) 成本: 不需要持续挂单, 按需注入 需要: 能监听待执行队列 (Solana 特有) 暴露时间: 极短 (~400ms, 一个 Solana slot) JIT 做市的核心优势: 做市商风险从 "长期暴露" 变成 "瞬间暴露" → 愿意给更窄的 spread → 用户得到更好价格

为什么 JIT 只在 Solana 可行?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
JIT 的前提: 做市商能在交易执行前看到交易, 并插入自己的流动性

Solana:
- 交易提交后有一个等待窗口 (~5s) 才执行
- 做市商可以通过验证者 / RPC 节点看到待执行交易
- 在同一个 slot 内插入自己的 JIT 交易
→ 技术上可行

Ethereum / Arbitrum:
- 交易进入 mempool, 任何人都能看到
- 但 "在别人交易前插入自己的交易" = 这就是 MEV (三明治攻击)!
- Uniswap 上的 JIT LP 被视为有争议的 MEV 行为
→ 技术上可行, 但有道德和监管争议

dYdX / Hyperliquid:
- 订单簿模型, 不需要 JIT, 做市商直接挂单就行
→ 不适用

JIT vs 普通做市 vs AMM LP:

维度 JIT 做市 (Drift) 普通做市 (dYdX) AMM LP (Uniswap)
流动性注入时机 看到交易后瞬间注入 提前挂单等待 存入池子长期锁定
风险暴露时间 ~400ms (1 个 slot) 秒~小时 (挂单期间) 永久 (直到撤出)
逆向选择风险 极低 (已知价格) 高 (价格可能跳变) 高 (无常损失)
资金效率 极高 (按需使用) 中 (资金锁在挂单里) 低 (大部分闲置)
技术门槛 高 (需要监听 + 快速响应) 高 (高频交易基础设施) 低 (存钱就行)
对用户的好处 更窄 spread, 更好价格 深度好, 大单滑点低 无需许可, 人人可参与

JIT 做市的本质: 做市商最怕的是 “我挂了单, 价格突然变了, 我被吃掉了” (逆向选择). JIT 把做市从 “猜价格方向的赌博” 变成了 “看到确定订单再报价的生意”. 风险大幅降低, 做市商愿意给更窄的 spread, 用户得到更好的价格. 这是 Drift 能在 Solana 上和订单簿型 DEX 竞争的关键.

6.4 为什么 Drift 选 Solana

Drift 选择 Solana 而非 EVM 链, 正是因为 JIT 做市需要 Solana 的特性:

  • 低延迟 (~400ms slot time): JIT 做市商必须在交易前瞬间响应, 400ms 窗口刚好够
  • 低 Gas: 频繁更新订单簿不会造成高成本
  • 并行执行: Sealevel 运行时允许不同市场并行处理

七、vAMM 的问题

7.1 Squeeze 风险 (挤压/逼仓)

这是 vAMM 最严重的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
场景: 大户 Squeeze 攻击

初始: x_v (虚拟base)=1000, y_v (虚拟quote)=2,000,000, k (常数)=2B, price (价格)=2000
多个小散户持有空头仓位

1) 大户 "鲸鱼" 开巨额多头 (notional (名义价值) = 1,000,000 USDC):
y_v' = 3,000,000
x_v' = 2B / 3M = 666.67
price = 3,000,000 / 666.67 = 4,500
→ 虚拟价格从 2000 暴涨到 4500 (+125%)

2) 空头们的 mark price 暴涨 → 触发清算
小散户被强制平仓 (买入平空) → 虚拟价格进一步上涨

3) 清算的级联效应:
清算空头 = 在虚拟池中买入 = 推高价格 = 触发更多清算
→ 清算螺旋 (Liquidation Cascade)

4) 鲸鱼在高价平多头获利, 走人
留下 Insurance Fund 的窟窿
vAMM Squeeze 攻击流程 1. 鲸鱼开巨额多头 虚拟价格暴涨 2000 → 4500 2. 空头触发清算 mark price > 清算价 强制平仓 (买入) 3. 清算级联 清算 = 买入 = 涨价 → 更多清算 4. 鲸鱼高价平仓获利 保险基金亏损 散户被清算出局 正反馈循环 (清算螺旋) 核心原因: vAMM 的价格完全由交易量决定, 没有外部锚定 在真实 AMM 中, 套利者会在价格偏离时搬砖; 在 vAMM 中, 没有现货可搬, 只能靠 funding rate 慢慢纠正

在真实 AMM (如 Uniswap) 中, 如果价格偏离, 套利者会立即搬砖纠正.
但 vAMM 没有真实的代币可以搬, 只有 funding rate 这一个纠偏手段, 而 funding rate 的纠偏速度太慢.

7.2 k 值治理难题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
k 太大:
✅ 滑点小, 交易体验好
❌ 价格不灵敏, 大量交易才能推动价格
❌ 和现货价格脱锚更严重 (需要更多交易来纠偏)

k 太小:
✅ 价格灵敏, 快速反映交易意图
❌ 滑点大, 交易成本高
❌ 容易被操纵 (少量资金就能大幅推动价格)

Perpetual Protocol 的治理投票:
→ 每次调 k 都是在两害之间取其轻
→ 而且市场状况实时变化, 治理投票的反应速度跟不上
→ 牛市需要大 k (高交易量), 熊市需要小 k (低交易量), 但治理无法快速切换

7.3 脱锚风险

vAMM 价格可能长时间偏离现货:

场景 原因 后果
单边行情 (如暴跌) 大量开空推低 vAMM 价格, 但 k 值限制纠偏速度 vAMM 价格 < 现货价格
流动性真空 某方向没人开仓, 无法推回价格 长期脱锚
Funding rate 不足 即使 funding rate 很高, 如果没人愿意反方向开仓, 价格也回不来 持续偏离

7.4 Funding Rate 的局限性

在 Oracle 型协议 (如 GMX) 中, funding rate 直接基于 OI 偏差计算, 且价格来自 Oracle.
在 vAMM 中, funding rate = f(vAMM_price - index_price), 但:

  • vAMM price 本身可能被操纵
  • Funding rate 是 “事后纠偏”, 无法阻止 “事中操纵”
  • 如果套利者不够多, funding 的经济激励不足以吸引反方向交易

八、vAMM vs Oracle 型 vs 订单簿: 综合对比

维度 vAMM (Perp v1/v2) Oracle 型 (GMX) 订单簿 (dYdX/Hyperliquid)
定价来源 虚拟储备计算 Chainlink Oracle 买卖挂单撮合
滑点 取决于 k 值和交易量 零滑点 (有 OI 上限) 取决于订单簿深度
流动性来源 协议设定 (v1) / Maker (v2) GLP 池 (LP 做对手方) 做市商挂单
价格锚定 Funding rate (慢) Oracle 直接锚定 套利者撮合
操纵风险 (Squeeze) 中 (Oracle 抢跑) (需要真实挂单)
资本效率 中 (v2 用集中流动性) 高 (零滑点复用) 高 (做市商管理)
无许可性 (无需做市商) 高 (LP 即可) 中 (依赖做市商)
上线新市场 容易 (设 k 值即可) 需要 Oracle 支持 需要做市商愿意做市
链上成本 低 (一次合约调用) 高 (频繁挂撤单)
代表协议 Perp Protocol, Drift GMX, Jupiter Perps dYdX, Hyperliquid

九、Solidity 实现: vAMM 核心逻辑

9.1 vAMM 合约 (简化版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/// @title SimpleVAMM - vAMM 核心逻辑演示
/// @notice 仅用于学习, 生产环境需要完整的安全检查和精度处理
contract SimpleVAMM {
// 虚拟储备 (使用 uint256, 实际协议用定点数表示)
uint256 public baseReserve; // x_v: 虚拟 base (如 vETH)
uint256 public quoteReserve; // y_v: 虚拟 quote (如 vUSDC)
uint256 public k; // 常数乘积

// 仓位信息
struct Position {
int256 size; // 正 = 多头, 负 = 空头 (base token 数量)
uint256 openNotional; // 开仓的 quote 金额
uint256 margin; // 保证金
}

mapping(address => Position) public positions;

// 保证金池 (简化: 实际应独立为 Clearing House)
uint256 public totalMargin;

event PositionChanged(
address indexed trader,
int256 sizeChange,
uint256 notional,
uint256 newPrice
);

constructor(uint256 _baseReserve, uint256 _quoteReserve) {
baseReserve = _baseReserve;
quoteReserve = _quoteReserve;
k = _baseReserve * _quoteReserve;
}

/// @notice 获取当前虚拟价格
/// @return price = quoteReserve / baseReserve (quote per base)
function getPrice() public view returns (uint256) {
return (quoteReserve * 1e18) / baseReserve; // 18 位精度
}

/// @notice 开多头仓位
/// @param _margin 保证金金额 (USDC)
/// @param _leverage 杠杆倍数
function openLong(uint256 _margin, uint256 _leverage) external {
require(positions[msg.sender].size == 0, "position exists");

uint256 notional = _margin * _leverage;

// 向虚拟池注入 quote, 计算获得的 base
uint256 newQuoteReserve = quoteReserve + notional;
uint256 newBaseReserve = k / newQuoteReserve;
uint256 baseReceived = baseReserve - newBaseReserve;

// 更新虚拟储备
quoteReserve = newQuoteReserve;
baseReserve = newBaseReserve;

// 记录仓位
positions[msg.sender] = Position({
size: int256(baseReceived),
openNotional: notional,
margin: _margin
});

totalMargin += _margin;

emit PositionChanged(
msg.sender,
int256(baseReceived),
notional,
getPrice()
);
}

/// @notice 开空头仓位
/// @param _margin 保证金金额
/// @param _leverage 杠杆倍数
function openShort(uint256 _margin, uint256 _leverage) external {
require(positions[msg.sender].size == 0, "position exists");

uint256 notional = _margin * _leverage;

// 计算等值的虚拟 base 数量: baseAmount = notional / currentPrice
// 即: baseAmount = notional * baseReserve / quoteReserve
uint256 baseAmount = (notional * baseReserve) / quoteReserve;

// 向虚拟池注入 base, 取出 quote
uint256 newBaseReserve = baseReserve + baseAmount;
uint256 newQuoteReserve = k / newBaseReserve;

// 更新虚拟储备
baseReserve = newBaseReserve;
quoteReserve = newQuoteReserve;

// 记录仓位 (空头 size 为负)
positions[msg.sender] = Position({
size: -int256(baseAmount),
openNotional: notional,
margin: _margin
});

totalMargin += _margin;

emit PositionChanged(
msg.sender,
-int256(baseAmount),
notional,
getPrice()
);
}

/// @notice 完全平仓
function closePosition() external {
Position storage pos = positions[msg.sender];
require(pos.size != 0, "no position");

uint256 closeNotional;

if (pos.size > 0) {
// 多头平仓: 卖出 base (反向操作)
uint256 baseToSell = uint256(pos.size);
uint256 newBaseReserve = baseReserve + baseToSell;
uint256 newQuoteReserve = k / newBaseReserve;
closeNotional = quoteReserve - newQuoteReserve;

baseReserve = newBaseReserve;
quoteReserve = newQuoteReserve;
} else {
// 空头平仓: 买入 base (反向操作)
uint256 baseToBuy = uint256(-pos.size);
uint256 newBaseReserve = baseReserve - baseToBuy;
uint256 newQuoteReserve = k / newBaseReserve;
closeNotional = newQuoteReserve - quoteReserve;

baseReserve = newBaseReserve;
quoteReserve = newQuoteReserve;
}

// 计算 PnL
int256 pnl;
if (pos.size > 0) {
// 多头: PnL = closeNotional - openNotional
pnl = int256(closeNotional) - int256(pos.openNotional);
} else {
// 空头: PnL = openNotional - closeNotional
pnl = int256(pos.openNotional) - int256(closeNotional);
}

// 结算: margin + pnl (简化, 实际需检查是否为负)
uint256 withdrawal = uint256(int256(pos.margin) + pnl);
totalMargin -= pos.margin;

// 清除仓位
delete positions[msg.sender];

emit PositionChanged(msg.sender, 0, closeNotional, getPrice());

// 实际应转 USDC 给 trader, 此处省略 token transfer
}

/// @notice 获取仓位未实现盈亏
function getUnrealizedPnl(address _trader) external view returns (int256) {
Position storage pos = positions[_trader];
if (pos.size == 0) return 0;

// 模拟平仓计算 PnL
if (pos.size > 0) {
uint256 baseToSell = uint256(pos.size);
uint256 newBase = baseReserve + baseToSell;
uint256 newQuote = k / newBase;
uint256 closeNotional = quoteReserve - newQuote;
return int256(closeNotional) - int256(pos.openNotional);
} else {
uint256 baseToBuy = uint256(-pos.size);
uint256 newBase = baseReserve - baseToBuy;
uint256 newQuote = k / newBase;
uint256 closeNotional = newQuote - quoteReserve;
return int256(pos.openNotional) - int256(closeNotional);
}
}
}

9.2 Go 实现: vAMM 模拟器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
package main

import (
"fmt"
"math/big"
)

// VAMM 虚拟自动做市商模拟器
type VAMM struct {
BaseReserve *big.Float // x_v: 虚拟 base 储备
QuoteReserve *big.Float // y_v: 虚拟 quote 储备
K *big.Float // 常数乘积 k = x_v * y_v
}

// Position 仓位信息
type Position struct {
Size *big.Float // 正 = 多头, 负 = 空头
OpenNotional *big.Float // 开仓 notional
AvgPrice *big.Float // 平均开仓价
}

// NewVAMM 创建 vAMM 实例
func NewVAMM(baseReserve, quoteReserve float64) *VAMM {
base := big.NewFloat(baseReserve)
quote := big.NewFloat(quoteReserve)
k := new(big.Float).Mul(base, quote)
return &VAMM{
BaseReserve: base,
QuoteReserve: quote,
K: k,
}
}

// GetPrice 获取当前虚拟价格 (quote per base)
func (v *VAMM) GetPrice() *big.Float {
return new(big.Float).Quo(v.QuoteReserve, v.BaseReserve)
}

// OpenLong 开多头: 注入 quote, 取出 base
func (v *VAMM) OpenLong(notionalQuote float64) *Position {
notional := big.NewFloat(notionalQuote)
priceBefore := v.GetPrice()

// y_v' = y_v + notional
newQuote := new(big.Float).Add(v.QuoteReserve, notional)

// x_v' = k / y_v'
newBase := new(big.Float).Quo(v.K, newQuote)

// Δx = x_v - x_v' (base received)
baseReceived := new(big.Float).Sub(v.BaseReserve, newBase)

// 更新储备
v.QuoteReserve = newQuote
v.BaseReserve = newBase

// 计算平均成交价
avgPrice := new(big.Float).Quo(notional, baseReceived)

priceAfter := v.GetPrice()

// 计算滑点
slippage := new(big.Float).Sub(avgPrice, priceBefore)
slippage.Quo(slippage, priceBefore)
slippageF, _ := slippage.Float64()

fmt.Printf(" 开多 notional=%.0f, base_received=%s, avg_price=%s\n",
notionalQuote, baseReceived.Text('f', 4), avgPrice.Text('f', 2))
fmt.Printf(" 价格: %s → %s, 滑点: %.2f%%\n",
priceBefore.Text('f', 2), priceAfter.Text('f', 2), slippageF*100)

return &Position{
Size: baseReceived,
OpenNotional: notional,
AvgPrice: avgPrice,
}
}

// OpenShort 开空头: 注入 base, 取出 quote
func (v *VAMM) OpenShort(notionalQuote float64) *Position {
notional := big.NewFloat(notionalQuote)
priceBefore := v.GetPrice()

// 计算等值 base: baseAmount = notional / currentPrice
baseAmount := new(big.Float).Quo(notional, priceBefore)

// x_v' = x_v + baseAmount
newBase := new(big.Float).Add(v.BaseReserve, baseAmount)

// y_v' = k / x_v'
newQuote := new(big.Float).Quo(v.K, newBase)

// 更新储备
v.BaseReserve = newBase
v.QuoteReserve = newQuote

priceAfter := v.GetPrice()
slippage := new(big.Float).Sub(priceBefore, priceAfter)
slippage.Quo(slippage, priceBefore)
slippageF, _ := slippage.Float64()

// 空头 size 为负
negBase := new(big.Float).Neg(baseAmount)

fmt.Printf(" 开空 notional=%.0f, base_sold=%s\n",
notionalQuote, baseAmount.Text('f', 4))
fmt.Printf(" 价格: %s → %s, 滑点: %.2f%%\n",
priceBefore.Text('f', 2), priceAfter.Text('f', 2), slippageF*100)

return &Position{
Size: negBase,
OpenNotional: notional,
AvgPrice: priceBefore,
}
}

// SimulateSlippage 模拟不同 k 值下的滑点
func SimulateSlippage(tradeSize float64) {
fmt.Printf("\n=== 滑点 vs k 值 (trade_size=%.0f USDC) ===\n", tradeSize)
fmt.Println("初始价格固定为 2000 USDC/ETH")
fmt.Println()

kValues := []float64{1e6, 5e6, 20e6, 100e6, 500e6, 2e9}
for _, kVal := range kValues {
// 从 k 和 price=2000 反推储备: y/x=2000, x*y=k
// x = sqrt(k/2000), y = sqrt(k*2000)
xReserve := sqrt(kVal / 2000)
yReserve := sqrt(kVal * 2000)

// 计算开多后的平均成交价
newQuote := yReserve + tradeSize
newBase := kVal / newQuote
baseReceived := xReserve - newBase
avgPrice := tradeSize / baseReceived
slippage := (avgPrice - 2000) / 2000 * 100

fmt.Printf(" k=%10.0f base=%.2f quote=%.0f 滑点=%.2f%%\n",
kVal, xReserve, yReserve, slippage)
}
}

func sqrt(x float64) float64 {
// Newton's method
z := x / 2
for i := 0; i < 100; i++ {
z = (z + x/z) / 2
}
return z
}

func main() {
fmt.Println("=== vAMM 模拟 ===")
fmt.Println()

// 创建 vAMM: 1000 vETH, 2,000,000 vUSDC, price = 2000
vamm := NewVAMM(1000, 2_000_000)
fmt.Printf("初始状态: base=%.0f, quote=%.0f, price=%s, k=%.0f\n",
1000.0, 2_000_000.0, vamm.GetPrice().Text('f', 2),
2_000_000_000.0)
fmt.Println()

// Alice 开多 100,000 USDC
fmt.Println("--- Alice 开多 (notional=100,000 USDC) ---")
alice := vamm.OpenLong(100_000)
fmt.Println()

// Bob 开空 50,000 USDC
fmt.Println("--- Bob 开空 (notional=50,000 USDC) ---")
_ = vamm.OpenShort(50_000)
fmt.Println()

fmt.Printf("Alice 仓位: size=%s vETH, openNotional=%s, avgPrice=%s\n",
alice.Size.Text('f', 4),
alice.OpenNotional.Text('f', 0),
alice.AvgPrice.Text('f', 2))

// 滑点对比
SimulateSlippage(100_000)
}

运行输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
=== vAMM 模拟 ===

初始状态: base=1000, quote=2000000, price=2000.00, k=2000000000

--- Alice 开多 (notional=100,000 USDC) ---
开多 notional=100000, base_received=47.6190, avg_price=2100.00
价格: 2000.00 → 2205.00, 滑点: 5.00%

--- Bob 开空 (notional=50,000 USDC) ---
开空 notional=50000, base_sold=22.6757
价格: 2205.00 → 2103.63, 滑点: 4.60%

Alice 仓位: size=47.6190 vETH, openNotional=100000, avgPrice=2100.00

=== 滑点 vs k 值 (trade_size=100000 USDC) ===
初始价格固定为 2000 USDC/ETH

k= 1000000 base=22.36 quote=44721 滑点=223.61%
k= 5000000 base=50.00 quote=100000 滑点=100.00%
k= 20000000 base=100.00 quote=200000 滑点=50.00%
k= 100000000 base=223.61 quote=447214 滑点=22.36%
k= 500000000 base=500.00 quote=1000000 滑点=10.00%
k=2000000000 base=1000.00 quote=2000000 滑点=5.00%

十、小结: 为什么 vAMM 在竞争中逐渐落后

10.1 数据说话

Perpetual Protocol 的 TVL 和交易量变化:

1
2
3
4
5
6
7
8
9
10
Perpetual Protocol TVL (近似值):
2021 Q1 (v1 高峰): ~$60M TVL, 日均交易量 ~$100M
2022 Q1 (v2 上线): ~$30M TVL, 日均交易量 ~$50M
2023 Q1: ~$5M TVL, 日均交易量 <$10M
2024 Q1: <$2M TVL, 基本停滞

同期对比:
GMX (Oracle 型): 2023 TVL ~$500M
dYdX (订单簿型): 2023 日均交易量 ~$1B
Hyperliquid: 2024 日均交易量 ~$3B

10.2 落后的根本原因

vAMM 竞争力衰退的根本原因 vAMM 核心缺陷 虚拟深度 ≠ 真实深度 k 值人为设定 无法反映真实市场 Squeeze 风险 大户可操纵虚拟价格 清算螺旋 脱锚风险 Funding rate 纠偏太慢 无套利搬砖机制 滑点竞争力差 GMX 零滑点, CEX 深度大 vAMM 无法竞争 市场选择结果 Oracle 型 (GMX): 零滑点 + 简单 | 订单簿 (dYdX/HL): 真实深度 + 专业 vAMM 既没有 Oracle 的简洁, 也没有订单簿的效率 → 被夹在中间

总结:

竞争对手 对 vAMM 的优势
GMX (Oracle 型) 零滑点, 简单直觉, LP 只需存入即可, 价格由 Oracle 保证
dYdX (订单簿) 真实订单深度, 专业交易体验, 做市商竞争提供最优价格
Hyperliquid (链上订单簿) 链上订单簿 + 自研 L1, 兼具去中心化和性能

vAMM 的历史贡献:

  • 开创了链上永续合约的定价范式, 证明了不需要订单簿就能做永续
  • 无许可性最强: 不需要 LP, 不需要做市商, 设个 k 值就能启动新市场
  • 但这些优势在市场成熟后变得不那么重要, 用户更在意滑点和安全性

十一、下一步

vAMM 型永续的故事, 本质上是 DeFi 永续合约定价机制探索的第一个阶段.

  • 从 vAMM 的 “虚拟深度” 到 Oracle 型的 “零滑点”, 再到订单簿的 “真实深度”
  • 市场在 2023-2024 年明确选择了 订单簿型 作为主流方向

下一篇: Hyperliquid 深度解析 - 看链上订单簿如何在自研 L1 上实现 CEX 级体验.


永续合约 05 - vAMM 永续合约演进史
https://mritd.com/2025/09/06/perp-vamm/
作者
Kovacs
发布于
2025年9月6日
许可协议