Uniswap V2合约分为核心合约(core)和周边合约(periphery),均使用solidity语言编写。其核心合约实现了Uniswap V2的完整功能:创建交易对,流动性供给,交易代币,价格预言机等,但对用户操作不友好;而周边合约是用来让用户更方便的和核心合约交互。
UniswapV2核心合约主要由factory合约(UniswapV2Factory.sol)、交易对模板合约(UniswapV2Pair.sol)及辅助工具库与接口定义等三部分组成。
1.Uniswap V2Factory.sol
factory合约主要作用是创建流动性池
-
属性
feeTo
是开发者团队的地址。用于切换开发团队手续费开关,在uniswapV2中,会收取0.3%的手续费给LP
,如果这里的feeTo
地址是0,则表明不给开发者团队手续费,如果不为0,则开发者会收取0.05%手续费。
feeToSetter
是用于改变开发者团队地址
allPairs
是用于存放所有交易对(代币对)合约地址信息
address public feeTo;
address public feeToSetter;
address[] public allPairs;
-
映射和事件
getPair
前两个地址分别对应交易对中的两种代币地址,最后一个地址是交易对合约本身地址
PairCreated
事件在createPair
方法中触发,保存交易对的信息(两种代币地址,交易对本身地址,创建交易对的数量)
mapping(address => mapping(address => address)) public getPair;
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
-
构造函数
在部署合约时对feeToSetter
地址进行初始化
constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}
-
只读方法
getPair
方法和allPairs
方法不是出现在合约代码里,而是solidity的映射和属性如果是public
修饰,则自带getX
方法
allPairsLengh()
返回到目前为止通过工厂创建的交易对的总数。
setFeeToSetter()
更改开发者团队地址
function getPair(address tokenA, address tokenB) external view returns (address pair);
function allPairs(uint) external view returns (address pair);
function allPairsLength() external view returns (uint) {
return allPairs.length;
}
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;
}
-
状态改变方法
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);
}
2.Uniswap V2ERC20.sol
该合约主要是实现了ERC-20代币的功能
-
常量
定义Uniswap V2代币的名称、代币的标识符、代币的精度全局变量信息
string public constant name = 'Uniswap V2';
string public constant symbol = 'UNI-V2';
uint8 public constant decimals = 18;
-
定义变量
DOMAIN_SEPARATOR
是用于不同Dapp之间区分相同结构和内容的签名消息,该值有助于用户辨识哪些为信任的dapp
PERMIT_TYPEHASH
用于keccak256方法的参数
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
nonces
用于记录合约中每个地址使用链下签名消息的交易数量,防止重放攻击
bytes32 public DOMAIN_SEPARATOR;
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
mapping(address => uint) public nonces;
在以太坊中,用keccak哈希算法来计算公钥的256位哈希,再截取这256位哈希的后160位哈希作为地址值。
-
重要方法
abi.encodePacked
将输入的参数根据其所需最低空间编码,类似abi.encode
,但是会把其中填充的很多0
给省略。当我们想要省略空间,且不与合约进行交互,可以使用abi.encodePacked
、。例如:算一些数据的hash
可以使用。
keccak
算法是在以太坊中计算公钥的256位哈希,再截取这256位哈希的后160位哈希作为地址值。是哈希函数其中一种。
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}
permit
方法实现的就是白皮书2.5节中介绍的“Meta transactions for pool shares 元交易”功能。上述代码中的digest的格式定义是来自EIP_721
中的离线签名规范。
用户签名的内容是(owner)授权(approve)某个合约(spender)在截止时间(deadline)之前花掉一定数量(value)的代币(Pair流动性代币)。
periphery
合约拿着签名的原始信息和签名生成的v,r,s,可以调用permit方法获得授权,permit方法可以使用ecrecover
方法还原出签名地址为代币所有人,验证通过则批准授权。
为什么有permit函数?
permit
函数主要实现了用户验证与授权,Uniswap V2的core函数虽然功能完善,但是对于用户来说却极不友好,用户需要借助它的周边合约才能和核心合约进行交互,但是在设计到流动性供给是,比如减少用户流动性,此时用户需要将自己的流动性代币燃烧掉,而由于用户调用的是周边合约,所以在未经授权的情况下是无法进行燃烧操作的,此时如果安装常规操作,那么用户需要先调用交易对合约对周边合约进行授权,之后再调用周边合约进行燃烧操作,而这个过程形成了两个不同合约的两个交易(无法合并到一个交易中)。
如果我们通过线下消息签名,则可以减少其中一个交易,将所有操作放在一个交易里执行,确保了交易的原子性,在周边合约里,减小流动性来提取资产时,周边合约在一个函数内先调用交易对的permit
函数进行授权,接着再进行转移流动性代币到交易对合约,提取代币等操作,所有操作都在周边合约的同一个函数中进行,达成了交易的原子性和对用户的友好性。
3.Unsiwap V2Pair.sol
Uniswap V2Pair 继承自IUniswap V2Pair, Uniswap V2ERC20,其中IUniswap V2Pair中定义了必须要实现的接口
-
定义全局变量
MINIMUM_LIQUIDITY
定义了最小流动性,在提供初始流动性时会被燃烧掉
SELECTOR
是用于计算ERC-20合约中转移资产的transfer对应的函数选择器
factory
是要用于存储factory合约地址,token0,token1
分别表示两种代币的地址
blockTimestampLast
记录交易时的区块创建时间,reserve0,reserve1
分别表示最新恒定乘积中两种代币的数量
price0CumulativeLast
,price1CumulativeLast
变量用于记录交易对中两种价格的累计值
KLast
用于表示某一时刻恒定乘积中的积的值,主要用于开发团队手续费的计算
unlocked = 1
表示未被锁上的状态,用于下面的修饰器
uint public constant MINIMUM_LIQUIDITY = 10**3;
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
address public factory;
address public token0;
address public token1;
uint112 private reserve0;
uint112 private reserve1;
uint32 private blockTimestampLast;
uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint public kLast;
uint private unlocked = 1;
-
modifier修饰器
该modifier的流程为:在调用该lock修饰器的函数首先检查unlocked
是否为1,如果不是则报错被锁上
,如果是为1,则将unlocked
赋值为0(锁上),之后执行被修饰的函数体,此时unlocked
已成为0,之后等函数执行完之后再恢复unlocked
为1:
modifier lock() {
require(unlocked == 1, 'UniswapV2: LOCKED');
unlocked = 0;
_;//表示被修饰的函数内容
unlocked = 1;
}
-
构造器
初始化factory
constructor() public {
factory = msg.sender;
}
-
函数
getReserves()
用于获取交易对的资产数量和最近一次交易的区块时间
function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
_safeTransfer()
函数用于发送代币
此时会使用代币的call函数去调用代币合约transfer
来发送代币,在这里会检查call调用是否成功以及返回值是否为true:
function _safeTransfer(address token, address to, uint value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
}
_unpdate()
用于更新reserves并进行价格累计的计算
函数内部解释:
-
require
用于验证 balance0 和 blanace1 是否 uint112 的上限 -
blockTimestamp
只取后32位 -
timeElapsed
计算当前区块和上一个区块之间的时间差 -
if
语句是要时间差(两个区块的时间差,不是同一个区块)大于0并且两种资产的数量不为0,才可以进行价格累计计算,如果是同一个区块的第二笔交易及以后的交易,timeElapsed则为0,此时不会计算价格累计值。 -
更新
reserve0
和reserve1
;同时更新block时间为当前blockTimestampLast
时间,之后通过emit触发同步事件:
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);
}
_mintFee()
用于在添加流动性和移除流动性时,计算开发团队手续费
函数内部解释:
feeTo
获取开发者团队地址feeOn
如果不是全0地址,那么 feeOn = true_KLast
获取恒定乘积值if
判断手续费开关是否开启(即手续费接受地址是否为空),如果手续费开关打开则根据以下公式来计算手续费的值,公式中的k1为旧的乘积值,对应下述代码中的_klast,k2为新的乘积值:
-
rootK
相当于K2,rootKLast
相当于K1(都平方式为了好计算手续费值) -
if (rootK > rootKLast)
表明现在池中是在添加流动性,可以计算手续费(由上节uniswap V2原理详解文章提过每次用户交易资产,K会变大) -
numerator
,denominator
分别计算的是上述公式中的分子和分母 -
调用uniswap V2ERC20中的
_mint()
,传入开发者团队地址和收益
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
address feeTo = IUniswapV2Factory(factory).feeTo();
feeOn = feeTo != address(0);
uint _kLast = kLast;
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()
用于用户提供流动性时(提供一定比例的两种ERC-20代币)增加流动性代币给流动性提供者
函数内部解释:
- 参数
to
是接收流动性代币的地址,liquidity
是增加流动性的数值 getReserves()
获取库存交易对的资产数量balance0
和balance1
是流动性池中当前交易对的资产数量amount0
和amount1
是计算用户新注入的两种ERC20代币的数量feeOn
发送开发团队的手续费_totalSupply
是存储当前已发行的流动性代币的总量(之所以写在feeOn
后面,是因为在_mintFee()
中会更新一次totalSupply值)- 在
if
语句中,如果_totalSupply
为0,则说明是初次提供流动性,会根据恒定乘积公式的平方根来计算,同时要减去已经燃烧掉的初始流动性值,具体为MINIMUM_LIQUIDITY
;如果_totalSupply
不为0,则会根据已有流动性按比例增发,由于注入了两种代币,所以会有两个计算公式,每种代币按注入比例计算流动性值,取两个中的最小值。
8.调用_mint(to,liquidity)
来增发新的流动性给接收者,_update()
更新流动性池中两种资产的值。
- 检查手续费是否开启,如果开启了,则更新恒定乘积的值。
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);
}
burn()
用于燃烧流动性代币来提取相应的两种资产,并减少交易对的流动性
函数内部解释:
getReserves()
获取库存交易对的资产数量_token0
和token1
获取代币地址balance0
和balance1
获取交易对两种代币实际数量liquidity
获取当前合约中流动性代币feeOn
计算手续费给开发团队_totalSupply
是存储当前已发行的流动性代币的总量(之所以写在feeOn
后面,是因为在_mintFee()
中会更新一次totalSupply值)amopunt0
和amount1
按比例计算提取资产- 将用户转入的流动性代币燃烧(通过燃烧代币得到方式来提取两种资产)
- 将两种资产token转到对应的地址
- 获取现在两种资产的余额
根据白皮书可知,uniswap为了节省交易手续费,uniswap V2只在 mint/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);
}
swap()
用于交易对中资产交易
函数内部解释:
1.传入的参数amount0Out
,amount1Out
,to
以及data
分别是要购买的token0的数量,token1的数量,接收者的地址,接收后执行回调传递数据。
2.先判断要购买的token数量是否大于0,接着使用getReserves()
获取当前库存的交易对资产数量,并判断购买的token是否小于reserve
的值。
3.如果amount0Out
大于0,说明要购买token0
,则将token0
转给 to
;如果amount1Out
大于0,则说明要购买token1
,则将token1
转给to
4.调用合约的uniswapV2Call回调函数将data传递过去,普通交易调用这个data为空
5.获取此时交易对资产的余额
6.amount0In
通过当前余额和库存余额比较可得出汇入流动性池的资产数量(使用的是三目运算符)
7.更新恒定乘积公式,并且新的值要大于等于原来的值。
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;
{
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
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');
{
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);
}
参考文章:UniswapV2协议解析
参考文章:Uniswap V2白皮书