Uniswap V2 被设计为向协议收取交易手续费(swap fee)的 1/6。由于交易手续费为 0.3%,其 1/6 也就是 0.05%,因此每笔交易的 0.05% 将归协议所有。
尽管该功能从未真正被激活过,但由于某些分叉(forks)可能会使用它,我们仍然对其进行讨论。此外,这部分的计算很容易出错,现在花时间去理解它,将有助于你在以后发现类似计算中的错误。
先决条件
你需要熟悉 Uniswap V2 Book 中之前的所有章节,才能跟上本文的内容。
在兑换期间收取协议费用效率低下
在每笔交易中都收取 0.05% 的费用是效率低下的,因为这需要额外的代币转账。转移 ERC20 代币需要进行存储更新,因此转账给两个地址而不是一个地址的成本会高得多。
因此,费用是在流动性提供者调用 burn or mint 时收取的。由于与 [swapping tokens](swapping tokens) 相比,这些操作并不频繁,这就能节省 Gas。为了收取 mintFee,合约会计算自上次操作以来收集到的手续费总额,并向受益人地址铸造足够数量的 LP 代币,使得受益人有权获得 1/6 的手续费。
fee 和 mintFee 的术语
为了避免术语上的混淆,我们将兑换期间向交易者收取的 0.3% 称为“fee”,并将这 0.3% 费用的 1/6 称为“mintFee”。是的,这两个术语中都包含 fee 并不是很好的命名法,但我们只能接受这个设定。
流动性(Liquidity)是资金池中代币余额乘积的平方根。关于该公式的合理性已在 Uniswap V2 swap 函数的相关文章中讨论过。一些文献将其称为 ,其中 ,而 和 是池中的代币余额(即 和 的储备量)。我们用 来表示流动性,因为这样写比 更简短。
计算 mintFee 的假设条件
为了使这一机制生效,Uniswap V2 依赖于以下两个不变量(invariants):
- 如果不调用
mint()和burn(),资金池的流动性只会增加。 - 流动性的增加纯粹是由手续费(或捐赠)引起的。
通过测量自上次mint()或burn()交易以来流动性的增加量,资金池就能知道收取了多少手续费。
这些都是很适合编写的不变量测试(invariant tests),但现在我们将理所当然地认为它们是成立的。
mintFee 计算示例
假设在 时刻,资金池初始有 10 个 token0 和 10 个 token1。
经过大量的交易和手续费收取后,在 时刻新的资金池余额变为 40 个 token0 和 40 个 token1。
流动性的衡量标准是两种代币乘积的平方根,即 。流动性在 时为 10,在 时为 40。换句话说, 且 。我们将对从 10 到 40 的这部分增长收取费用。
在 时刻总共 40 个单位的流动性中,有 30 个单位是由交易手续费产生的。
我们要铸造足够数量的 LP 代币,即“mintFee”,以便受益人能获得资金池中“费用部分”的 1/6。也就是说,他们应该有权获得来自利润的 5 个单位(30 / 6)的流动性。
协议不应针对任何“原始流动性”(即 )收取费用。协议只应针对增量(即 )收取费用。
当调用 mint() 或 burn() 时,Uniswap 会向协议费用接收者铸造 LP 代币。这会导致一种稀释效应,使得当前供应的 LP 代币可以赎回原始流动性加上 5/6 的“利润流动性”(来自交易手续费的流动性)。
推导 mint fee 公式
我们使用以下符号:
-
为在铸造具有稀释作用的协议费用 LP 代币之前的 LP 代币供应量。
-
为即将铸造给协议的 LP 代币数量。它应该足以赎回 1/6 的利润流动性。
-
为原始存款的流动性,即 LP 提供的流动性。
-
为原始存款以及由交易手续费产生的流动性之和。
-
为扣除协议费用后应归 LP 所有的流动性数量。也就是说,LP 应有权获得其原始存款以及 5/6 的利润。
-
为应归协议所有的流动性数量。即 的 1/6。
为了计算 ,我们观察到以下不变量必定成立:
换句话说,之前的总供应量 数量的 LP 代币可以赎回应归 LP 所有的流动性,而 数量的 LP 代币可以赎回应归协议所有的流动性。
下图展示了如何根据流动性的变化来求解 。

Uniswap V2 的 _mintFee() 代码
在了解了该推导过程后,Uniswap V2 _mintFee 函数的大部分内容应该是不言自明的。这里有一些符号上的变化:
- 扣除费用后的当前流动性 对应
rootK - 之前的流动性 对应
kLast - 稀释前的 LP 代币供应量 对应
totalSupply - 该函数会改变状态,它在函数内部直接铸造 mintFee,而不是返回 mintFee 的计算结果(蓝色高亮部分)
- 可以通过
feeOn标志来开启和关闭费用,这一点我们尚未讨论过

我们将进一步深入探讨这个函数,但首先我们要弄清楚 kLast 是在哪里被更新的。
klast 在哪里更新
在上述代码中,除非 feeOn 被切换为 false,否则不会设置 kLast。它是在 mint 和 burn 完成时设置的,而不是在 swap 时,因为我们需要测量的是流动性存入和提取事件之间由兑换产生的手续费增长。设置 kLast 的位置用黄色框标出。
Mint 函数更新 klast

Burn 函数更新 klast

_mintFee 代码条件
既然我们明白了 kLast 是如何更新的,就可以全面解释 _mintFee 函数了。

让我们来看看上面代码片段中的各种可能情况,为了方便起见,这里重述如下:
feeOn为false,没有铸造任何代币(绿色高亮部分)feeOn为false,kLast为零(黄色高亮部分)feeOn为false,kLast不为零(黄色高亮部分)feeOn为true,但流动性没有增长(橙色高亮部分)feeOn为true,且流动性出现增长(橙色高亮部分),因此收取 mint fee(蓝色高亮部分)
在决策树中更容易看清这里的逻辑,因此下面给出了这棵决策树,其分支的颜色与 if 语句相同。

通过 RareSkills 了解更多
请查阅我们的 blockchain bootcamp 以了解我们的课程内容。
首发于 2023 年 11 月 14 日