前置介绍
发现一个叫 Uniswap 的项目非常火,这是一个类似交易所一样的交易平台,所有全部是合约来执行的。因为没有上币费用,而且任何人都可以上架任何币。身边越来越多的人已经开始使用它了,被身边朋友安利,我也在 Uniswap 上购买了最新比较火的 AMPL 币,全程下来的体验还是很赞的。
这个项目的合约由两部分代码组成,核心是v2-core,配合的是v2-periphery;对应的如下:
- Uniswap Github 代码:
- v2-core: https://github.com/Uniswap/v2-core
- 这里有 3 个合约文件:
UniswapV2ERC20.sol
: 实现 ERC20 标准方法UniswapV2Factory.sol
: 工厂合约,用于创建 Pair 合约(以及设置协议手续费接收地址)UniswapV2Pair.sol
: Pair(交易对)合约,定义和交易有关的几个最基础方法,如 swap/mint/burn,价格预言机等功能,其本身是一个 ERC20 合约,继承UniswapV2ERC20
- v2-periphery: https://github.com/Uniswap/v2-periphery
- 这里有 1 个合约文件:
UniswapV2Router02.sol
- v2-core: https://github.com/Uniswap/v2-core
- UniswapV2 合约地址:
- UniswapV2Router02: 0x7a250d5630b4cf539739df2c5dacb4c659f2488d
- UniswapV2Factory: 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f
v2-core : 只处理最核心的 LP 创建和手续费设置,以及资产的 mint
/ burn
/ swap
等。除此之外的数据转换等均不关心。
v2-periphery : 针对用户使用场景提供多种封装方法,比如用户想买 TokenA 这种币。池子里只有 WETH/TokenA
的交易对。此时用户手里用户 TokenB,想用 TokenB 去买 TokenA,智能先把 TokenB 转为 WETH,中间的资产转换处理逻辑就是 v2-periphery 做的事情,这些是属于数据整合的事情,一旦牵扯资产的互换,则内部依赖 v2-core 核心 。
依赖关系大概如下:
v2-periphery | UniswapV2Router02
------------------------------------
| UniswapV2ERC20
| ⬇
v2-core | UniswapV2Pair mint / burn / swap
| ⬆
| UniswapV2Factory createPair
篇幅有限,这里只分析 v2-core 仓库下的合约。
功能的简单预览:
这里核心逻辑在 UniswapV2Pair.sol
合约, UniswapV2Factory
核心是创建池子(createPair
)以及相关的辅助 feeTo、setFeeTo
和feeToSetter、setFeeToSetter
。createPair
内部是使用 UniswapV2Pair
的 bytecode,使用 token0
和 token1
排序 encodePacked 后作为 salt,然后使用type(UniswapV2Pair).creationCode
的 bytecode 做 create2
.这样做的好处首先是可以预先知道要创建的合约地址,然后是避免重复创建。通过 createPair 创建出来的是一个两种代币组成的交易对,叫pair
,俗称"池子"。
通过 UniswapV2Factory 可以创建一个个 Pair(交易池),Pair 的核心逻辑在 UniswapV2Pair 内,LP 的形态是由 UniswapV2ERC20 来决定的。
- 每个交易对有一些基本属性:
reserve0
/reserve1
以及totalSupply
。reserve0
/reserve1
是交易对中两种代币的储存量。totalSupply
是当前流动性代币的总量。每个交易对都对应一个流动性代币(LP Token / liquidity provider token)。LP Token 记录了所有流动性提供者的贡献。所有 LP Token 的总和就是totalSupply
。- Pair 中管理了
reserve0
/reserve1
以及totalSupply
。用户向合约质押两种 Token,会获得流动性 Token 作为凭证,totalSupply 是所有 Token。
- Uniswap 协议的思想是
reserve0
*reserve1
的乘积不变。
下面按照合约来逐个分析
UniswapV2Factory.sol
UniswapV2ERC20.sol
UniswapV2Pair.sol
第一、 UniswapV2Factory.sol
- 只读方法
- 构造函数 (部署时候需要的配置)
- 核心方法
- 辅助方法
- 管理方法
- 合约事件
1)只读方法
feeTo()
: 获取手续费的接收地址。feeToSetter()
: 获取feeTo
的设置者getPair(tokanA,tokenB)
: 通过 token 地址获取 pair 地址。allPairs(pair_id)
: 通过索引获取 pair 地址allPairsLength()
: 获取已经生成的 piar 数量
2)构造函数 (部署时候需要的配置)
constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}
这里部署的时候使用的是 0xc0a4272bb5df52134178Df25d77561CfB17ce407
,这里仅设置了 feeToSetter
没有设置 feeTo
。
3)核心方法
只有一个 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);
}
函数的前四行代码是第一部分,核心是将 token0 token1 将地址进行排序,并确保参数符合预期。
- 将地址进行排序:确保
token0
字面地址小于token1
,这可以 A,B 两个地址无论什么顺序传入,得到的 token0 和 token1 结果都是一样的。 - 确保参数符合预期:
- AB 两个 Token 不能相同,否则抛错
'UniswapV2: IDENTICAL_ADDRESSES'
- Token 都不能等于
address(0)
,否则抛错'UniswapV2: ZERO_ADDRESS'
- 没有创建过 pair,抛错
'UniswapV2: PAIR_EXISTS'
- AB 两个 Token 不能相同,否则抛错
函数的 5 至 10 行是第二部分,核心是创建 pair,并且初始化。
- 第 5 行,bytecode 使用 UniswapV2Pair 的创建代码。
- 为了确保
salt
的唯一性,将 token0 和 token1 作为 salt 生成的唯一方式。 - 然后使用 assembly 的方式创建 create2.
- 注: 实际上在最新版的 EMV 中,已经直接支持给
new
方法传递 salt 参数,如下所示:pair = new UniswapV2Pair{salt: salt}();
,因为 Uniswap v2 合约在开发时还没有这个功能,所以使用 assembly create2。
- 注: 实际上在最新版的 EMV 中,已经直接支持给
- 然后借助
IUniswapV2Pair
做新创建 pair 的初始化
函数的最后 4 行是做 pair 数据在本合约的记录信息。
- 为了兼容前端的搜索,
getPair
上兼容不同顺序 token 的搜索处理。 - 抛出事件
4)辅助方法
5)管理方法
6)合约事件
第二、 UniswapV2ERC20.sol
这个合约主要定义了 UniswapV2 的 ERC20 标准实现,代码比较简单。这里介绍下 permit 方法:
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 元交易”功能。EIP-712 定义了离线签名的规范,即 digest 的格式定义,用户签名的内容是其(owner)授权(approve)某个合约(spender)可以在截止时间(deadline)之前花掉一定数量(value)的代币(Pair 流动性代币),应用(periphery 合约)拿着签名的原始信息和签名后生成的 v, r, s,可以调用 Pair 合约的 permit 方法获得授权,permit 方法使用 ecrecover 还原出签名地址为代币所有人,验证通过则批准授权。
每个具体实现逻辑在 UniswapV2Pair
中。Pair 合约主要实现了三个方法:mint(添加流动性)、burn(移除流动性)、swap(兑换)。
第三、 UniswapV2Pair.sol
1.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);
// gas savings, must be defined here since totalSupply can update in _mintFee
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
// permanently lock the first MINIMUM_LIQUIDITY tokens
_mint(address(0), MINIMUM_LIQUIDITY);
} 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);
}
首先,getReserves()获取两种代币的缓存余额。在白皮书中提到,保存缓存余额是为了防止攻击者操控价格预言机。此处还用于计算协议手续费,并通过当前余额与缓存余额相减获得转账的代币数量。
因为在调用 mint 函数之前,在 addLiquidity 函数已经完成了转账,所以,从这个函数的角度,两种代币数量的计算方式如下:
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
_mintFee 用于计算协议手续费:关于协议手续费的计算公式可以参考白皮书。
// if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
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;
}
}
当前的 balance 是当前的 reserve 加上注入的流动性的代币数量。
// gas savings, must be defined here since totalSupply can update in _mintFee
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
// permanently lock the first MINIMUM_LIQUIDITY tokens
_mint(address(0), MINIMUM_LIQUIDITY);
} else {
liquidity = Math.min(
amount0.mul(_totalSupply) / _reserve0,
amount1.mul(_totalSupply) / _reserve1
);
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
流动性 liquidity 的计算方式在第一次提供流动性时和其他时候稍稍不同。mint 方法中判断,如果是首次提供该交易对的流动性,则根据根号 xy 生成流动性代币,并销毁其中的 MINIMUM_LIQUIDITY(即 1000wei);否则根据转入的代币价值与当前流动性价值比例铸造流动性代币。
第一次提供流动性的计算公式如下:
liquidity = sqrt(x0*y0) - min
其中 min 是 10^3。也就是说,第一次提供流动性是有最小流动性要求的。
其他提供流动性的计算公式如下:按照注入的流动性和当前的 reserve 的占比一致。
liquidity = min((x0/reserve0*totalsupply), (y0/reserve1*totalsupply))
2.burn
burn 函数用在抽取流动性。burn 逻辑和 mint 逻辑类似。
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);
// gas savings, must be defined here since totalSupply can update in _mintFee
uint _totalSupply = totalSupply;
// using balances ensures pro-rata distribution
amount0 = liquidity.mul(balance0) / _totalSupply;
// using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply;
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 类似,burn 方法也会先计算协议手续费。参考白皮书,为了节省交易手续费,Uniswap v2 只在 mint/burn 流动性时收取累计的协议手续费。移除流动性后,根据销毁的流动性代币占总量的比例获得对应的两种代币。
3.swap
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');
// optimistically transfer tokens
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
// optimistically transfer tokens
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');
{ // 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 操作支持两个方向的兑换,可以从 TokenA 换到 TokenB,或者 TokenB 换到 TokenA。
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
因为在swapExactTokensForTokens
的getAmountOut
函数已经确定兑换处的金额。所以,先直接转账。
在不做 swap 之前,balance 应该和 reserve 相等的。通过balance
和reserve
的差值,可以反推出输入的代币数量:
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');
为了兼容闪电贷功能,以及不依赖特定代币的 transfer 方法,整个 swap 方法并没有类似 amountIn 的参数,而是通过比较当前余额与缓存余额的差值来得出转入的代币数量。
由于在 swap 方法最后会检查余额(扣掉手续费后)符合 k 常值函数约束(参考白皮书公式),因此合约可以先将用户希望获得的代币转出,如果用户之前并没有向合约转入用于交易的代币,则相当于借币(即闪电贷);如果使用闪电贷,则需要在自定义的 uniswapV2Call 方法中将借出的代币归还。
在 swap 方法最后会使用缓存余额更新价格预言机所需的累计价格,最后更新缓存余额为当前余额。
// update reserves and, on the first call per block, price accumulators
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);
}
注意,其中区块时间戳和累计价格都是溢出安全的。(具体推导过程请参考白皮书)