UniswapV2 原理与源码解析

2,390 阅读16分钟

UniswapV2 原理

Uniswap V2是Uniswap V1的升级版本,基于v1同样的公式实现,建议大家先去看我写的V1,因为V1和V2核心思想都是恒定乘积公式,相比Uniswap V1主要有如下新特性:

1.支持ERC20代币/ERC20代币交易对

V2不用再向v1那样,需要先使用TokenA来换取ETH,之后再通过ETH来换取TokenB。
好处
(1)此改动减少了流动性提供者的成本,因为有ETH作为过渡代币总是会比两个token直接相关的损失大(如果ETH价格波动)。
(2)减少了交易者的成本,因为有ETH作为过渡代币时交易者必须支付的费用是直接购买ABC/XYZ对的费用的两倍,并且遭受两次滑点。
问题
当ERC20交易对数量激增时,找到指定货币对的最佳路径就会困难很多,于是V2引入了路由。

2.价格预言

uniswap v2通过测算和记录每个区块第一笔交易之前的价格(也就是前一个区块最后的价格)。
具体算法:追踪每个和合约交互的区块的开始处的价格累加和,每个价格用距离上一个更新价格的区块的时间进行加权,意思是:累加器的值在任意时间的值等于合约历史上每秒现货价格的和。

image.png

计算加权平均价,外部调用者拿到T1和T2时间累加器的值,用(后值-前值)/经过的时间。 用户可以选择起始和结束时间。

uniswap会同时追踪A/B和B/A的价格,因为采取均值的方式他们两就不是倒数的关系了。

核心合约在每次交互后缓存它的资金储备,用缓存的资金储备更新价格预言而不用当前资金储备。(预防有人发送资产给交易对合约,用来改变它的余额和边际价格,但又不和它交互)

好处:
大大减少价格受到操控的风险。

v1作为链上价格预言是不安全的,因为它非常容易被操纵。在一个block的开头大量卖出某种资产A从而影响价格,在该block的中间根据这个大幅波动的价格进行其它合约的其它操作(非uniswap交易对合约),在该block的最后再买入相同数量的资产A使价格回到正常水平。

V2如果攻击者提交了一笔交易(transaction)尝试在区块末尾处操纵价格,其他的套利者可以提交另一个交易(transaction)立即进行反向交易。除非他们可以挖出下一个区块,否则他们他们没有特殊的的套利优势。

3.价格计算精度

uniswap v2用简单的二进制定点数格式编码和控制价格。价格存储为UQ112.112格式,意思是在小数点的任意一边都有112位精度,无符号。 储备资金各自存在uint112中,剩余32位存储空间用于描述累加过程,保存当前区块的创建时间

问题:
unit时间戳会在2106年7月2日溢出
解决方法是在每个间隔至少检查一次价格(大约136年一次)

4.Flash Swaps

相当于闪电贷,V2允许先接收和使用资产再支付,只需要他们在一个原子操作里。swap函数里可以调用户指定的回调合约,转出用户请求的代币并保持不变,回调完成后合约检查新余额,是否不变。(会收取0.3%的手续费)

问题:
可能会出现空手套利,只要两个交易所有价格差。
比如有uniswap v2和其他的DEX,都是A/B代币对。 先在uniswap这借A,换B。然后再拿B去DEX里换A0,再把A0还回来。A0-A就是我们得到的利。

5.协议手续费

v2可以选择是否打开0.05%的协议手续费,如果打开,手续费会被发送给工厂合约中指定的feeTo地址。将0.3%的交易手续里的1/6发送给feeto.这个费用只有在增加/减小流动性时才会做相应计算。

具体计算:

IMG_9472.JPG

6.资金池份额的元交易

用户可以签名授权他们资金池份额转账,而不用他们的地址进行链上的转账。也就是说其他人可以拿到授权后直接用用户的名义签名交易、转账执行其他操作。

7.手续费调整

uniswap v1里面合约强制确保:(x1-0.003X-in)*y1=X0*Y0

V2的flash中,可能存在用户token1和token2各还一点,保证还回同样的资产。所以此时的x-in和y-in可能都不为0,那么就需要换一套计算方式: (x1-0.003X-in)*(y1-0.003Y-in)=X0*Y0

8.救援函数sync()和skim()

sync()用来同步合约中缓存的资产数量(储备金)为当前值,目的是处理合约目前汇率不合理并且没有流动性提供者的情况,一种优雅的恢复机制。
skim()是发送到代币的数量溢出了uint112大小的储备金存储空间时的恢复机制,它允许用户提出合约资产值和uint112最大值之间的差值。

9.处理非标准和非常规代币

v1会将不标准函数的返回值直接设为false,并回滚。导致泰达币和币安币无法使用(因为他们的transfer和transferfrom没有返回值,而ERC20的标准是实现的)

v2将没有返回值的视为成功

10.初始化流动性供给

Uniswap v2初始铸造份额等于存入代币数量的几何平均值:

image.png

公式确保了流动性资金池份额的价值在任意时间和在本质上和初始存入的比例无关。

好处:
规避v1中第一个存入代币得到的流动性值与初始化ETH的值相等,导致过分依赖初始化流动性比例

问题:
流动性资金池份额的价值随时间增长是可能的,比如:通过累加交易手续费或者向流动性资金池“捐款”。为了解决这个问题,Uniswap v2销毁第一次铸造的1e-15资金池份额,发送到全零地址而不是铸造者。

虽然对交易微不足道但是会大大提升攻击成本,比如想要提高流动性资金池份额价值到100美元,攻击者需要捐献100000美元到资金池总。

11.WETH

ETH的转账接口和ERC20交互用的标准接口不同,因为Uniswap v2支持任意ERC20交易对,它现在不再支持无包装的ETH。如果添加这个特性需要两倍的核心代码,并且产生ETH和WETH流动性分散的风险。原生ETH需要包装后才能在Uniswap v2上交易。

所以Uniswap V2中不支持ETH,用户在使用交易对前必须先换成WETH,实际上Uniswap V2内部自动把用户提供的ETH转化成WETH了。

12. 确定交易对地址

v1是create构造代币地址的,v2用create2构造交易对地址

13.最大代币余额

就是之前说的,代币的总额最大只能为2^112-1,如果超过了这个值,交易会报错。此时任何人可以调用skim()函数就行恢复。

UniswapV2 源码解析

代码仓库:
core: bengda233/uniswap-v2-core: 🎛 Core smart contracts of Uniswap V2 (github.com)
Periphery: bengda233/uniswap-v2-periphery: 🎚 Peripheral smart contracts for interacting with Uniswap V2 (github.com)

uniswap v2分为2个部分:

Core:包含Factory、Pair、Pair ERC20
Periphery:Routers

core主要为核心逻辑,单个swap的逻辑;Periphery是外围的服务,在所有swap的基础上构建服务。

Core

UniswapV2 Factory

Factory主要用来创建Pair(交易池)

Factory里所有的方法合集:

  • allPairsLength查出目前的所有交易对个数。
  • createPair创建一个交易对。

流程: 1.检查输入的两种token是否一样
2.对两种token进行一个简单的排序,检查小的地址是否为0地址,检查这两种token的交易对是否已经存在
3.获取模板合约UniswapV2Pair的字节码 4.用两种token的地址生成盐值
5.用create2构造出Pair交易对的地址
6.将pair记录

  • setFeeTo这个方法就是上面第5点协议手续费的实现,只有feeToSetter(初始为合约部署者,可转让),可以设置feeTo的地址。如果feeTo未设地址,就表示0.05%的手续费未开启

  • setFeeToSetter该方法用来修改feeToSetter

UniswapV2 ERC20

该合约主要是Uniswap V2代币的信息和操作方法。定义Uniswap V2代币的名称、代币的标识符、代币的精度等.

DOMAIN_SEPARATOR主要用来不同Dapp之间区分相同结构和内容的签名消息,该值也有助于用户辨识哪些为信任的Dapp

PERMIT_TYPEHASH是根据事先约定使用permit函数的部分定义计算得出的哈希值

nonces值用于记录合约中每个地址使用链下签名消息交易的数量,用来防止重放攻击

V2 ERC20里所有的方法合集:

  • _mint用来增发代币
  • _burn用来销毁代币
  • approve授权的函数
  • transfer转账函数
  • transferFrom授权转账的函数
  • permit用户实现验证与授权,因为用户需要借助周边合约才能和核心合约。
    当用户需要燃烧流动性代币提取代币时,需要将自己的流动性代币销毁掉,但是由于调用的是周边合约,未经授权的时候是无法进行操作的。
    如果按照正常操作,用户需要先对周边合约进行授权,再再调用周边合约进行燃烧操作。这个过程需要两个交易。
    但是如果通过线下消息签名,就可以减少一个交易。
    在周边合约里,减小流动性来提取资产时,周边合约在一个函数内先调用交易对的permit函数进行授权,接着再进行转移流动性代币到交易对合约,提取代币等操作,所有操作都在周边合约的同一个函数中进行,达成了交易的原子性和对用户的友好性。

UniswapV2 Pair

UniswapV2Pair即交易对合约的核心逻辑。

MINIMUM_LIQUIDITY 表示最小流动值
SELECTOR 是transfer的函数选择器的值

blockTimestampLast 最新交易区块
reserve0、reserve1 两种资产的数量

price0CumulativeLast、price1CumulativeLast 记录交易中两种价格的累计值
kLast 最新时刻的k值

UniswapV2 Pair里的方法合集

  • lock()是一个锁机制的函数修饰符,用来防止重入
  • getReserves 获取当前两种token的数量和最近一次交易的时间
  • _safeTransfer 内部转账的一个逻辑,调用token的转账函数并检查返回值是否都为true
  • initialize初始化token0和token1的地址
  • _update内部的更新reserve的方法

流程:
1.检查当前两个token的余额是否超出限制
2.更新blockTimestamp为当前区块(占32位,由于区块时间是uint类型,所以可能会超过uint32的最大值,于是这里进行取模。)
3.计算与当前与上一次block的时间差,检查是否大于0,大于0就更新交易价格累计值(因为是同一个区块的第二笔及以后的交易,timeElapsed就为0,不会算到交易累加值,对应到上面的第2点价格预言)
4.更新reserve和blockTimestampLast

  • _mintFee内部函数,用来生成协议手续费。将1/6的流动性给feeto。V2的手续费会全部累计起来,流动性发生改变时才会对手续费进行分配.

流程:
1.拿到feeTo的地址,判断是否打开
2.如果打开了,就拿到当前reserve乘积出的k,和之前保存的k
3.如果k增加了,就计算出返回的fee(计算过程涉及到之前的第五点协议手续费)
4.如果没打开就会把kLast归零(因为feeOn是可以随时打开和关闭的,klast的更新位于mint函数与burn函数中,如果打开了更新了klast,但是没有清零又关闭了,下一次打开就是用的之前的数据)

公式:

image.png

  • mint用来生成流动性代币的方法(流动性增加时调用)

流程:
1.拿到reserve0和reserve1
2.算出目前的两种token的余额,将他们与reserve相减算出增加的余额。
3.生成feeto的流动性代币
4.如果是初次提供流动性,会生成MINIMUM_LIQUIDITY个代币给0地址(对应上面第10点初始化流动性供给),再根据恒定乘积公式中积的平方根来计算流动性:S-mint=根号(X-deposit*Y-deposit)
5.如果不是初次提供流动性,根据两种token的增加比例计算并取两者间最小的。流动性=存入的token数量/token之前的总数 * 总的流动性
6.更新reserve和Klast

  • burn销毁流动性将token归还流动性提供者。

流程:
1.拿到reserve0和reserve1
2.拿到合约实际两种token的余额,以及合约的流动性代币总数(一般情况下,合约是没有流动性代币的,因为mint是直接mint给流动性提供者,但是burn之前需要把流动性代币转给合约,于是合约就有流动性代币了)
3.生成feeto的流动性代币
4.计算返回的token0和token1的数量,按照比例计算:要燃烧的流动性代币/总流动性代币数量*目前token的数量
5.燃烧代币
6.将token0和token1转给to
7.更新reserve和Klast

  • swap用于交易对中资产的交换

流程:
1.输入要购买的token0和token1数量,验证他们是否超过合约的余额
2.对地址进行to校验
3.将想要的amount数量转过去
4.调用合约的uniswapV2Call回调函数将data传递过去
5.再拿到两种token目前的balance
6.算出注入的token0和token1的数量(比较他们是否在借出去的基础上增加以及增加的数目)
7.计算是否还完(比较是否大于原来的k)。算法:目前的token1和token2数量-手续费(0.03%)将他们两相乘算出现在的k,比较是否大于原来的k。
8.更新reserve

  • skim当合约token余额超出了112位就可以调用这个方法,取出提出合约资产值和uint112最大值之间的差值

  • sync用来将合约中缓存的资产数量同步为合约的当前值

Periphery

Periphery周边合约是外部账户和核心合约之间的桥梁,周边合约包含了接口定义、工具类库、Router和示例实现四部分内容。

挨个解析:

libraries:

1.UniswapV2Library.sol

  • sortTokens该方法的作用是对上下文合约地址进行排序,检查两个地址是否相等,是否是0地址
  • pairFor用于计算生成的交易对地址
  • getReserves得到交易对合约token的恒定积产的值
  • quote根据一种资产依据比例算出另一种资产
  • getAmountOut 给定一个资产的输入金额和交易对储备,计算出能够置换到的资产。

image.png

  • getAmountIn 给定一个想要的资产金额和交易对储备,计算出需要给出的资产

image.png

  • getAmountsOut链式交易计算出能够置换到的资产,例如 A/B=>B/C=>C/D,用A对换B

  • getAmountsIn链式交易给出想要置换到的资产数,计算需要付出资产。例如 A/B=>B/C=>C/D,给出想要的B计算需要的A

UniswapV2OracleLibrary.sol

  • currentBlockTimestamp返回当前的区块时间
  • currentCumulativePrices用于计算当前区块累积价格

UniswapV2LiquidityMathLibrary.sol

这个依赖用来做交易对里的liquidity的数学计算

  • computeProfitMaximizingTrade计算最大化利润的交易方向
  • getReservesAfterArbitrage在外部观察到真实价格的情况下,套利后的准备金将价格移动到利润最大化比率
  • computeLiquidityValue计算流动性值
  • getLiquidityValue拿到流动值
  • getLiquidityValueAfterArbitrageToPrice计算当给定两个Token——TokenA和TokenB,和它们的"真实价格"和流动性金额,计算以TokenA和TokenB的流动性值

UniswapV2Router02.sol

  • ensure用于检查是否超过截止时间
  • receive只接受WETH传来的ETH
  • _addLiquidity用于增加流动性

流程:
1.检查交易对是否存在,不存在就创建
2.拿到tokenA和tokenB的reserve
3.如果是第一次增加流动性就直接将注入的资产全部转换为流动性 4.如果不是第一次,根据池子的比例计算。根据传入的A的数量算出需要传入的B的数量。
5.如果需要的B小于传入的B,那么流动性就是用输入的A数量和需要传入的B的数量
6.如果需要的B大于传入的B,那么流动性就是用输入的B数量和需要传入的A的数量

  • addLiquidity用户增加流动性,调用之前的 _addLiquidity函数计算出需要提供的A、B的实际数量。将token转给交易对合约,生成流动性

  • addLiquidityETH与addLiquidity函数类似,只不过将其中一种token换成ETH。将amountToken量的token代币转移到交易对中,之后将ETH兑换成WETH,将刚刚兑换的WETH转移至交易对合约

  • removeLiquidity移除流动性,拿到交易对,将燃烧流动性代币转给合约, 调用交易对的burn函数燃烧掉转进去的流动性代币,提取相应的两种代币给接收者,检查是否提取的对应代币是否大于最小的提取代币数量

  • removeLiquidityETH移除流动性,返回给流动性提供者ETH和token

  • removeLiquidityWithPermit移除流动性,允许下签名消息来进行授权验证,从而不需要提前进行授权

  • removeLiquidityETHWithPermit同理,返回的是ETH和token

  • removeLiquidityETHSupportingFeeOnTransferTokens支持使用转移代币支付手续费

  • _swap交换代币,支持链式交易

  • swapExactTokensForTokens 用特定数量的某种token来换取其他的token

  • swapTokensForExactTokens其他Token来换取指定数量的期望token

  • swapExactETHForTokensswapTokensForExactETH``swapExactTokensForETH,swapETHForExactTokens同理

  • _swapSupportingFeeOnTransferTokens和_Swap功能类似,该函数支持在转移代币时支付手续费

UniswapV2Router01.sol

与02类似,是不打开交易费的时候用的

UniswapV2Migrator

  • migrate于将UniswapV1交易对中的流动性迁移到V2交易对中

参考: 去中心化交易所:Uniswap v2白皮书中文版 - 知乎 (zhihu.com)
UniswapV2协议解析 - 腾讯云开发者社区-腾讯云 (tencent.com)