🦄

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

2023/08/31に公開

はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

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

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

https://www.bunzz.dev/decipher

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

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

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

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

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

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

概要

Uniswap V3プロトコルの一部であるV3Migratorコントラクトは、、ユーザーが以前のUniswap V2の流動性を新しいUniswap V3に移すのを支援する役割を果たしています。

https://zenn.dev/cryptogames/articles/644169eae6e90c

主な役割としては以下の点が挙げられます。

  1. Uniswap V3の中核となるコントラクトと連携し、古いUniswap V2から新しいUniswap V3への流動性の移行を処理します。
    これにより、ユーザーは新しいプロトコルを利用しながら、以前の流動性を活用できます。
  2. LowGasSafeMathライブラリを活用して、数学的な計算を効率的かつガス消費の少ない方法で行い、コストを抑えつつ安全な操作が実現されます。
  3. Uniswap V2のペアコントラクトと連携し、Uniswap V2のプロトコルから流動性情報を取得し、新しいプロトコルに移すための処理を行います。
  4. NonfungiblePositionManagerコントラクトと連携して、移行中の流動性ポジションを管理・操作します。
  5. TransferHelperライブラリを使用して、トークンの転送を安全に行い、トークン移動に関するリスクを最小限に抑えます。
  6. IV3Migratorインターフェースを実装することで、他のコントラクトとの互換性と標準化が確保されます。
    これにより、異なるコントラクト間での連携がスムーズに行えます。
  7. さまざまなベースコントラクトからの機能を取り入れることで、V3Migratorコントラクトの機能性とセキュリティが向上し、安全かつ強力な機能が提供されます。
  8. 外部コントラクトと連携し、異なるトークン規格に対応するためのインターフェースを提供し、さまざまなトークンを移行プロセスで扱えるようになります。

使い方

V3Migratorコントラクトは、Uniswap V2からUniswap V3への流動性の移行をサポートするスマートコントラクトです。
同時に、移行された流動性の管理や操作にも関する便利な機能を提供します。
これにより、流動性プロバイダーは、Uniswap V3が提供する集中型の流動性や複数の手数料レベルといった強化された特徴を活用できます。

手順

具体的な手順は以下の通りです。

  1. 流動性移行を管理するためのスマートコントラクトであるV3Migratorコントラクトをデプロイします。
  2. コントラクトを必要なパラメータで初期化します。
  3. Uniswap V2のプールから流動性トークンをV3Migratorコントラクトが使用できるように承認し、トークンの移行を可能にします。
  4. V3Migratorコントラクト上で移行関数を呼び出し、流動性移行が開始されます。
  5. 移行プロセスを監視し、イベントやエラーを処理しながら、移行がスムーズに進行することを確認します。
  6. 移行された流動性をUniswap V3のプールで操作するために、提供された関数やイベントを使用し、新しいプールでの流動性の管理が可能になります。

関数呼び出し

Uniswap V2からUniswap V3への流動性移行には、以下の関数を呼び出す必要があります。

constructor

V3Migratorコントラクトを必要なパラメータで初期化します。

approve

Uniswap V2プールから、V3Migratorコントラクトが指定した量の流動性トークンを使用できるように承認し、流動性を移行する準備を整えます。

migrate

Uniswap V2からUniswap V3へ指定された流動性を移行します。

使い方

Uniswap V2からUniswap V3への流動性の移行。

  1. V3Migratorコントラクトをデプロイします。
  2. コントラクトを必要なパラメータで初期化します。
  3. Uniswap V2のプールから、移行する流動性トークンの指定された量をV3Migratorコントラクトが使用できるように承認します。
  4. V3Migratorコントラクト上でmigrate関数を呼び出し、移行に必要なパラメータを提供し、Uniswap V2からV3への流動性移行が実行されます。

Uniswap V3で移行された流動性とのやり取り。

  1. 提供された関数を使用して、Uniswap V3プール内の移行された流動性とやり取りし、新しいプール内での流動性の操作を可能にします。
  2. V3Migratorコントラクトが発行するイベントを監視し、移行された流動性に関するアップデートや変更に適切に対処します。

関連EIP/ERC

  • ERC20: ファンジブルトークンの標準インターフェースを定義。
  • ERC721: ノンファンジブルトークンの標準インターフェースを定義。
  • ERC165: コントラクトのインスペクションのための標準インターフェースを定義。
  • ERC721Metadata: 追加のメタデータ関数を持つERC721インターフェースを拡張。
  • ERC721Enumerable: 追加の列挙関数を持つERC721インターフェースを拡張。
  • ERC20Permit: パーミット機能を持つERC20トークンの標準インターフェースを定義。
  • IUniswapV2Pair: Uniswap V2ペアコントラクトのインターフェース。
  • INonfungiblePositionManager: Uniswap V3非ファンジブルポジションマネージャーコントラクトのインターフェース。
  • IUniswapV3Factory: Uniswap V3ファクトリーコントラクトのインターフェース。
  • IUniswapV3Pool: Uniswap V3プールコントラクトのインターフェース。
  • IUniswapV3PoolImmutables: Uniswap V3プールの不変データのインターフェース。
  • IUniswapV3PoolState: Uniswap V3プールの状態のインターフェース。
  • IUniswapV3PoolDerivedState: Uniswap V3プールの派生状態のインターフェース。
  • IUniswapV3PoolActions: **Uniswap **V3プールのアクションのインターフェース。
  • IUniswapV3PoolOwnerActions: Uniswap V3プールのオーナーアクションのインターフェース。
  • IUniswapV3PoolEvents: Uniswap V3プールのイベントのインターフェース。

パラメーター

_factory

Uniswap V3のファクトリーコントラクトのアドレス。
このコントラクトはUniswap V3のプールを生成する役割を担っています。
つまり、新しいトレーディングペアのプールを作成する際にこのアドレスを参照します。

_WETH9

WETH9コントラクトのアドレスを指します。
WETH(Wrapped Ether) は、EthereumのネイティブトークンであるEther(ETH) をEthereumのスマートコントラクトでトレードや操作するためにラップしたものです。
このアドレスはWETH9トークンの操作に使用されます。

_nonfungiblePositionManager

非ファンジブルポジションマネージャーコントラクトのアドレス。
Uniswap V3の非ファンジブルポジションマネージャーコントラクトは、ユーザーがUniswap V3のポジションを管理し、移動するための機能を提供します。
このアドレスはそのコントラクトの操作に使用されます。

コントラクト

V3Migrator

nonfungiblePositionManager

nonfungiblePositionManager
address public immutable nonfungiblePositionManager;

概要
非ファンジブルポジションマネージャーコントラクトのアドレス。

詳細
Uniswap V3の非ファンジブルポジションマネージャーコントラクトのアドレスを格納します。
非ファンジブルポジションマネージャーコントラクトは、Uniswap V3の流動性ポジションを管理するための機能を提供します。
immutableが使用されているため、コントラクトのデプロイ後に値を変更することはできません。

receive

receive
receive() external payable {
    require(msg.sender == WETH9, 'Not WETH9');
}

概要
コントラクトがETHを受け取る際に呼び出される関数。

詳細
ETHがコントラクトに送信される際に自動的に呼び出される関数。
関数内で、送信者がWETH9コントラクトであることを確認し、それ以外のアドレスからのETH送信を拒否します。

migrate

migrate
function migrate(MigrateParams calldata params) external override {
        require(params.percentageToMigrate > 0, 'Percentage too small');
        require(params.percentageToMigrate <= 100, 'Percentage too large');

        // burn v2 liquidity to this address
        IUniswapV2Pair(params.pair).transferFrom(msg.sender, params.pair, params.liquidityToMigrate);
        (uint256 amount0V2, uint256 amount1V2) = IUniswapV2Pair(params.pair).burn(address(this));

        // calculate the amounts to migrate to v3
        uint256 amount0V2ToMigrate = amount0V2.mul(params.percentageToMigrate) / 100;
        uint256 amount1V2ToMigrate = amount1V2.mul(params.percentageToMigrate) / 100;

        // approve the position manager up to the maximum token amounts
        TransferHelper.safeApprove(params.token0, nonfungiblePositionManager, amount0V2ToMigrate);
        TransferHelper.safeApprove(params.token1, nonfungiblePositionManager, amount1V2ToMigrate);

        // mint v3 position
        (, , uint256 amount0V3, uint256 amount1V3) =
            INonfungiblePositionManager(nonfungiblePositionManager).mint(
                INonfungiblePositionManager.MintParams({
                    token0: params.token0,
                    token1: params.token1,
                    fee: params.fee,
                    tickLower: params.tickLower,
                    tickUpper: params.tickUpper,
                    amount0Desired: amount0V2ToMigrate,
                    amount1Desired: amount1V2ToMigrate,
                    amount0Min: params.amount0Min,
                    amount1Min: params.amount1Min,
                    recipient: params.recipient,
                    deadline: params.deadline
                })
            );

        // if necessary, clear allowance and refund dust
        if (amount0V3 < amount0V2) {
            if (amount0V3 < amount0V2ToMigrate) {
                TransferHelper.safeApprove(params.token0, nonfungiblePositionManager, 0);
            }

            uint256 refund0 = amount0V2 - amount0V3;
            if (params.refundAsETH && params.token0 == WETH9) {
                IWETH9(WETH9).withdraw(refund0);
                TransferHelper.safeTransferETH(msg.sender, refund0);
            } else {
                TransferHelper.safeTransfer(params.token0, msg.sender, refund0);
            }
        }
        if (amount1V3 < amount1V2) {
            if (amount1V3 < amount1V2ToMigrate) {
                TransferHelper.safeApprove(params.token1, nonfungiblePositionManager, 0);
            }

            uint256 refund1 = amount1V2 - amount1V3;
            if (params.refundAsETH && params.token1 == WETH9) {
                IWETH9(WETH9).withdraw(refund1);
                TransferHelper.safeTransferETH(msg.sender, refund1);
            } else {
                TransferHelper.safeTransfer(params.token1, msg.sender, refund1);
            }
        }
}

概要
Uniswap V2からUniswap V3への流動性移行を実行する関数。

詳細
指定されたパラメータに基づいてUniswap V2からUniswap V3への流動性移行プロセスを実行します。
移行に必要なパラメータを受け取り、移行の実行前に一定のバリデーションを行います。
移行が完了すると、Uniswap V3の流動性が生成され、必要に応じてアセットの払い戻しも行われます。

Multicall

multicall

multicall
function multicall(bytes[] calldata data) external payable override returns (bytes[] memory results) {
    results = new bytes[](data.length);
    for (uint256 i = 0; i < data.length; i++) {
        (bool success, bytes memory result) = address(this).delegatecall(data[i]);

        if (!success) {
            // Next 5 lines from https://ethereum.stackexchange.com/a/83577
            if (result.length < 68) revert();
            assembly {
                result := add(result, 0x04)
            }
            revert(abi.decode(result, (string)));
        }

        results[i] = result;
    }
}

概要
複数のコントラクト関数を一括して呼び出すためのメカニズムを提供する関数。
指定されたデータの配列を使用して、デリゲートコールを行い、複数の関数呼び出しの結果を返します。

詳細
この関数は、複数のコントラクト関数を一括して呼び出すことができる便利な機能を提供します。data配列には、呼び出す関数のデータ(コールデータ)が含まれています。
関数呼び出しの結果は、results配列に格納されて返されます。
delegatecallを使用して関数を呼び出し、その結果と成功ステータスを取得します。

引数

  • data
    • 関数呼び出しのためのコールデータの配列。

戻り値

  • results
    • 各関数呼び出しの結果が格納されたバイトデータの配列。

SelfPermit

selfPermit

selfPermit
function selfPermit(
    address token,
    uint256 value,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) public payable override {
    IERC20Permit(token).permit(msg.sender, address(this), value, deadline, v, r, s);
}

概要
トークンの所有者がSelfPermitコントラクトに対してトークンの送付許可を与えるメカニズムを提供する関数。

詳細
トークンの所有者が署名したメッセージを使用して、トークンの送付許可をSelfPermitコントラクトに与えるために使用されます。

引数

  • token
    • トークンのアドレス。
  • value
    • 許可するトークンの数量。
  • deadline
    • 署名の有効期限。
  • v, r, s
    • 署名のパラメータ。

selfPermitIfNecessary

selfPermitIfNecessary
function selfPermitIfNecessary(
    address token,
    uint256 value,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) external payable override {
    if (IERC20(token).allowance(msg.sender, address(this)) < value) selfPermit(token, value, deadline, v, r, s);
}

概要
必要な場合にのみトークンの許可をSelfPermitコントラクトに与えるメカニズムを提供する関数。

詳細
トークンの許可が要求された数量を満たしていない場合にのみ、selfPermit関数を呼び出してトークンの送付許可を与えるために使用されます。
トークンの許可をチェックし、要求された数量を満たしていない場合にはselfPermit関数を呼び出します。

引数

  • token
    • トークンのアドレス。
  • value
    • 許可するトークンの数量。
  • deadline
    • 署名の有効期限。
  • v, r, s
    • 署名のパラメータ。

selfPermitAllowed

selfPermitAllowed
function selfPermitAllowed(
    address token,
    uint256 nonce,
    uint256 expiry,
    uint8 v,
    bytes32 r,
    bytes32 s
) public payable override {
    IERC20PermitAllowed(token).permit(msg.sender, address(this), nonce, expiry, true, v, r, s);
}

概要
トークンの所有者が送付許可をSelfPermitコントラクトに与えるための拡張メカニズムを提供する関数。

詳細
トークン所有者は、署名を使用して関数を呼び出すことによって、指定された条件でトークンの許可をコントラクトに与えることができます。

引数

  • token
    • トークンのアドレス。
  • nonce
    • トランザクションのユニーク性を確保するための数値。
  • expiry
    • 署名の有効期限。
  • v, r, s
    • 署名のパラメータ。

selfPermitAllowedIfNecessary

selfPermitAllowedIfNecessary
function selfPermitAllowedIfNecessary(
    address token,
    uint256 nonce,
    uint256 expiry,
    uint8 v,
    bytes32 r,
    bytes32 s
) external payable override {
    if (IERC20(token).allowance(msg.sender, address(this)) < type(uint256).max)
        selfPermitAllowed(token, nonce, expiry, v, r, s);
}

概要
必要な場合にのみトークンの送付許可をSelfPermitコントラクトに与える拡張メカニズムを提供する関数。

詳細
トークンの許可がすでに最大値を超えていない場合にのみ、selfPermitAllowed関数を呼び出してトークンの許可を与えるために使用されます。
トークンの許可をチェックし、最大値を超えていない場合にはselfPermitAllowed関数を呼び出します。

引数

  • token
    • トークンのアドレス。
  • nonce
    • トランザクションのユニーク性を確保するための数値。
  • expiry
    • 署名の有効期限。
  • v, r, s
    • 署名のパラメータ。

PoolInitializer

createAndInitializePoolIfNecessary

createAndInitializePoolIfNecessary
function createAndInitializePoolIfNecessary(
    address token0,
    address token1,
    uint24 fee,
    uint160 sqrtPriceX96
) external payable override returns (address pool) {
    require(token0 < token1);
    pool = IUniswapV3Factory(factory).getPool(token0, token1, fee);

    if (pool == address(0)) {
        pool = IUniswapV3Factory(factory).createPool(token0, token1, fee);
        IUniswapV3Pool(pool).initialize(sqrtPriceX96);
    } else {
        (uint160 sqrtPriceX96Existing, , , , , , ) = IUniswapV3Pool(pool).slot0();
        if (sqrtPriceX96Existing == 0) {
            IUniswapV3Pool(pool).initialize(sqrtPriceX96);
        }
    }
}

概要
必要に応じてUniswap V3プールを作成および初期化するためのメカニズムを提供する関数。

詳細
この関数は、指定されたトークンと手数料率に対応するUniswap V3プールが存在するかどうかをチェックし、存在しない場合は新しいプールを作成して初期化します。
既に存在するプールがある場合は、そのプールのスロット0情報を取得し、初期化が行われているかを確認します。
初期化が行われていない場合は、プールを初期化します。

引数

  • token0
    • トークン0のアドレス。
  • token1
    • トークン1のアドレス。
  • fee
    • 手数料率。
  • sqrtPriceX96
    • 初期の平方根プライス。

戻り値

  • pool
    • 作成または初期化されたUniswap V3プールのアドレス。

TransferHelper

safeTransferFrom

safeTransferFrom
function safeTransferFrom(
    address token,
    address from,
    address to,
    uint256 value
) internal {
    (bool success, bytes memory data) =
        token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value));
    require(success && (data.length == 0 || abi.decode(data, (bool))), 'STF');
}

概要
指定されたトークンをfromアドレスからtoアドレスに安全に転送する関数。

詳細
この関数では、tokenアドレスのトークンコントラクトに対してIERC20.transferFrom関数を呼び出してトークンを転送しています。
転送が成功したかどうかはsuccess変数で確認し、転送が正常に行われなかった場合には、STFエラーが発生します。

引数

  • token
    • トークンのコントラクトアドレス。
  • from
    • トークンの転送元アドレス。
  • to
    • トークンの転送先アドレス。
  • value
    • 転送するトークンの量。

safeTransfer

safeTransfer
function safeTransfer(
    address token,
    address to,
    uint256 value
) internal {
    (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value));
    require(success && (data.length == 0 || abi.decode(data, (bool))), 'ST');
}

概要
msg.senderから指定されたアドレスtoにトークンを安全に転送する関数。

詳細
tokenアドレスのトークンコントラクトに対してIERC20.transfer関数を呼び出してトークンを転送しています。
転送が成功したかどうかは success変数で確認し、転送が正常に行われなかった場合には、STエラーが発生します。

引数

  • token
    • トークンのコントラクトアドレス。
  • to
    • トークンの転送先アドレス。
  • value
    • 転送するトークンの量。

safeApprove

safeApprove
function safeApprove(
    address token,
    address to,
    uint256 value
) internal {
    (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.approve.selector, to, value));
    require(success && (data.length == 0 || abi.decode(data, (bool))), 'SA');
}

概要
指定されたアドレスtoに対してトークンの承認を安全に行う関数。

詳細
この関数では、tokenアドレスのトークンコントラクトに対してIERC20.approve関数を呼び出して承認を行っています。
承認が成功したかどうかはsuccess変数で確認し、承認が正常に行われなかった場合には、SAエラーが発生します。

引数 or パラメータ

  • token
    • トークンのコントラクトアドレス。
  • to
    • 承認対象のアドレス。
  • value
    • 承認するトークンの量。

safeTransferETH

safeTransferETH
function safeTransferETH(address to, uint256 value) internal {
    (bool success, ) = to.call{value: value}(new bytes(0));
    require(success, 'STE');
}

概要
指定されたアドレスtoETHを安全に転送する関数。

詳細
この関数では、toアドレスに対して指定した量のETHを転送しています。
転送が成功したかどうかはsuccess変数で確認し、転送が正常に行われなかった場合には、STEエラーが発生します。

引数

  • to
    • ETHの転送先アドレス。
  • value
    • 転送するETHの量。

PoolAddress

POOL_INIT_CODE_HASH

POOL_INIT_CODE_HASH
bytes32 internal constant POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54;

概要
はプールのイニシャライズコードのハッシュ値を表す定数。

PoolKey

PoolKey
struct PoolKey {
    address token0;
    address token1;
    uint24 fee;
}

概要
プールの識別キーを表す構造体。
この構造体はプールのトークンと手数料レベルを格納します。

getPoolKey

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

概要
プールキーを取得する関数。
与えられたトークンと手数料レベルを元に、トークンを昇順に並べたプールキーを生成します。

引数

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

戻り値

  • PoolKey
    • PoolKey構造体。
    • 昇順に並べられたトークンと手数料レベルが含まれます。

computeAddress

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

概要
プールのアドレスを計算する関数。
ファクトリーのアドレスとプールキーを元に、プールのアドレスを決定的に計算します。

引数

  • factory
    • Uniswap V3ファクトリーコントラクトのアドレス。
  • key
    • プールキー。

戻り値
プールのアドレスを返します。

イベント

なし。

コード

インターフェース

  • contracts/interfaces

ライブラリ

  • contracts/libraries

ベースコントラクト

  • contracts/base

最後に

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

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

https://chaldene.net/

https://qiita.com/cardene

DeCipher |"Read me" for All of Contracts

Discussion