Uniswap core源碼學習
uniswap的core代碼分為兩部分,Factory和Pair,其中Factory是工廠合約,主要用來創(chuàng)建交易對,而Pair就是交易對合約,控制LP的mint和burn,以及用戶的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;
}
feeTo和feeToSetter負責協(xié)議手續(xù)費的去向控制,構(gòu)造合約的時候需要設(shè)置feeToSetter,做好權(quán)限控制。
getPair和allPairs用于記錄所有的流動性交易對以及映射關(guān)系。
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;
}
}
提供了兩個可以用來修改手續(xù)費setter和to地址的方法。
最重要的核心就是下面的createPair,用于創(chuàng)建交易對。
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);
}
權(quán)限控制
第一部分的代碼寫了三個require:
- 交易對兩端token不可相同
- 交易對token不可為零地址
- 交易對還未創(chuàng)建
可以看到這里對tokenA和tokenB做了一個排序,這是為了保證唯一性,不論傳入什么樣順序的交易對,都能輸出一樣的結(jié)果。
因為token0小于token1,所以在檢查零地址的時候只需要檢查一個即可。
部署合約
此處部署了新交易對的合約,使用了內(nèi)聯(lián)assembly,create2是創(chuàng)建合約的方法,它有一個特性就是創(chuàng)建得到的地址可預測:
address = keccak256(
0xff, // 固定前綴
deployer, // 部署者地址(Factory)
salt, // 鹽值
keccak256(bytecode) // 字節(jié)碼哈希
)
這里的salt是用交易對中兩個token的地址生成的,這也就意味著對于任意一對token,最終生成的合約地址是唯一且可預測的,即使合約沒部署也可以通過計算提前知道合約地址。
初始化
部署好合約后,調(diào)用了initialize()對合約進行了初始化,并在map里登記了交易對互相之間的映射關(guān)系,然后發(fā)出一條event,標志著交易對創(chuàng)建完成。
為什么使用
initialize調(diào)用進行初始化,而不是在create創(chuàng)建合約的時候通過構(gòu)造函數(shù)初始化呢?
這是因為如果定義了構(gòu)造函數(shù),那在create的時候傳入的字節(jié)碼里就需要帶上參數(shù)類型并且傳入實參,導致最終得到的hash都不相同。
特別是外部合約或者其他代碼中計算pair address時,只需要傳入一個固定的常量creationCodeHash即可(直接由uniswap分享出來),而不需要試圖去獲取uniswap的creationCode(得不到)。
Pair
Pair合約是uniswap core代碼里面最復雜的部分,負責交易對的相關(guān)內(nèi)容。
首先從變量定義開始:
uint public constant MINIMUM_LIQUIDITY = 10**3;
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
兩個常量MINIMUM_LIQUIDITY和SELECTOR。
其中MINIMUM_LIQUIDITY是對最小流動性的要求,在初次添加流動性的時候會有MINIMUM_LIQUIDITY數(shù)量的LP token被永久鎖定,即使所有的LP都贖回,也保證了池子不會被抽干,LP計算公式永遠有效。
而SELECTOR的預定義是solidity中節(jié)約gas的方法,提前計算selector的字節(jié)碼在合約編譯的時候?qū)懭?/strong>,后續(xù)調(diào)用的時候就無需花費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不同,因為合約是支持接收轉(zhuǎn)賬的,所以balance的數(shù)量可能因為其他的行為而發(fā)生改變,但reserve的值是統(tǒng)計所有符合Pair邏輯的行為之后得到的流動性池中合法的代幣存量。
所以reserve并不是一個實時量,而是需要依賴更新操作,因此還需要一個時間變量blockTimestampLast來記錄上次的更新時間。
在數(shù)據(jù)類型的設(shè)計上,reserve用了uint112而不是uint256之類常見的int長度,這是因為blockTimestampLast需要32位存儲,對于一個uint256來說,還剩下224位,正好分給兩個reserve,112位已經(jīng)能夠滿足單個代幣的供應量。
這種設(shè)計可以將三個變量放在一個slot中,節(jié)約存儲空間,減少gas的使用。
在solidity中支持任意8的倍數(shù)的int類型,如
uint16,uint32都是可以的
address public factory;
address public token0;
address public token1;
定義了最基本的三個元素:
factory,創(chuàng)建工廠的地址,避免非法調(diào)用token0和token1:交易對的兩側(cè)
uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint public kLast;
priceCumulativeLast代表了代幣價格的累積值,用于計算代幣的時間加權(quán)平均價格(TWAP),可以提供給外部作為預言機使用。
kLast是上次k值(x與y的乘積常量)變動時存儲的值,使用場景在協(xié)議手續(xù)費的計算中。
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,還有流動性池數(shù)量的更新。
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里面的關(guān)鍵方法之一,調(diào)用時間在合約轉(zhuǎn)入流動性池資產(chǎn)之后,根據(jù)轉(zhuǎn)入的數(shù)量會給對應的用戶mint出LP token。
首先通過reserve和balance的差值計算出amount,也就是用戶轉(zhuǎn)入作為lp的代幣數(shù)量。
feeOn是uniswap中手續(xù)費的設(shè)計,不影響主流程,放到最后再講。
在_mintFee之后,讀取了當前l(fā)p token的總供應量,這里有兩個注意的點:
- 順序問題,
_mintFee中會影響supply的數(shù)量,所以必須在其之后讀取 - gas優(yōu)化問題,在方法如果要讀取合約的成員變量,應當使用一個臨時變量去做記錄,方法內(nèi)變量的使用gas要低于讀取合約的變量。
根據(jù)totalSupply分成兩種邏輯:
- 初次添加流動性,計算公式為$\sqrt {x*y}$,額外還需要減去
MINIMUM_LIQUIDITY,這也是上面提到過的鎖定流動性,然后這部分流動性會被打到零地址去。 - 正常有池子的情況下流動性的計算公式是$\frac{totalSupply}{reserve}*amount$,也就是保證
totalSupply和reserve比值不變的情況下增加amount的數(shù)量,- 如果在添加單個代幣流動性的情況下,直接這么計算就可以,用戶得到的LP token價值與當前流動性池子內(nèi)的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位,這樣設(shè)計是因為uniswap中用的是時間差值而非時間本身,即 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired,因為是無符號整數(shù),只要兩次時間的差值不超過uint32的表示范圍,那么即使是溢出取模的情況下依然可以保證差值是正確的。
在計算Cumulative的時候要注意,這里出現(xiàn)了UQ112x112,UQ112x112是uniswap自己實現(xiàn)的庫,作用是用一個uint224表示定點數(shù),整數(shù)和小數(shù)部分各分配112位,這是因為solidity沒有原生的小數(shù)類型,而此處又涉及到了除法。encode的作用是將uint值左移112位,右邊的112位用于表示小數(shù),uqdiv是UQ112x112中自定義的除法,計算的結(jié)果依然是UQ112x112類型。
回到方法本身,timeElapsed * (reserve1/reserve0)表示price(reserve1/reserve0)持續(xù)了timeElapsed這么久,稱為時間加權(quán)的價格累計。使用的時候?qū)蓚€時間點的累積值相減再除以間隔時間,就可以得到這段時間內(nèi)的時間加權(quán)平均價格。
最后更新合約變量,輸出事件,_update結(jié)束。
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中,可以看到計算提取出代幣數(shù)量amount的時候,基數(shù)用的是balance/totalSupply而不是reserve,這是因為reserve的更新有滯后性,并且uniswap認為Pair中的所有資產(chǎn)都是屬于LP的,即使是不通過合約方法存入的部分,都可以根據(jù)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方法的參數(shù)中只有amountOut的值,而沒有amountIn,說明在swap中amountIn是依賴于amountOut計算出來的,并且在實現(xiàn)中是先轉(zhuǎn)出out資產(chǎn),再去判斷in是否滿足,這種設(shè)計有以下的原因:
- 支持閃電貸功能,因為閃電貸的功能依賴于轉(zhuǎn)出資產(chǎn)套利后再補回,用戶先要得到
out資產(chǎn)才可以 - 保證
balance的正確性,因為swap中的流動性池需要滿足常數(shù)k條件,必須用新的balance參與計算得到另一個token的balance
進入具體方法里面,首先是對數(shù)值有效性的判斷,然后就直接將amountOut通過_safeTransfer轉(zhuǎn)給了to地址,這也是我們前面提到的先out再in。
轉(zhuǎn)賬之后做了一個data長度的判斷,此處就是對閃電貸支持的實現(xiàn),借貸的對象需要實現(xiàn)IUniswapV2Callee中的uniswapV2Call方法供uniswap調(diào)用,并在其中實現(xiàn)套利-還款的邏輯,保證最終的amountIn與out的資產(chǎn)相匹配。
amountIn的計算公式是:balance - (_reserve - amountOut),雖然通常在dex中都是單邊輸入單邊輸出,但swap的底層實現(xiàn)其實是支持雙輸出和雙輸入的,只要保證最后的余額滿足常數(shù)k的約束即可。
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));是uniswap計算手續(xù)費的公式,首先mul(1000)是為了用整數(shù)的精度計算,其實等價于為balance-(amountIn*0.003),即收取amountIn 0.3%的手續(xù)費。 而require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');規(guī)定了收取手續(xù)費之后的balance的常數(shù)k需要不小于流動性池現(xiàn)有存儲的常數(shù)k。那么再加上手續(xù)費,流動性池的k值其實是上升的,意外著LP能夠兌換的資產(chǎn)也變多了,所有的LP都能夠通過手續(xù)費受益。
對于uniswap來說,用戶發(fā)起swap后會馬上用戶轉(zhuǎn)出out資產(chǎn),但在最后結(jié)算時,合約中必須新增滿足條件的in資產(chǎn),即扣減手續(xù)費之后流動性池的常數(shù)k值不能減少,至于中間發(fā)生了什么,合約并不關(guān)心。
協(xié)議費
uniswap中協(xié)議費是可以手動控制開啟關(guān)閉的,協(xié)議費的來源就是手續(xù)費,開啟feeOn的情況下,uniswap可以從手續(xù)費中得到協(xié)議分成。
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;
}
}
協(xié)議費的公式可以寫作:協(xié)議費LP代幣 = S × (rootK - rootKLast) / (5 × rootK + rootKLast),其中S為totalSupply,這個公式是由[S × (rootK - rootKLast)/rootKLast] × (1/6)得到的,也就是要分成從上次kLast到這次k中間新增LP數(shù)量的1/6。
kLast只有在swap的過程中才會變更,并且保證了k緩慢增長的時候,協(xié)議能夠根據(jù)這些額外增長的k去mint出LP token,再根據(jù)LP token來獲取分成受益。
如果池子里:
- 純粹的 mint/burn 操作(不涉及 swap)
- 沒有交易活動的靜態(tài)池子
- 剛剛收取過協(xié)議費的池子(此時 kLast 會被更新)
那么也就無法計算出協(xié)議費,因為k未改變。

浙公網(wǎng)安備 33010602011771號