主頁 >  其他 > UniswapV2核心合約學習(3)——UniswapV2Pair.sol

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

2020-10-11 04:07:48 其他

記得朋友圈看到過一句話,如果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/qita/167257.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)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more