Uniswap V4 探析(二) —— 通过 Hook 实现 TWAMM

上文介绍了 Uniswap V4 中的 Hooks 特性,Uniswap V4 开源的 V4-periphery 仓库中包含了几个 Hook 合约范例,其中大部分实现的功能比较简单,读者有兴趣可以自行阅读。

本文将介绍一个比较复杂的 Hook 合约:TWAMM.sol。它基于 Uniswap V4 Hook 实现了 TWAMM(Time-Weighted Average Market Maker)的功能。

什么是 TWAMM

TWAMM 的概念出自 paradigm 的文章:TWAMM,它被用来执行用户提交的 TWAP order,这种 order 会在指定时间内以固定的速率进行交易,从而减少短时间内价格波动对最终成交价格的影响。

TWAMM 有一个内置的 AMM,这个 AMM 和其他的 AMM 并没有什么不同,用户可以通过这个 AMM 直接进行现货交易,也可以向其中添加流动性。但是 TWAMM 同时还有两个 TWAP order pool,分别用来执行两个方向的 TWAP order,用户提交 order 时,指定交易的 token input 数量和时长,TWAMM 会将相同交易方向的 order 放入对应的 pool 中,并按照指定的交易速度自动进行交易。当用户的 order 被完全执行后,用户就可以拿出交易得到的 token。当然,在用户的 order 在被执行完成之前,用户也可以提前取消 order 或者修改 order 需要交易的 token 数量。

在以太坊中,智能合约只能由 EOA 地址主动发起交易触发执行,而不能自动执行。因此 TWAMM 需要由 EOA 账户定期发送交易来结算其 order pool 中待交易的 token,这样就需要一个 keeper 账号来执行这些交易。

当然,也可以让 TWAMM 在每次有用户与其交互时,自动结算 order pool,这样就省去了 keeper 的开销,这也是 DeFi 协议处理流式数据常用的方式。

例如,在 ETH/USDC long order pool 中,需要交易的 token 速度为 100 USDC/s,那么每次有用户与 TWAMM 合约交互时,TWAMM 首先计算出距离上一次 long order pool 结算经过的时间,然后计算出需要交易的 token 数量,最后通过内置 AMM 进行相应的交易。

TWAMM 并没有限制内置 AMM 的类型,它可以是 Uniswap V2 类型的 AMM 也可以是 V4 这种集中流动性的 AMM。在 paradigm 的论文里面,使用了 x*y=k AMM 作为范例。

在某一个时间,假设TWAMM 需要结算 long/short 两个 order pool 中待交易的 token 时,数量分别为 $x_{in}$ 和 $y_{in}$,那么会有一个问题:TWAMM 该先买还是先卖?

顺序的不同会导致最终的成交价格不同,例如,假设内置的 AMM 为 x*y=k 模型的 AMM,AMM 中余额 ETH=1, USDC=1000, k=1000,此时 long order pool (pool1) 需要买入 500USDC,short order pool (pool2) 需要卖出 1ETH。我们来看看不同交易顺序对结果的影响:

  • 假设先执行 pool1 中的交易,交易完成后 AMM 状态为 ETH=0.6667, USDC=1500,pool1 买入均价为 1500,接着执行 pool2 的交易,完成后 AMM 状态为 ETH=1.6667, USDC=599.98 ,pool2 卖出均价为 900.02
  • 假设先执行 pool2 中的交易,交易完成后 AMM 状态为 ETH=2, USDC=500,pool2 卖出均价为 500,接着执行 pool1 的交易,完成后 AMM 状态为 ETH=1, USDC=1000,pool1 买入均价为 500

很明显,后交易的一方一定会获得优势。

因此,需要找到一种更公平的交易方式,来结算两个 order pool 中的 token,而不能简单的直接按顺序结算两个 pool 中的 token。可以这样想象:

假设两个 order pool 同时开始交易,并且将交易的数量切成非常微小的份数,每次成交的价格都会非常接近 AMM 的价格,可以认为此次交易对 long/short 都是公平的。

接着接续交易,直到两个 order pool 中的 token 都被完全交易完毕,这样就可以保证两个 order pool 的成交价格都是公平的。

具体的数学计算推导过程可以参考这篇文档:TWAMM 做市商的数学原理

最后就得到了使用 x*y=k 的内置 AMM 的 TWAMM 结算 order pool 的数学计算公式:

$$ \begin{align} x_{ammEnd} & = \sqrt{\frac{kx_{in}}{y_{in}}} \cdot \frac{e^{2\sqrt{\frac{x_{in}y_{in}}{k}}} + c}{e^{2\sqrt{\frac{x_{in}y_{in}}{k}}} - c} \\ c & = \frac{\sqrt{x_{ammStart}y_{in}} - \sqrt{y_{ammStart}x_{in}}}{\sqrt{x_{ammStart}y_{in}} + \sqrt{y_{ammStart}x_{in}}} \\ y_{ammEnd} & = \frac{x_{ammStart}y_{ammStart}}{x_{ammEnd}} \\ \end{align} $$

其中 $x_{in}$ 为 pool1 中需要交易的 token 数量,$y_{in}$ 为 pool2 中需要交易的 token 数量,$x_{ammEnd}$ 为 AMM 结算后 AMM 中的 x token 数量。

有了 $x_{ammEnd}$,就可以计算出 $y_{ammEnd}$,两个 order pool 可以得到的 token 数量为:

$$ y_{out} = y_{ammStart} + y_{in} - y_{ammEnd} \\ x_{out} = x_{ammStart} + x_{in} - x_{ammEnd} $$

当 $x_{in} = y_{in} * \frac{x}{y}$ 时,通过上面公式计算出的 $x_{ammEnd} = x_{ammStart}$, $y_{ammEnd} = y_{ammStart}$,即 AMM 状态不会改变,long/short 交易均价相同且等于 AMM 现价,这也是符合预期的。

Uniswap V4 + TWAMM

前面了解了 TWAMM 的原理和数学计算,接下来就来看看如何将 TWAMM 和 Uniswap V4 结合起来。利用 Uniswap V4 的 Hook,可以实现一个 TWAMM,官方的范例代码在:v4-periphery/contracts/hooks/examples/TWAMM.sol

这个 TWAMM 是这样工作的:

  • 此 Hook 维护两个 TWAP order pool,分别表示两个交易方向的 TWAP order
  • 用户可以通过此 Hook 提交 TWAP order,需要指定交易的 token,数量以及时间长度
  • 此 Hook 注册 beforeSwap 和 beforeModifyPosition,每次用户交易或者调整仓位时,都会触发此 Hook
  • 被触发后,Hook 负责对 2个 TWAP order pool 进行结算
  • 用户也可以在任意时刻手动触发结算
  • 用户可以取消或者修改 TWAP order 中的 token 数量

为了在 Uniswap V4 pool 中结算 TWAP order,需要将 TWAMM 的数学计算公式转换成 Uniswap V4 的数学计算公式。

在 Uniswap V4 中有:

$$ x = \frac{L}{\sqrt{P}} \\ y = L\sqrt{P} $$

同时,在 Uniswap V4 中,流动性是不均匀分布的。我们首先假设 AMM 的流动性不变,那么我们需要计算的是结算完成后 AMM 的价格 $P_{ammEnd}$。利用之前推导的公式,可以得到:

$$ \sqrt{P_{ammEnd}} = \frac{L}{\sqrt{\frac{kx_{in}}{y_{in}}} \cdot \frac{e^{2\sqrt{\frac{x_{in}y_{in}}{k}}} + c}{e^{2\sqrt{\frac{x_{in}y_{in}}{k}}} - c}} $$

假设 x token 交易速率为 $x_{rate}$,y token 交易速率为 $y_{rate}$,需要结算的时长为 $t$,那么 $x_{in} = x_{rate} \cdot t$, $y_{in} = y_{rate} \cdot t$,上面的公式可以写成:

$$ \sqrt{P_{ammEnd}} = \sqrt{\frac{y_{rate}}{x_{rate}}} \cdot \frac{e^{\frac{2t\sqrt{x_{rate}y_{rate}}}{L}} - c}{e^{\frac{2t\sqrt{x_{rate}y_{rate}}}{L}} + c} $$

$c$ 可以写成:

$$ \begin{align} c & = \frac{\sqrt{x_{ammStart}y_{in}} - \sqrt{y_{ammStart}x_{in}}}{\sqrt{x_{ammStart}y_{in}} + \sqrt{y_{ammStart}x_{in}}} \\ & = \frac{\sqrt{\frac{y_{in}}{\sqrt{P_{ammStart}}}} - \sqrt{x_{in}\sqrt{P_{ammStart}}}}{\sqrt{\frac{y_{in}}{\sqrt{P_{ammStart}}}} + \sqrt{x_{in}\sqrt{P_{ammStart}}}} \\ & = \frac{\sqrt{\frac{y_{rate}}{x_{rate}}} - \sqrt{P_{ammStart}}}{\sqrt{\frac{y_{rate}}{x_{rate}}} + \sqrt{P_{ammStart}}} \\ \end{align} $$

上面计算结算价格的公式对应的代码实现为:TwammMath.getNewSqrtPriceX96()

这样就得到了当 AMM 流动性不变时,结算完成后 AMM 的价格 $P_{ammEnd}$,接着就可以计算出结算后的 $x_{out}$ 和 $y_{out}$ 了。

但是实际情况下,AMM 的流动性是不均匀分布的,在 swap 的过程中,AMM 的流动性 L 可能会发生变化。

所以还需要通过读取 V4 AMM 中的 tick bitmap 来判断流动性 L 是否产生了变化,这里使用了 V4 提供的 extload() 来完成对 V4 中内部状态的读取,对应的代码为:TWAMM._isCrossingInitializedTick()

接下来考虑复杂一些的情形,即 AMM 的流动性 L 在 swap 过程中会发生变化,那么结算过程就变成一个迭代过程:用上面的公式可以计算出在流动性不变的区间内的结算结果,然后再计算下一个区间内的结算结果,直到结算完成。

现在假设 AMM 在 $[P_a \sim P_b]$ 这段价格区间中流动性为 $L_1$,且 AMM 的初始价格在 $[P_a \sim P_b]$ 之中。

通过前面的公式,可以计算出在 AMM 流动性 $L = L_1$ 的情况下,结算完成后 AMM 的价格 $P_{ammEnd}$。现在假设 $P_{ammEnd} < P_a$,那么当 AMM 价格移动到 $P_a$ 的时候,AMM 的流动性就会发生改变。可以肯定的是,在结算的过程中,AMM 会被 swap 到一个小于 $P_a$ 的价格。那么就可以把 $P \rightarrow P_a\ (L = L_1)$ 的 swap 过程作为结算 swap 迭代中的一步。

接下来还需要确定的是,总共需要 swap 的 x, y token 数量为 $x_{in} = x_{rate} \cdot t, y_{in} = y_{rate} \cdot t$,需要分配多少 x token 和 y token 到 $L_1$ 这个区间。

因为交易速率固定,token 数量的分配其实就是时间长度的分配,假设分配的时间长度为 $t_1$,可以通过下面的公式计算 $t_1$:

$$ \begin{align} \sqrt{P_a} & = \sqrt{\frac{y_{rate}}{x_{rate}}} \cdot \frac{e^{\frac{2t_1\sqrt{x_{rate}y_{rate}}}{L_1}} - c}{e^{\frac{2t_1\sqrt{x_{rate}y_{rate}}}{L_1}} + c} \\ \sqrt{P_a} \cdot (e^{\frac{2t_1\sqrt{x_{rate}y_{rate}}}{L_1}} + c) &= \sqrt{\frac{y_{rate}}{x_{rate}}} \cdot (e^{\frac{2t_1\sqrt{x_{rate}y_{rate}}}{L_1}} - c) \\ \frac{2t_1\sqrt{x_{rate}y_{rate}}}{L_1} & = \ln{(\frac{\sqrt{\frac{y_{rate}}{x_{rate}}} + \sqrt{P_a}}{\sqrt{\frac{y_{rate}}{x_{rate}}} - \sqrt{P_a}} \cdot c)} \\ t_1 & = \frac{\ln{(\frac{\sqrt{\frac{y_{rate}}{x_{rate}}} + \sqrt{P_a}}{\sqrt{\frac{y_{rate}}{x_{rate}}} - \sqrt{P_a}} \cdot \frac{\sqrt{\frac{y_{rate}}{x_{rate}}} - \sqrt{P_{ammStart}}}{\sqrt{\frac{y_{rate}}{x_{rate}}} + \sqrt{P_{ammStart}}})} \cdot L_1}{2\sqrt{x_{rate}y_{rate}}} \\ \end{align} $$

上面的计算对应的代码实现为:TwammMath.calculateTimeBetweenTicks()

有了这个时间长度 $t$,就可以计算出需要分配到 $L_1$ 这个区间的 x/y token 数量了,同时也可以计算出两个 order pool swap 获得的 token 数量 $x_{out}$ 和 $y_{out}$。

接下来要做的就是对以下变量重新赋值:

$$ \begin{align} x_{in} & := x_{in} - x_{out} \\ y_{in} & := y_{in} - y_{out} \\ t & := t - t_1 \\ L & := L_1 - \Delta L \\ \end{align} $$

进入下一个流动性区间,计算结算的结果。不断迭代直至结算 swap 完成。

最后,就可以计算出最终的 AMM 价格 $P_{ammEnd}$,以及对应的 $x_{out}$ 和 $y_{out}$ 了。

那么如何将 $x_{out}$ 和 $y_{out}$ 分配给每个用户呢。这里使用了一种类似 Liquidity Mining 的累加算法,以 $y_{out}$ 为例,定义:

$$ earningsFactorPool0 = \sum{\frac{y_{out}}{x_{rate}}} $$

假设某用户的 order 中 x token 卖出速率为 $x_{rate1}$,那么用户得到的 y token 总数 $y_{owed}$ 需要这样结算:

$$ \begin{align} & y_{owed} \mathrel{+}= (earningsFactorPool0 - earningsFactorPool0_{last}) \cdot x_{rate1} \\ & earningsFactorPool0_{last} := earningsFactorPool0 \\ \end{align} $$

这样就完成了 TWAMM 核心计算逻辑,代码中还有很多实现的细节,本文不再一一赘述。

风险分析

  • 用户的 TWAP Order 会不会被三明治攻击?例如恶意抬高价格,导致 long order pool 的成交价格变高,然后再将价格还原?

这种攻击很难实施,由于一个区块内 timestamp 不会改变,攻击者必须要在一个区块的最后一个交易中,将 pool 的价格拉高,这样下一个区块中的 TWAMM 结算才会受到影响。这就要求三明治攻击发生在多个区块中,这无疑会给攻击者带来很大的风险,因为其他套利者有可能在中间介入,导致攻击者遭受损失。

同时,因为套利者的存在,这样的价格操纵注定无法持久,由于 TWAP order 的特性它并不会在短时间内交易太多的 token,因此大部分情况损失也一定是有限的。

  • 假设 token 价格保持不变,一直没有人在 pool 中交易,也没有 keeper 来触发 TWAP order 结算,TWAMM 的 order pool 中累计了太多的待交易 token,下一次结算时会不会导致巨大的交易滑点?

如果 TWAMM 的 order pool 中累计了很多的待交易 token,数量大到会让 AMM 价格偏离过多,那么套利者可以手动触发 TWAMM 的结算,然后在 AMM 中 swap 进行套利。这样的话,TWAMM 的 order pool 中的 token 数量就很难积累太多了。

  • 如果有巨鲸提交大额的 TWAP Order,因为区块链信息透明,其他人都可以看到这笔订单,会不会导致对市场产生影响,例如导致其他用户提前抢跑 TWAP Order 的交易?

大额的 TWAP 一定会某种程度上对市场产生心理影响,但是因为 TWAP Order 是随时可以取消和修改数量的,巨鲸提交的 TWAP Order 可以被取消或修改,这反而可能成为一个心理战术,其他用户抢跑交易也并不一定会有利可图,因为 TWAP Order 随时可以取消。

总结

TWAMM 使用了 V4 的 beforeSwap/beforeModifyPosition Hook,实现了基于 V4 AMM 的 TWAMM。

每当用户与 Hook 合约交互,或者通过 pool 合约进行 swap, add/remove liquidity 之前,都会触发 Hook 合约结算 TWAP order,从而实现 TWAMM。

除了 TWAMM,v4-periphery 中还开源了一些实现其他功能的 Hook,感兴趣的读者可以自行研究。