Uniswap V2 的生命周期如下:有人首次 mint(铸造)LP 代币(提供流动性,即将代币存入流动性池),然后第二个存款人铸造流动性,接着发生 swap(兑换),最终流动性提供者 burn(销毁)他们的 LP 代币以赎回流动性池代币。
事实证明,逆向研究这些函数会更容易:先是销毁,然后是铸造流动性,最后是铸造初始流动性。
那么让我们从 burn 开始。
Uniswap V2 Burn
在销毁流动性代币之前,流动性池中必须有流动性,因此我们先做这个假设。我们假设系统中存在两个代币:token0 和 token1。
我们在下方对 burn 函数进行了注释,并将解释那些不太直观的部分。

在第 140 行(紫色框),流动性是通过流动性池合约所拥有的 LP 代币数量来衡量的。这里假设销毁者在调用 burn 之前已经发送了 LP 代币,但建议将其作为同一笔交易的一部分。(如果它们作为两笔交易发送,其他人就可以销毁你的 LP 代币并移除你的流动性!)用户发送到合约的数量将被销毁。通常情况下,我们可以假设合约的 LP 代币余额为零,因为如果 LP 代币只是停留在交易对合约中,就会有人将其销毁并免费领走部分 token0 和 token1。在同一笔交易中发送代币的机制在 Uniswap V2 Swap 一文中已有介绍。
第 142 行和 154 行的红色框表示手续费,由于 Uniswap 不对流动性提供者收取费用,我们暂时跳过这部分。
第 144 到 145 行的橙色框是计算 LP 提供者将收回的金额的地方。如果流动性代币的资金总供应量是 1,000,而他们销毁了 100 个 LP 代币,那么他们将获得资金池中 10% 的 token0 和 token1。Liquidity / totalSupply 就是他们销毁的份额占 LP 代币总供应量的比例。
第 147 到 149 行的蓝色框是实际销毁 LP 代币并将 token0 和 token1 发送给流动性提供者的地方。
第 150-151 行的黄色框更新了余额变量,以便第 153 行(绿色框)对 _update 的调用能够更新 _reserve 变量。除了更新 TWAP 之外,_update 函数的作用仅仅是更新 _reserve 变量。

安全检查
假设池中包含等量的 token0 和 token1。这意味着销毁者期望在销毁 LP 代币时收到等量的这两种代币。然而,在签名 burn 交易到它被确认的这段时间里,池中 token0 与 token1 的比例可能会发生变化。如果销毁者有依赖于接收特定数量 token0 或 token1 的下游逻辑(例如偿还闪电贷),那么如果收到的 token0 或 token1 略少于预期,该逻辑可能会崩溃。执行销毁的合约必须做好准备,应对收到的 token0 或 token1 少于预期的情况,并在需要时触发回滚(revert)。
当流动性池非空时的流动性铸造
下面是铸造流动性的函数。其大部分功能与 burn 相似,因此我们将不再重复那些显而易见的部分。

如果流动性池为空,即流动性代币的总供应量为零,说明尚未提供任何流动性。这在第 119 行(黄色框)进行了检查。在本节中,我们重点关注已经提供过流动性的情况(第 123 行的黄色框)。记入用户账户并随后在第 126 行(绿色框)为他们铸造的流动性,是两个计算值中的较小者。

这行代码计算的比例是 amount0 / _reserve0 —— 并乘以 LP 代币的 totalSupply 进行缩放。
假设有 10 个 token0 和 10 个 token1。如果用户提供了 10 个 token0 和 0 个 token1,他们将获得 (10/10, 0/10) 中的最小值,即获得零个流动性代币!另一个例子:如果他们增加 LP 代币的供应量(请记住,这个比例是由 _totalSupply 即当前 LP 代币的供应量来缩放的)。
用户将获得他们所提供代币的两个比例(amount0 / _reserve0 或 amount1 / _reserve1)中较差的一个,这一事实激励着他们在不改变 token0 和 token1 比例的情况下增加 token0 和 token1 的供应量。
为什么要强制执行这一点?假设资金池当前有 100 个 token0 和 1 个 token1,并且 LP 代币的供应量为 1。假设这两种代币的总价值(以美元计)均为 100 美元,那么资金池的总价值就是 200 美元。
如果我们取两个比例中的最大值,某人就可以额外提供 1 个 token1(成本为 100 美元),并将资金池价值提高到 300 美元。他们使资金池价值增加了 50%。然而,在采用最大值计算的情况下,他们将被铸造出 1 个 LP 代币,这意味着他们拥有了 LP 代币 50% 的供应量,因为现在的总流通供应量变成了 2 个 LP 代币。现在他们只需存入 100 美元的价值,就能控制价值 300 美元的资金池的 50%(价值 150 美元)。这显然是在窃取其他 LP 提供者的资产。
供应比例安全检查
用户可能会尽量遵守代币比例,但如果有另一笔交易在他们之前执行,并改变了 token0 与 token1 的余额比例,那么他们收到的流动性代币就会少于预期。
Uniswap 并不要求完全精确的数量,否则交易很可能会被回滚。在铸造者发送交易到该交易被打包进区块的这段时间里,最先执行的另一笔交易就会改变这个数量要求。
TotalSupply 安全检查
就像 burn 的情况一样,LP 代币的 totalSupply 在此时可能会发生变化,因此必须实现某些滑点保护机制。
首个铸币者问题
与任何 LP 池一样,Uniswap V2 需要防御“通胀攻击”(inflation attack)。我们已经在关于 ERC4626 的文章中描述了这个问题及其防御机制,所以在此不再赘述。Uniswap V2 的防御措施是销毁最开始的 MINIMUM_LIQUIDITY 个代币,以确保没有人能拥有 LP 代币的全部供应量并借此轻易操纵价格。同样,如果你对这种攻击向量不熟悉,请参阅另一篇文章。

为什么 Uniswap 使用 K 的平方根来计算流动性
更有趣的问题是,为什么 Uniswap V2 要取所提供代币乘积的平方根来计算要铸造的 LP 份额。

具体来说,在减去 MINIMUM_LIQUIDITY 之后,liquidity = sqrt(amount0*amount)。
似乎我们可以为第一个 LP 铸造任意数量的代币——他们拥有 100% 的份额(减去被销毁的部分),那么无论是乘以 0.01 还是 100 缩放,又有什么区别呢?
以下是白皮书中的解释:
Uniswap v2 initially mints shares equal to the geometric mean of the amounts, liquidity = sqrt(xy). This formula ensures that the value of a liquidity pool share at any time is essentially independent of the ratio at which liquidity was initially deposited… The above formula ensures that a liquidity pool share will never be worth less than the geometric mean of the reserves in that pool.
这到底是什么意思?
获得直观理解的最佳方法之一就是代入具体的数值看看会发生什么,所以让我们来试一试。
示例:流动性翻倍
假设我们没有使用平方根函数来衡量流动性,并且初始时资金池中有 10 个 token0 和 10 个 token1。后来,资金池里有了 20 个 token0 和 20 个 token1。
直观地看,流动性是翻了一倍还是翻了两番(四倍)?因为如果不取平方根,流动性将从 100(10x10)开始,最终变为 400(20x20)。可以说,流动性并没有翻四倍。起初,你能获得的 token0 的最大数量(渐近地)是 100,但在流动性增长后,该代币流动性的“深度”只翻了一倍,而不是四倍。
但是,如果未来的流动性提供者在铸造或销毁时并不是使用平方根来计算流动性,这又有什么关系呢?我们看到,新的流动性提供者被“强制”按照当前比例提供资产,而销毁者也只能按照当前比例进行赎回——这里并没有涉及平方根。
答案在于,如果 Uniswap 选择向 LP 收取费用,它会如何计算。
手续费
回到我们前面的例子:资金池从 100 个 token0 和 100 个 token1 增长到各 200 个,流动性提供者的利润是 100%,因此他们应该支付与该利润成比例的手续费。如果我们把资金池的规模衡量为从 100 增长到 400,那么他们就必须按照四倍的利润来支付手续费。
Uniswap 选择在移除流动性时收取费用,因为在兑换期间收取协议费用会增加这种极其常见的操作的 gas 成本。不过,Uniswap V2 实际上从未开启过协议费用,所以这个讨论带有一定的理论性质。
最初发布于 2023 年 10 月 30 日