uniswap-v2实现过程分析

uniswap v2的实现流程分为uniswapv2-core 以及uniswap-v2-periphery 。这两部分内容相对独立,第一部分是uniswapv2的实现核心,第二部分是外围合约,一般称为路由合约,前端通过该外围合约来与核心合约进行交互。

1. 概述

  1. uniswap-v2-periphery 作为外围合约,只是用来接收前端请求,做简单的数据处理后,转发给底层合约执行。逻辑很简单,自行查阅即可。
    1. 该项目中,主要关注这两个文件:UniswapV2Router02.solUniswapV2Library.sol,别的文件主要是用于兼容早期合约的,无需关注。
      1. UniswapV2Library.sol:工具库,可以从v2-core 中读取并封装一些关键信息
      2. UniswapV2Router02.sol:路由合约,用于衔接v2-core 是与用户进行交互的入口,主要提供了添加流动性、移除流动性和兑换的系列接口,并提供了几个查询接口。属于该项目的最重要的一个文件
  2. uniswapv2-core 是uniswap v2的底层核心合约:
    1. 协议费和手续费区分
      1. 协议费:表示平台抽成,需要手动在合约中开启,每次mint或burn后会触发
      2. 手续费:每次swap,会保留的千分之三的交易费用。用户burn时,可以根据自己的份额连同该手续费提走。
    2. 主要分为以下几个部分:
      1. 池子初始化:createPair
      2. 增加池子流动性:mint
      3. 减少池子流动性:burn
      4. 交换:swap,也就是在满足x * y=k的情况下,如何从池子中获取其中一种token
      5. 同步更新资产:sync,用于将池子账户的两种资产数量与池子合约中参数标记的数量保持一致
      6. 从池子中提走多余的资产:skim,就是池子账户上两种资产数量如果大于池子合约中参数标记的两种资产数量,那就将多余的资产提取出来

2. 概念解释

uniswap v2中涉及到的一些概念

2.1 恒定乘积做市模型(CPMM)

Uniswap-v2 使用了恒定乘积做市模型来实现自动化做市商。其计算步骤如下:

  1. 甲在 Uniswap-v2 上提供了 1000 个 TokenA 和 100 个 TokenB 作为流动池,计乘数 Kab = 1000 * 100 = 100000
  2. 乙想要在 Uniswap-v2 上使用 100 个 TokenA 兑换 TokenB,那么 Uniswap-v2 会首先计算交易后流动池会有 1000 + 100 = 1100 个 TokenA;为了维持 Kab 的不变,就需要 TokenB 的数量减少为 100000/1100 = 90.909… 。因此,Uniswap-v2 会给乙兑换出100 - 90.909 = 9.091 个 TokenB 。
  3. 乙成功地使用 100 个 TokenA 在 Uniswap-v2 上兑换了 9.091 个 TokenB。可以预见的是,根据这套算法,乙需要兑换的 TokenA 越多,平均每个 TokenA 能兑换的 TokenB 就会越少。这就产生了滑点的概念。
    简化公式:(TokenA余额 + 你出售的TokenA)*(TokenB余额 - 你获得的TokenB) = 常数K

2.2 滑点

滑点一般指预设成交价位与真实成交价位的偏差。恒定乘积AMM中同样存在滑点,一旦发生交易,池中资产的储备发生变化,资产实际的交易执行价就会发生变化,产生滑点。交易额越大,滑点越大,交易者的损失就越大。
根据恒定乘积,当用dx个x兑换dy个y时(忽略手续费),有:

1
2
x * y = k 
(x + dx)(y - dy) = k

可得,兑换量:

1
dy = (y * dx)/(x + dx)

则在实际兑换中,y相对x的单价为:

1
dx / dy = (x + dx) / y

而兑换前,池中的y单价为x/y,那么y单价的滑点就产生了:

1
p = (dx / dy) - (x/y) = dx / y

当然,这里总结出的滑点计算还只是通过AMM机制所算出的理论滑点,实际上滑点还会受很多因素影响,比如网络延时、区块确认等等。

2.3 无常损失

无常损失的发生与AMM的机制有关,例如当BTC的价格为50 USDT时,假设用户A向流动性池中添加10 BTC与 500 USDT,此时用户A的总资产价值1000USDT。假设此时流动性池中总的流动性为100 BTC与 5000 USDT, 用户A占总流动性池的10%。
若BTC的价格上涨为 200 USDT,则根据恒定常数自动做市商的机制,总流动性池中资产的量变为50 BTC与10,000 USDT,若此时用户A撤出流动性,因为此前用户A提供的流动性占总流动性池的10%,则用户A成功取出 5 BTC与 1000 USDT,此时用户A的资产价值为2000 USDT(不计手续费收入)。
但若用户A不向流动性池提供流动性,而是单纯持有则用户A的资产应该价值 10*200 + 500 =2500 USDT,中间的损失为500USDT ,提供流动性与单纯持有之间相较产生的损失称为无常损失。无常损失的大小只与提供流动性时币对的价格(P0)与撤出流动性时币对价格(P1)的比值(P1/P0)有关。
但是用户可以通过提供流动性赚取交易的手续费,一定程度上可以弥补价格波动带来的无常损失。

3. 通过案例理解交易和手续费的计算过程

HelloSwap是一个独立的Uniswap交易所,它与其他交易所没有互联,该交易所的手续费比例为0.3.%,返还0.25%给LP,剩余0.05%给开发团队即协议抽成ϕ=1/6,有个资源池为LAM-MUT代币对,假设该资源池的持有者只有一个用户名称为Tom,如下图所示,即Tom占有LAM-MUT资源池的比例为100%,他初始添加流动性的比例为LAM:MUT = 4000:1000,分2次通过售出100LAM来买入MUT,请问Tom每次交易接收到的MUT是多少?Tom移除LAM-MUT 100%的流动性时,返回给Tom的LP手续费(UNI的个数)是多少?

3.1 每次接收到MUT的计算过程

售出100LAM第1次后
Tom接收到的MUT为:1000-4000000/(4100-1000.003)=24.3188526
MUT流动量:4000000/(4100-100
0.003)=975.6811474
:4000000为当前x和y各自的流动性相乘;1000为售出LAM前,池中MUT的流动量;100为当前售出的LAM数量;4100为售出LAM后,LAM在池中的流动量;0.003为总的手续费比例

售出100LAM第2次后
计算结果,与第1次的计算方式一模一样

想要补充说明的是,手续费是从你要出售的token中按比例0.003扣除的。计算手续费的合约代码如下:

1
2
3
4
5
6
7
8
9
10
...
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); //减去手续费的数额,手续费是池子剩余余额的0.3%
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); //x*y=k,交易后的k要不小于交易前的k
}
...

可知,swap时,并没有把手续费分发给管理员或者搭建池子的人。而是保留在了池子中。具体的手续费计算,需要关注_mintFee方法,里面涉及到一系列的公式,比较复杂。下面章节中详细解释

3.2 每次接收到MUT后手续费的计算

图中可知,0.003的手续费,最终分配给了协议费账户(uni官方)LP手续费账户(追加流动性用户)。需要说明:

  1. 每次swap,总手续费都会累加,并不分发(不分发是因为swap调用频次高,若每次都分发,gas扛不住。真正的分发都放在了mintburn中,低频方法)
  2. mintburn时,都会执行_mintFee方法,通过一系列的数学计算,将属于官方的手续费(LP)转移至官方指定账户。
  3. mint时,也就是客户追加了流动性。此时根据客户追加的各token比例,折算出对应的LP给到客户账户(此LP中包系统给的手续费,就是从累加的总手续费按相同比例折算的)。
  4. 这会儿的手续费是LP代币,分别在各自账户上。需要折现时,将LP转入合约账户,然后合约操作burn时,会将LP按比例折合成对应的两种token,分发给burn指定的账户。

_mintFee会划转1/6手续费(LP)到官方账户,先看看该方法代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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).mul(_reserve1));
uint rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) {
uint numerator = totalSupply.mul(rootK.sub(rootKLast));
uint denominator = rootK.mul(5).add(rootKLast);
uint liquidity = numerator / denominator;
if (liquidity > 0) _mint(feeTo, liquidity);
}
}
} else if (_kLast != 0) {
kLast = 0;
}
}

mint 的数量 为什么是 (rootK - rootKLast)/(5 倍 rootK + 1 倍 rootKLast) 而不是 (rootK - rootKLast)/6 倍 rootKLast ? 通过下图, 看看要增发多少 lp, 使得 lp 分到的财富刚好是增量的 1/6:

如上图所示, 为了得到新增财富的 1/6, 需要增发的 lp 应该满足:

1
lp/lp_supply = (∆/6) / [(∆5/6) + rootKLast ]

这里 ∆ = rootK - rootKLast 解出 :

1
lp = lp_supply * ∆ / (5rootK + rootKLast)

与源代码的计算方法一致, 证实了 Uniswap 收取的协议手续费就是总手续费的 1/6.

需要知道每次池子LP的总量,图中没有提供,但思路基本一致。直接套用上面理论即可计算出官方手续费。网上还有另一种方式计算手续费,根据白皮书公式给的,看着有点绕,有兴趣可以看看:Uniswap V2里的手续费换算

6. 价格预言机

这个没有具体去研究,只记录下,在_update方法,会更新累积价格,用于具体的预言机计算,有兴趣的可以去研究下:
price0CumulativeLast和price1CumulativeLast的价格累加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// update reserves and, on the first call per block, price accumulators
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
...
// 时间戳是uint256,只需要保留最低的32位即可,但不可直接强转。
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// 计算时间加权的累计价格,256位中,前112位用来存整数,后112位用来存小数,多的32位用来存溢出的值
// * never overflows, and + overflow is desired
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
...
blockTimestampLast = blockTimestamp;
...
}

5. uniswap v2缺点

  1. 资金利用率低
  2. 用户承担无常损失的高风险
  3. 用户得到的手续费低
  4. 因此,出来了uniswap v3

关于资金利用率的问题,这里解释下:
假设 ETH/DAI 交易对的实时价格为 1500 DAI/ETH,交易对的流动性池中共有资金:4500 DAI 和 3 ETH,根据 x⋅y=k,可以算出池内的 k 值:

1
k= 4500×3 = 13500

假设 x 表示 DAI,y 表示 ETH,即初始阶段 x1=4500 , y1=3,当价格下降到 1300 DAI/ETH 时:

1
2
3
4
5
6
x2⋅y2=13500
x2 / y2=1300

# 得到:
x2=4192.54
y2=3.22

资金利用率为:

1
2
Δx = x1 - x2 
Δx / x1 = 6.84%

同样的计算方式,当价格变为 2200 DAI/ETH 时,资金利用率约为 21.45%.
也就是说,在大部分的时间内池子中的资金利用与低于 25%. 这个问题对于稳定币池来说更加严重。

通俗来说,池子只是消耗了Δx资金,如果价格不会跌破1300,则剩下的x2资金永远不会用到,理论上讲,理想状态下,池子里只需要有Δx资金量就够了

因为资金使用率是v2最大的一个问题,如果不理解上面说的,这里再进一步解释下:

上图为资金池中的x/y(这比值其实表示两种币的币价,x对y的价格)的量变化曲线。资金池中的当前价格在c点,并且假设会在a价格点和b价格点之间波动。从c点向a点滑动,消耗最大y_real,从c点向b点滑动,消耗最大为x_real。也就是说,当前价格c点,在a点和b点之间震荡的话,最大只需要消耗x_realy_real。理论上只要提供x_realy_real就足够了。而事实上,如上图所示,在价格c点,分别提供了大于x_realy_real的x和y。明显可以看出,x-x_realy-y_real的资金在这种情况下是永远用不上的,也就称为闲置资金。
在这种情况下,资金利用率为x_real/x或者y_real/y。如果价格波动非常小的话,资金利用率是非常低的。

4. 总结

按我对源码的解读,核心内容,一个是计算兑换收益,一个是计算手续费。
技术上,一个是结构分层设计,一个是为了节省gas消耗,而设计的手续费的分发实现。
想要更好的理解uniswap v2,测试环境部署一个,然后来来回回反复操作理解吧。

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2017-2023 Jason
  • Visitors: | Views:

谢谢打赏~

微信