uniswap的core代码分为两部分,FactoryPair,其中Factory是工厂合约,主要用来创建交易对,而Pair就是交易对合约,控制LP的mintburn,以及用户的swap交易。

Factory

首先来看一下Factory合约,定义了四个变量:

  address public feeTo;
address public feeToSetter; mapping(address => mapping(address => address)) public getPair;
address[] public allPairs; constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}

feeTofeeToSetter负责协议手续费的去向控制,构造合约的时候需要设置feeToSetter,做好权限控制。

getPairallPairs用于记录所有的流动性交易对以及映射关系。

  function setFeeTo(address _feeTo) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeTo = _feeTo;
} function setFeeToSetter(address _feeToSetter) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeToSetter = _feeToSetter;
}
}

提供了两个可以用来修改手续费setterto地址的方法。

最重要的核心就是下面的createPair,用于创建交易对。

  function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
IUniswapV2Pair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}

权限控制

第一部分的代码写了三个require:

  • 交易对两端token不可相同
  • 交易对token不可为零地址
  • 交易对还未创建

可以看到这里对tokenAtokenB做了一个排序,这是为了保证唯一性,不论传入什么样顺序的交易对,都能输出一样的结果。

因为token0小于token1,所以在检查零地址的时候只需要检查一个即可。

部署合约

此处部署了新交易对的合约,使用了内联assemblycreate2是创建合约的方法,它有一个特性就是创建得到的地址可预测:

address = keccak256(
0xff, // 固定前缀
deployer, // 部署者地址(Factory)
salt, // 盐值
keccak256(bytecode) // 字节码哈希
)

这里的salt是用交易对中两个token的地址生成的,这也就意味着对于任意一对token,最终生成的合约地址是唯一且可预测的,即使合约没部署也可以通过计算提前知道合约地址。

初始化

部署好合约后,调用了initialize()对合约进行了初始化,并在map里登记了交易对互相之间的映射关系,然后发出一条event,标志着交易对创建完成。

为什么使用initialize调用进行初始化,而不是在create创建合约的时候通过构造函数初始化呢?

这是因为如果定义了构造函数,那在create的时候传入的字节码里就需要带上参数类型并且传入实参,导致最终得到的hash都不相同。

特别是外部合约或者其他代码中计算pair address时,只需要传入一个固定的常量creationCodeHash即可(直接由uniswap分享出来),而不需要试图去获取uniswap的creationCode(得不到)。

Pair

Pair合约是uniswap core代码里面最复杂的部分,负责交易对的相关内容。

首先从变量定义开始:

uint public constant MINIMUM_LIQUIDITY = 10**3;
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));

两个常量MINIMUM_LIQUIDITYSELECTOR

其中MINIMUM_LIQUIDITY是对最小流动性的要求,在初次添加流动性的时候会有MINIMUM_LIQUIDITY数量的LP token被永久锁定,即使所有的LP都赎回,也保证了池子不会被抽干,LP计算公式永远有效。

SELECTOR的预定义是solidity中节约gas的方法,提前计算selector的字节码在合约编译的时候写入,后续调用的时候就无需花费gas重复计算。

uint112 private reserve0;           // uses single storage slot, accessible via getReserves
uint112 private reserve1; // uses single storage slot, accessible via getReserves
uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves

reserve是流动性池中代币的存量,但是和单纯的balance不同,因为合约是支持接收转账的,所以balance的数量可能因为其他的行为而发生改变,但reserve的值是统计所有符合Pair逻辑的行为之后得到的流动性池中合法的代币存量。

所以reserve并不是一个实时量,而是需要依赖更新操作,因此还需要一个时间变量blockTimestampLast来记录上次的更新时间。

在数据类型的设计上,reserve用了uint112而不是uint256之类常见的int长度,这是因为blockTimestampLast需要32位存储,对于一个uint256来说,还剩下224位,正好分给两个reserve,112位已经能够满足单个代币的供应量。

这种设计可以将三个变量放在一个slot中,节约存储空间,减少gas的使用。

在solidity中支持任意8的倍数的int类型,如uint16uint32都是可以的

address public factory;
address public token0;
address public token1;

定义了最基本的三个元素:

  • factory,创建工厂的地址,避免非法调用
  • token0token1交易对的两侧
uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint public kLast;

priceCumulativeLast代表了代币价格的累积值,用于计算代币的时间加权平均价格(TWAP),可以提供给外部作为预言机使用。

kLast是上次k值(x与y的乘积常量)变动时存储的值,使用场景在协议手续费的计算中。

event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);

Pair里面有四个事件,分别代表着LP的添加和减少,代币的swap,还有流动性池数量的更新。

mint

function mint(address to) external lock returns (uint liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1); bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity); _update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
}

mint是Pair里面的关键方法之一,调用时间在合约转入流动性池资产之后,根据转入的数量会给对应的用户mint出LP token。

首先通过reservebalance的差值计算出amount,也就是用户转入作为lp的代币数量。

feeOn是uniswap中手续费的设计,不影响主流程,放到最后再讲。

_mintFee之后,读取了当前lp token的总供应量,这里有两个注意的点:

  • 顺序问题,_mintFee中会影响supply的数量,所以必须在其之后读取
  • gas优化问题,在方法如果要读取合约的成员变量,应当使用一个临时变量去做记录,方法内变量的使用gas要低于读取合约的变量。

根据totalSupply分成两种逻辑:

  • 初次添加流动性,计算公式为$\sqrt {x*y}$,额外还需要减去MINIMUM_LIQUIDITY,这也是上面提到过的锁定流动性,然后这部分流动性会被打到零地址去。
  • 正常有池子的情况下流动性的计算公式是$\frac{totalSupply}{reserve}*amount$,也就是保证totalSupplyreserve比值不变的情况下增加amount的数量,
    • 如果在添加单个代币流动性的情况下,直接这么计算就可以,用户得到的LP token价值与当前流动性池子内的LP token价值是相等的。
    • 如果是双代币添加,那么就要取两个值中的较小值。
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// * never overflows, and + overflow is desired
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}

block.timestamp的类型是uint256,但这里只保留了低32位,这样设计是因为uniswap中用的是时间差值而非时间本身,即 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired,因为是无符号整数,只要两次时间的差值不超过uint32的表示范围,那么即使是溢出取模的情况下依然可以保证差值是正确的。

在计算Cumulative的时候要注意,这里出现了UQ112x112UQ112x112是uniswap自己实现的库,作用是用一个uint224表示定点数,整数和小数部分各分配112位,这是因为solidity没有原生的小数类型,而此处又涉及到了除法。encode的作用是将uint值左移112位,右边的112位用于表示小数,uqdiv是UQ112x112中自定义的除法,计算的结果依然是UQ112x112类型。

回到方法本身,timeElapsed * (reserve1/reserve0)表示price(reserve1/reserve0)持续了timeElapsed这么久,称为时间加权的价格累计。使用的时候将两个时间点的累积值相减再除以间隔时间,就可以得到这段时间内的时间加权平均价格。

最后更新合约变量,输出事件,_update结束。

burn

function burn(address to) external lock returns (uint amount0, uint amount1) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)]; bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this)); _update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);
}

在mint中,可以看到计算提取出代币数量amount的时候,基数用的是balance/totalSupply而不是reserve,这是因为reserve的更新有滞后性,并且uniswap认为Pair中的所有资产都是属于LP的,即使是不通过合约方法存入的部分,都可以根据lp token获得分成。

其他部分与mint基本类似,就不重复说明了。

swap

function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); uint balance0;
uint balance1;
{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
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));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
} _update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}

可以看到swap方法的参数中只有amountOut的值,而没有amountIn,说明在swap中amountIn是依赖于amountOut计算出来的,并且在实现中是先转出out资产,再去判断in是否满足,这种设计有以下的原因:

  • 支持闪电贷功能,因为闪电贷的功能依赖于转出资产套利后再补回,用户先要得到out资产才可以
  • 保证balance的正确性,因为swap中的流动性池需要满足常数k条件,必须用新的balance参与计算得到另一个token的balance

进入具体方法里面,首先是对数值有效性的判断,然后就直接将amountOut通过_safeTransfer转给了to地址,这也是我们前面提到的先outin

转账之后做了一个data长度的判断,此处就是对闪电贷支持的实现,借贷的对象需要实现IUniswapV2Callee中的uniswapV2Call方法供uniswap调用,并在其中实现套利-还款的逻辑,保证最终的amountIn与out的资产相匹配。

amountIn的计算公式是:balance - (_reserve - amountOut),虽然通常在dex中都是单边输入单边输出,但swap的底层实现其实是支持双输出和双输入的,只要保证最后的余额满足常数k的约束即可。

uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));是uniswap计算手续费的公式,首先mul(1000)是为了用整数的精度计算,其实等价于为balance-(amountIn*0.003),即收取amountIn 0.3%的手续费。 而require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');规定了收取手续费之后的balance的常数k需要不小于流动性池现有存储的常数k。那么再加上手续费,流动性池的k值其实是上升的,意外着LP能够兑换的资产也变多了,所有的LP都能够通过手续费受益。

对于uniswap来说,用户发起swap后会马上用户转出out资产,但在最后结算时,合约中必须新增满足条件的in资产,即扣减手续费之后流动性池的常数k值不能减少,至于中间发生了什么,合约并不关心。

协议费

uniswap中协议费是可以手动控制开启关闭的,协议费的来源就是手续费,开启feeOn的情况下,uniswap可以从手续费中得到协议分成。

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;
}
}

协议费的公式可以写作:协议费LP代币 = S × (rootK - rootKLast) / (5 × rootK + rootKLast),其中S为totalSupply,这个公式是由[S × (rootK - rootKLast)/rootKLast] × (1/6)得到的,也就是要分成从上次kLast到这次k中间新增LP数量的1/6。

kLast只有在swap的过程中才会变更,并且保证了k缓慢增长的时候,协议能够根据这些额外增长的k去mint出LP token,再根据LP token来获取分成受益。

如果池子里:

  • 纯粹的 mint/burn 操作(不涉及 swap)
  • 没有交易活动的静态池子
  • 刚刚收取过协议费的池子(此时 kLast 会被更新)

那么也就无法计算出协议费,因为k未改变。

Uniswap core源码学习的更多相关文章

  1. ASP.NET Core源码学习(一)Hosting

    ASP.NET Core源码的学习,我们从Hosting开始, Hosting的GitHub地址为:https://github.com/aspnet/Hosting.git 朋友们可以从以上链接克隆 ...

  2. ASP.NET Core 源码学习之 Options[2]:IOptions

    在上一篇中,介绍了一下Options的注册,而使用时只需要注入IOption即可: public ValuesController(IOptions<MyOptions> options) ...

  3. ASP.NET Core 源码学习之 Options[1]:Configure

    配置的本质就是字符串的键值对,但是对于面向对象语言来说,能使用强类型的配置是何等的爽哉! 目录 ASP.NET Core 配置系统 强类型的 Options Configure 方法 源码解析 ASP ...

  4. ASP.NET Core 源码学习之 Options[4]:IOptionsMonitor

    前面我们讲到 IOptions 和 IOptionsSnapshot,他们两个最大的区别便是前者注册的是单例模式,后者注册的是 Scope 模式.而 IOptionsMonitor 则要求配置源必须是 ...

  5. ASP.NET Core 源码学习之 Logging[1]:Introduction

    在ASP.NET 4.X中,我们通常使用 log4net, NLog 等来记录日志,但是当我们引用的一些第三方类库使用不同的日志框架时,就比较混乱了.而在 ASP.Net Core 中内置了日志系统, ...

  6. ASP.NET Core 源码学习之 Logging[2]:Configure

    在上一章中,我们对 ASP.NET Logging 系统做了一个整体的介绍,而在本章中则开始从最基本的配置开始,逐步深入到源码当中去. 默认配置 在 ASP.NET Core 2.0 中,对默认配置做 ...

  7. ASP.NET Core 源码学习之 Logging[3]:Logger

    上一章,我们介绍了日志的配置,在熟悉了配置之后,自然是要了解一下在应用程序中如何使用,而本章则从最基本的使用开始,逐步去了解去源码. LoggerFactory 我们可以在构造函数中注入 ILogge ...

  8. 【ASP.NET Core 】ASP.NET Core 源码学习之 Logging[1]:Introduction

    在ASP.NET 4.X中,我们通常使用 log4net, NLog 等来记录日志,但是当我们引用的一些第三方类库使用不同的日志框架时,就比较混乱了.而在 ASP.Net Core 中内置了日志系统, ...

  9. ASP.NET Core 源码学习之 Options[3]:IOptionsSnapshot

    在 上一章 中,介绍了 IOptions 的使用, 而我们知道,在ConfigurationBuilder的AddJsonFile中,有一个reloadOnChange参数,设置为true时,在配置文 ...

  10. ASP.NET Core 源码学习之 Logging[4]:FileProvider

    前面几章介绍了 ASP.NET Core Logging 系统的配置和使用,而对于 Provider ,微软也提供了 Console, Debug, EventSource, TraceSource ...

随机推荐

  1. FastDFS分布式文件服务器搭建以及Golang和Python调用

    FastDFS 1.介绍 FastDFS是基于http协议的分布式文件系统,其设计理念是一切从简.主要解决了海量数据存储的问题,特别适合系统中的中小文件的存储和在线服务.中小文件的范围大致为4KB-5 ...

  2. go 进阶训练营 微服务可用性(下)笔记

    降级: 减少工作量,丢弃不重要的请求. 确定具体采用哪个指标作为流量评估和优雅降级的决定性指标: 如 CPU.延迟.队列长度.线程数量.错误等 当服务进入降级时,需要执行什么动作? 流量抛弃或者优雅降 ...

  3. 蛟分承影,雁落忘归 —— 袋鼠云一站式全自动化运维管家 ChengYing(承影)正式开源

    ​ 原文地址: 交流蛟分承影,雁落忘归--袋鼠云一站式全自动化运维管家ChengYing(承影)正式开源 技术交流:30537511(钉钉群) 我们兴奋的向大家宣布一个好消息 DTstackCon新成 ...

  4. Dify发布V1.5.0:可视化故障排查!超实用

    Dify 本周又发布了一个实用的大版本,直接从 V1.4.3 版本干到 V1.5.0 了,那问题来了,这次更新了哪些内容呢?接下来我们一起来看. 官方给这次更新的定义是:一个简洁.强大的更新,通过简化 ...

  5. Elastic学习之旅 (1) 初识ElasticSearch

    大家好,我是Edison. 最近需要用到ElasticSearch,于是想要系统学习了解下,于是这就开始啦. 什么是ElasticSearch? ElasticSearch是一款开源的分布式搜索分析引 ...

  6. JavaScript Quine揭秘:如何让程序输出自身源代码?

    介绍 如何写一段javascript程序,输出自身的源代码?这个问题非常有意思,大家不妨先尝试一下,反正在尝试了半个小时之后,我果断放弃了. 这种能输出自身的程序在英文里被称为quine. 准备知识 ...

  7. Cursor 实战万字经验分享,与 AI 编码的深度思考

    (本文属于面向全公司的一次 AI 编码经验分享) 零 ❀ 引 在使用 cursor 编程的过程中,我知道大家偶尔会有如下感受: 我只是单纯想和 cursor 聊天聊问题,为什么 cursor 莫名其妙 ...

  8. cf908(div2)题解(补题)

    第一次akdiv2,赛后ak怎么不算是ak呢 比赛链接cf908div2 A 这题是个骗人题,整个比赛会停下来就是一个人赢够了回合数,那么在谁这停下来就是谁赢了整个比赛,不用管每回合赢得规则. #in ...

  9. 移动设备控制网络IO模块案例

    网络IO模块设备配置画面一 首先ip模式设置为动态获取,工作模式设置为tcp客户端模式,目的ip或域名设置为47.95.144.92,目的端口设置为9797,波特率设置为115200,这是这个页面需要 ...

  10. Wordpress设置必须登录才能查看内容

    参考文章地址 我是一个不会编程的小白,在网上查了好多篇的文章都没有实现这个功能.都是在改完php的代码后,网站就报废了.后来我还是求助了万能的谷歌,找了这篇文章. 上代码.大概猜测了一下,就是判断你现 ...