banner
zach

zach

github
twitter
medium

ゼロから始めるUniswapV2 | パート1 流動性

UniswapV2 は V1 に比べて大きな変更があり、現在最も人気のある DEX バージョンであり、多くのプロトコルは UniswapV2 からフォークされています。本シリーズの記事では、Foundry を契約のテストフレームワークとして使用し、OpenZeppelin ではなく solmate を ERC20 などの基盤プロトコルの実装として使用し、Uniswap のコアフレームワークコードをゼロから再現します。

UniswapV2 のコードベースは core と periphery の 2 つの部分に分かれており、core には以下が含まれます:

  • UniswapV2ERC20:LP トークンのための ERC20 拡張
  • UniswapV2Factory:取引ペアを管理するためのファクトリー契約
  • UniswapV2Pair:コアの取引ペア契約

periphery には主に UniswapV2Router と UniswapV2Library が含まれ、重要な補助取引契約です。

流動性の追加#

まずコアの流動性追加から始めます。Uniswap は設計上 V1 と似ており、トレーダー、LP、ユーザーは依然としてプロトコルのコア参加者です。V1 と比較して、V2 のコード設計には違いがあります。LP が流動性を注入するのは 2 つの部分に分かれており、基盤の実装は UniswapV2Pair 契約であり、上層のエントリは UniswapV2Router 契約です。ここでは基盤の実装に焦点を当てます。

流動性の追加:つまり、LP が取引ペア契約内に一定の割合で 2 つの基盤資産を転送し、取引ペアが投入された資産に応じて相応の数量の流動性トークンを鋳造するプロセスです。

UniswapV2Pair の基盤として、ここで実装する必要がある機能は、ユーザーが投入した基盤資産の量を計算し、相応の LP トークンを計算してユーザーに mint することです。

function mint() public {
   uint256 balance0 = IERC20(token0).balanceOf(address(this));
   uint256 balance1 = IERC20(token1).balanceOf(address(this));
   uint256 amount0 = balance0 - reserve0;
   uint256 amount1 = balance1 - reserve1;
	 // ユーザーが投入した資産の数量amount0とamount1を計算
   uint256 liquidity;
	 // 初回mintかどうかを区別し、流動性トークンの計算方法が異なる
   if (totalSupply == 0) {
      liquidity = ???
      _mint(address(0), MINIMUM_LIQUIDITY);
   } else {
      liquidity = ???
   }

   if (liquidity <= 0) revert InsufficientLiquidityMinted();
	 // 流動性トークンをmint
   _mint(msg.sender, liquidity);
	 // 取引ペア内の準備金量キャッシュを更新
   _update(balance0, balance1);

   emit Mint(msg.sender, amount0, amount1);
}

コードからわかるように、ここでの取引ペアは reserve0 と reserve1 の 2 つの変数を通じて現在の取引ペア内の 2 つのトークンの数量をキャッシュし、balanceOf を直接使用してカウントするのではありません(注:これは契約の安全性を考慮して、外部からの操作を避けるためでもあります)。

mint メソッドを呼び出す前に、ユーザーは期待通りに現在の契約に token0 と token1 を転送する必要があります。ここで現在の契約内のトークン残高 balance0 と balance1 を計算し、以前のキャッシュを引いた amount0 と amount1 が今回ユーザーが転入したトークンの数量になります。

鋳造すべき LP トークンの数を計算する際には、totalSupply が 0 かどうかを区別します。つまり、現在が初めて流動性を提供するかどうかです。現在の取引ペア内のトークン状況は以下のようになります:

token0token1liquidity
reserve0reserve1totalSupply
amount0amount1totalSupply+lp

固定の割合に従い、今回鋳造される LP トークンの数の出所は 2 つあり、ユーザーが投入した 2 つのトークンのいずれも基準として使用できます。

  • amount0/totalSupply*reserve0
  • amount1/totalSupply*reserve1

実際の開発において、UniswapV2 のルールは 2 つのうち小さい方を選択します。規定により、ユーザーが提供する流動性は厳密に割合に従います。2 つの値は等しいべきですが、ユーザーが不均衡な流動性を提供した場合、これらの値には差異が生じます。もしプロトコルが大きい方で LP を計算すると、これはこの方法を奨励することになります。したがって、小さい流動性トークンの数をユーザーへの罰として選択します。

totalSupply=0 の条件分岐に戻ると、統一された割合で LP トークンの数量を計算できないため、UniswapV2 は amount0*amount1 の平方根を計算し、MINIMUM_LIQUIDITY(1000)を統一的に引きます。

  • ある LP が初めて token0 と token1 をそれぞれ 1 wei 投入したと仮定します。MINIMUM_LIQUIDITY を引かなければ、1 枚の LP トークンが mint され、その後直接 1000 枚の token0 と token1 が転入されると、取引ペア内には 1000*10^18+1 個の token0 と token1 が存在しますが、LP は 1 wei しか持っていません。したがって、後の LP にとって、最小単位の 1 wei の流動性を提供したい場合でも、2000 ether のトークンを支払わなければなりません。説明参照
  • MINIMUM_LIQUIDITY を統一的に引くと、1000 の流動性下限が存在し、ユーザーは mint を通さずに直接トークンを転入できます。攻撃プロセスを再実行すると、流動性単価の最大値は (1001+2000×10^18) 1001≈2×10^18 となり、前述のものよりも大幅に低下しますが、ここで初回流動性提供者の利益が失われます。

整理されたコードは以下の通りです:

if (totalSupply == 0) {
   liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
   _mint(address(0), MINIMUM_LIQUIDITY);
} else {
   liquidity = Math.min(
      (amount0 * totalSupply) / _reserve0,
      (amount1 * totalSupply) / _reserve1
   );
}

対応するテストコード:

	 function testInitialMint() public {
        vm.startPrank(lp);
        token0.transfer(address(pair),1 ether);
        token1.transfer(address(pair),1 ether);
        
        pair.mint();
        uint256 lpToken = pair.balanceOf(lp);
        assertEq(lpToken, 1e18-1000);
    }

    function testExistLiquidity() public {
        testInitialMint();
        vm.startPrank(lp);
        token0.transfer(address(pair),1 ether);
        token1.transfer(address(pair),1 ether);
        
        pair.mint();
        uint256 lpToken = pair.balanceOf(lp);
        assertEq(lpToken, 2e18-1000);
    }

    function testUnbalancedLiquidity() public {
        testInitialMint();
        vm.startPrank(lp);
        token0.transfer(address(pair),2 ether);
        token1.transfer(address(pair),1 ether);
        
        pair.mint();
        uint256 lpToken = pair.balanceOf(lp);
        assertEq(lpToken, 2e18-1000);
    }

流動性の削除#

流動性追加のプロセスから全体の流れがわかります:ユーザーが基盤資産 token0 と token1 を転入し、対応する数の LP トークンを mint します。

したがって、流動性の削除は逆のプロセスです。削除の前提は、ユーザーが LP トークンを持っていることです。ここでの LP トークンは、ユーザーが流動性を提供した証明です。具体的なコードは以下の通りです:

  • まず、ユーザーが保有する LP の数に基づいて、彼が受け取るべき amount0 と amount1 を再計算します。
  • ユーザーの全ての LP トークンを焼却します(ここでは部分的な流動性の削除はサポートされていないことがわかります)。
  • 計算された相応の数の token0 と token1 をユーザーに転送します。
  • 取引ペア内の資金準備量を更新します。
function burn() external{
        uint256 balance0 = IERC20(token0).balanceOf(address(this));
        uint256 balance1 = IERC20(token1).balanceOf(address(this));
        uint256 liquidity = balanceOf[msg.sender];
        // ユーザーの流動性占有比率に基づくトークン数量を計算
        uint256 amount0 = liquidity * balance0 / totalSupply;
        uint256 amount1 = liquidity * balance1 / totalSupply;
        if (amount0 <=0 || amount1 <=0) revert InsufficientLiquidityBurned();
        // 流動性トークンをburn
        _burn(msg.sender, liquidity);
        // トークンをユーザーに転送
        _safeTransfer(token0, msg.sender, amount0);
        _safeTransfer(token1, msg.sender, amount1);
        // 現在の準備金を更新
        balance0 = IERC20(token0).balanceOf(address(this));
        balance1 = IERC20(token1).balanceOf(address(this)); 
        _update(balance0, balance1);
        emit Burn(msg.sender, amount0, amount1);
    }

テストコードは以下の通りです:

    function testBurn() public{
        testInitialMint();
        vm.startPrank(lp);
        pair.burn();
        assertEq(pair.balanceOf(lp), 0);
        assertEq(token0.balanceOf(lp), 10 ether-1000);
        assertEq(token1.balanceOf(lp), 10 ether-1000);
    }

    function testUnbalancedBurn() public {
        testInitialMint();
        vm.startPrank(lp);
        token0.transfer(address(pair),2 ether);
        token1.transfer(address(pair),1 ether);
        
        pair.mint();
        uint256 lpToken = pair.balanceOf(lp);
        assertEq(lpToken, 2e18-1000);

        pair.burn();
        assertEq(pair.balanceOf(lp), 0);
        assertEq(token0.balanceOf(lp), 10 ether-1500);
        assertEq(token1.balanceOf(lp), 10 ether-1000);
    }
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。