🦄

[Bunzz Decipher] UniswapV3の『QuoterV2』コントラクトを理解しよう!

2023/09/03に公開

はじめに

初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

https://cryptogames.co.jp/

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

https://cryptospells.jp/

今回はBunzzの新機能『DeCipher』を使用して、UniswapV3の「QuoterV2」のコントラクトを見てみようと思います。

DeCipher』はAIを使用してコントラクトのドキュメントを自動生成してくれるサービスです。

https://www.bunzz.dev/decipher

詳しい使い方に関しては以下の記事を参考にしてください!

https://zenn.dev/heku/articles/33266f0c19d523

今回使用する『DeCipher』のリンクは以下になります。

https://app.bunzz.dev/decipher/chains/1/addresses/0x61fFE014bA17989E743c5F6cB21bF9697530B21e

Etherscanのリンクは以下になります。

https://etherscan.io/address/0x61fFE014bA17989E743c5F6cB21bF9697530B21e

概要

QuoterV2コントラクトは、Uniswap V3プロトコルの中でトークンのスワップに関する正確な見積もりを提供します。
このコントラクトは、Uniswapプラットフォーム上で効率的かつ信頼性の高いトークンスワップの実行をサポートします。

このコントラクトの主な目的は、ユーザーがトークンをスワップする際に、送受信するトークンの量を正確に計算することです。
これには、流動性プールの保有トークン量や価格、スリッページ(価格変動による影響)の許容度など、さまざまなことが考慮されます。
これにより、ユーザーは望むトークンスワップに関して正確な見積もりを得ることができます。

具体的な役割としては、以下の点が挙げられます。

  1. 見積もりの計算
    QuoterV2コントラクトは、複雑な計算を行うために必要なライブラリやインターフェースを利用して、正確なトークン量を計算します。
    流動性プール内のリザーブ量やトークン価格など、プールの現在の状態を考慮しながら計算が行われます。

  2. 正確性の確保
    市場の変動に影響されず、常に正確な見積もりを提供することが求められます。
    高度な数学的手法やUniswap V3プロトコルからのデータを駆使して、高精度な見積もりがされます。

  3. コールバックの処理
    コントラクトは、スワップトランザクションの結果に応じて追加の処理を行うためにコールバックを使用します。
    これにより、スワップが実行された後に特定のアクションを実行することができます。

  4. 複数のトークンのサポート
    異なるトークン間でのスワップに対応するため、QuoterV2コントラクトは複数のトークンペアをサポートします。
    これにより、様々なトークンの組み合わせに対して正確な見積もりを提供できます。

使い方

QuoterV2コントラクトは、Uniswap V3プール内でトークンをスワップする時に、受け取ることができるトークン0またはトークン1の量を見積もるためのスマートコントラクトです。
このコントラクトは、Uniswap V3プロトコルの一部であり、開発者や技術者がトークンスワップの時に正確な見積もりを得るために利用されます。

目標

コントラクトの目標は、Uniswap V3プール内でのトークンスワップに対する正確な見積もりを提供することです。
具体的には、与えられた入力トークンの量をスワップする際に、受け取られるトークン0またはトークン1の量を計算します。

この目標を達成するためには、以下の手順が必要です。

  1. QuoterV2コントラクトをデプロイします。

  2. quoteExactInputSingle関数を呼び出して、特定の入力トークンの量を指定します。
    この関数は、与えられた入力トークン量に対して、トークン0またはトークン1とのスワップによって受け取られる想定トークン量を計算します。

  3. quoteExactOutputSingle関数を呼び出して、特定の出力トークンの量を指定します。
    この関数は、出力トークン量に対して、逆に考えて必要な入力トークン量を計算し、それをもとにトークン0またはトークン1とのスワップによって受け取れる想定トークン量を計算します。

関数

quoteExactInputSingle

特定の入力トークンの量(amountIn)を指定することで、Uniswap V3プール内でのトークンスワップによって得られる出力トークンの量(amountOut)を見積もる関数です。
スワップの経路を表すトークンアドレスの配列(path)を指定します。
配列内の最初の要素が入力トークン、最後の要素が出力トークンです。
また、プールの現在の価格範囲内での価格制限を表す平方根の価格制限(sqrtPriceLimitX96)も指定します。

戻り値
指定した入力トークン量に対して受け取られる出力トークンの量。

quoteExactOutputSingle

この関数は、特定の出力トークンの量(amountOut)を指定することで、Uniswap V3プール内でのトークンスワップによって必要な入力トークンの量(amountIn)を見積もる関数です。
引数として、スワップの経路を表すトークンアドレスの配列(path)と、価格制限の平方根(sqrtPriceLimitX96)が必要です。

戻り値
指定した出力トークン量を受け取るために必要な入力トークンの量。

イベント

Quoted

特定のトークンスワップの見積もりが生成された時に発行されるイベント。
トークンスワップの際に、どれくらいのトークンを入力し、どれくらいのトークンを出力するかの見積もり情報を示すものです。

パラメーター

  • path
    • トークンスワップの経路を表すトークンアドレスの配列。
    • 最初の要素が入力トークンを示し、最後の要素が出力トークンを示します。
  • amountIn
    • 見積もりに使用された入力トークンの量。
    • 具体的なトークンスワップにおいて、どれだけの量のトークンを入力として使ったかが示されます。
  • amountOut
    • 指定された入力トークンの量に対して、受け取ることができる出力トークンの量。
    • スワップ結果として、どれだけの量のトークンを受け取ることができるかを示します。

関連EIP/ERC

パラメーター

_factory

Uniswap V3のファクトリーコントラクトのアドレスを指します。
このコントラクトは、Uniswap V3プロトコルの一部であり、異なるトークンの組み合わせに対して流動性プールを生成する際に使用されます。
つまり、新しいトークンペアのプールを作成するための場所です。

_WETH9

WETH9コントラクトのアドレスを指します。
WETH9は、Ethereumネットワーク上でのEther(ETH) を表すERC20トークンです。
ETHは元々ネイティブな通貨であり、スマートコントラクトとの相互運用性を向上させるために、ERC20トークンとしてラップされたものです。
DeFiプロジェクトやトークンスワッププロトコルでETHと同じように取り扱うために使用されます。

コントラクト

QuoterV2

amountOutCached

amountOutCached
uint256 private amountOutCached;

概要
トークンスワップの安全性を確認するために使用される一時的なストレージ変数。

詳細
特定のトランザクション内でのトークンスワップにおいて、安全性を確認するために使用されます。
トークンスワップが安全であるかどうかをチェックするための条件判定に利用されます。


getPool

getPool
function getPool(
        address tokenA,
        address tokenB,
        uint24 fee
    ) private view returns (IUniswapV3Pool) {
        return IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee)));
    }

概要
Uniswap V3のプールを取得する関数。

詳細
指定されたトークンと手数料率に基づいて、対応するプールのアドレスを計算し、そのプールのインスタンスを返します。

引数

  • tokenA
    • トークンAのアドレス。
  • tokenB
    • トークンBのアドレス。
  • fee
    • 手数料率。

戻り値

  • IUniswapV3Pool
    • 指定されたトークンと手数料率に対応するUniswap V3のプールインスタンス。

uniswapV3SwapCallback

uniswapV3SwapCallback
function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes memory path
    ) external view override {
        require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
        (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
        CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);

        (bool isExactInput, uint256 amountToPay, uint256 amountReceived) =
            amount0Delta > 0
                ? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta))
                : (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta));

        IUniswapV3Pool pool = getPool(tokenIn, tokenOut, fee);
        (uint160 sqrtPriceX96After, int24 tickAfter, , , , , ) = pool.slot0();

        if (isExactInput) {
            assembly {
                let ptr := mload(0x40)
                mstore(ptr, amountReceived)
                mstore(add(ptr, 0x20), sqrtPriceX96After)
                mstore(add(ptr, 0x40), tickAfter)
                revert(ptr, 96)
            }
        } else {
            // if the cache has been populated, ensure that the full output amount has been received
            if (amountOutCached != 0) require(amountReceived == amountOutCached);
            assembly {
                let ptr := mload(0x40)
                mstore(ptr, amountToPay)
                mstore(add(ptr, 0x20), sqrtPriceX96After)
                mstore(add(ptr, 0x40), tickAfter)
                revert(ptr, 96)
            }
        }
    }

概要
Uniswap V3のスワップコールバック関数。
スワップが発生した時に呼び出され、トークンの入出力量やパスなどを処理しています。

詳細

  1. 0-liquidity領域内でのスワップのエラーチェック。
  2. トークンの入出力と手数料率をデコード。
  3. プールのインスタンスを取得。
  4. 入力が正確な場合と不正確な場合で異なる処理を実行。

引数

  • amount0Delta
    • トークン0の数量の変化量。
  • amount1Delta
    • トークン1の数量の変化量。
  • path
    • トークンパス(経路)。

parseRevertReason

parseRevertReason
function parseRevertReason(bytes memory reason)
        private
        pure
        returns (
            uint256 amount,
            uint160 sqrtPriceX96After,
            int24 tickAfter
        )
    {
        if (reason.length != 96) {
            if (reason.length < 68) revert('Unexpected error');
            assembly {
                reason := add(reason, 0x04)
            }
            revert(abi.decode(reason, (string)));
        }
        return abi.decode(reason, (uint256, uint160, int24));
    }

概要
リバート(失敗)の理由を解析し、数値の引用や価格情報を取得する関数。

詳細
リバート(失敗)の理由が96バイトの長さでない場合、エラーチェックを行います。
その後、96バイトの理由から数値情報を抽出し、それを戻り値として返します。

引数

  • reason
    • リバートの理由。

戻り値

  • amount
    • 数量。
  • sqrtPriceX96After
    • 価格の平方根(X96フォーマット)。
  • tickAfter
    • 価格ティック。

handleRevert

handleRevert
function handleRevert(
        bytes memory reason,
        IUniswapV3Pool pool,
        uint256 gasEstimate
    )
        private
        view
        returns (
            uint256 amount,
            uint160 sqrtPriceX96After,
            uint32 initializedTicksCrossed,
            uint256
        )
    {
        int24 tickBefore;
        int24 tickAfter;
        (, tickBefore, , , , , ) = pool.slot0();
        (amount, sqrtPriceX96After, tickAfter) = parseRevertReason(reason

);

        initializedTicksCrossed = pool.countInitializedTicksCrossed(tickBefore, tickAfter);

        return (amount, sqrtPriceX96After, initializedTicksCrossed, gasEstimate);
    }

概要
リバート(失敗)が発生した場合に、リバートの理由から情報を抽出して返す関数。

詳細

  1. プールの価格ティック情報を取得。
  2. リバートの理由から数値情報を解析。
  3. 価格ティックの変化を計算。
  4. 解析された情報を戻り値として返す。

引数

  • reason
    • リバートの理由。
  • pool
    • Uniswap V3プールのインスタンス。
  • gasEstimate
    • ガス推定値。

戻り値

  • amount
    • 数量。
  • sqrtPriceX96After
    • 価格の平方根(X96フォーマット)。
  • initializedTicksCrossed
    • 初期化されたティックの数。
  • gasEstimate
    • ガス推定値。

quoteExactInputSingle

quoteExactInputSingle
function quoteExactInputSingle(QuoteExactInputSingleParams memory params)
        public
        override
        returns (
            uint256 amountOut,
            uint160 sqrtPriceX96After,
            uint32 initializedTicksCrossed,
            uint256 gasEstimate
        )
    {
        bool zeroForOne = params.tokenIn < params.tokenOut;
        IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee);

        uint256 gasBefore = gasleft();
        try
            pool.swap(
                address(this), // address(0) might cause issues with some tokens
                zeroForOne,
                params.amountIn.toInt256(),
                params.sqrtPriceLimitX96 == 0
                    ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                    : params.sqrtPriceLimitX96,
                abi.encodePacked(params.tokenIn, params.fee, params.tokenOut)
            )
        {} catch (bytes memory reason) {
            gasEstimate = gasBefore - gasleft();
            return handleRevert(reason, pool, gasEstimate);
        }
    }

概要
正確な入力量でのスワップの見積もりを提供する関数。

詳細

  1. トークンの入出力順序を確認。
  2. プールのインスタンスを取得。
  3. スワップを試行し、ガス消費を計測。
  4. スワップが失敗した場合、リバートの理由を処理。

引数

  • params
    • スワップのパラメータを含む構造体。

戻り値

  • amountOut
    • 出力量の数量。
  • sqrtPriceX96After
    • 価格の平方根(X96フォーマット)。
  • initializedTicksCrossed
    • 初期化されたティックの数。
  • gasEstimate
    • ガス消費の推定値。

quoteExactInputSingle

quoteExactInputSingle
function quoteExactInputSingle(QuoteExactInputSingleParams memory params)
        public
        override
        returns (
            uint256 amountOut,
            uint160 sqrtPriceX96After,
            uint32 initializedTicksCrossed,
            uint256 gasEstimate
        )
    {
        // スワップの入力トークンが出力トークンよりも小さいかどうかを判定
        bool zeroForOne = params.tokenIn < params.tokenOut;
        
        // プールのインスタンスを取得
        IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee);

        // ガス消費量の計測を開始
        uint256 gasBefore = gasleft();
        try
            // スワップを試行
            pool.swap(
                address(this), // address(0) might cause issues with some tokens
                zeroForOne,
                params.amountIn.toInt256(),
                params.sqrtPriceLimitX96 == 0
                    ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                    : params.sqrtPriceLimitX96,
                abi.encodePacked(params.tokenIn, params.fee, params.tokenOut)
            )
        {} catch (bytes memory reason) {
            // ガス消費量の計測を終了
            gasEstimate = gasBefore - gasleft();
            // リバートの理由を処理して戻り値を返す
            return handleRevert(reason, pool, gasEstimate);
        }
    }

概要
特定の入力量でのスワップの見積もりを行う関数。

詳細

  1. スワップの入力トークンが出力トークンよりも小さいかどうかを判定し、zeroForOneフラグを設定。
  2. プールのインスタンスを取得。
  3. スワップを試行し、ガス消費量を計測。
  4. スワップが失敗した場合、リバートの理由を処理して戻り値を返す。

引数

  • params
    • スワップのパラメータを含む構造体。

戻り値

  • amountOut
    • 出力量の数量。
  • sqrtPriceX96After
    • 価格の平方根(X96フォーマット)。
  • initializedTicksCrossed
    • 初期化されたティックの数。
  • gasEstimate
    • ガス消費の推定値。

quoteExactInput

quoteExactInput
function quoteExactInput(bytes memory path, uint256 amountIn)
        public
        override
        returns (
            uint256 amountOut,
            uint160[] memory sqrtPriceX96AfterList,
            uint32[] memory initializedTicksCrossedList,
            uint256 gasEstimate
        )
    {
        sqrtPriceX96AfterList = new uint160[](path.numPools());
        initializedTicksCrossedList = new uint32[](path.numPools());

        uint256 i = 0;
        while (true) {
            (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();

            // 前のスワップの出力を次のスワップの入力として使用
            (uint256 _amountOut, uint160 _sqrtPriceX96After, uint32 _initializedTicksCrossed, uint256 _gasEstimate) =
                quoteExactInputSingle(
                    QuoteExactInputSingleParams({
                        tokenIn: tokenIn,
                        tokenOut: tokenOut,
                        fee: fee,
                        amountIn: amountIn,
                        sqrtPriceLimitX96: 0
                    })
                );

            sqrtPriceX96AfterList[i] = _sqrtPriceX96After;
            initializedTicksCrossedList[i] = _initializedTicksCrossed;
            amountIn = _amountOut;
            gasEstimate += _gasEstimate;
            i++;

            // 続行するか終了するかを判断
            if (path.hasMultiplePools()) {
                path = path.skipToken();
            } else {
                return (amountIn, sqrtPriceX96AfterList, initializedTicksCrossedList, gasEstimate);
            }
        }
    }

概要
連続したスワップにおける特定の出力量に対する入力量の見積もりを行う関数。

詳細

  1. パスのトークン数に合わせて、出力量と価格ティックのリストを初期化。
  2. パスの全てのプールに対してスワップを行い、各スワップの詳細を記録。
  3. 前のスワップの出力を次のスワップの入力として使用。
  4. スワップが成功した場合、詳細情報をリストに追加していく。
  5. スワップが失敗した場合、リバートの理由を処理して戻り値を返す。

引数

  • path
    • スワップのトークンパス。
  • amountIn
    • 入力量。

戻り値

  • amountOut
    • 出力量の数量。
  • sqrtPriceX96AfterList
    • 各スワップ後の価格の平方根(X96フォーマット)のリスト。
  • initializedTicksCrossedList
    • 各スワップ後の初期化されたティックの数のリスト。
  • gasEstimate
    • ガス消費の推定値。

quoteExactOutputSingle

quoteExactOutputSingle
function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params)
        public
        override
        returns (
            uint256 amountIn,
            uint160 sqrtPriceX96After,
            uint32 initializedTicksCrossed,
            uint256 gasEstimate
        )
    {
        // スワップの出力トークンが入力トークンよりも小さいかどうかを判定
        bool zeroForOne = params.tokenIn < params.tokenOut;
        
        // プールのインスタンスを取得
        IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee);

        // 価格制限が指定されていない場合、出力量をキャッシュして後続のスワップで比較できるようにする
        if (params.sqrtPriceLimitX96 == 0) amountOutCached = params.amount;
        uint256 gasBefore = gasleft();
        try
            // スワップを試行
            pool.swap(
                address(this), // address(0) might cause issues with some tokens
                zeroForOne,
                -params.amount.toInt256(),
                params.sqrtPriceLimitX96 == 0
                    ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                    : params.sqrtPriceLimitX96,
                abi.encodePacked(params.tokenOut, params.fee, params.tokenIn)
            )
        {} catch (bytes memory reason) {
            // ガス消費量の計測を終了
            gasEstimate = gasBefore - gasleft();
            // 価格制限が指定されていない場合、キャッシュをクリア
            if (params.sqrtPriceLimitX96 == 0) delete amountOutCached;
            // リバートの理由を処理して戻り値を返す
            return handleRevert(reason, pool, gasEstimate);
        }
    }

概要
特定の出力量でのスワップの見積もりを行う関数。

詳細

  1. スワップの出力トークンが入力トークンよりも小さいかどうかを判定し、zeroForOneフラグを設定。
  2. プールのインスタンスを取得。
  3. 価格制限が指定されていない場合、出力量をキャッシュに残して実際のスワップ時に比較できるようにする。
  4. スワップを試行し、ガス消費量を計測。
  5. スワップが失敗した場合、リバートの理由を処理して戻り値を返す。

引数

  • params
    • スワップのパラメータを含む構造体。

戻り値

  • amountIn
    • 入力量の数量。
  • sqrtPriceX96After
    • 価格の平方根(X96フォーマット)。
  • initializedTicksCrossed
    • 初期化されたティックの数。
  • gasEstimate
    • ガス消費の推定値。

quoteExactOutput

quoteExactOutput
function quoteExactOutput(bytes memory path, uint256 amountOut)
        public
        override
        returns (
            uint256 amountIn,
            uint160[] memory sqrtPriceX96AfterList,
            uint32[] memory initializedTicksCrossedList,
            uint256 gasEstimate
        )
    {
        sqrtPriceX96AfterList = new uint160[](path.numPools());
        initializedTicksCrossedList = new uint32[](path.numPools());

        uint256 i = 0;
        while (true) {
            (address tokenOut, address tokenIn, uint24 fee) = path.decodeFirstPool();

            // 前のスワップの入力を次のスワップの出力として使用
            (uint256 _amountIn, uint160 _sqrtPriceX96After, uint32 _initializedTicksCrossed, uint256 _gasEstimate) =
                quoteExactOutputSingle(
                    QuoteExactOutputSingleParams({
                        tokenIn: tokenIn,
                        tokenOut: tokenOut,
                        amount: amountOut,
                        fee: fee,
                        sqrtPriceLimitX96: 0
                    })
                );

            sqrtPriceX96AfterList[i] = _sqrtPriceX96After;
            initializedTicksCrossedList[i] = _initializedTicksCrossed;
            amountOut = _amountIn;
            gasEstimate += _gasEstimate;
            i++;

            // 続行するか終了するかを判断
            if (path.hasMultiplePools()) {
                path = path.skipToken();
            } else {
                return (amountOut, sqrtPriceX96AfterList, initializedTicksCrossedList, gasEstimate);
            }
        }
    }

概要
連続したスワップにおいて特定の入力量に対する出力量の見積もりを行う関数。

詳細
1.パスのトークン数に合わせて、価格ティックのリストと初期化されたティックのリストを初期化。
2. パスの全てのプールに対してスワップを行い、各スワップの詳細情報を記録。
3. 前のスワップの入力を次のスワップの出力として使用。
4. スワップが成功した場合、詳細情報をリストに追加していく。
5. スワップが失敗した場合、リバートの理由を処理して戻り値を返す。

引数

  • path
    • スワップのトークンパス。
  • amountOut
    • 出力量。

PoolTicksCounter

countInitializedTicksCrossed

countInitializedTicksCrossed
function countInitializedTicksCrossed(
        IUniswapV3Pool self,
        int24 tickBefore,
        int24 tickAfter
    ) internal view returns (uint32 initializedTicksCrossed) {
        // 各種変数とフラグの初期化
        int16 wordPosLower;
        int16 wordPosHigher;
        uint8 bitPosLower;
        uint8 bitPosHigher;
        bool tickBeforeInitialized;
        bool tickAfterInitialized;

        // ローカルスコープで変数を計算
        {
            // スワップ前と後のアクティブティックのキーとオフセットを取得
            int16 wordPos = int16((tickBefore / self.tickSpacing()) >> 8);
            uint8 bitPos = uint8((tickBefore / self.tickSpacing()) % 256);

            int16 wordPosAfter = int16((tickAfter / self.tickSpacing()) >> 8);
            uint8 bitPosAfter = uint8((tickAfter / self.tickSpacing()) % 256);

            // スワップ後のティックが初期化されている場合、スワップが下向きであるかどうかを確認
            tickAfterInitialized =
                ((self.tickBitmap(wordPosAfter) & (1 << bitPosAfter)) > 0) &&
                ((tickAfter % self.tickSpacing()) == 0) &&
                (tickBefore > tickAfter);

            // スワップ前のティックが初期化されている場合、スワップが上向きであるかどうかを確認
            tickBeforeInitialized =
                ((self.tickBitmap(wordPos) & (1 << bitPos)) > 0) &&
                ((tickBefore % self.tickSpacing()) == 0) &&
                (tickBefore < tickAfter);

            if (wordPos < wordPosAfter || (wordPos == wordPosAfter && bitPos <= bitPosAfter)) {
                wordPosLower = wordPos;
                bitPosLower = bitPos;
                wordPosHigher = wordPosAfter;
                bitPosHigher = bitPosAfter;
            } else {
                wordPosLower = wordPosAfter;
                bitPosLower = bitPosAfter;
                wordPosHigher = wordPos;
                bitPosHigher = bitPos;
            }
        }

        // ティックビットマップをイテレートして初期化されたティックの数を数える
        // 最初のマスクには、下向きティックとその左側のティックを含める
        uint256 mask = type(uint256).max << bitPosLower;
        while (wordPosLower <= wordPosHigher) {
            // 最後のティックビットマップページの場合、終了ティックまでのカウントに制限
            if (wordPosLower == wordPosHigher) {
                mask = mask & (type(uint256).max >> (255 - bitPosHigher));
            }

            uint256 masked = self.tickBitmap(wordPosLower) & mask;
            initializedTicksCrossed += countOneBits(masked);
            wordPosLower++;
            // 次のイテレーションですべてのビットを考慮するためにマスクをリセット
            mask = type(uint256).max;
        }

        if (tickAfterInitialized) {
            initializedTicksCrossed -= 1;
        }

        if (tickBeforeInitialized) {
            initializedTicksCrossed -= 1;
        }

        return initializedTicksCrossed;
    }

概要
スワップによってカバーされる初期化されたティックの数を数える関数。

詳細

  1. 初期化されたティックの数をカウントするための各種変数とフラグを初期化。
  2. ローカルスコープ内で、アクティブなスワップ前と後のティックのキーとオフセットを計算。
  3. スワップ後のティックが初期化されている場合、スワップの方向に応じてスワップ後のティックをカウントしない。
  4. スワップ前のティックが初期化されている場合、スワップの方向に応じてスワップ前のティックをカウントしない。
  5. ティックビットマップを1つずつ回し、カバーされる初期化されたティックの数をカウント。
  6. スワップ後のティックが初期化されている場合、カウントから1を減算。
  7. スワップ前のティックが初期化されている場合、カウントから1を減算。
  8. カウントされた初期化されたティックの数を戻り値として返す。

引数

  • self
    • IUniswapV3Poolのインスタンス。
  • tickBefore
    • スワップ前のティック。
  • tickAfter
    • スワップ後のティック。

戻り値

  • initializedTicksCrossed
    • カウントされた初期化されたティックの数。

countOneBits

countOneBits
function countOneBits(uint256 x) private pure returns (uint16) {
        uint16 bits = 0;
        while (x != 0) {
            bits++;
            x &= (x - 1);
        }
        return bits;
    }
}

概要
与えられた整数のビット表現における1のビットの数をカウントする関数。

詳細

  1. ビット表現内の1のビットの数をカウントするための変数bitsを初期化。
  2. ビット表現が0でない限り、ビット表現内の1のビットを数え、その都度変数bitsを増加させる。
  3. ビット表現内の1のビットを数え終えた後、変数bitsを戻り値として返す。

引数

  • x
    • ビット表現の整数。

戻り値

  • bits
    • ビット表現内の1のビットの数。

イベント

なし。

コード

QuoterV2.sol

QuoterV2.sol
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity =0.7.6;
pragma abicoder v2;

import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol';
import '@uniswap/v3-core/contracts/libraries/SafeCast.sol';
import '@uniswap/v3-core/contracts/libraries/TickMath.sol';
import '@uniswap/v3-core/contracts/libraries/TickBitmap.sol';
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol';
import '@uniswap/v3-periphery/contracts/libraries/Path.sol';
import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol';
import '@uniswap/v3-periphery/contracts/libraries/CallbackValidation.sol';

import '../interfaces/IQuoterV2.sol';
import '../libraries/PoolTicksCounter.sol';

/// @title Provides quotes for swaps
/// @notice Allows getting the expected amount out or amount in for a given swap without executing the swap
/// @dev These functions are not gas efficient and should _not_ be called on chain. Instead, optimistically execute
/// the swap and check the amounts in the callback.
contract QuoterV2 is IQuoterV2, IUniswapV3SwapCallback, PeripheryImmutableState {
    using Path for bytes;
    using SafeCast for uint256;
    using PoolTicksCounter for IUniswapV3Pool;

    /// @dev Transient storage variable used to check a safety condition in exact output swaps.
    uint256 private amountOutCached;

    constructor(address _factory, address _WETH9) PeripheryImmutableState(_factory, _WETH9) {}

    function getPool(
        address tokenA,
        address tokenB,
        uint24 fee
    ) private view returns (IUniswapV3Pool) {
        return IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee)));
    }

    /// @inheritdoc IUniswapV3SwapCallback
    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes memory path
    ) external view override {
        require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
        (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
        CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);

        (bool isExactInput, uint256 amountToPay, uint256 amountReceived) =
            amount0Delta > 0
                ? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta))
                : (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta));

        IUniswapV3Pool pool = getPool(tokenIn, tokenOut, fee);
        (uint160 sqrtPriceX96After, int24 tickAfter, , , , , ) = pool.slot0();

        if (isExactInput) {
            assembly {
                let ptr := mload(0x40)
                mstore(ptr, amountReceived)
                mstore(add(ptr, 0x20), sqrtPriceX96After)
                mstore(add(ptr, 0x40), tickAfter)
                revert(ptr, 96)
            }
        } else {
            // if the cache has been populated, ensure that the full output amount has been received
            if (amountOutCached != 0) require(amountReceived == amountOutCached);
            assembly {
                let ptr := mload(0x40)
                mstore(ptr, amountToPay)
                mstore(add(ptr, 0x20), sqrtPriceX96After)
                mstore(add(ptr, 0x40), tickAfter)
                revert(ptr, 96)
            }
        }
    }

    /// @dev Parses a revert reason that should contain the numeric quote
    function parseRevertReason(bytes memory reason)
        private
        pure
        returns (
            uint256 amount,
            uint160 sqrtPriceX96After,
            int24 tickAfter
        )
    {
        if (reason.length != 96) {
            if (reason.length < 68) revert('Unexpected error');
            assembly {
                reason := add(reason, 0x04)
            }
            revert(abi.decode(reason, (string)));
        }
        return abi.decode(reason, (uint256, uint160, int24));
    }

    function handleRevert(
        bytes memory reason,
        IUniswapV3Pool pool,
        uint256 gasEstimate
    )
        private
        view
        returns (
            uint256 amount,
            uint160 sqrtPriceX96After,
            uint32 initializedTicksCrossed,
            uint256
        )
    {
        int24 tickBefore;
        int24 tickAfter;
        (, tickBefore, , , , , ) = pool.slot0();
        (amount, sqrtPriceX96After, tickAfter) = parseRevertReason(reason);

        initializedTicksCrossed = pool.countInitializedTicksCrossed(tickBefore, tickAfter);

        return (amount, sqrtPriceX96After, initializedTicksCrossed, gasEstimate);
    }

    function quoteExactInputSingle(QuoteExactInputSingleParams memory params)
        public
        override
        returns (
            uint256 amountOut,
            uint160 sqrtPriceX96After,
            uint32 initializedTicksCrossed,
            uint256 gasEstimate
        )
    {
        bool zeroForOne = params.tokenIn < params.tokenOut;
        IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee);

        uint256 gasBefore = gasleft();
        try
            pool.swap(
                address(this), // address(0) might cause issues with some tokens
                zeroForOne,
                params.amountIn.toInt256(),
                params.sqrtPriceLimitX96 == 0
                    ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                    : params.sqrtPriceLimitX96,
                abi.encodePacked(params.tokenIn, params.fee, params.tokenOut)
            )
        {} catch (bytes memory reason) {
            gasEstimate = gasBefore - gasleft();
            return handleRevert(reason, pool, gasEstimate);
        }
    }

    function quoteExactInput(bytes memory path, uint256 amountIn)
        public
        override
        returns (
            uint256 amountOut,
            uint160[] memory sqrtPriceX96AfterList,
            uint32[] memory initializedTicksCrossedList,
            uint256 gasEstimate
        )
    {
        sqrtPriceX96AfterList = new uint160[](path.numPools());
        initializedTicksCrossedList = new uint32[](path.numPools());

        uint256 i = 0;
        while (true) {
            (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();

            // the outputs of prior swaps become the inputs to subsequent ones
            (uint256 _amountOut, uint160 _sqrtPriceX96After, uint32 _initializedTicksCrossed, uint256 _gasEstimate) =
                quoteExactInputSingle(
                    QuoteExactInputSingleParams({
                        tokenIn: tokenIn,
                        tokenOut: tokenOut,
                        fee: fee,
                        amountIn: amountIn,
                        sqrtPriceLimitX96: 0
                    })
                );

            sqrtPriceX96AfterList[i] = _sqrtPriceX96After;
            initializedTicksCrossedList[i] = _initializedTicksCrossed;
            amountIn = _amountOut;
            gasEstimate += _gasEstimate;
            i++;

            // decide whether to continue or terminate
            if (path.hasMultiplePools()) {
                path = path.skipToken();
            } else {
                return (amountIn, sqrtPriceX96AfterList, initializedTicksCrossedList, gasEstimate);
            }
        }
    }

    function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params)
        public
        override
        returns (
            uint256 amountIn,
            uint160 sqrtPriceX96After,
            uint32 initializedTicksCrossed,
            uint256 gasEstimate
        )
    {
        bool zeroForOne = params.tokenIn < params.tokenOut;
        IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee);

        // if no price limit has been specified, cache the output amount for comparison in the swap callback
        if (params.sqrtPriceLimitX96 == 0) amountOutCached = params.amount;
        uint256 gasBefore = gasleft();
        try
            pool.swap(
                address(this), // address(0) might cause issues with some tokens
                zeroForOne,
                -params.amount.toInt256(),
                params.sqrtPriceLimitX96 == 0
                    ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                    : params.sqrtPriceLimitX96,
                abi.encodePacked(params.tokenOut, params.fee, params.tokenIn)
            )
        {} catch (bytes memory reason) {
            gasEstimate = gasBefore - gasleft();
            if (params.sqrtPriceLimitX96 == 0) delete amountOutCached; // clear cache
            return handleRevert(reason, pool, gasEstimate);
        }
    }

    function quoteExactOutput(bytes memory path, uint256 amountOut)
        public
        override
        returns (
            uint256 amountIn,
            uint160[] memory sqrtPriceX96AfterList,
            uint32[] memory initializedTicksCrossedList,
            uint256 gasEstimate
        )
    {
        sqrtPriceX96AfterList = new uint160[](path.numPools());
        initializedTicksCrossedList = new uint32[](path.numPools());

        uint256 i = 0;
        while (true) {
            (address tokenOut, address tokenIn, uint24 fee) = path.decodeFirstPool();

            // the inputs of prior swaps become the outputs of subsequent ones
            (uint256 _amountIn, uint160 _sqrtPriceX96After, uint32 _initializedTicksCrossed, uint256 _gasEstimate) =
                quoteExactOutputSingle(
                    QuoteExactOutputSingleParams({
                        tokenIn: tokenIn,
                        tokenOut: tokenOut,
                        amount: amountOut,
                        fee: fee,
                        sqrtPriceLimitX96: 0
                    })
                );

            sqrtPriceX96AfterList[i] = _sqrtPriceX96After;
            initializedTicksCrossedList[i] = _initializedTicksCrossed;
            amountOut = _amountIn;
            gasEstimate += _gasEstimate;
            i++;

            // decide whether to continue or terminate
            if (path.hasMultiplePools()) {
                path = path.skipToken();
            } else {
                return (amountOut, sqrtPriceX96AfterList, initializedTicksCrossedList, gasEstimate);
            }
        }
    }
}

PoolTicksCounter

PoolTicksCounter.sol
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.6.0;

import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';

library PoolTicksCounter {
    /// @dev This function counts the number of initialized ticks that would incur a gas cost between tickBefore and tickAfter.
    /// When tickBefore and/or tickAfter themselves are initialized, the logic over whether we should count them depends on the
    /// direction of the swap. If we are swapping upwards (tickAfter > tickBefore) we don't want to count tickBefore but we do
    /// want to count tickAfter. The opposite is true if we are swapping downwards.
    function countInitializedTicksCrossed(
        IUniswapV3Pool self,
        int24 tickBefore,
        int24 tickAfter
    ) internal view returns (uint32 initializedTicksCrossed) {
        int16 wordPosLower;
        int16 wordPosHigher;
        uint8 bitPosLower;
        uint8 bitPosHigher;
        bool tickBeforeInitialized;
        bool tickAfterInitialized;

        {
            // Get the key and offset in the tick bitmap of the active tick before and after the swap.
            int16 wordPos = int16((tickBefore / self.tickSpacing()) >> 8);
            uint8 bitPos = uint8((tickBefore / self.tickSpacing()) % 256);

            int16 wordPosAfter = int16((tickAfter / self.tickSpacing()) >> 8);
            uint8 bitPosAfter = uint8((tickAfter / self.tickSpacing()) % 256);

            // In the case where tickAfter is initialized, we only want to count it if we are swapping downwards.
            // If the initializable tick after the swap is initialized, our original tickAfter is a
            // multiple of tick spacing, and we are swapping downwards we know that tickAfter is initialized
            // and we shouldn't count it.
            tickAfterInitialized =
                ((self.tickBitmap(wordPosAfter) & (1 << bitPosAfter)) > 0) &&
                ((tickAfter % self.tickSpacing()) == 0) &&
                (tickBefore > tickAfter);

            // In the case where tickBefore is initialized, we only want to count it if we are swapping upwards.
            // Use the same logic as above to decide whether we should count tickBefore or not.
            tickBeforeInitialized =
                ((self.tickBitmap(wordPos) & (1 << bitPos)) > 0) &&
                ((tickBefore % self.tickSpacing()) == 0) &&
                (tickBefore < tickAfter);

            if (wordPos < wordPosAfter || (wordPos == wordPosAfter && bitPos <= bitPosAfter)) {
                wordPosLower = wordPos;
                bitPosLower = bitPos;
                wordPosHigher = wordPosAfter;
                bitPosHigher = bitPosAfter;
            } else {
                wordPosLower = wordPosAfter;
                bitPosLower = bitPosAfter;
                wordPosHigher = wordPos;
                bitPosHigher = bitPos;
            }
        }

        // Count the number of initialized ticks crossed by iterating through the tick bitmap.
        // Our first mask should include the lower tick and everything to its left.
        uint256 mask = type(uint256).max << bitPosLower;
        while (wordPosLower <= wordPosHigher) {
            // If we're on the final tick bitmap page, ensure we only count up to our
            // ending tick.
            if (wordPosLower == wordPosHigher) {
                mask = mask & (type(uint256).max >> (255 - bitPosHigher));
            }

            uint256 masked = self.tickBitmap(wordPosLower) & mask;
            initializedTicksCrossed += countOneBits(masked);
            wordPosLower++;
            // Reset our mask so we consider all bits on the next iteration.
            mask = type(uint256).max;
        }

        if (tickAfterInitialized) {
            initializedTicksCrossed -= 1;
        }

        if (tickBeforeInitialized) {
            initializedTicksCrossed -= 1;
        }

        return initializedTicksCrossed;
    }

    function countOneBits(uint256 x) private pure returns (uint16) {
        uint16 bits = 0;
        while (x != 0) {
            bits++;
            x &= (x - 1);
        }
        return bits;
    }
}

最後に

今回の記事では、Bunzzの新機能『DeCipher』を使用して、UniswapV3の「QuoterV2」のコントラクトを見てきました。
いかがだったでしょうか?
今後も特定のNFTやコントラクトをピックアップしてまとめて行きたいと思います。

普段はブログやQiitaでブロックチェーンやAIに関する記事を挙げているので、よければ見ていってください!

https://chaldene.net/

https://qiita.com/cardene

DeCipher |"Read me" for All of Contracts

Discussion