很多同学都知道Uniswap V2的常数乘积做市模型(x·y=K),即在兑换时保持不变,但是LP和项目方如何赚取手续费,是怎么收取的呢,我猜很多同学和我一样有一些细节搞不清楚。前几天有一个学员在面试时,需要约到了这个问题,这篇文章我们一起来看一看。1.理清常数乘积K与
很多同学都知道 Uniswap V2 的常数乘积做市模型(x · y = K), 即在兑换时保持不变,但是 LP 和 项目方如何赚取手续费,是怎么收取的呢,我猜很多同学和我一样有一些细节搞不清楚。前几天有一个学员在面试时,需要约到了这个问题,这篇文章我们一起来看一看。
在 Uniswap V2 的某个交易对池(token0, token1)中,记储备量为 reserve0 = x、reserve1 = y,有不变式 x · y = K。
当第一次添加流动性时,协议会按两边资产的乘积开方为基准铸造 LP:
liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITYliquidity = 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)。
V2 每次 swap 收取 0.30% 手续费,但并非单独记账发给 LP,而是“留在池子里”,使储备上升、K 增大,LP 净值变大。
swap 的核心公式是:
amountInWithFee = amountIn * 997(1000 基点中预留 3 个基点=0.3% 手续费)amountOut = amountInWithFee * reserveOut / (reserveIn * 1000 + amountInWithFee)这意味着:
amountOut,差额(0.3%)留在池子里,储备净增加。举个极简例子(忽略价格滑点的影响,仅展示计算逻辑):
x = 1000 token0,y = 1000 token1,k = 1,000,000。x 略增、y 略减但整体 k 上升到 k' > k。具体数字示例(以使用 20 个 token0 为例兑换 token1):
amountIn = 20amountInWithFee = 20 * 997 = 19,940amountOut = 19,940 * 1000 / (1000*1000 + 19,940) ≈ 19.556x' = 1000 + 20 = 1020,y' = 1000 - 19.556 ≈ 980.444k' = 1020 * 980.444 ≈ 1,000,053 > 1,000,000,√k 从 1000 增至 ≈ 1000.026关键点:V2 不把手续费单独分账给 LP,而是通过“留池增厚”的方式让 LP 份额增值,这也是 V2 模式简单且 Gas 省的原因之一。
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:当前池子里的两种资产数量。kLast:上一次记录的 $K$(通常在上一次 mint/burn 时保存)。令 $r_0 = \sqrt{kLast}$。totalSupply,是在铸新 token 之前的数值)。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$)。
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))
LP 的手续费是通过 LP Token 的增值提现的(不考虑无常损失)。
项目方协议费,在 feeTo 地址被设定开启,是通过增发一小部分 LP ,所有 LP 的份额被等比例稀释来提现的。feeTo 需要资金时,直接 burn 手中的 LP,即完成“提现”。
以下为 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 = √k 与 rootKLast 的增长,并按增长额给 feeTo 地址“增发一笔 LP”。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!