CoinRebate
返回教程列表
通用教程

Uniswap v2 合約程式碼深度拆解:老韭菜帶你讀懂核心架構與關鍵方法

深入解析 Uniswap v2 智慧合約架構,從工廠合約到交易對核心方法,幫你徹底理解 DeFi 底層邏輯,避免踩坑。

Uniswap v2 合約程式碼深度拆解:老韭菜帶你讀懂核心架構與關鍵方法

💰 加密交易省錢入口

7 大主流交易所返佣比較,最高永久減免 30%

查看返佣排名免費永久 · 無需訂閱

Uniswap v2 合約代碼深度拆解:老韭菜帶你讀懂核心架構與關鍵方法

上回咱們聊了 Uniswap v2 白皮書,今天直接上硬貨——合約代碼。別慌,咱不搞逐行翻譯那套,重點抓架構和核心方法。想細摳代碼的,建議去翻以太坊官方的 Uniswap v2 代碼走讀,那裡更全。

合約架構:core 和 periphery 是啥關係?

Uniswap v2 的合約分兩大類:core 合約periphery 合約

  • core 合約:就幹最基礎的交易活兒,代碼精簡到 200 行左右。爲啥這麼摳?因爲用戶的錢都擱這兒,必須穩如老狗,少一行代碼就少一個 bug 風險。
  • periphery 合約:這是給咱們用戶用的封裝層,比如支持直接用 ETH 交易(自動轉成 WETH)、搞多路徑交換(一口氣完成 A→B→C)。你在 app.uniswap.org 上操作,背後調的都是 periphery 合約,它再底層去戳 core 合約。

下面咱掰開揉碎了說幾個主要合約:

uniswap-v2-core

  • UniswapV2Factory:工廠合約,專門生 Pair 合約(還能設協議手續費接收地址)。
  • UniswapV2Pair:交易對合約,定義了 swap/mint/burn 這些核心方法,還集成了價格預言機。它本身是個 ERC20 合約,繼承了 UniswapV2ERC20
  • UniswapV2ERC20:實現 ERC20 標準,基礎操作都在這兒。

uniswap-v2-periphery

  • UniswapV2Router02:最新版路由合約,比老版本 UniswapV2Router01 多了對 FeeOnTransfer 代幣的支持。添加/移除流動性、代幣兌換、ETH 換幣這些常用功能,都是它提供的接口。
  • UniswapV1Router01:舊版路由,現在基本沒人用了,因爲不支持 FeeOnTransferTokens

uniswap-v2-core 核心合約詳解

UniswapV2Factory:工廠怎麼造交易對?

工廠合約裏最關鍵的 createPair 方法,咱們瞅一眼代碼:

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);
}

先給 token0token1 排個序,確保 token0 地址字面值更小。接着用 assembly + create2 創建合約——這玩意兒是 Solidity 裏的底層操作,直接玩 EVM,夠硬核。

上回白皮書裏提過,create2 主要是爲了生成確定性的交易對地址。你給倆代幣地址,就能直接算出來 pair 地址,不用再去鏈上查。

CREATE2 來自 EIP-1014,核心是那個 salt 值。同一個交易對,salt 得一樣,所以這裡直接用排序後的代幣地址當 salt。這樣不管你先傳 A 還是 B,都能算出同一個 pair(A,B)。

其實現在新版 EVM 已經支持給 new 方法傳 salt 了,比如:

pair = new UniswapV2Pair{salt: salt}();

但 Uniswap v2 開發那會兒還沒這功能,所以用了 assembly create2

根據 Yul 規範,create2 的參數這麼理解:

  • v=0:往新合約裏打多少 ETH(單位 wei),這兒是 0。
  • p=add(bytecode, 32):合約字節碼從哪兒開始。因爲 bytecodebytes 類型,ABI 編碼時前 32 字節存長度,所以真正內容得從 bytecode+32 開始。
  • n=mload(bytecode):字節碼總長度。mload 讀前 32 字節,剛好就是長度。
  • s=salt:自定義的 salt,就是 token0token1 拼一起。

UniswapV2ERC20:permit 玩轉離線籤名

這合約主要實現 ERC20 標準,代碼簡單。重點說說 permit 方法:

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
    require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
    bytes32 digest = keccak256(
        abi.encodePacked(
            '\x19\x01',
            DOMAIN_SEPARATOR,
            keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
        )
    );
    address recoveredAddress = ecrecover(digest, v, r, s);
    require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
    _approve(owner, spender, value);
}

permit 幹的就是白皮書裏說的「元交易」功能。EIP-712 定了離線籤名的規矩,用戶籤個名,授權某個合約在截止時間前能動用一定數量的流動性代幣。應用拿着籤名和生成的 v, r, s,調用 permit 就能拿到授權。ecrecover 還原籤名地址,驗證通過就批了。

UniswapV2Pair:mint、burn、swap 三大核心

Pair 合約的核心就三個方法:mint(加流動性)、burn(撤流動性)、swap(換幣)。

mint:加流動性怎麼算?

// 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);
}

先拿緩存餘額(防預言機攻擊用),再算實際轉入的代幣量。_mintFee 處理協議手續費——白皮書裏有公式,這兒不展開了。

如果是第一個來提供流動性的,按根號 xy 發代幣,但得永久鎖住 MINIMUM_LIQUIDITY(1000 wei),防歸零。不是第一次的話,就按價值比例鑄幣。

burn:撤流動性咋分幣?

// 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);
}

mint 類似,先算手續費。撤流動性時,按你銷毀的流動性代幣佔比,分對應的兩種代幣。簡單說,你佔多少份,就拿多少幣。

swap:換幣和閃電貸的祕密

// 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);
}

爲了兼容閃電貸和不同代幣的 transferswap 方法壓根兒沒 amountIn 參數,而是靠比較當前餘額和緩存餘額來算你轉入了多少幣。

因爲最後會檢查 k 值(扣掉 0.3% 手續費後),所以合約可以先把你要的幣轉出去。如果你沒提前轉幣進來,那就相當於借幣——這就是閃電貸的玩法。搞閃電貸的話,你得在 uniswapV2Call 裏把借的幣還上。

最後更新價格預言機的累計價格,再把緩存餘額設爲當前餘額,一次交易就閉環了。


嘮點實在的:理解這些底層代碼,能幫你少踩很多坑。但如果你是剛入圈的小白,建議先從主流 CEX 玩起,比如用咱們的邀請碼註冊幣安、OKX,手續費打折還安全。別一上來就硬剛合約,容易懵。

想省手續費?戳這裡:幣安永久折扣註冊 | OKX 最高返佣入口。老韭菜的經驗:工具用對了,賺錢才不累。

相關文章

💰 加密交易省錢入口

7 大主流交易所返佣比較,最高永久減免 30%

查看返佣排名免費永久 · 無需訂閱
分享:

Comments (0)