🦄

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

2023/09/05に公開

はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

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

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

https://www.bunzz.dev/decipher

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

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

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

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

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

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

概要

Quoter(クォーター) コントラクトは、Uniswap v3プロトコル内で非常に重要な役割を果たしています。
このコントラクトは、Uniswap分散型取引所でのトークンスワップに関する見積もり機能を提供します。
具体的には、どれだけのトークンがスワップ取引で送受信されるかを計算し、ユーザーに正確な情報を提供します。

Quoterコントラクトは、Uniswap v3エコシステム内で他のコントラクトやライブラリと連携して働きます。
これらは、SafeCastやTickMathIUniswapV3PoolIUniswapV3SwapCallbackPathPoolAddressCallbackValidationBytesLibなどのコントラクトやライブラリです。

IUniswapV3Poolインターフェースを使用して、QuoterコントラクトはUniswap v3プールから必要なデータを取得します。
また、プール内のティックシステムに関連する計算は、TickMathライブラリを使って行います。
さらに、トークンスワップの際に最適な経路を見つけるために、Pathライブラリも活用されます。

責務

正確な見積もりの提供

市場の状況に基づいて、トークンスワップ時に受け取るか送信されるトークンの量を予測します。
これにより、ユーザーに正確な取引情報を提供します。

他のコントラクトとの連携

Quoterコントラクトは、Uniswap v3エコシステム内のさまざまなコントラクトやライブラリと連携して、必要なデータを取得し、計算を行います。
これにより、正確な見積もりが実現されます。

セキュリティと検証の確保

Quoterコントラクトは、CallbackValidationライブラリを使用して、スワップ取引のセキュリティと有効性を保護します。
これにより、不正な操作を防止し、安全なトークンスワップが可能となります。

使い方

Quoter(クォーター) コントラクトは、Uniswap V3プール内でのトークンスワップに関する価格見積もりを提供するスマートコントラクトです。
このコントラクトは、技術オペレーターや開発者がトークンスワップの価格を簡単に計算するための関数を備えています。

目標

Uniswap V3プール内でのトークンスワップに関する価格見積もりを手軽に取得することです。
これにより、技術オペレーターや開発者は、プールの現在の状態を考慮して、与えられた入力トークン量に対する出力トークン量を計算することができます。

手順

  1. Quoterコントラクトをデプロイします。
  2. quoteExactInputSingle関数を呼び出して、単一のトークンスワップにおいて、指定された入力トークン量に対する出力トークン量の見積もりを取得します。
  3. quoteExactOutputSingle関数を呼び出して、単一のトークンスワップにおいて、望む出力トークン量を受け取るために必要な入力トークン量の見積もりを取得します。
  4. quoteExactInput関数を呼び出して、複数のホップを経るトークンスワップにおいて、指定された入力トークン量に対する出力トークン量の見積もりを取得します。
  5. quoteExactOutput関数を呼び出して、複数のホップを経るトークンスワップにおいて、望む出力トークン量を受け取るために必要な入力トークン量の見積もりを取得します。

関数

Quoter(クォーター) コントラクトでは、以下の関数を呼び出してトークンスワップの価格見積もりを取得することができます。

quoteExactInputSingle

  • path
    • トークンスワップのパスを表すトークンアドレスの配列。
  • amountIn
    • スワップする入力トークンの量。
  • sqrtPriceLimitX96
    • プールの現在のティック範囲内の価格制限の平方根。

quoteExactOutputSingle

単一のトークンスワップにおいて、望む出力トークン量を受け取るために必要な入力トークン量を計算します。

  • path
    • トークンスワップのパスを表すトークンアドレスの配列。
  • amountOut
    • 受け取りたい出力トークンの量。
  • sqrtPriceLimitX96
    • プールの現在のティック範囲内の価格制限の平方根。

quoteExactInput

複数のホップを経るトークンスワップにおいて、与えられた入力トークン量に対する出力トークン量を計算します。

  • path
    • トークンスワップのパスを表すトークンアドレスの配列。
  • amountIn
    • スワップする入力トークンの量。
  • sqrtPriceLimitX96
    • プールの現在のティック範囲内の価格制限の平方根。

quoteExactOutput

複数のホップを経るトークンスワップにおいて、望む出力トークン量を受け取るために必要な入力トークン量を計算します。

  • path
    • トークンスワップのパスを表すトークンアドレスの配列。
  • amountOut
    • 受け取りたい出力トークンの量。
  • sqrtPriceLimitX96
    • プールの現在のティック範囲内の価格制限の平方根。

関連EIP/ERC

  • EIP-165: Standard Interface Detection

https://qiita.com/cardene/items/541184977a5cba51b759

https://chaldene.net/erc20

  • ERC-721: Non-Fungible Token Standard

https://chaldene.net/erc721

https://chaldene.net/erc1155

パラメーター

_factory

Uniswapのファクトリーコントラクトのアドレスを指します。
このアドレスは、新しいUniswapのトークンペアのインスタンスを作成するために使用されます。
Uniswapのファクトリーコントラクトは、異なるトークン同士のペアを作成し、取引所で利用可能にする際に重要な役割を果たします。
例えば、ETHと別のトークンをペアにして取引できるようになります。

_WETH9
WETH9コントラクトのアドレスを指します。
これはEthereumネットワーク上のラップドエーテル(Wrapped Ether)トークンを表します。
WETHは、EthereumのネイティブトークンであるETHERC20トークンとして扱うためにラップされたものです。
これにより、ETHをスマートコントラクトとの相互運用性が高い形式で利用できるようになります。

コントラクト

Quoter

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));
    if (isExactInput) {
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, amountReceived)
            revert(ptr, 32)
        }
    } 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)
            revert(ptr, 32)
        }
    }
}

概要
Uniswap V3のスワップコールバック処理を行う関数。

詳細

  • スワップの結果として得られたトークンの数量変化を受け取ります。
  • pathパラメータからスワップに関連するトークンと手数料情報を抽出します。
  • ファクトリーを使用してコールバックの妥当性を検証します。
  • 入力が正確な場合とそうでない場合に分けて処理を行います。
    • 正確な場合、受け取ったトークンの数量を使用してリバートします。
    • 正確でない場合、キャッシュが存在する場合は出力トークンの数量が正しいことを確認し、リバートします。

引数

  • amount0Delta
    • トークン0の数量変化。
  • amount1Delta
    • トークン1の数量変化。
  • path
    • スワップのトークンパス。

parseRevertReason

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

概要
エラーメッセージのバイト数を解析して数値を取得する関数。

詳細

  • リバートの理由(エラーメッセージ)を受け取り、その内容を解析します。
  • バイト列の長さが32でない場合、エラーメッセージが68バイト未満の場合は予期しないエラーをリバートします。
  • エラーメッセージの先頭に4バイトを足してから、その位置から文字列デコードを行い、数値を取得します。

引数

  • reason
    • リバートの理由。

戻り値

  • 解析された数値。

quoteExactInputSingle

quoteExactInputSingle
function quoteExactInputSingle(
    address tokenIn,
    address tokenOut,
    uint24 fee,
    uint256 amountIn,
    uint160 sqrtPriceLimitX96
) public override returns (uint256 amountOut) {
    bool zeroForOne = tokenIn < tokenOut;

    try
        getPool(tokenIn, tokenOut, fee).swap(
            address(this),
            zeroForOne,
            amountIn.toInt256(),
            sqrtPriceLimitX96 == 0
                ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                : sqrtPriceLimitX96,
            abi.encodePacked(tokenIn, fee, tokenOut)
        )
    {} catch (bytes memory reason) {
        return parseRevertReason(reason);
    }
}

概要
指定された入力量に基づいて、Uniswap V3Uでのトークンスワップにおける出力量を見積もる関数。

詳細

  • トークンインとトークンアウト、手数料、入力量、プライスリミット情報を受け取ります。
  • トークンインがトークンアウトよりも小さい場合、zeroForOneフラグは真となります。
  • トークンスワップを試み、成功した場合は出力量を取得します。
  • 失敗した場合、リバートの理由から数値を取得して戻り値とします。

引数

  • tokenIn
    • 入力トークンのアドレス。
  • tokenOut
    • 出力トークンのアドレス。
  • fee
    • 手数料。
  • amountIn
    • 入力量。
  • sqrtPriceLimitX96
    • プライスリミット情報。

戻り値

  • 出力トークンの量。

quoteExactInput

quoteExactInput
function quoteExactInput(bytes memory path, uint256 amountIn) external override returns (uint256 amountOut) {
    while (true) {
        bool hasMultiplePools = path.hasMultiplePools();

        (address tokenIn, address tokenOut,

 uint24 fee) = path.decodeFirstPool();

        amountIn = quoteExactInputSingle(tokenIn, tokenOut, fee, amountIn, 0);

        if (hasMultiplePools) {
            path = path.skipToken();
        } else {
            return amountIn;
        }
    }
}

概要
複数のトークン間での連続したスワップにおいて、指定された入力量に対する最終的な出力量を見積もる関数。

詳細

  • パス情報と入力量を受け取ります。
  • パス内に複数のプールが存在するかどうかを確認し、ループ内で各スワップの出力量を見積もります。
  • パス内に複数のプールがある場合、次のトークンに進みます。
  • そうでない場合、最終的な出力量を戻り値とします。

引数

  • path
    • トークンパス。
  • amountIn
    • 入力量。

戻り値

  • 最終的な出力量。

quoteExactOutputSingle

quoteExactOutputSingle
function quoteExactOutputSingle(
    address tokenIn,
    address tokenOut,
    uint24 fee,
    uint256 amountOut,
    uint160 sqrtPriceLimitX96
) public override returns (uint256 amountIn) {
    bool zeroForOne = tokenIn < tokenOut;

    if (sqrtPriceLimitX96 == 0) amountOutCached = amountOut;
    try
        getPool(tokenIn, tokenOut, fee).swap(
            address(this),
            zeroForOne,
            -amountOut.toInt256(),
            sqrtPriceLimitX96 == 0
                ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                : sqrtPriceLimitX96,
            abi.encodePacked(tokenOut, fee, tokenIn)
        )
    {} catch (bytes memory reason) {
        if (sqrtPriceLimitX96 == 0) delete amountOutCached;
        return parseRevertReason(reason);
    }
}

概要
指定された出力量に基づいて、Uniswap V3での逆方向のトークンスワップにおける入力量を見積もる関数。

詳細

  • トークンインとトークンアウト、手数料、出力量、プライスリミット情報を受け取ります。
  • プライスリミット情報が0の場合、出力量をキャッシュします。
  • トークンスワップを試み、成功した場合は入力量を取得します。
  • 失敗した場合、リバートの理由から数値を取得して戻り値とします。

引数

  • tokenIn
    • 入力トークンのアドレス。
  • tokenOut
    • 出力トークンのアドレス。
  • fee
    • 手数料。
  • amountOut
    • 出力量。
  • sqrtPriceLimitX96
    • プライスリミット情報。

戻り値

  • 入力トークンの量。

quoteExactOutput

quoteExactOutput
function quoteExactOutput(bytes memory path, uint256 amountOut) external override returns (uint256 amountIn) {
    while (true) {
        bool hasMultiplePools = path.hasMultiplePools();

        (address tokenOut, address tokenIn, uint24 fee) = path.decodeFirstPool();

        amountOut = quoteExactOutputSingle(tokenIn, tokenOut, fee, amountOut, 0);

        if (hasMultiplePools) {
            path = path.skipToken();
        } else {
            return amountOut;
        }
    }
}

概要
指定された出力量に対する最大の入力量を見積もる関数。
連続したトークンスワップの逆方向で、出力量から逆算して入力量を計算します。

詳細

  • 与えられたパス情報と出力量を元に、連続した逆トークンスワップを行って入力量を見積もります。
  • 各スワップのトークンアウトとトークンイン、手数料を抽出します。
  • 前のスワップの出力が後続のスワップの入力となり、逆方向のトークンスワップを実行して入力量を逆算します。
  • パス内に複数のプールが存在するかどうかを確認し、必要な回数だけループを繰り返します。
  • すべてのスワップが完了したら最終的な入力量を戻り値として返します。

引数

  • path
    • トークンパス(バイト列)。
  • amountOut
    • 出力量。

戻り値

  • 最大の入力量。

Path

ADDR_SIZE

ADDR_SIZE
uint256 private constant ADDR_SIZE = 20;

概要
バイトエンコードされたアドレスの長さを表す定数。

詳細
Solidityのコード内でアドレスをバイトエンコードする際の長さを示すものです。
アドレスは通常20バイトの固定長ですが、その長さをコード内で使用する際に便利です。


FEE_SIZE

FEE_SIZE
uint256 private constant FEE_SIZE = 3;

概要
バイトエンコードされた手数料の長さを表す定数。

詳細
コード内で手数料をバイトエンコードする際の長さを示すものです。
手数料の長さは3バイトです。
手数料は通常固定の長さを持ち、その長さをコード内で使用する際に利用されます。


NEXT_OFFSET

NEXT_OFFSET
uint256 private constant NEXT_OFFSET = ADDR_SIZE + FEE_SIZE;

概要
単一のトークンアドレスとプール手数料のオフセットを表す定数。

詳細
アドレスと手数料がエンコードされたデータ構造内で、次のトークンアドレスとプール手数料の位置を示すために使用されます。
アドレスの長さと手数料の長さを足し合わせた値です。


POP_OFFSET

POP_OFFSET
uint256 private constant POP_OFFSET = NEXT_OFFSET + ADDR_SIZE;

概要
エンコードされたプールキーのオフセットを表す定数。

詳細
エンコードされたデータ構造内で、プールキーの位置を示すために使用されます。
前のアドレスと手数料、そして次のトークンアドレスとプール手数料の合計長に加えた値です。


MULTIPLE_POOLS_MIN_LENGTH

MULTIPLE_POOLS_MIN_LENGTH
uint256 private constant MULTIPLE_POOLS_MIN_LENGTH = POP_OFFSET + NEXT_OFFSET;

概要
2つ以上のプールが含まれるエンコードの最小長を表す定数。

詳細
エンコードされたデータ構造内で、2つ以上のプールが含まれる場合の最小のエンコード長を示します。
プールキー、トークンアドレス、およびプール手数料が複数回現れる場合に、データ構造の長さがどれだけ少なくとも必要かを示します。


hasMultiplePools

hasMultiplePools
function hasMultiplePools(bytes memory path) internal pure returns (bool) {
    return path.length >= MULTIPLE_POOLS_MIN_LENGTH;
}

概要
与えられたスワップパスが2つ以上のプールを含むかどうかを判定する関数。

詳細

  • エンコードされたスワップパスの長さが、最小の複数プールエンコーディングの長さ以上であれば、2つ以上のプールを含むとみなします。

引数

  • path
    • エンコードされたスワップパス。

戻り値
パスが2つ以上のプールを含む場合true、それ以外の場合falseを返す。


decodeFirstPool

decodeFirstPool
function decodeFirstPool(bytes memory path)
    internal
    pure
    returns (
        address tokenA,
        address tokenB,
        uint24 fee
    )
{
    tokenA = path.toAddress(0);
    fee = path.toUint24(ADDR_SIZE);
    tokenB = path.toAddress(NEXT_OFFSET);
}

概要
エンコードされたスワップパスから最初のプールの情報をデコードする関数。

詳細

  • エンコードされたスワップパスから最初のトークンアドレス、プールの手数料レベル、2番目のトークンアドレスを順にデコードします。

引数

  • path
    • エンコードされたスワップパス。

戻り値

  • tokenA
    • プールの最初のトークンアドレス。
  • tokenB
    • プールの2番目のトークンアドレス。
  • fee
    • プールの手数料レベル。

getFirstPool

getFirstPool
function getFirstPool(bytes memory path) internal pure returns (bytes memory) {
    return path.slice(0, POP_OFFSET);
}

概要
エンコードされたスワップパスから最初のプールに対応するセグメントを取得する関数。

詳細

  • エンコードされたスワップパスから、パスの最初のプールに対応するデータを含むセグメントを取得します。

引数

  • path
    • エンコードされたスワップパス。

戻り値

  • パスの最初のプールに対応するデータを含むセグメント。

skipToken

skipToken
function skipToken(bytes memory path) internal pure returns (bytes memory) {
    return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET);
}

概要
エンコードされたスワップパスからトークンと手数料の要素をスキップし、残りのパスを返す関数。

詳細

  • エンコードされたスワップパスからトークンと手数料の要素をスキップし、残りのパスを返します。

引数

  • path
    • スワップパス。

戻り値

  • パス内の残りのトークンと手数料要素を含むバイト列。

PoolAddress

以下を参考にしてください。

https://zenn.dev/cryptogames/articles/e59072e94c78e7#pooladdress

CallbackValidation

以下を参考にしてください。

https://zenn.dev/cryptogames/articles/96c6fa9d04460c#callbackvalidation

BytesLib

slice

slice
function slice(
    bytes memory _bytes,
    uint256 _start,
    uint256 _length
) internal pure returns (bytes memory) {
    require(_length + 31 >= _length, 'slice_overflow');
    require(_start + _length >= _start, 'slice_overflow');
    require(_bytes.length >= _start + _length, 'slice_outOfBounds');

    bytes memory tempBytes;

    assembly {
        switch iszero(_length)
            case 0 {
                tempBytes := mload(0x40)

                let lengthmod := and(_length, 31)

                let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod)))
                let end := add(mc, _length)

                for {
                    let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start)
                } lt(mc, end) {
                    mc := add(mc, 0x20)
                    cc := add(cc, 0x20)
                } {
                    mstore(mc, mload(cc))
                }

                mstore(tempBytes, _length)

                mstore(0x40, and(add(mc, 31), not(31)))
            }
            default {
                tempBytes := mload(0x40)
                mstore(tempBytes, 0)

                mstore(0x40, add(tempBytes, 0x20))
            }
    }

    return tempBytes;
}

概要
バイト列から指定した位置と長さでサブセットを切り出す関数。

詳細

  • 切り出すバイト列、開始位置、および長さを引数として受け取ります。
  • オーバーフローチェックを行い、切り出す範囲がバイト列の範囲内であることを確認します。
  • 新しいバイト列(tempBytes)を作成し、アセンブリコードを使用して切り出し処理を行います。
  • 切り出しの際、バイト列の最初のワードには切り出し範囲の一部のデータが含まれる可能性があるため、最終的なバイト列の長さを最初のワードに書き込みます。

引数

  • _bytes
    • 切り出すバイト列。
  • _start
    • 開始位置。
  • _length
    • 切り出す長さ。

戻り値

  • 切り出されたサブセットのバイト列。

toAddress

toAddress
function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) {
    require(_start + 20 >= _start, 'toAddress_overflow');
    require(_bytes.length >= _start + 20, 'toAddress_outOfBounds');
    address tempAddress;

    assembly {
        tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000)
    }

    return tempAddress;
}

概要
バイト列から指定した位置でアドレスをデコードする関数。

詳細

  • バイト列と開始位置を引数として受け取り、指定された位置から20バイトのデータをアドレスにデコードします。
  • デコードされたアドレスを一時変数(tempAddress)に格納し、アセンブリコードを使用してデコードを行います。

引数

  • _bytes
    • バイト列。
  • _start
    • 開始位置。

戻り値

  • デコードされたアドレス。

toUint24

toUint24
function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) {
    require(_start + 3 >= _start, 'toUint24_overflow');
    require(_bytes.length >= _start + 3, 'toUint24_outOfBounds');
    uint24 tempUint;

    assembly {
        tempUint := mload(add(add(_bytes, 0x3), _start))
    }

    return tempUint;
}

概要
バイト列から指定した位置で24ビットの整数をデコードする関数。

詳細

  • バイト列と開始位置を引数として受け取り、指定された位置から3バイトのデータを24ビットの整数にデコードします。
  • デコードされた整数を一時変数(tempUint)に格納し、アセンブリコードを使用してデコードを行います。

引数

  • _bytes
    • バイト列。
  • _start
    • 開始位置。

戻り値

  • デコードされた24ビットの整数。

コード

Quoter

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

import '@uniswap/v3-core/contracts/libraries/SafeCast.sol';
import '@uniswap/v3-core/contracts/libraries/TickMath.sol';
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol';

import '../interfaces/IQuoter.sol';
import '../base/PeripheryImmutableState.sol';
import '../libraries/Path.sol';
import '../libraries/PoolAddress.sol';
import '../libraries/CallbackValidation.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 Quoter is IQuoter, IUniswapV3SwapCallback, PeripheryImmutableState {
    using Path for bytes;
    using SafeCast for uint256;

    /// @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));
        if (isExactInput) {
            assembly {
                let ptr := mload(0x40)
                mstore(ptr, amountReceived)
                revert(ptr, 32)
            }
        } 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)
                revert(ptr, 32)
            }
        }
    }

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

    /// @inheritdoc IQuoter
    function quoteExactInputSingle(
        address tokenIn,
        address tokenOut,
        uint24 fee,
        uint256 amountIn,
        uint160 sqrtPriceLimitX96
    ) public override returns (uint256 amountOut) {
        bool zeroForOne = tokenIn < tokenOut;

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

    /// @inheritdoc IQuoter
    function quoteExactInput(bytes memory path, uint256 amountIn) external override returns (uint256 amountOut) {
        while (true) {
            bool hasMultiplePools = path.hasMultiplePools();

            (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();

            // the outputs of prior swaps become the inputs to subsequent ones
            amountIn = quoteExactInputSingle(tokenIn, tokenOut, fee, amountIn, 0);

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

    /// @inheritdoc IQuoter
    function quoteExactOutputSingle(
        address tokenIn,
        address tokenOut,
        uint24 fee,
        uint256 amountOut,
        uint160 sqrtPriceLimitX96
    ) public override returns (uint256 amountIn) {
        bool zeroForOne = tokenIn < tokenOut;

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

    /// @inheritdoc IQuoter
    function quoteExactOutput(bytes memory path, uint256 amountOut) external override returns (uint256 amountIn) {
        while (true) {
            bool hasMultiplePools = path.hasMultiplePools();

            (address tokenOut, address tokenIn, uint24 fee) = path.decodeFirstPool();

            // the inputs of prior swaps become the outputs of subsequent ones
            amountOut = quoteExactOutputSingle(tokenIn, tokenOut, fee, amountOut, 0);

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

PeripheryImmutableState

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

import '../interfaces/IPeripheryImmutableState.sol';

/// @title Immutable state
/// @notice Immutable state used by periphery contracts
abstract contract PeripheryImmutableState is IPeripheryImmutableState {
    /// @inheritdoc IPeripheryImmutableState
    address public immutable override factory;
    /// @inheritdoc IPeripheryImmutableState
    address public immutable override WETH9;

    constructor(address _factory, address _WETH9) {
        factory = _factory;
        WETH9 = _WETH9;
    }
}

Path

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

import './BytesLib.sol';

/// @title Functions for manipulating path data for multihop swaps
library Path {
    using BytesLib for bytes;

    /// @dev The length of the bytes encoded address
    uint256 private constant ADDR_SIZE = 20;
    /// @dev The length of the bytes encoded fee
    uint256 private constant FEE_SIZE = 3;

    /// @dev The offset of a single token address and pool fee
    uint256 private constant NEXT_OFFSET = ADDR_SIZE + FEE_SIZE;
    /// @dev The offset of an encoded pool key
    uint256 private constant POP_OFFSET = NEXT_OFFSET + ADDR_SIZE;
    /// @dev The minimum length of an encoding that contains 2 or more pools
    uint256 private constant MULTIPLE_POOLS_MIN_LENGTH = POP_OFFSET + NEXT_OFFSET;

    /// @notice Returns true iff the path contains two or more pools
    /// @param path The encoded swap path
    /// @return True if path contains two or more pools, otherwise false
    function hasMultiplePools(bytes memory path) internal pure returns (bool) {
        return path.length >= MULTIPLE_POOLS_MIN_LENGTH;
    }

    /// @notice Decodes the first pool in path
    /// @param path The bytes encoded swap path
    /// @return tokenA The first token of the given pool
    /// @return tokenB The second token of the given pool
    /// @return fee The fee level of the pool
    function decodeFirstPool(bytes memory path)
        internal
        pure
        returns (
            address tokenA,
            address tokenB,
            uint24 fee
        )
    {
        tokenA = path.toAddress(0);
        fee = path.toUint24(ADDR_SIZE);
        tokenB = path.toAddress(NEXT_OFFSET);
    }

    /// @notice Gets the segment corresponding to the first pool in the path
    /// @param path The bytes encoded swap path
    /// @return The segment containing all data necessary to target the first pool in the path
    function getFirstPool(bytes memory path) internal pure returns (bytes memory) {
        return path.slice(0, POP_OFFSET);
    }

    /// @notice Skips a token + fee element from the buffer and returns the remainder
    /// @param path The swap path
    /// @return The remaining token + fee elements in the path
    function skipToken(bytes memory path) internal pure returns (bytes memory) {
        return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET);
    }
}

PoolAddress

PoolAddress
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0;

/// @title Provides functions for deriving a pool address from the factory, tokens, and the fee
library PoolAddress {
    bytes32 internal constant POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54;

    /// @notice The identifying key of the pool
    struct PoolKey {
        address token0;
        address token1;
        uint24 fee;
    }

    /// @notice Returns PoolKey: the ordered tokens with the matched fee levels
    /// @param tokenA The first token of a pool, unsorted
    /// @param tokenB The second token of a pool, unsorted
    /// @param fee The fee level of the pool
    /// @return Poolkey The pool details with ordered token0 and token1 assignments
    function getPoolKey(
        address tokenA,
        address tokenB,
        uint24 fee
    ) internal pure returns (PoolKey memory) {
        if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA);
        return PoolKey({token0: tokenA, token1: tokenB, fee: fee});
    }

    /// @notice Deterministically computes the pool address given the factory and PoolKey
    /// @param factory The Uniswap V3 factory contract address
    /// @param key The PoolKey
    /// @return pool The contract address of the V3 pool
    function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) {
        require(key.token0 < key.token1);
        pool = address(
            uint256(
                keccak256(
                    abi.encodePacked(
                        hex'ff',
                        factory,
                        keccak256(abi.encode(key.token0, key.token1, key.fee)),
                        POOL_INIT_CODE_HASH
                    )
                )
            )
        );
    }
}

CallbackValidation

CallbackValidation
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity =0.7.6;

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

/// @notice Provides validation for callbacks from Uniswap V3 Pools
library CallbackValidation {
    /// @notice Returns the address of a valid Uniswap V3 Pool
    /// @param factory The contract address of the Uniswap V3 factory
    /// @param tokenA The contract address of either token0 or token1
    /// @param tokenB The contract address of the other token
    /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip
    /// @return pool The V3 pool contract address
    function verifyCallback(
        address factory,
        address tokenA,
        address tokenB,
        uint24 fee
    ) internal view returns (IUniswapV3Pool pool) {
        return verifyCallback(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee));
    }

    /// @notice Returns the address of a valid Uniswap V3 Pool
    /// @param factory The contract address of the Uniswap V3 factory
    /// @param poolKey The identifying key of the V3 pool
    /// @return pool The V3 pool contract address
    function verifyCallback(address factory, PoolAddress.PoolKey memory poolKey)
        internal
        view
        returns (IUniswapV3Pool pool)
    {
        pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
        require(msg.sender == address(pool));
    }
}

BytesLib

BytesLib
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * @title Solidity Bytes Arrays Utils
 * @author Gonçalo Sá <goncalo.sa@consensys.net>
 *
 * @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity.
 *      The library lets you concatenate, slice and type cast bytes arrays both in memory and storage.
 */
pragma solidity >=0.5.0 <0.8.0;

library BytesLib {
    function slice(
        bytes memory _bytes,
        uint256 _start,
        uint256 _length
    ) internal pure returns (bytes memory) {
        require(_length + 31 >= _length, 'slice_overflow');
        require(_start + _length >= _start, 'slice_overflow');
        require(_bytes.length >= _start + _length, 'slice_outOfBounds');

        bytes memory tempBytes;

        assembly {
            switch iszero(_length)
                case 0 {
                    // Get a location of some free memory and store it in tempBytes as
                    // Solidity does for memory variables.
                    tempBytes := mload(0x40)

                    // The first word of the slice result is potentially a partial
                    // word read from the original array. To read it, we calculate
                    // the length of that partial word and start copying that many
                    // bytes into the array. The first word we copy will start with
                    // data we don't care about, but the last `lengthmod` bytes will
                    // land at the beginning of the contents of the new array. When
                    // we're done copying, we overwrite the full first word with
                    // the actual length of the slice.
                    let lengthmod := and(_length, 31)

                    // The multiplication in the next line is necessary
                    // because when slicing multiples of 32 bytes (lengthmod == 0)
                    // the following copy loop was copying the origin's length
                    // and then ending prematurely not copying everything it should.
                    let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod)))
                    let end := add(mc, _length)

                    for {
                        // The multiplication in the next line has the same exact purpose
                        // as the one above.
                        let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start)
                    } lt(mc, end) {
                        mc := add(mc, 0x20)
                        cc := add(cc, 0x20)
                    } {
                        mstore(mc, mload(cc))
                    }

                    mstore(tempBytes, _length)

                    //update free-memory pointer
                    //allocating the array padded to 32 bytes like the compiler does now
                    mstore(0x40, and(add(mc, 31), not(31)))
                }
                //if we want a zero-length slice let's just return a zero-length array
                default {
                    tempBytes := mload(0x40)
                    //zero out the 32 bytes slice we are about to return
                    //we need to do it because Solidity does not garbage collect
                    mstore(tempBytes, 0)

                    mstore(0x40, add(tempBytes, 0x20))
                }
        }

        return tempBytes;
    }

    function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) {
        require(_start + 20 >= _start, 'toAddress_overflow');
        require(_bytes.length >= _start + 20, 'toAddress_outOfBounds');
        address tempAddress;

        assembly {
            tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000)
        }

        return tempAddress;
    }

    function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) {
        require(_start + 3 >= _start, 'toUint24_overflow');
        require(_bytes.length >= _start + 3, 'toUint24_outOfBounds');
        uint24 tempUint;

        assembly {
            tempUint := mload(add(add(_bytes, 0x3), _start))
        }

        return tempUint;
    }
}

最後に

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

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

https://chaldene.net/

https://qiita.com/cardene

DeCipher |"Read me" for All of Contracts

Discussion