深入 Uniswap V2 的手续费

  • Tiny熊
  • 发布于 5小时前
  • 阅读 58

很多同学都知道Uniswap V2的常数乘积做市模型(x·y=K),即在兑换时保持不变,但是LP和项目方如何赚取手续费,是怎么收取的呢,我猜很多同学和我一样有一些细节搞不清楚。前几天有一个学员在面试时,需要约到了这个问题,这篇文章我们一起来看一看。1.理清常数乘积K与

很多同学都知道 Uniswap V2 的常数乘积做市模型(x · y = K), 即在兑换时保持不变,但是 LP 和 项目方如何赚取手续费,是怎么收取的呢,我猜很多同学和我一样有一些细节搞不清楚。前几天有一个学员在面试时,需要约到了这个问题,这篇文章我们一起来看一看。

1. 理清常数乘积 K 与 LP Token 关系

在 Uniswap V2 的某个交易对池(token0, token1)中,记储备量为 reserve0 = xreserve1 = y,有不变式 x · y = K

当第一次添加流动性时,协议会按两边资产的乘积开方为基准铸造 LP:

  • 初次铸造:liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY
  • 之后追加:liquidity = min(amount0 * totalSupply / reserve0, amount1 * totalSupply / reserve1)

MINIMUM_LIQUIDITY 是为了防止通胀攻击、min(...) 是为了保持保持池子比例不变,按较少的那一侧来计算你真正能增加多少 LP token,剩下的资产会退还。

LP token 代表池子的份额。若你持有 s 枚 LP,占比约为 s / S,你可按该比例从池子中赎回两种资产。我们可以看到 LP 总供应 S√(x·y) 成近似正比,可粗略理解为 LP ≈ √K。

理解 K 与 LP Token 的关系是后续理解手续费的基础,因为: 1) 交易手续费留在池内,会让 K 增长,从而“隐含地”提高 LP token 的价值; 2) 而项目方的协议费通过比较 √K 的增长,给 feeTo 地址“铸造相应 LP”(增长的 1/6)。

2. 交易手续费如何派发给 LP

V2 每次 swap 收取 0.30% 手续费,但并非单独记账发给 LP,而是“留在池子里”,使储备上升、K 增大,LP 净值变大。

swap 的核心公式是:

  • amountInWithFee = amountIn * 997(1000 基点中预留 3 个基点=0.3% 手续费)
  • amountOut = amountInWithFee * reserveOut / (reserveIn * 1000 + amountInWithFee)

这意味着:

  • 相比无手续费情形,实际流出更少的 amountOut,差额(0.3%)留在池子里,储备净增加。
  • k 变大,但你手上的 LP 数量不变,等价于“每份 LP 对应的底层资产更多了”。
  • 当你未来 burn LP(赎回)时,按份额把这段期间累计的手续费一并取走,LP 的收益就此实现。

举个极简例子(忽略价格滑点的影响,仅展示计算逻辑):

  • 初始:x = 1000 token0y = 1000 token1k = 1,000,000
  • 发生一次从 token0→token1 的 swap,收 0.3% 手续费,部分 token0 留在池里,使 x 略增、y 略减但整体 k 上升到 k' > k
  • 由于 k 增大、√k 也增大,LP 的隐含价值上升。LP 不用“领取”,收益在 burn 时自然结算。

具体数字示例(以使用 20 个 token0 为例兑换 token1):

  • amountIn = 20
  • amountInWithFee = 20 * 997 = 19,940
  • amountOut = 19,940 * 1000 / (1000*1000 + 19,940) ≈ 19.556
  • 更新后:x' = 1000 + 20 = 1020y' = 1000 - 19.556 ≈ 980.444
  • k' = 1020 * 980.444 ≈ 1,000,053 > 1,000,000√k1000 增至 ≈ 1000.026

关键点:V2 不把手续费单独分账给 LP,而是通过“留池增厚”的方式让 LP 份额增值,这也是 V2 模式简单且 Gas 省的原因之一。

3. 协议费如何实现

Uniswap V2 允许开启协议费(Protocol Fee):把 0.30% LP 手续费收益中的 1/6(即 0.05%)分给协议方(即 feeTo 地址 ,不过目前 Uniswap 并没有设置feeTo 地址 )。

LP 手续费是体现在 LP 的增值中的,那如何分出一部分给协议方呢? Uniswap 的方案是给 feeTo 地址“增发一点 LP” , 让协议方获得的增发的这一点 LP 价值,约等于这段期间 LP 总收益的 1/6 。

大家查看 Uniswap v2 会看到这样一个看起来比较奇怪的公式(详细参考附录 A 代码) :

if (feeOn) {
    uint liquidity = totalSupply * (sqrt(k) - sqrt(kLast)) / (5 * sqrt(k) + sqrt(kLast))
}

这个公式是怎么来的,我们来推导一下:

标记与符号:

  • reserve0, reserve1:当前池子里的两种资产数量。
  • $K = reserve0 \times reserve1$。
  • 令 $r = \sqrt{K}$(我下面用 $r$ 表示当前的 $\sqrt{K}$)。
  • kLast:上一次记录的 $K$(通常在上一次 mint/burn 时保存)。令 $r_0 = \sqrt{kLast}$。
  • $S$:当前的 LP token 总供应量(totalSupply,是在铸新 token 之前的数值)。
  • 要计算的数:当检测到 $r > r_0$(代表池子因交易手续费而增长)时,要铸造给 feeTo 的 LP token 数量 $f$。

Uniswap V2 的目标是:把这段增长中相当于 protocol share(即协议占比 1/6)的价值分配给 feeTo。我们下面把“价值”用 $r$ 单位来表示(因为 LP token 的单位价值与 $\sqrt{K}$ 成正比)。

推导方程

铸造 $f$ 个 LP 给 feeTo 后,feeTo 持有的 LP 的市值(按铸完后的每个 LP token 的价值算)应等于这次增长的 1/6(即 $(r - r_0)/6$)。

  • 铸完之后每个 LP token 的价值为:$\dfrac{r}{S + f}$。(总池价值在 $r$ 单位下由 $S+f$ 个 token 平分)
  • feeTo 的 LP 价值为:$f \times \dfrac{r}{S + f}$。

将两者等式化: $$ f \cdot \frac{r}{S + f} = \frac{r - r_0}{6} $$ 这是我们要解的方程,左边是铸给 feeTo 后它持有的价值,右边是协议应得的 1/6 增益。

两边同乘 $6(S+f)$: $$ 6 f r = (S + f) (r - r_0) $$ 展开右边: $$ 6 f r = S(r - r_0) + f(r - r_0) $$ 把含 $f$ 的项移到左边: $$ 6 f r - f(r - r_0) = S(r - r_0) $$ 提取 $f$: $$ f \big(6r - (r - r_0)\big) = S(r - r_0) $$ 合并括号内项: $$ 6r - (r - r_0) = 6r - r + r_0 = 5r + r_0 $$ 因此: $$ \boxed{\, f = \dfrac{S\,(r - r_0)}{5r + r_0} \,} $$ 这就是 Uniswap V2 合约里实际实现的公式:

    uint liquidity = totalSupply * (sqrt(k) - sqrt(kLast)) / (5 * sqrt(k) + sqrt(kLast))

总结

  1. LP 的手续费是通过 LP Token 的增值提现的(不考虑无常损失)。

  2. 项目方协议费,在 feeTo 地址被设定开启,是通过增发一小部分 LP ,所有 LP 的份额被等比例稀释来提现的。feeTo 需要资金时,直接 burn 手中的 LP,即完成“提现”。

附录 :Uniswap代码片段

以下为 Uniswap V2 Pair 合约核心逻辑:

1) 手续费与协议费计算入口(在 mint/burn/swap 后的 _update 前调用):

function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
    address feeTo = IUniswapV2Factory(factory).feeTo();
    feeOn = feeTo != address(0);
    uint _kLast = kLast; // gas savings
    if (feeOn) {
        if (_kLast != 0) {
            uint rootK = Math.sqrt(uint(_reserve0) * _reserve1);
            uint rootKLast = Math.sqrt(_kLast);
            if (rootK > rootKLast) {
                uint numerator = totalSupply() * (rootK - rootKLast);
                uint denominator = rootK * 5 + rootKLast;
                uint liquidity = numerator / denominator; // ≈ 1/6 收益
                if (liquidity > 0) _mint(feeTo, liquidity);
            }
        }
    } else if (_kLast != 0) {
        kLast = 0;
    }
}

协议在 Pair 合约里维护 kLast(更准确地用 rootKLast = √kLast),仅当 fee 开启时更新。

每次流动性变化(mint/burn/swap 更新储备)时,如果 fee 开启,合约会比较当前 rootK = √krootKLast 的增长,并按增长额给 feeTo 地址“增发一笔 LP”。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Tiny熊
Tiny熊
0xD682...E8AB
登链社区发起人 通过区块链技术让世界变得更好而尽一份力。