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的有序進行,實現不同流動性池之間的傳遞。

浙公網安備 33010602011771號