主頁 > 軟體設計 > UniswapV2核心合約學習(3)——UniswapV2Pair.sol

UniswapV2核心合約學習(3)——UniswapV2Pair.sol

2020-10-11 08:47:31 軟體設計

記得朋友圈看到過一句話,如果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,reserve1blockTimestampLast這三個狀態變數記錄了最新的恒定乘積中兩種資產的數量和交易時的區塊(創建)時間,

  • price0CumulativeLastprice1CumulativeLast,記錄交易對中兩種價格的累計值,

  • 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為舊的乘積值,即代碼中的_klastk2為新的乘積值,函式中的代碼邏輯和計算公式相符,注意到該陳述句里面還嵌套一個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函式發生在更新reserve0reserve1之前),它們的差額不是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 = deltax + 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提到了兩層意思:

    1. 永遠不會溢位,個人認為是指:價格是uint224timeElapseduint32,一個uint32乘于一個uint224顯然是永遠不會溢位uint256的,

    2. 考慮到了+溢位,白皮書講到這個方法是溢位安全的,這里個人認為需要從價格預言機的真實應用方式來理解,因為代碼只是記錄了價格累計值,預言機真實價格計算取得是區間平均值,也就是(P2-P1)/(T2-T1),或者為deltaP/deltaT,這里P1和P2分別代表某個區塊的價格累計值,T2和T1分別代表區塊時間,deltaPdeltaT分別代表價格變化值與時間變化值,此價格計算公式我們可以進一步寫成(P1 + deltaP - P1)/(T1 + deltaT - T1),從這里看出什么門道沒有?我們在分析第三行代碼時已經得出結論:在x + delta 溢位情況下,x + delta - x = delta仍然成立,也就是說,不管是分子的價格溢位了還是分母的區塊時間溢位了,deltaPdeltaT總是正確的,所以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,注意這里的x1y1不是reserve,而是balance,而x0y0reservexinyin為注入的資產數量,因此要扣除千分之三的交易手續費,這個公式的意思為新的恒定乘積的積必須大于舊的值,因為此時reserve未更新,所以使用的是balance,驗證完成后reserve會更新為balancexinyin中任意一個為0,就變成V1版本的驗證公式了,

  • 24行是更新恒定乘積中的資產值reservebalance

  • 25行是觸發一個事件便于客戶端進行追蹤

對于該函式,有兩點額外說明:

  1. 函式是先將購買的代幣發送出去,然后如果data不為空的話,會呼叫接收者合約的回呼函式,完成之后才會再計算轉入的另一種代幣數量,很明顯,這是一個先花后支付設計,為什么這樣設計呢?這里是方便大家進行套利(套利的同時可以讓Uniswap交易對中的價格更接近于外部價格),假定該交易對為一個A/B交易對,你可以先得到購買的代幣B而不支付任何代幣A,然后利用購買的代幣B在別的交易所中進行交易,得到一定數量的代幣A,然后再將支付的代幣A還給交易對,如果此時A還有剩余,那么你就獲得了利潤,然而這種套利并不需要你提前擁有A或者B這兩種資產,屬于無成本套利(不過需要支付gas費用),當然個人賬號是沒有代碼的,所以也就沒有回呼函式,只有使用智能合約進行這種無成本套利(正常手動套利不受影響,不過需要擁有對應的代幣),

  2. 從代碼上看并無直接支付交易手續費的操作,但是實際上在驗證恒定乘積時,由于手續費的存在,用戶付出的代幣數量Xin是高于交易前根據恒定乘積公式計算出來的數量Xp的,由于手續費是千分之三,可以得到:
    X i n = 1000 997 ? X p Xin = \frac {1000} {997} \cdot Xp Xin=9971000??Xp

好了,這次的學習就到此結束了,由于個人能力有限,難免有理解錯誤或者不正確的地方,還請大家多多留言指正,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/167562.html

標籤:其他

上一篇:ctf--crypto(加密原理和工具匯總)(持續更新)

下一篇:Ubuntu16golang環境搭建

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more