UniswapV2Periphery 源码学习
Periphery是uniswap的外围合约,将core合约封装起来提供给外部调用,比如我们在网页操作Swap时,请求的就是Periphery的合约。
Periphery里面写了Migrator和Router两个合约,其中Migrator是迁移合约,将流动性从Uniswap的V1版本迁移到V2版本,不涉及swap的功能,这里就不写了。
Router合约
using SafeMath for uint;
address public immutable override factory;
address public immutable override WETH;
modifier ensure(uint deadline) {
require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
_;
}
constructor(address _factory, address _WETH) public {
factory = _factory;
WETH = _WETH;
}
receive() external payable {
assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
}
从基础部分开始看起,router合约中记录了factory
和WETH
地址,其中factory
用于获取pair
和创建新的pair
合约,而特别记录下WETH
的地址是为了支持以太坊链的主网币ETH
。
Uniswap中的代币操作都是基于ERC20
类型,但是ETH本身既不是ERC20
,也没有合约地址,因此为了ETH
也能参与swap,需要先将ETH
转换成WETH
,再进行后续的操作。Uniswap为了减少用户手动转换的麻烦,会在有ETH
参与的交易中自动执行ETH
与WETH
的相互转换,因此需要记录下WETH
的合约地址。
receive
方法中限制了只允许接收来自WETH
合约的ETH,即调用withdraw
方法取出ETH,除此之外不可直接向合约中转入ETH。
addLiquidity
addLiquidity
是向合约添加流动性的方法,其主要逻辑在_addLiquidity
中,根据用户提供的token数量,再根据流动性池中已有的token数量,计算出实际参与添加流动性的token数量,返回两个uint值:
function _addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
// create the pair if it doesn't exist yet
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}
第一步判断交易对是否存在,如果不存在那么调用facotry
创建一个新的交易对。
如果此时流动性池为空,那么用户提供的数量就是最后实际添加到池子中的数量,无需进一步计算;但如果池子非空,就需要通过UniswapV2Library
中的quote
方法去计算合理的数量。
quote方法如下:
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
amountB = amountA.mul(reserveB) / reserveA;
}
逻辑很简单,就是根据A和B当前数量的比值,计算新增数量的A需要匹配多少数量的B,保证最终池子内A与B的比值不变。
回到_addLiquidity
的逻辑,先根据A传入的数量去计算出需要多少相匹配的B,如果传入的B数量满足,那么就以amountADesired, amountBOptimal
作为最后添加到流动性池子的数量;如果不满足,说明B相对池子的数量较少,那么就以B的数量为基准,反过来去计算所需要A的数量。在计算中,还需要满足amountMin
的限制。
了解了主要逻辑之后,再回归到addLiquidity
方法本身就很简单了:
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
}
pairFor
方法就是之前提到过的唯一pair地址生成器,根据factory,tokenA和tokenB的地址就能生成对应的pair地址,无需去factory中查询。
safeTransferFrom
是uniswap封装的转账方法,因为标准的ERC20实现中tranferFrom要求返回bool,但是实际有许多代币在实现的时候并没有遵守这一规则,导致返回内容各不相同,还可能不返回,因此通过底层调用的绕过类型检查的限制,并且手动根据返回的data元数据进行判断调用是否成功,保证了对不同token的兼容。
function safeTransferFrom(
address token,
address from,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::transferFrom: transferFrom failed'
);
}
addLiquidityETH
addLiquidityETH
的使用场景是交易中存在一方为ETH的时候,需要执行前面提到的WETH
转换操作,并且ETH是通过msg.Value
的形式传递的,所以对于多余的部分,需要手动执行退回。
function addLiquidityETH(
address token,
uint amountTokenDesired,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
(amountToken, amountETH) = _addLiquidity(
token,
WETH,
amountTokenDesired,
msg.value,
amountTokenMin,
amountETHMin
);
address pair = UniswapV2Library.pairFor(factory, token, WETH);
TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
IWETH(WETH).deposit{value: amountETH}();
assert(IWETH(WETH).transfer(pair, amountETH));
liquidity = IUniswapV2Pair(pair).mint(to);
// refund dust eth, if any
if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}
removeLiquidity
removeLiquidity
的基本逻辑:
- 获取交易对
Pair
- 将
sender
的LP token发送
到Pair
- 调用
burn
方法,销毁LP token
,将两种token
发回给用户,并得到tokenA
和tokenB
的数量 - 保证数量满足
min
的要求
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}
removeLiquidityETH
removeLiquidityETH
同样是用于ETH参与交易对的场景,可以看到这里直接调用了removeLiquidity
,但调用的时候to
参数传的是路由合约的地址address(this)
,这意味着burn
取回流动性之后,代币会先发送到路由合约上。因此下面的逻辑补上了从路由合约将token和ETH转回到to地址的过程。
function removeLiquidityETH(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
(amountToken, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
TransferHelper.safeTransfer(token, to, amountToken);
IWETH(WETH).withdraw(amountETH);
TransferHelper.safeTransferETH(to, amountETH);
}
这么写是因为:
- 需要处理
WETH
和ETH
的转换,因此必须将WETH
先取出,存到路由合约中 - 复用了
removeLiquidity
逻辑,简化代码
其他remove
uniswap中还支持了removeLiquidityWithPermit
和removeLiquidityETHSupportingFeeOnTransferTokens
这两种类型,其中WithPermit
是基于EIP712实现的链下签名代执行的方法,而SupportingFeeOnTransferTokens
则是支持特殊的ERC20token,这种token会在交易的过程中收取手续费或者燃烧,因为不涉及核心逻辑,所以就不深入了。
swap
swap有四种类型:
- swapExactTokensForTokens,拿指定数量的A换B
- swapTokensForExactTokens,拿A换指定数量的B
- swapExactETHForTokens,拿指定数量的ETH换token
- swapTokensForExactETH,拿ETH换指定数量的token
可以看到,关键的区别在于先确定输入还是先确定输出,以及是否有ETH的参与。
以swapExactTokensForTokens
为例:
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
path
是token转换的路径,因为对于用户想要提供A换取B的场景, 可能没有现成的A-B池子,那么就需要一条路径,先将A换成C,再从C换成B,最典型的C就是WETH
,因为绝大部分的代币都会优先提供和WETH
组成的交易对,那么只要通过WETH,基本上就可以实现任意两种代币的兑换。
根据path
可以得到amounts
,即转换路径上每种代币应有的数量,因为这里是已知输入的方法,所以用到了getAmountsOut
方法:
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[0] = amountIn;
for (uint i; i < path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}
getAmountsOut
即轮询path中的代币组合,模拟token
的swap
;getAmountOut
是对于已知reserve
的pair
,提供amountIn
得到amountOut
。
getAmountOut
中是以下数学逻辑的实现:
交换前:x × y = k
交换后:(x + Δx) × (y - Δy) = k因为k是常数,所以:
x × y = (x + Δx) × (y - Δy)展开:
x × y = x × y - x × Δy + Δx × y - Δx × Δy简化:
0 = -x × Δy + Δx × y - Δx × Δy
x × Δy = Δx × y - Δx × Δy
x × Δy = Δx × (y - Δy)求解Δy:
Δy = (Δx × y) / (x + Δx)
也就是amountOut = (amountIn × reserveOut) / (reserveIn + amountIn)
。
因为uniswap中会收取0.3%的手续费,所以实际的amountIn是 amountIn *997/100
,为了避免浮点数运算,分子分母都乘以1000,最终得到amountOut = (amountIn × 997 × reserveOut) / (reserveIn × 1000 + amountIn × 997)
。
计算出amounts
后,将input token
发送到即path[0]
和path[1]
组成的流动性池,调用_swap
进行链式的交换,直到最终得到output
。
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
for (uint i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
uint amountOut = amounts[i + 1];
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}
_swap主要做了参数的处理工作,遍历path
和amounts
得到input
,output
,amount0Out
,amount1Out
等参数,传入Pair合约的swap
方法中进行实际的swap工作。
注意的几个点:
amountOut
等于amounts[i+1]
且需要分配给非input
的token
作为amount
。- swap的时候,
path[i]
和path[i+1]
的输出token要发给path[i+1]
和path[i+2]
的pair池子,所以当i=path.length-2
的时候,i+1
为最后一个token,此时发送的对象为_to
,也就是输出给指定的用户地址而非Pair合约。
swapExactETHForTokens
swapExactETHForTokens
的逻辑基本类似,但是所有用到ETH
的地方都必须做WETH
的转换,比如一开始就要求 path[0]
必须为WETH
。然后将ETH
转换为WETH
后发给第一个交易对,开始swap
的流程。
function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
external
virtual
override
payable
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
IWETH(WETH).deposit{value: amounts[0]}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
_swap(amounts, path, to);
}
总结
在Router中主要实现的是对于参数的处理,无论是流动性的变更还是swap,在用户提供了token
和amount
之后,路由合约会进行相应的计算,得到满足条件的amount
参与到swap流程中,保证了传递给swap方法的参数合法性。同时也要负责多链路swap的有序进行,实现不同流动性池之间的传递。
UniswapV2Periphery 源码学习的更多相关文章
- Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结
2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...
- jQuery源码学习感想
还记得去年(2015)九月份的时候,作为一个大四的学生去参加美团霸面,结果被美团技术总监教育了一番,那次问了我很多jQuery源码的知识点,以前虽然喜欢研究框架,但水平还不足够来研究jQuery源码, ...
- MVC系列——MVC源码学习:打造自己的MVC框架(四:了解神奇的视图引擎)
前言:通过之前的三篇介绍,我们基本上完成了从请求发出到路由匹配.再到控制器的激活,再到Action的执行这些个过程.今天还是趁热打铁,将我们的View也来完善下,也让整个系列相对完整,博主不希望烂尾. ...
- MVC系列——MVC源码学习:打造自己的MVC框架(三:自定义路由规则)
前言:上篇介绍了下自己的MVC框架前两个版本,经过两天的整理,版本三基本已经完成,今天还是发出来供大家参考和学习.虽然微软的Routing功能已经非常强大,完全没有必要再“重复造轮子”了,但博主还是觉 ...
- MVC系列——MVC源码学习:打造自己的MVC框架(二:附源码)
前言:上篇介绍了下 MVC5 的核心原理,整篇文章比较偏理论,所以相对比较枯燥.今天就来根据上篇的理论一步一步进行实践,通过自己写的一个简易MVC框架逐步理解,相信通过这一篇的实践,你会对MVC有一个 ...
- MVC系列——MVC源码学习:打造自己的MVC框架(一:核心原理)
前言:最近一段时间在学习MVC源码,说实话,研读源码真是一个痛苦的过程,好多晦涩的语法搞得人晕晕乎乎.这两天算是理解了一小部分,这里先记录下来,也给需要的园友一个参考,奈何博主技术有限,如有理解不妥之 ...
- 我的angularjs源码学习之旅2——依赖注入
依赖注入起源于实现控制反转的典型框架Spring框架,用来削减计算机程序的耦合问题.简单来说,在定义方法的时候,方法所依赖的对象就被隐性的注入到该方法中,在方法中可以直接使用,而不需要在执行该函数的时 ...
- ddms(基于 Express 的表单管理系统)源码学习
ddms是基于express的一个表单管理系统,今天抽时间看了下它的代码,其实算不上源码学习,只是对它其中一些小的开发技巧做一些记录,希望以后在项目开发中能够实践下. 数据层封装 模块只对外暴露mod ...
- leveldb源码学习系列
楼主从2014年7月份开始学习<>,由于书籍比较抽象,为了加深思考,同时开始了Google leveldb的源码学习,主要是想学习leveldb的设计思想和Google的C++编程规范.目 ...
- 【iScroll源码学习04】分离IScroll核心
前言 最近几天我们前前后后基本将iScroll源码学的七七八八了,文章中未涉及的各位就要自己去看了 1. [iScroll源码学习03]iScroll事件机制与滚动条的实现 2. [iScroll源码 ...
随机推荐
- UFT 获取linux log
- vscode GDB远程调试安卓
如果是比较新的android ndk的版本,建议使用lldb进行调试,参考:vscode lldb远程调试 - OpenFDE - OpenFDE Docs,将lldbserver push到移动端, ...
- XXL-JOB分布式任务调度平台
简介 概述 XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速.学习简单.轻量级.易扩展.现已开放源代码并接入多家公司线上产品线,开箱即用. 特性 1.简单:支持通过Web页面对任务进行 ...
- pg 锁表 取消或者中断正在执行的命令
查询出锁表的pid 进行中断或者取消 --取消后台操作,回滚未提交事物 select pg_cancel_backend(pid) --中断session,回滚未提交事物 select pg_term ...
- SQL server跨库链接服务器
SQL server进阶技能篇:SQL的跨库查询与链接服务器 - 知乎 (zhihu.com)各位小伙伴们,关于MSSQL的基本技能篇前面一共写了10篇,也基本上算是告一段落,接下来将开始介绍进阶技能 ...
- Visual Studio2019 的 Git增加忽略文件
https://www.cnblogs.com/wucg/archive/2011/08/16/2141647.html Git增加忽略文件 如果是第一次,在提交得时候选择要忽略得文件就行, 如果已经 ...
- java--jdbc加强
事务 事务使指一组最小逻辑操作单元,里面有多个操作组成. 组成事务的每一部分必须要同时提交成功,如果有一个操作失败,整个操作就回滚. 事务ACID特性 原子性(Atomicity) 原子性是指事务 ...
- jqgrid工作记录1(单元格内容过长三个点显示,表头自动换行,取消滚动条预留空间)
①单元格内容过长三个点显示 效果如下: 页面加入css: 1 <style> 2 .ui-jqgrid tr.jqgrow td { 3 white-space: nowrap; 4 te ...
- java 套接字
简介 RT code package com.kuang; import java.io.ByteArrayOutputStream; import java.io.IOException; impo ...
- Ansys 圣维南原理
简介 圣维南原理 分布于弹性体上一小块面积(或体积)内的荷载所引起的物体中的应力,在离 荷载作用区稍远的地方,基本上只同荷载的合力和合力矩有关:荷载的具体分布只 影响荷载作用区附近的应力分布.还有一种 ...