記得朋友圈看到過一句話,如果Defi是以太坊的皇冠,那么Uniswap就是這頂皇冠中的明珠,Uniswap目前已經是V2版本,相對V1,它的功能更加全面優化,然而其合約原始碼卻并不復雜,本文為個人學習UniswapV2核心合約原始碼的系列文章的第三篇,
在上一篇文章中已經學習了UniswapV2核心合約中的第二個原始碼–合約UniswapV2ERC20.sol的原始碼,這次我們來學習第三個核心合約–UniswapV2Pair.sol的原始碼,該合約是交易對合約,在其父合約UniswapV2ERC20的基礎上增加了資產交易及流動性供給等功能,
建議讀者在開始之前閱讀我的另一篇文章:UniswapV2介紹 來對UniswapV2的整體機制有個大致了解,這樣更有助于理解原始碼,
一、合約原始碼
照例先貼出合約原始碼,該合約不長,代碼只有202行(包括空行),但是相對于前面學習的兩個合約,卻復雜了許多,
學習該合約需要弄清下面這兩個概念:交易對中保存的恒定乘積計算公式中的兩種代幣的數量ValueA及交易對合約地址擁有的實際代幣數量ValueP,這兩者通常狀態下是相同,但在交易時會發生變化,交易完成后會將ValueA設定為ValueP的值,但某些特殊情況下,它們的值可能是不同的,例如有人由于某種原因誤向交易對合約發送了其中一種代幣而又沒有觸發交易,
另外,交易對本身也是一種ERC20合約,它的代幣用來代表流動性供給,合約本身不擁有自已的流動性代幣,所有代幣全部在流動性提供者手里,提供流動性時自動增發代幣給提供者,提取流動性時燃燒提供者的代幣,
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Pair.sol';
import './UniswapV2ERC20.sol';
import './libraries/Math.sol';
import './libraries/UQ112x112.sol';
import './interfaces/IERC20.sol';
import './interfaces/IUniswapV2Factory.sol';
import './interfaces/IUniswapV2Callee.sol';
contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
using SafeMath for uint;
using UQ112x112 for uint224;
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; // 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
uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event
uint private unlocked = 1;
modifier lock() {
require(unlocked == 1, 'UniswapV2: LOCKED');
unlocked = 0;
_;
unlocked = 1;
}
function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
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');
}
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);
constructor() public {
factory = msg.sender;
}
// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
token0 = _token0;
token1 = _token1;
}
// 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);
}
// 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;
}
}
// this low-level function should be called from a contract which performs important safety checks
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);
}
// this low-level function should be called from a contract which performs important safety checks
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);
}
// this low-level function should be called from a contract which performs important safety checks
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);
}
// force balances to match reserves
function skim(address to) external lock {
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
_safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
_safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}
// force reserves to match balances
function sync() external lock {
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}
}
二、原始碼中的簡單部分
下面我們分類來學習該合約的原始碼,注意,本文余下的內容中,闡述的第幾行均不包含空行,
-
pragma solidity =0.5.16;照例指定確定的Solidity編譯器版本, -
import './interfaces/IUniswapV2Pair.sol'; import './UniswapV2ERC20.sol';這兩行匯入了交易對需要實作的介面和交易對的父合約,
-
import './libraries/Math.sol';匯入一個自定義的Math庫,只有兩個功能,一個是求兩個uint的最小值,另一個是對一個uint進行開方運算, -
import './libraries/UQ112x112.sol';匯入自定義的資料格式庫,在UniswapV2中,價格為兩種代幣的數量比值,而在Solidity中,對非整數型別支持不好,通常兩個無符號整數相除為地板除,會截斷,為了提高價格精度,UniswapV2使用uint112來保存交易對中資產的數量,而比值(價格)使用UQ112x112表示,一個代表整數部分,一個代表小數部分, -
import './interfaces/IERC20.sol;匯入標準ERC20介面,在獲取交易對合約資產池的代幣數量(余額)時使用, -
import './interfaces/IUniswapV2Factory.sol';匯入factory合約相關介面,主要是用來獲取開發團隊手續費地址, -
import './interfaces/IUniswapV2Callee.sol';有些第三方合約希望接收到代幣后進行其它操作,好比異步執行中的回呼函式,這里IUniswapV2Callee約定了第三方合約如果需要執行回呼函式必須實作的介面格式,當然了,定義了此介面后還可以進行FlashSwap, -
contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {該行定義了本合約實作了IUniswapV2Pair并繼承了UniswapV2ERC20,繼承一個合約表明它繼承了父合約的所有非私有的介面與狀態變數, -
using SafeMath for uint;和using UQ112x112 for uint224;指定庫函式的應用型別, -
uint public constant MINIMUM_LIQUIDITY = 10**3;定義了最小流動性,它是最小數值1的1000倍,用來在提供初始流動性時燃燒掉, -
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));用來計算標準ERC20合約中轉移代幣函式transfer的函式選擇器,雖然標準的ERC20合約在轉移代幣后回傳一個成功值,但有些不標準的并沒有回傳值,在這個合約里統一做了處理,并使用了較低級的call函式代替正常的合約呼叫,函式選擇器用于call函式呼叫中, -
address public factory;,address public token0;,address public token1;用來記錄factory合約地址和交易對中兩種代幣的合約地址,注意它們是public的狀態變數,意味著合約外可以直接使用同名函式獲取對應的值, -
reserve0,reserve1和blockTimestampLast這三個狀態變數記錄了最新的恒定乘積中兩種資產的數量和交易時的區塊(創建)時間, -
price0CumulativeLast和price1CumulativeLast,記錄交易對中兩種價格的累計值, -
uint public kLast;記錄某一時刻恒定乘積中積的值,主要用于開發團隊手續費計算, -
uint private unlocked = 1; modifier lock() { require(unlocked == 1, 'UniswapV2: LOCKED'); unlocked = 0; _; unlocked = 1; }這段代碼是用來防重入攻擊的,在modifier(函式修飾器)中,
_;代表執行被修飾的函式體,所以這里的邏輯很好理解,當函式(外部介面)被外部呼叫時,unlocked設定為0,函式執行完之后才會重新設定為1,在未執行完之前,這時如果重入該函式,lock修飾器仍然會起作用,這時unlocked仍然為0,無法通過修飾器中的require檢查,整個交易會被重置,當然這里也可以不用0和1,也可以使用布爾型別true和false, -
getReserves函式,用來獲取當前交易對的資產資訊及最后交易的區塊時間, -
_safeTransfer函式,使用call函式進行代幣合約transfer的呼叫(使用了函式選擇器),注意,它檢查了回傳值(首先必須呼叫成功,然后無回傳值或者回傳值為true), -
接下來四個
event定義是方便客戶端進行各種追蹤的, -
constructor構造器,很簡單,記錄factory合約的地址,其實按照Solidity代碼規范(建議),這里的構造器和它前面的四個event定義應該放在getReserves函式之前, -
initialize函式,進行合約的初始化,在第一篇核心合約原始碼學習中提到,因為factory合約使用create2函式創建交易對合約,無法向構造器傳遞引數,所以這里寫了一個初始化函式用來記錄合約中兩種代幣的地址, -
skim函式,這里從注釋就可以看出來,強制交易對合約中兩種代幣的實際余額和保存的恒定乘積中的資產數量一致(多余的發送給呼叫者),注意:任何人都可以呼叫該函式來獲取額外的資產(前提是如果存在多余的資產), -
sync函式,和skim函式剛好相反,強制保存的恒定乘積的資產數量為交易對合約中兩種代幣的實際余額,用于處理一些特殊情況,通常情況下,交易對中代幣余額和保存的恒定乘積中的資產數量是相等的,
三、幾個比較復雜的函式
原始碼中還有幾個比較復雜的函式,下面我們分別來學習,
3.1、_mintFee函式
在我的那篇《UniswapV2介紹》中提到,如果開發團隊手續費打開后,用戶每次交易手續費的1/6會分給開發團隊,剩下的5/6才會發給流動性提供者,如果每次用戶交易都計算并發送手續費,無疑會增加用戶的gas,Uniswap開發團隊為了避免這種情況的出現,將開發團隊手續費累積起來,在改變流動性時才發送,_mintFee函式就是計算并發送開發團隊手續費的,函式的引數為交易對中保存的恒定乘積中的兩種代幣的數值,
下面我們來看它的代碼:
-
前兩行用來獲取開發團隊手續費地址,并根據該地址是否為零地址來判斷開關是否打開,
-
第三行
uint _kLast = kLast;使用一個區域變數記錄過去某時刻的恒定乘積中的積的值,注釋表明使用區域變數可以減少gas(估計是因為減少了狀態變數操作), -
接下來是個
if(feeOn)陳述句,如果手續費開關打開,計算手續費的值(手續費以增發該交易對合約流動性代幣的方式體現),閱讀其白皮書,計算公式為:
S m = k 2 ? k 1 5 ? k 2 + k 1 S_m = \frac{\sqrt k_2 - \sqrt k_1} {5 \cdot \sqrt k_2 + \sqrt k_1 } Sm?=5?k ?2?+k ?1?k ?2??k ?1??
其中k1為舊的乘積值,即代碼中的_klast,k2為新的乘積值,函式中的代碼邏輯和計算公式相符,注意到該陳述句里面還嵌套一個if(_kLast != 0)條件陳述句,這是為什么呢?要理解這一點,需要看
if(feeOn)的else陳述句,這里判定如果記錄的舊的某時刻的乘積值不為0,則設定為0,這么做的目的是因為手續費開關是可以重復打開關閉的,從后面的mint或者burn函式中,我們可以看到只有手續費打開才會更新這個kLast的值,關閉后是不會更新的,假定打開后再關閉,此時如果不設定kLast為0,那它就是一個無法更新的舊值,然后我們再打開開關,此時kLast是一個很久前的舊值,而不是最近更新的值,而使用舊值會將開關再次打開前的的資料也計算進去(而不是從開關打開的那一時刻開始計算),同樣這里因為在手續費關閉時將
kLast設定為0,if(_kLast != 0)這個條件陳述句就很好理解了,因為此時代表開關打開,但是最近一次還未更新(開關打開后更新發生在_mint函式之后,此時值為0),所以不能計算,開關打開后只有先更新一次最新的kLast值有了比較才能繼續計算,從這里可以看出,開關打開后的第一次流動性操作只是建立了一個過去時刻的快照值
kLast,第二次流動性操作才會有新的快照值,才能使用上面的公式計算手續費,這里有人可能會有疑惑,我第一次流動性操作和第一次流動性操作的恒定乘積中
K的值從代碼中是無法看到變化(_mintFee函式發生在更新reserve0和reserve1之前),它們的差額不是0么,哪有什么手續費,是的,如果只是連續的兩次流動性操作,k2是和k1是相等的,但是連續兩次流動性操作之間是可以存在多次資產(代幣)交易的,由于資產交易手續費的存在,雖然是恒定乘積演算法,但是這個乘積值K實質上是在慢慢變大的,于是這兩個K之間就會有差額了,
3.2、_update函式
這個函式也有幾個難點不好理解,注釋中的意思為:它用來更新reserves,并且在每個block的第一次呼叫,更新價格累計值,理解的難點在于理解UniswapV2的資料型別設計、溢位安全函式及價格預言機功能,UniswapV2使用UQ112x112是經過周密考慮的了,第一個使用的地方是使用它保存價格,剩下的32位保存溢位位,第二個使用的地方是它使用uint112保存每種代幣的reserve,剛好剩下32位保存當前區塊時間(雖然位數會不夠,見下面的內容),
該函式的四個輸入引數分別為當前合約兩種代幣余額及保存的恒定乘積中兩種代幣的數值,函式功能就是將保存的數值更新為實時代幣余額,并同時進行價格累計的計算,
-
函式內的第一行用來驗證余額值不能大于uint112型別的最大值,因為余額是uint256型別的,
-
函式的第二行解釋,因為一個存盤插槽為256位,兩個代幣數量各112位,這樣就是224位,只剩下32位沒有用了,UniswapV2用它來記錄當前的區塊時間,因為區塊時間是uint型別的,有可能超過uint32的最大值,所以對它取模,這樣
blockTimestamp的值就永遠不會溢位了,但真實的時間值是會超過32位大小的,大約在02/07/2106,見其白皮書,這里有一點疑惑,使用取模操作和溢位后直接進行Unit32型別轉換得到的結果是相同的,不知道為什么要進行一下取模操作,網上有人發起了多個相同的issue,這里是其中一名開發者的回答:
Pretty sure this is just an oversight, given the two are exactly equivalent. Not sure if it makes a difference in terms of gas–may be optimized out.
google翻譯了一下:這兩個值完全相同,肯定是一個疏忽,不確定是否會對gas產生影響,可能會進行優化,
-
函式的第三行用來計算當前block時間和上一次block時間的差值,注釋中提到已經考慮過溢位了,這個因為筆者不是IT專業出身,從事IT行來時間也比較短,自身基本功不扎實,對二進制、溢位,負數啊,反碼啊、補碼啊之類的不是很熟悉,因此這里無法完全弄清楚,但是綜合這一行和上一行的代碼,可以得到一個結論:就是
x + delta - x = delta在x + delta溢位的時候仍然成立(未溢位時顯然是成立的),這里我舉一個非常牽強的示例(未必正確),例子中x和delta均為uint8型別:
-
從
uit8(-1) = 255我們可以得到第一點結論:一個負數x在uint中會被視為正數,它的值為x + 255 + 1(按位取反+1,按位取反就是+255,如果有溢位位,溢位位超過了資料長度,因此可以忽略), -
從平常應用中我們可以得到第二點結論:如果x + delta溢位(不是為負數),那么它的真實值為
x + delta - 255 - 1, -
將上面兩點綜合起來,會得到
(x + delta) - x = x + delta - 255 - 1 - x = delta - 255 - 1,它肯定是一個負數,按照負數在uint中的計算規則,它會+ 255 + 1,所以進一步得到:(x + delta) - x = delta - 255 - 1 + 255 + 1 = delta,所以在x + delta溢位情況下,x + delta - x = delta仍然成立,
從這個合約的實際應用來看,
x就是上一次的區塊時間,x + delta就是當前區塊時間,delta就是時間間隔,只不過資料型別從uint8變成了uint32,因為區塊時間被轉換成了uint32型別,而取模操作和溢位后低位數值是相同的,所以這里就算新的區塊時間在取模轉換后小于舊的區塊時間(相當于溢位了),這個時間間隔也是正確的, -
-
函式的第四行是一個
if陳述句,如果是同一個區塊的第二筆及以后交易,timeElapsed就會為0,此時就不會計算價格累計值, -
函式的第五行及第六行是計算兩種價格的累積值,注釋
// * never overflows, and + overflow is desired提到了兩層意思:-
永遠不會溢位,個人認為是指:價格是
uint224,timeElapsed是uint32,一個uint32乘于一個uint224顯然是永遠不會溢位uint256的, -
考慮到了
+溢位,白皮書講到這個方法是溢位安全的,這里個人認為需要從價格預言機的真實應用方式來理解,因為代碼只是記錄了價格累計值,預言機真實價格計算取得是區間平均值,也就是(P2-P1)/(T2-T1),或者為deltaP/deltaT,這里P1和P2分別代表某個區塊的價格累計值,T2和T1分別代表區塊時間,deltaP與deltaT分別代表價格變化值與時間變化值,此價格計算公式我們可以進一步寫成(P1 + deltaP - P1)/(T1 + deltaT - T1),從這里看出什么門道沒有?我們在分析第三行代碼時已經得出結論:在x + delta溢位情況下,x + delta - x = delta仍然成立,也就是說,不管是分子的價格溢位了還是分母的區塊時間溢位了,deltaP與deltaT總是正確的,所以deltaP/deltaT(平均價格)也總是正確的,也就是該區間價格也總是正確的,從這里可以看到價格累計設計非常巧妙,既防止了在同一區塊內操縱價格(見介紹文章),又是溢位安全的,這里的個人理解基于上面個人結論:x + delta溢位情況下,x + delta - x = delta仍然成立,如果上面結論錯誤,這里個人理解也會錯誤,切記,這里有細心的讀者可能會發現,如果
x + delta資料太大,不只溢位一次怎么辦?這里白皮書給出了一個建議,就是每個周期(232-1秒)內至少進行一次價格檢查,因為從累積公式可以得出,一個周期內(232-1秒)最多只會溢位一次(當然也可能不會溢位),
-
-
函式的第8,9,10行用來更新交易對中恒定乘積中的
reserve的值,同時更新block時間為當前block時間(這樣一個區塊內價格只會累積計算一次), -
函式的最后一行觸發了同步事件,用于客戶端追蹤,
3.3、mint函式
該函式的注釋表明這個低等級函式應該從一個合約呼叫,并且需要執行重要的安全檢查,在系列文章最開始已經講過,核心合約對用戶不友好,需要通過周邊合約來間接互動,因此,從周邊合約呼叫也剛好符合這個要求,
mint函式的主要功能就是在用戶提供流動性時(提供一定比例的兩種ERC20代幣到交易對)增發流動性代幣給提供者,注意流動性代幣也是一種ERC20代幣,是可以交易的,由此還衍生了一些其它型別的DeFi,函式的引數為接收流動性代幣的地址,函式的回傳值為增加的流動性數值,
- 函式的第一行用來用來獲取當前交易對的
reverse,注意它的元組賦值的語法,當左邊個數小于右邊時,它使用類似javascript的語法,而不是類似golang的那種使用一個"_“代替未使用變數,但是在函式引數中,如果有未使用變數,是可以使用”_"來代替未使用的變數名的,否則有些編譯器會給出未使用變數的警告, - 函式的2-5行用來獲取當前合約注入的兩種資產數量,注意UniswapV2采用了先轉移代幣,再呼叫合約的交易方式,因此,除了
FlashSwap外,所有需要支付的代幣都必須事先轉移到交易對中,但是這樣就不方便外部賬號進行此類操作,一般是通過周邊合約進行類似操作, - 函式的第6行發送開發團隊手續費(如果相應開關打開的了話)
- 第七行
uint _totalSupply = totalSupply;使用一個區域變數來保存已經發行流動性代幣的總量,這樣可以少操作狀態變數,節省gas,注意,注釋中提到了因為_mintFee函式可能更新已發行流動性代幣的數量(具體在if (liquidity > 0) _mint(feeTo, liquidity);這一行代碼),所以必須在它之后賦值, - 接下來的
if-else陳述句根據是否為初次提供流動性作了不同處理,如果是初次,其計算方法為恒定乘積公式中積的平方根,同時還需要燃燒掉部分最初始的流動性,具體數值為MINIMUM_LIQUIDITY,這樣做的原因見我的介紹文章或者查閱其白皮書及官方檔案,如果不是初次提供,則會根據已有流動性按比例增發,由于注入了兩種代幣,所以會有兩個計算公式,每種代幣按比例計算一次增發的流動性數量,取其中的最小值, - 接下來的
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');陳述句是講增發的流動性必須大于0,等于0相當于無增發,白做無用功, _mint(to, liquidity);這一句代碼增發新的流動性給接收者,_update(balance0, balance1, _reserve0, _reserve1);更新當前保存的恒定乘積中兩種資產的值,if (feeOn) kLast = uint(reserve0).mul(reserve1);,如果手續費打開了,更新最近一次的乘積值,該值不隨平常的代幣交易更新,僅用來流動性供給時計算開發團隊手續費,可以參考一下_mintFee函式的解釋,- 最后一行
emit Mint(msg.sender, amount0, amount1);,很簡單,觸發一個增發事件讓客戶端追蹤,
3.4、burn函式
該函式剛好和mint函式功能相反,mint函式是通過同時注入兩種資產來獲取流動性(以增發流動性代幣的形式表現);而burn函式是通過燃燒流動性代幣的形式來提取相應的兩種資產,從而減小該交易對的流動性,
函式的引數為代幣接收者的地址,回傳值是提取的兩種代幣數量,注意,它需要事先將流動性代幣轉回交易對中,
-
函式的前三行用來獲取交易對的reverse及代幣地址,并保存在區域變數中,注釋中提到也是為了節省gas,
-
第4-5行用來獲取交易對合約地址擁有兩種代幣的實際數量,
-
第6行用來獲取事先轉入的流動性的數值,正常情況下,交易對合約是沒有任何流動性代幣的,雖然它是發幣合約,所有的流動性代幣全在流動性提供者手里,
-
第7行計算手續費,見
mint函式,雖然提取資產并不涉及到流動性增發,但是這里還是要計算并發送手續費,如果僅在注入資產時計算并發送手續費,用戶提取資產時就會計算不準確, -
uint _totalSupply = totalSupply;作用同mint函式, -
amount0 = liquidity.mul(balance0) / _totalSupply; amount1 = liquidity.mul(balance1) / _totalSupply;這里按比例計算提取資產,
-
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');需要提取資產數量大于0,也就是有最小燃燒值需求, -
_burn(address(this), liquidity);將用戶事先轉入的流動性燃燒掉,因為此時流動性代幣已經轉移到交易對,所以燃燒的地址為address(this), -
_safeTransfer(_token0, to, amount0); _safeTransfer(_token1, to, amount1);用來將相應數量的ERC20代幣發送給接收者,
-
balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this));這里重新獲取了交易對合約地址擁有的兩種代幣的余額,那么這個值可不可以通過原余額減去發送的數量來得到呢?如果能這里為什么還是通過代幣合約來獲取呢?這里我的個人理解為:通過代幣合約獲取會更準確一些,因為我們不知道這兩種ERC20代幣的合約原始碼,有可能有些代幣合約對余額的計算方式有特殊處理(比如增加一個動態變化的系數等),使用原數量減去發送的數量未必就是正確的余額,
-
_update(balance0, balance1, _reserve0, _reserve1);更新當前保存的恒定乘積中兩種資產的值,同mint函式, -
if (feeOn) kLast = uint(reserve0).mul(reserve1);,更新KLast的值,同mint函式, -
emit Burn(*msg*.sender, amount0, amount1, to);很簡單,觸發一個燃燒事件讓客戶端追蹤,
3.5、swap函式
該函式實作交易對中資產(ERC20代幣)交易的功能,也就兩種ERC20代幣互相買賣,而多個交易對可以組成一個交易鏈,
該函式定義為:
function swap(uint amount0Out, uint amonun1Out, address to, bytes calldata data) external lock {
它有四個引數,分別為購買的token0的數量,購買的token1的數量,接收者地址,接收后執行回呼時的傳遞資料,
它和V1版本不同的是函式引數中不再有出售資產的數量了,因為出售的資產(ERC20代幣)需要事先轉入到交易對中,通過比較交易對中的代幣余額和恒定乘積中的reserve來計算得到,
它最后有一個lock修飾符,是防重入的,因為在UniswapV1中,假定所有代幣的回呼函式不會有重入風險,但是在實際應用中發現,部分非ERC20代幣打破了這一假定,因此在V2版本中,對必要的函式都做了防重入處理,
-
函式的第一行用來校驗輸入引數不能為0,不作無意義的事,
-
第二行用來獲取交易對的
reverse, -
第三行校驗購買的數量必須小于
reverse,否則沒有那么多代幣賣,根據恒定乘積計算公式,等于也是不行的,那樣輸入就是無窮大, -
4-5行定義了兩個區域變數,它們來保存當前交易對的兩種代幣余額
-
第6行和第15行組成一對
{},它是一個特殊的語法,注釋說是用來避免堆疊過深錯誤,為什么會有堆疊過深錯誤呢,因為以太坊虛擬機(EVM)訪問堆疊時最多只能訪問16個插槽,當訪問的插槽數超過16個時在編譯時就會產生stack too deep errors,這個錯誤產生的原因也比較復雜(比如函式內引數、回傳引數及區域變數過多,或者參考過深等),和部分操作碼也有一定關聯,但是這里應該是函式內區域變數過多引起的,UniswapV2使用下面的語法來避免這個問題 :uint var1; { (uint varA, uint varB) = getVars(); var1 = varA + varB; } // now use var1該方法的原理未知,網上能搜索到的文章都是說受Uniswap啟發,可以使用scope變數的方式解決區域變數過多的問題,
這里有一篇文章,簡要講述了堆疊過深錯誤產生的原因和五種解決方法,希望大家有空時可以看一下,對自己撰寫智能合約還是有幫助的 =>>> Stack Too Deep
-
7-9行使用兩個區域變數記錄token地址并驗證接收者地址不能為token地址,
-
10-11行先行轉出購買資產,
-
12行的意思是如果引數data不為空,那么執行呼叫合約的
uniswapV2Call回呼函式并將data傳遞過去,普通交易呼叫時這個data為空, -
13-14行用來獲取交易對合約地址兩種代幣的余額并保存在4-5行定義的變數中,
-
16-17行用來計算實際轉移進來的代幣數量,
-
18行對上面計算出來的數量進行驗證,你必須轉入某種資產(大于0)才能交易成另一種資產,
-
19-23行又是個scope variables,用來防止stack too deep errors,
-
20-22行是進行最終的恒定乘積驗證,V2版本的驗證公式為:
(x1 - 0.003 * xin) * (y1 - 0.003 * yin) >= x0 * y0,注意這里的x1和y1不是reserve,而是balance,而x0和y0是reserve,xin和yin為注入的資產數量,因此要扣除千分之三的交易手續費,這個公式的意思為新的恒定乘積的積必須大于舊的值,因為此時reserve未更新,所以使用的是balance,驗證完成后reserve會更新為balance,xin和yin中任意一個為0,就變成V1版本的驗證公式了, -
24行是更新恒定乘積中的資產值
reserve為balance, -
25行是觸發一個事件便于客戶端進行追蹤
對于該函式,有兩點額外說明:
-
函式是先將購買的代幣發送出去,然后如果
data不為空的話,會呼叫接收者合約的回呼函式,完成之后才會再計算轉入的另一種代幣數量,很明顯,這是一個先花后支付設計,為什么這樣設計呢?這里是方便大家進行套利(套利的同時可以讓Uniswap交易對中的價格更接近于外部價格),假定該交易對為一個A/B交易對,你可以先得到購買的代幣B而不支付任何代幣A,然后利用購買的代幣B在別的交易所中進行交易,得到一定數量的代幣A,然后再將支付的代幣A還給交易對,如果此時A還有剩余,那么你就獲得了利潤,然而這種套利并不需要你提前擁有A或者B這兩種資產,屬于無成本套利(不過需要支付gas費用),當然個人賬號是沒有代碼的,所以也就沒有回呼函式,只有使用智能合約進行這種無成本套利(正常手動套利不受影響,不過需要擁有對應的代幣), -
從代碼上看并無直接支付交易手續費的操作,但是實際上在驗證恒定乘積時,由于手續費的存在,用戶付出的代幣數量
Xin是高于交易前根據恒定乘積公式計算出來的數量Xp的,由于手續費是千分之三,可以得到:
X i n = 1000 997 ? X p Xin = \frac {1000} {997} \cdot Xp Xin=9971000??Xp
好了,這次的學習就到此結束了,由于個人能力有限,難免有理解錯誤或者不正確的地方,還請大家多多留言指正,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/167257.html
標籤:其他
