🤑

【3部作】これであなたもUniswapV2チョットデキル【前編】:流動性を基軸にUniswapV2モデルとその実装を整理する

2024/09/09に公開

導入

経緯

私は、UniswapV2やその派生プロジェクトのコントラクトに対して、

ABI経由で操作するプログラムをプライベートで実装してます。
 
 
しかし、安定稼働後の改修に入るタイミングで、

いつもUniswapV2の実装や構成がどうなっていたか細かいところの記憶が曖昧になってしまいます。
 
 
そこで、自分のためにも、UniswapV2がそもそもどのようなコンセプトであり、

そのコンセプトをどのように実装しているのか、整理して記録しておくことにしました。
 
 
公開した後もより詳しく調べたところは都度追記していきます。

※ コードのコメントはChatGPTの4oモデルで翻訳しています。
 
 

理解の道筋

UniswapV2は、美しいコンセプトの集合体のようなものです。
 
 

個々のコンセプトはシンプルでありつつも、

そのコンセプトを実現するための全体のシステムはかなり複雑で、

「スワップとは?」「流動性提供とは?」などと個別に確認して行ったところで、

このUniswapV2というシステムの全体像が見えてきません。
 
 

少なくとも私には今までずっと見えてこず、

いつもモヤモヤとした曖昧な理解を抱えながら、

UniswapV2やそのフォークのプロジェクトと向き合ってきました。
 
 

しかし、このままでは、半端なものしか作れないと感じ、

一貫した切り取り方で全体像の把握を今回試みます。

 
 

流動性を基軸に全体を俯瞰する

UniswapV2は、UniswapV1から継承されてきた

2つのトークンの交換を前提とした場合のリザーブ(用語の詳細は後述)と

その量的な制限に関しての数理モデル

x \cdot y = k

というコンセプトをベースに実装されており、

一般的に、UniswapV2の解説と言えば、

このモデルをベースにスワップの説明がなされる記事が多いです。
 
 
しかし、スワップの仕組みを理解しても、

「UniswapV2チョットデキルヨウニナッテキタ」と全然なりません。
 
 

実際、UniswapV2の実装コードにおいて、

スワップの記述はほんの少しで、

多くは流動性をどう管理するかに中心にあるように思われます。
 
 

Defiは、ファイナンスです。

Defiを理解する、Uniswapチョットデキルというのは、

自身の資産管理に役立てるようになることだと思います。
 
 

本記事では、その足がかりになるところまでの全体像の理解の助けになることを願っています。
 
 

準備

UniswapV2は何が難しいのか

右も左もわからなかった1年前を振り返ってみる

およそ1年ほど前に、某ブロックチェーンゲームのギルドコミュニティにて、

Defiを話題にコミュニティに貢献しようとしたことがありました。

その初期テーマの1つにUniswapV1〜V4を理解するというものがあったのですが、

私の理解力が無さすぎて頓挫しました。

というのも素人がいきなり理解するには難しすぎました。
 
 

まず難解だったのは、

UniswapV2の有名な数理モデルx \cdot y = kをそもそもどう使うのか、

結局どこのコントラクトに実装されているのかというところでした。
 
 

x \cdot y = k

xy: リザーブ(後述)
k : 流動性 (各トークン量の変化に縛りを設ける、後述)
 
 

このモデルが、何をして、結局どこで実現されているのか全然わからなかったわけです。
 
 

また、モデルを仮に理解できたとして、

各基礎トークン(後述)を供給したり(Supply)、引出したり(Withdraw)した場合、

その変化はモデルにどのように還元されるのかというところもわかりませんでした。
 
 

x \cdot y = kは、非常にシンプルで美しいです。
 
 

ただ、これを成り立たせるには、様々な実装上の工夫が不可欠です。

UniswapV2は、トークンの量を丁寧に管理することでシステムを成立させています。
 
 

本記事では、

UniswapV2のモデルに関する理解も深めつつも、

モデルを適切に実現するための実装周りも

トークン量、つまり、流動性sに着目した切り口で丁寧に理解していきます。
 
 

UniswapV2の概観

大まかな実装構成

まず、UniswapV2の実装は以下のように2つに分かれています。

(出典:https://github.com/adshao/publications/blob/master/uniswap/dive-into-uniswap-v2-contracts/README.md)

 
 
v2-coreにおいて、
UniswapV2に必要な最小限の機能の実装が行われており、
 
 
v2-peripheryにおいて、

実際にUniswapV2のアプリケーションで使用する際に必要な機能が実装されています。
 
 

先ほど、x \cdot y = kがどこにあるのかわからないと述べましたが、この構成も原因の1つです。
 
 

v2-coreにはUniswapV2Pairコントラクトという、

UniswapV2の本体とも呼べるコントラクトがあります。

ここで、トークンの量を「実際に」管理しています。
 
 

「実際に」とは、このコントラクトは、

管理するトークンをコントラクト内に所有しています。
 
 

このトークンを管理しているUniswapV2Pairコントラクトには、

swapメソッドというx \cdot y = kが記載されてそうなコードが存在しますが、

  • x \cdot y = kが成立したかの確認ロジック
  • トークンの転送ロジック

しか存在しません。
 
 

つまり、x \cdot y = kを使用したスワップの詳細は記載されていません。
 
 

以下が具体的なswapメソッドです。気になる方は確認してみてください。

後ほどより詳しくみていきますし、基本的にコードの理解はしなくていいと思います。

v2-periphery/contracts /UniswapV2Router02.sol > swapExactTokensForTokens etc.
 // 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
      // _token{0,1}のスコープを限定してスタックが深すぎるエラーを回避
      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);
      // 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
      // reserve{0,1}Adjustedのスコープを限定してスタックが深すぎるエラーを回避
      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');
      // Kのチェック
    }

    _update(balance0, balance1, _reserve0, _reserve1);
    // リザーブを更新
    emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    // スワップイベントを発行
}

では、swapといういかにもx \cdot y = kがありそうなところにないなら、

一体どこにあるのかという話になりますが、

 
 
具体的には、

v2-peripheryのUniswapV2Router02コントラクトに記述された、

例えば、swapExactTokensForTokensメソッド内から呼び出される、

UniswapV2LibraryコントラクトのgetAmountOutメソッドなどに実装されてます。
 
 

以下にそれぞれのコントラクトの該当箇所を紹介しておきます。

v2-periphery/contracts /UniswapV2Router02.sol > swapExactTokensForTokens etc.
    // **** SWAP ****
    // requires the initial amount to have already been sent to the first pair
    // **** スワップ ****
    // 最初のペアに初期の金額が既に送信されている必要があります
    function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
        for (uint i; i < path.length - 1; i++) {
            (address input, address output) = (path[i], path[i + 1]);
            (address token0,) = UniswapV2Library.sortTokens(input, output);
            uint amountOut = amounts[i + 1];
            (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
            address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
            IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
                amount0Out, amount1Out, to, new bytes(0)
            );
        }
    }

    // swapExactTokensForTokens
    // swaps an exact amount of input tokens for as many output tokens as possible
    // swapExactTokensForTokens
    // 正確な量の入力トークンをできるだけ多くの出力トークンに交換します
    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
        amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
        require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, to);
    }
v2-periphery/contracts/libraries /UniswapV2Library.sol > getAmountOut, getAmountIn
    // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
    // ある資産の入力額とペアのリザーブを与えられた場合、他の資産の最大出力額を返します
    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
        require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
        require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
        uint amountInWithFee = amountIn.mul(997);
        uint numerator = amountInWithFee.mul(reserveOut);
        uint denominator = reserveIn.mul(1000).add(amountInWithFee);
        amountOut = numerator / denominator;
    }

    // given an output amount of an asset and pair reserves, returns a required input amount of the other asset
    // ある資産の出力額とペアのリザーブを与えられた場合、他の資産の必要な入力額を返します
    function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
        require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
        require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
        uint numerator = reserveIn.mul(amountOut).mul(1000);
        uint denominator = reserveOut.sub(amountOut).mul(997);
        amountIn = (numerator / denominator).add(1);
    }

結論として、以下がx \cdot y = kをベースとする計算です。

v2-periphery/contracts/libraries /UniswapV2Library.sol > getAmountOut, getAmountIn
        uint amountInWithFee = amountIn.mul(997);
        uint numerator = amountInWithFee.mul(reserveOut);
        uint denominator = reserveIn.mul(1000).add(amountInWithFee);
        amountOut = numerator / denominator;

初心者は、全くなぜこれがx \cdot y = kに該当するのか、マジでわからないと思います。

ほんと、こんなのわかるわけないです😂
 
 

このようにx \cdot y = kのコンセプト1つをとっても、

プログラミングの素養がない人にとっては迷路のようになっています。
 
 

Defiユーザーの大半は、コードが読めないと思いますし、

さらには、初心者となるとわかるわけがありません。
 
 

UniswapV2は、やはり初心者にはかなり難しいと思います。

かつての私も全く理解できませんでした。
 
 
初心者向けの解説記事では、

x \cdot y = kを用いてスワップできる具体例などは教えてくれますが、

その詳細が不明であり、結局、運用やヘッジに利用するまでの理解を得られません。
 
 

なので、この記事は、当時の私でも理解できるような、

コードのソースを記載しながらも、

コードを読めなくても何となくUniswapV2が理解できるような、
 
 

また、現在では、

UniswapV2やその系列単体で運用することはあまりないと思いますが、

LPを利用したプロジェクトを少し理解して運用検討の一歩目の直前までは

整理して提供できればと考えています。
 
 

用語の整理

では、前置きが長くなってしまいましたが、

具体的な話に移っていきます。
 
 

あらかじめ、混乱を避けるため、あらかじめ本記事で使用する用語は整えておきます。

明確な間違いがあるかもしれませんので、その際はご指摘いただけますとありがたいです。
 
 

まず、よくクリプト界隈、Web3界隈で

「スマコン」という言葉がよく使われますが、

これを「コントラクト」と捉え、

「特定のアドレス」を持っている、

「状態」と「状態を操作するもの」をセットで持っている情報の構造、

または、その構造を有するもの

という意味で使用させていただきます。

先ほど使用していた、メソッドというのは、「状態を操作するもの」のことを言います。

プログラミングにおける「関数」という言葉を知っている方は、

「関数」と理解していただいて構いません。
 
 

また、「トークン」という言葉もありますが、これは、

ERC20という規格に準拠した誰でも作成可能な貨幣のようなもの

としておきます。
 
 

UniswapV2では、

2つのトークン量の情報をコントラクトの「状態」に記録し、

x \cdot y = kのルールをもとにして、

「状態を操作するもの」を使用してスワップを実現しています。

言い換えると、トークンの交換を実現しています。
 
 

UniswapV2がトークンの交換を実現しているコントラクトにおいて、

このコントラクトの状態に記録された2つの各トークンの量を「リザーブ」、

このコントラクト自体を抽象的に「ペア」と呼びますし、「プール」ともいいます。

後者の場合は、トークンが貯まっている、リザーブが貯まっているという

意味合いが強いと思います。

また、2つのトークンを合わせて「ペア」ともいいます。
 
 
「リザーブ」について、補足しますと、

「リザーブ」は、

UniswapV2Pairコントラクトにおける2つのトークンの「残高、バランス(balance)」とほぼ同じです。

UniswapV2Pairコントラクトが持っているお財布の中の残高というイメージです。
より正確にいうと、「残高、バランス(balance)」の方が、敏感に変動します。
 
 

後述しますが、「リザーブ」「残高、バランス(balance)」はコードで明確に区別され、

UniswapV2のシステムの弱点とならないように配慮されています。
 
 

ここまでの用語を、抽象的に言い換えると、

「ペア」は「リザーブ」として「プール」に預けられているというイメージです。
 
 

また、このトークンの交換を実現しているUniswapV2Pairコントラクトである、

「ペア」「プール」自身もトークンです。
 
 

つまり、

2つのトークンを管理しているトークンが存在し、

そのトークンはコントラクト(UniswapV2Pairコントラクト)であり、

そのコントラクトがUniswapV2の機能を実現しています。
 
 

ややこしい...
 
 

以下にまとめておきます。

  • コントラクト

    • 「特定のアドレス」を持っている、「状態」と「状態を操作するもの」をセットで持っている情報の構造、または、その構造を有するもの。
  • トークン

    • ERC20という規格に準拠した誰でも作成可能な貨幣のようなもの。
    • ERC20という規格をもとにトークンが作成されることで、各Defiプロジェクトは、各トークンの規格をいちいち意識しないで、トークンを絡めたアプリケーションを実装できます。
    • 規格とは具体的にいうと、コントラクトで説明した「状態」と「状態を操作するもの」をあらかじめ決定しておき、その実現ルールを決めたものと理解すればいいと思います。
  • プール(流動性プール)

    • UniswapV2 Pairコントラクトのことを抽象的にいっている気がしています。
    • より正確には、UniswapV2 Pairコントラクトのインスタンスです。
    • リザーブの言い換えのこともある気がします。
    • いずれにせよ、2つのトークンが何らかの入れ物に貯まっているイメージです。
  • ペア

    • UniswapV2 Pairコントラクトのこと。
    • これも正確には、UniswapV2 Pairコントラクトのインスタンスだと思います。
    • プールを構成する2つのトークンのこと。
  • リザーブ

    • プールを構成している、コントラクトの状態に記録されている各トークンの量。
    • もっと具体的にいうと、UniswapV2Pairコントラクトで記録されている「状態」。
    • 「プールに預けられているトークンの量」とも抽象的に言い換えられる。
    • UniswapV2Pairコントラクトが保持している残高とほぼ同じ。
  • (特定のプールにおける)価格

    • リザーブの比
    • 正の値。
  • 流動性

    • UniswapV3において明確になってきますが、x \cdot y = kにおけるkが流動性と言えます。
    • より正確には、kは、リザーブの積であるため、幾何平均を取り、\sqrt{k}を流動性とするのが良いと思います。
  • 流動性提供者(liquidity provider, LP)

    • UniswapV2は、AMMですが(後述)、流動性を提供するユーザーが存在して初めて機能します。この流動性を提供するユーザーを、流動性提供者、liquidity provider、略して、LPといいます。
  • 基礎トークン(underlying token)

    • 基礎トークンという訳が適切かわかりませんが、本記事ではこの表現を使わせていただきます。
    • プールを構成するトークンであり、underlying asset という表現で記述されています。
    • underlyingという言葉は、そもそもデリバティブ(原資産から派生した商品、金融派生商品)の文脈で使われるのではないかと思います。(詳しい方教えて下さい。)
    • 後述のLPトークンは、基礎トークンを原資産とするデリバティブでしょう。
       
       

理解する上で抑えるべきポイント

以下の点を押さえておけば、理解の助けになるかと思います。

(出典:https://github.com/adshao/publications/blob/master/uniswap/dive-into-uniswap-v2-contracts/README.md)

  • v2-peripheryのコントラクトからv2-coreのコントラクトの「状態を操作するもの」(関数、メソッド)を実行するという流れがある
  • 流動性&\sqrt{k}&が一定である場合に、数理モデルx \cdot y = kが機能する
  • 流動性が変動する場合に、\sqrt{k}は変化する
  • 流動性の変動、つまり、&\sqrt{k}&の変化とx \cdot y = kの利用は同時に起こらない
  • コントラクトが持っているトークン残高を前提とするロジックは見落としやすい
     
     
    では、そろそろ本題に入っていきましょう。
     
     

本題

UniswapV2のシステムは、AMMというカテゴリに分類されます。

まずは、AMMをざっくりと理解して、Uniswapの全体像を掴む足掛かりとします。

AMM(Automated Market Maker)

トークンの交換:オーダーブック(板)とAMM

Uniswapとは、その名の通り、任意のトークンのスワップ、

つまり、交換を実現したDefiプロジェクトです。
 
 

さらに、正確に表現すると、

Uniswapは、分散的な交換を実現したDEX(Decentralized Exchange, 分散型取引所)で、

板取引をベースとしたものではない、

AMM(Automated Market Maker)という設計での交換を実現したものです。
 
 

特定のアルゴリズム(ロジック、ルール)で自動的に価格を決定し、

いつでも基礎トークンを取引可能にしたシステムがAMMです。

 
 

AMMという設計では、

オーダーブックを用いて(板取引)トークンを提供する他の「人」と交換するわけではなく、

(Uniswapのような)システムのルールに従い、

トークンが集められているプールを取り扱う「システム」を相手に交換します。
 
 

 
 

UniswapV2をAMMという表現で括るのは大雑把過ぎますので、より詳細に定義しておくと、
 
 

DEX

-> Automated Market Maker(AMM)

->> Constant Function Market Maker (CFMM)

->>> Constant Product Market Maker (CPMM)

->>>> UniswapV2
 
 

という位置付けにあります。
 
 

  • DEX補足:DEX発展の流れ
    • 過去に、A Brief History of Decentralized Exchangeの記事内の画像を編集し、以下の発展の流れの図を作成しました。(原本は先日HDDを落下させ天に召されました。)

 
 
では、より理解を深めるため、AMMにおけるトークン価格について見ていきましょう。
 
 

AMMのトークン価格の捉え方

AMMにおけるトークンの価格とはなんでしょうか。
 
 
そもそも価格とは何でしょうか。
 
 

例えば、1 USD = 144.37 YEN(8月25日 3:19 UTC)

は1ドルで買える日本円の量と捉えられます。
 
 
つまり、価格は交換における、

1単位となるもの(今回だとUSD、これが、リンゴとかでもいい)で取得できる

ペアとなる相手方の量を決めたルールです。
 
 

さらにいうと、どちらかを1としたときの量の比です。
 
 

話を戻します。
 
 

AMMにおけるトークン価格について整理する上で、

比較的わかりやすいオーダーブックを参考に比較しながら考えていくが良さそうです。
 
 

まず、オーダーブックでは、この量の比の値を提示しあって、

売りと買いの参加者が提示する価格がマッチするポイントが、

決定された量の比、

つまり、トークンの価格といえそうです。
 
 

そして、

「売りと買いの参加者が提示する量の比がマッチするポイントで取引を確定する」

といった取引における取り決めがあります。
 
 

では、AMMの場合は、どのような取り決めがあり、どのように価格が決定されるのでしょうか。
 
 

まず、AMMにおける取引の取り決めですが、相手が人間ではなく、システムである以上、

取引を確定する際のルールを前もって決めておいて、

そのルールをもとに自動的に動くようにしておく必要がありそうです。
 
 

そのルールを自動的に実行していき、その実行時の交換量の比が価格といえるでしょう。
 
 

ここで、具体例を用いながらこの前提ルールを考えていきましょう。
 
 

仮に、MyToken(MTK)というオリジナルのトークンがあったとして、

このトークンの価格を考えてみることにします。
 
 

交換を前提とするため、

ひとまずは、USDCとの比較において、MTKの価格を考えます。
 
 

例えばですが、

MTKとUSDCが物理的なコインだとして、適当な量がボトルに入っていたとしましょう。

あなたは、このボトルの管理者であり、

人々がこのボトルの中を交換する際は、あなたを介して行います。

 
 
この前提のもと、

ボトル中の量でのみ(前提とする系でのみ)MTKとUSDCの交換が可能だとして、

あなたは、ひとまず、MTKとUSDCの価値がよくわからないので、

ボトルの中の量の比で、なんとなくの交換比を決定することにしました。
 
 

そして、人々が、そのボトルの中のMTKとUSDCを

「1MTKは大体10USDCぐらいのものを買えそうだ!」とか、

「いやいや、20USDCは下らないだろ!」とか、

考えながら交換を続けるとします。
 
 

もっと抽象的に言えば、

自身の考えよりもMTKを割安で買えるなら、その人はMTKを購入するでしょうし、

自身の考えよりもMTKを割高で売れるなら、その人はMTKを売却するでしょう。
 
 

上記のように交換が続けられると、

短い期間では、MTKとUSDCの量は、およそ一定の範囲内で固定されていきそうです。

このように考えてみると、

固定された量の比がAMMのトークンの「価格」と言えそうではないでしょうか?
 
 

数理モデルx \cdot y = kの雑な構築考察

さて、ここで具体的なルールが必要となってきます。
 
 

先ほどの考察では、重要な前提が明言されていませんでした。
 
 

それは、「ボトルの中が空にならない」

つまり、ボトルの中の基礎トークンは有限であるはずなのに、

どちらのトークンも失われない前提で話をしていたということです。
 
 

有限であるという前提を加味した場合、

交換によって、基礎トークンのどちらか増えると、相方のトークンは減るわけですから、

MTKというわけのわからないトークンは、誰も価値を感じずに、

すぐにUSDCに交換するでしょう。
 
 

そして、ひたすら交換され続けたUSDCは減っていき、

最終的には、ボトルのUSDCは枯渇してしまうでしょう。
 
 

そうなるとMTKとUSDCは交換できなくなるため、この交換ボトルは死を迎えます。
あなたは管理者として失格となりますね。
 
 

このように、死んでしまう前提で交換の場を作ってしまうと、

この仕組み(AMM)はそもそも成り立ちません。
 
 

したがって、どうにかして枯渇しない仕組み、ルールを導入しなければなりません。
 
 

つまり、どちらかの量が増えると、

相方のトークンの取得難易度が動的に変更されるようなルールが必要になりそうです。
 
 

まとめると、

取得難易度である価格が、売り手の思考をベースとするオーダーブックとは違い、

システムであるAMMは、

アルゴリズム、ルール、数式で動的に変化するよう決めておく必要があります。
 
 

ここまで、具体的な量を決めずに、定性的に話を進めて来ましたが、

ここからは、具体的な数字を扱った定量的な議論をするために、

ボトルの中のMTKの量をx = 100、USDCの量をy = 100とすることにします。
 
 

では、先ほど言及した、

アルゴリズム、ルール、数式といったところを具体的に数値が導いていける形で考えてみましょう。
 
 

つまり、トークンの取得難易度を考慮した、

ボトル内のトークン量を決定する数理モデルを考えます。
 
 

まずは、価格が常に1:1の関係である、

つまり、取得難易度に動的な変更がない

最も簡単な数理モデルを考えてみましょう。
 
 

この最も簡単な数理モデルは、

y=-x+x_{max}+y_{max}

の数式をもとに考えられます。
 
 

ボトルの中のトークンの量をxyとすると、

xを1入れると、1のyと交換できるわけですから、

ボトルの中身だけで見ると

x: +1
y: -1

となるルールを満たすため、

y=-x

 
 

ここで、

ボトルの中のxの初期値がx_0=100

ボトルの中のyの初期値がy_0=100

となることから

x_0=100

y_0=100
 
 

この条件から

y=-x+x_0+y_0

となります。
 
 

整えると、以下のようになります。

y=-x+200

 
 

具体的に確認してみると、

例えばxであるMyTokenを25MTKボトルに入れると、以下のように

  y = -(1) \cdot (100+25) + 200 = 75

25USDCが排出され、ボトル内は、75USDCになります。

もちろんこのときのxは、125MTKです。
 
 

以上をグラフにすると以下のようになります。

青線はボトルの中のトークンの量がxが100の時は、

yは100というようにプロットしていったものであり、

赤線はトークン同士の取得難易度(交換比、価格)を表しています。
 
 

今回は常にその難易度(交換比)は-1である様子を赤線で示しています。

まあ、これは、正にすると価格と同じ意味です。
 
 

また、この難易度が動的に変わらない、静的なものとして、

1:1ではない、1:5である場合のグラフが以下になります。

さて、これらのグラフは、

価格の変化なし、取得難易度の変化なしでスワップを続けると、

ボトル内のどちらかのトークンの量が0になってしまうことを明らかにしています。
 
 

では、

AMMの取引のルール、価格決定のルールを決めようとしている今、

枯渇しないようなモデルはどうすれば作れそうでしょうか?
 
 

その前に、まず、先ほどの価格を固定した場合の数理モデルをより丁寧に噛み砕いてみてみましょう。
 
 

y = -px + x_0+ y_0

こちらが先ほどの数理モデルを一般化したものです。

このモデルの傾きは、-pです。
 
 

これにマイナスを乗じると、pとなり、よく見る価格となりますが、

-pのままでも、ほぼ価格と同じ意味で、取得比、取得難易度と位置付けられます。
 
 

つまり、

xを1つ獲得するために、どれだけyをマイナスする必要があるかを表しています。
 
 

より専門的に記載すると以下のようになります。

y = -px + x_0+ y_0
\frac{d}{dx}y = \frac{dy}{dx} (-px + x_0+ y_0)
\frac{dy}{dx} = -p

 
 

先ほどの具体例で言えば、

$p = 1 $ですので、結果は次の通りです、

\frac{dy}{dx} = -1

 
 

はい。では、忘れそうでしたので、今何してたかと言いますと

AMMにおける、

「取引を確定する際のルールを前もって決めておく」

「そのルールをもとに自動的に動くようにしておく必要がある」

「ルールを自動的に実行していき、その実行時の交換量の比が価格といえる」

「枯渇しないようなモデルはどうすれば作れそうか?」

という話でした。
 
 

つまり、

青のグラフが0にならないものを作りたいですし、

今言及した、

\frac{dy}{dx} = -p

つまり、赤線のpxyに合わせて変化する値、p(x,y)にしたいです。
 
 

この2つの条件の時点で何となく、

青グラフは、曲線を使いたくなってきます。
 
 

しかも、xyそれぞれが\inftyに行っても決して0にならない、

xy軸に漸近する曲線です。
 
 

さらに、0にならない正の数>0であると考え、

第一象限(xyもプラスの範囲のこと)内で表現できる最もシンプルな

xy軸に漸近する曲線が欲しいです。
 
 

とすると

y = 1/x

という曲線が候補にあがりそうに思いませんか?
 
 

グラフを見るとまさに、ピッタリ。

また、

y = 1/x

において、分子の値の平方根の値(幾何平均)が、

凸部分のx yの座標になります。
 
 

つまり、この座標(x, y)=(x_0, y_0)とすれば、

こちらもイメージを助けてくれそうです。
 
 

したがって、以下のようなイメージでしょうか。

y =\frac{ x_0\cdot y_0}{x}

 
 

ということで、かなり強引ではありますが、

このような流れを経て、UniswapV1, V2は、

x \cdot y = k

の採用に至ったのではと考察することもできます。
 
 

さらに、深堀ります。
 
 

本当に、x \cdot y = kの価格難易度が動的に変わっていて、

しかも、トークンが少なくなるほどそのトークンの取得が難しくなっているのか

確認してみましょう。
 
 

イメージとしては、以下でしょうか?

xを1取得するのに、yをどれほどマイナスしなければならないかというものを表したものが、

赤と緑の直線です。
 
 

赤❌の地点から黒✖️の地点に移動するとみます。

最初、赤❌にMTKとUSDCがあった場合から、

各黒✖️に移動(xが、MTKが、減っていく方向に移動)していくと、傾きが急になっていっています。
 
 

これはxを1取得するのに、yをどれほどマイナスしなければならないか

のマイナスの値が大きくなっていることを意味しています。
 
 

つまり、xが、MTKが、減っていくと

ちゃんと取得難易度が動的に上がっています。
 
 

正確には、以下のようになります。

x \cdot y = k

この式を x で微分します。

\frac{d}{dx} (x \cdot y) = \frac{d}{dx} (k)
\frac{d}{dx} (x \cdot y) = x \cdot \frac{dy}{dx} + y \cdot \frac{dx}{dx}

ここで、\frac{dx}{dx} = 1なので、

x \cdot \frac{dy}{dx} + y = 0
\frac{dy}{dx} = -\frac{y}{x}

 
 

グラフとしては、以下です。

青色の線が元の曲線 $y = \frac{k}{x} $ を表しており、

赤色の線がその微分\frac{dy}{dx} = -\frac{y}{x} を示しています。
 
 

また、微分の考え方は、先ほども使用した、

以下のグラフがイメージの助けになりますので、再度掲載しておきます。

では、ここまでのことを最後にまとめて、次の話題に移ります。
 
 

ここまで、オーダーブックと比較とボトルという例えを手がかりとして、

AMMにおける、

「取引を確定する際のルールを前もって決めておく」

「そのルールをもとに自動的に動くようにしておく必要がある」

「ルールを自動的に実行していき、その実行時の交換量の比が価格といえる」

「枯渇しないようなモデルはどうすれば作れそうか?」

という話題を考えて来ました。
 
 

斬新なアイデアや0から1を実現するときに、

シンプルでパラメータの少ない数理モデル採用したいものです。
 
 

Uniswapもその例で、

ボトルの中に限定した量をもとに価格、取得難易度を決め、

その取得難易度は、ボトル内が0に近づくほど\inftyに発散する最も単純な曲線

y=\frac{1}{x}

の採用に至り、枯渇しないようなモデルを実現したと推測されます。
 
 

そして、これをイーサリアムのブロックチェーン(EVM)に配置(デプロイ)して、

ユーザーのトレード要求に答える相手として実現しました。
 
 

現状、この最もシンプルなモデルは改良され、

Uniswapの後継バージョン、Curveなどの別プロジェクト等で改善を重ねられています。
 
 

非常に長くなりましたが、

x \cdot y = k

は空からアイデアが降って来たわけではなく、

順に必要なことを整理していった結果の産物であると言えます。

ここまでは、xyをベースに、モデルを確認して来ましたので、

つづいては、

一度、主要なコントラクトの詳細を確認し、準備を整えた上で、

さらにxykを交えて、深掘りしていきましょう。
 
 

主要なコントラクトの大まかな役割

UniswapV2Pairコントラクト

さて、ここまででAMMやUniswapV2の思想的なところを見てきました。

したがって、ある程度UniswapV2を理解していく上でのベースが整ったと言えます。
 
 
先ほど

Uniswapとは、その名の通り、任意のトークンのスワップ、
つまり、交換を実現したDefiプロジェクトです。

と述べましたが、

このスワップを実現するための中心を担うのが、UniswapV2Pairコントラクトです。

(全コードはこちらで確認ください:UniswapV2Pair.sol)
 

(出典:https://github.com/adshao/publications/blob/master/uniswap/dive-into-uniswap-v2-contracts/README.md)
 
 

UniswapV2をWebアプリケーションで経由で皆さん利用しているかと思いますが、

その各機能のエッセンスが詰まったコアロジックが

ここに記載されていると考えていただいて良いと思います。

 
 
逆に、Webアプリケーションの各機能を実装しているのは、

後述のUniswapV2Router02コントラクトです。
 
 

このコントラクトの役割としては、UniswapV2で必要な計算を色々やってあげて、

その結果本当に必要なものをUniswapV2Pairコントラクトに渡しているようなイメージになります。
 
 

UniswapV2Pairコントラクトに話を戻しますが、

実際の基礎トークンの残高などを管理しているのがこのコントラクトでもあります。

皆さんがいつもMetamask(最近の私はRabby Wallet派)などで使用しているアドレス

(外部所有アカウント、EOA、Externally Owned Account、)と同様に、

コントラクトのアドレスでもトークンを所有できます。
 
 

また、UniswapV2Pairコントラクトは、ERC-20を拡張した形になっており、トークンです。

つまり、トークンを保持して管理しているトークンがUniswapV2Pairコントラクトです。

入れ子のようになっているとイメージしてください。(まさに、デリバティブという印象)
 
 

この後、流動性に話を移していきますが、

あらかじめ流動性について説明しておきますと、
 
 
ユーザーがプールに預けるトークンの量のことを流動性(liquidity、リクイディティ)といいます。

より正確には、ユーザーがUniswapV2Pairコントラクトに預けるトークンの量のことです。
 
 
したがって、全てのユーザが預けたその流動性(とスワップによる手数料を合わせたもの)が

このコントラクトアドレスの所持分であるといえます。
 
 

UniswapV2Router02コントラクト

(出典:https://github.com/adshao/publications/blob/master/uniswap/dive-into-uniswap-v2-contracts/README.md)

 
 
では、先ほど少し言及しましたが、

UniswapV2Router02コントラクトについても簡単に整理しておきます。
 
 

逆に、Webアプリケーションの各機能を実装しているのは、
後述のUniswapV2Router02コントラクトです。

とすでにお話ししましたが、
 
 
例えば、Webアプリケーションで、

インプットするトークンと量を指定するとアウトプット量が把握できたり、

逆に、アウトプットするトークンと量を指定するとインプット量が把握できたりするのは、

このコントラクトの以下のメソッド群のおかげです。

v2-periphery/contracts /UniswapV2Router02.sol
    // **** LIBRARY FUNCTIONS ****
    function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
        return UniswapV2Library.quote(amountA, reserveA, reserveB);
    }

    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
        public
        pure
        virtual
        override
        returns (uint amountOut)
    {
        return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
    }

    function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
        public
        pure
        virtual
        override
        returns (uint amountIn)
    {
        return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
    }

    function getAmountsOut(uint amountIn, address[] memory path)
        public
        view
        virtual
        override
        returns (uint[] memory amounts)
    {
        return UniswapV2Library.getAmountsOut(factory, amountIn, path);
    }

    function getAmountsIn(uint amountOut, address[] memory path)
        public
        view
        virtual
        override
        returns (uint[] memory amounts)
    {
        return UniswapV2Library.getAmountsIn(factory, amountOut, path);
    }

 
 

また、その実行や、流動性の提供、解除も

このコントラクトがもっているメソッドを実行することで実現できます。
 
 
基本的には、このUniswapV2Router02コントラクトで実行した情報が、

UniswapV2Pairコントラクトに渡されることで、

システムが稼働していきます。
 
 

流動性と価格(取得難易度)

流動性とは何か...そして価格(取得難易度)への影響

さて、ここまで、

x \cdot y = k

というモデルを雑に構築し、価格について考察してきましたが、

続いては、xyに加えて、kについて考えてみます。
 
 

私は、kを流動性を表すものと捉えていますが、

そもそも流動性とは何かというと、すでに言及した通り、

ユーザーがプールに預けるトークンの量のことです。

より正確には、

ユーザーがUniswapV2Pairコントラクトに預けるトークンの量のことです。
 
 

流動性の話と来れば、

やっと流動性提供者、LP、LPトークンの話!

如何に流動性を提供していくか!

という話題から始めたいところですが、

ちょっと難しくなりすぎそうなので、

ここまで話して来たグラフの延長線上から整理を進めてみましょう。
 
 

まず、先ほど丁寧に導いたx \cdot y = kのグラフをより詳しく調べてみます。
グラフでの凸部分が特徴的に見えるので、
この座標を求めるところから始めてみましょう。
 
 

この座標は、第一象限におけるy=xに対する対称性を利用して求めることができます。

一応証明しておくと、

下グラフのように、

(x, y)y = xに対する対称点は、座標を入れ替えることで得られるので、

座標(x,y)の集合であるグラフx \cdot y = kが、

座標(y,x)の集合であるグラフでも同じ形式の関数を満たすかどうか確認すればいいです。


つまり、

x \cdot y = kを変形した、

y = \frac{k}{x}

において、

yxの代わりに、xyの代わりに代入した、

x = \frac{k}{y}

が成り立つかどうかを確認します。
 
 

y = \frac{k}{x}

難なく

x= \frac{k}{y}

となるため、

関数x \cdot y = kは第一象限において、直線$y = x $に対して対称であることが証明されます。
 
 

では、そのまま、凸部分の座標を求めてしまいます。

今後この座標を流動性kの座標とします。
 
 

では、

x \cdot y = k

において、流動性kの座標はx = yとなる場所にあるため、これを元の式に代入します。

x \cdot x = k
x^2 = k
x = \sqrt{k}

x = \sqrt{k} $のとき、y = x なので、流動性ky$座標も

y = \sqrt{k} \)

となります。
 
 

したがって、流動性kの座標は

(x, y) = (\sqrt{k}, \sqrt{k})

です。
 
 

以下のように表せます。

では、このkですが、そもそもどのように求めていたかというと、

xyの掛け算で求めていました。
 
 

この点に注目して、一旦UniswapV2の実装から離れて、

あなたは、再び、ボトルの管理者となることにしましょう。
 
 

再び、MTK、USDCの入ったボトルを管理することになります。

今回のあなたはこれまでとは一味違い、

ある程度ボトルの特性を分析して、準備を行うことにしました。
 
 

x \cdot y = k

のルールをベースにすると、あなたは管理者としては、最低限のスタート位置にいます。

では、このルールは、あくまでの最低限ボトルが空っぽにならないというものでした。
 
 

あなたは、考えます。

「これ、kの値をいじると何かいいことがあるのでは?」と。
 
 
試しにkの値をいじってみましょう。

kの値が大きくなるほど、カーブは緩やかになりました。
 
 

とすると、過去にこのようなグラフを見ましたよね?


あなたは気がつきます。

kの変化が、価格、取得難易度に影響を与えているのでは?」と。
 
 

ということで試してみました。

同じ価格、同じ取得難易度のところからスタートしたとき、

kの違いによって、同じ量のxを取得するのにどれほどyが必要になるかを可視化しています。


みてわかる通り、

kが大きくなると価格変化、取得難易度が小さくなっています。

つまり、kが大きいほど、ユーザーはスワップの際に損をしにくくなります。
 
 
さらに一般化してグラフにしたものが以下です。


難易度の変化が緩やかになっていることが下段グラフをみるとわかります。
 
 
あなたは考えます。

xyの積がkになるのであれば、

最初に入れるkを大きくしてあげた方が、ユーザーに優しいのでは?」と。
 
 
ここで、より、kについて深掘りしていきましょう。

xyの掛け算がkなわけですから、

そもそもkは、トークンの量を表す何かであると考えられます。

これが大きいほど、大幅な価格の影響を受けない、安定度の高い環境になるというわけです。
 
 

これは直感と合致している気がします。

たくさんの量を扱っているほど、何かに対する影響からは安定しそうですよね。
 
 
この量的な何かであるkですが、モデルの構築の流れから掛け算になってしまいましたが、

掛け算は少し難易度高いので、

xyの掛け算が足し算だったと仮定した場合を考えてみましょう。
 
 

すでに言及しましたが、

したがって、全てのユーザが預けたその流動性(とスワップによる手数料を合わせたもの)が
このコントラクトアドレスの所持分であるといえます。

流動性の合計は、UniswapV2Pairコントラクトの残高であり、リザーブでもあります。
 
 
したがって、kが、xyの和だとすれば、

これは、ボトルの中の総量を端的に表しているとイメージしやすくないでしょうか?
 
 
話を単純に平均(算術平均)してみると、

\frac{x+y}{2}

このようになります。
 
 
これはボトルの中の基礎トークンに何も重み付けをしないで、

数量だけ計算した場合の平均した1基礎トークンあたりの量を表しているといえます。
 
 
この延長線上で、kが、xyの掛け算というもとの考え方で見直してみましょう。

掛け算の平均(幾何平均)は、かけた数だけn乗根を取ればいいので、

平均した1基礎トークンあたりの量は、

\sqrt{k}

と表せます。
 
 
※ただし、一般的に幾何平均は、変化率の平均と捉えられると多いますが、

掛け算をベースとした場合の1つあたりの平均値と今回は捉えてみます。
 
 

さて、このイメージを幾何的に捉えると以下のようになります。

kというのは、

どうやら、凸部の座標と原点からなる正方形の面積のことのようです。
 
 
一度まとめますと、

kというのは、リザーブの乗算的な合計であり、

幾何的には、kにおける座標と原点までの正方形の面積である。
 
 
このイメージをもとに、

いよいよ、初めてUniswapV2の機能の1つに触れることとなります。
 
 

締め:中編に続く

圧倒的に紙面が紙面が足りませんでした...

中編に続きます...

現在準備中。

https://defi-math.net/blogs/uniswap-v2-2

三部作通しての参考文献

下記ページの最下部をご確認ください。
https://defi-math.net/blogs/uniswap-v2-1

Discussion