🦄

UniswapV3のドキュメントを読んでみよう!

2023/09/09に公開

はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回はUniswap V3について公式ドキュメントを翻訳・補足しながらまとめていきたいと思います。

公式ドキュメントは以下になります。

https://docs.uniswap.org/contracts/v3/overview

概要

Uniswap V3は、スマートコントラクトを使用したプロトコルであり、分散型取引所(DEX)の一つです。
このプロトコルは、ユーザーが異なるトークンを交換したり流動性を提供したりすることを可能にします。
Uniswap V3のスマートコントラクトに関するドキュメントによると、以下のような情報が提供されています。

ガイド

Uniswap V3スマートコントラクトについて初めて学ぶ方は、基本的な概念から始めることをおすすめします。
これにより、プロトコルの基本的な動作原理や用語を理解できます。
その後、ローカル環境をセットアップし、最初のトークンスワップ(交換)を実行する方法について学ぶことができます。

リファレンス

深く掘り下げて理解したい場合は、技術リファレンスドキュメントを読むことができます。
ここでは、スマートコントラクトの詳細な仕様や関数、メソッドについて詳しく説明されています。

リソース

V3コアおよびV3パリフェリを含む、さまざまなリソースが提供されています。
これらのリソースを通じて、さらに詳細な情報や実際のコード例を得ることができます。

Uniswap V3のスマートコントラクトは、Solidityなどの言語を使用して開発されています。
エンジニアとしてのスキルセットを活かして、これらのスマートコントラクトを理解し、開発に活用することができます。
コードの品質や可読性にも注意を払いつつ、新たな流動性戦略やトークン交換の仕組みを構築する際に役立ててください。

ガイド

スワップ

シングルスワップ

Uniswapでは、仮想通貨間のスワップ(通貨交換)を行うことができます。
例を通じて、Uniswapでスワップを行う2つの関数「swapExactInputSingle」と「swapExactOutputSingle」について説明していきます。

swapExactInputSingle
この関数は、特定の数量のトークンを別のトークンに交換する時に使います。
例えば、あなたが一定量のETHを持っており、それを可能な限り多く別のトークンに交換したい場合に使います。
この関数では、交換の詳細を「ExactInputSingleParams」というデータで指定し、Uniswapの「ISwapRouter」インターフェース内の「exactInputSingle」関数を呼び出します。

swapExactOutputSingle
この関数は、特定の数量のトークンを欲しいトークンに交換する時に使います。
例えば、最低限のトークン量で別のトークンと交換したい場合に使います。
この関数では、交換の詳細を「ExactOutputSingleParams」というデータで指定し、、Uniswapの「ISwapRouter」インターフェース内の「exactOutputSingle」関数を呼び出します。

この例では、特定のトークンのアドレスを直接コードに組み込んでいますが、実際の取引ではトランザクションごとに異なるトークンやプールを選択でき、柔軟性を持たせられます。

スマートコントラクトからUniswapを利用する場合、外部の価格情報が不可欠です。
これがないと、他のトレーダーによって取引が先取りされ、損失が生じる可能性があります。

コントラクトのセットアップ

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

import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol';
import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';

contract SwapExamples {
	// For the scope of these swap examples,
	// we will detail the design considerations when using `exactInput`, `exactInputSingle`, `exactOutput`, and  `exactOutputSingle`.
	// It should be noted that for the sake of these examples we pass in the swap router as a constructor argument instead of inheriting it.
	// More advanced example contracts will detail how to inherit the swap router safely.
	// This example swaps DAI/WETH9 for single path swaps and DAI/USDC/WETH9 for multi path swaps.

	ISwapRouter public immutable swapRouter;

	address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
	address public constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
	address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

	// For this example, we will set the pool fee to 0.3%.
	uint24 public constant poolFee = 3000;

	constructor(ISwapRouter _swapRouter) {
		swapRouter = _swapRouter;
	}
	
	...

上記コントラクトでは、トークンのコントラクトアドレスとプール内の手数料をハードコードしています。

正確なinputスワップ

スマートコントラクトからトークンを引き出してスワップを実行するには、以下のステップを踏みます。
以下のステップには、呼び出し元(ユーザー)からのトークン引き出しとUniswapプロトコルのルーターコントラクトへのトークン承認が含まれます。

  1. トークン引き出しの承認
    まず、呼び出し元(ユーザー)は、Uniswapプロトコルのルーターコントラクトに対して、ユーザーが所有しているトークンの引き出しを許可する必要があります。
    これにより、Uniswapプロトコルがユーザーが所有するトークンを引き出して使用できるようになります。

  2. Daiトークンの転送
    次に、呼び出し元(ユーザー)のアドレスから一定数量のDaiをコントラクトに転送します。
    この際、転送するDaiの数量を「amount」として指定します。

	...
	
	/// @notice swapExactInputSingle swaps a fixed amount of DAI for a maximum possible amount of WETH9
	/// using the DAI/WETH9 0.3% pool by calling `exactInputSingle` in the swap router.
	/// @dev The calling address must approve this contract to spend at least `amountIn` worth of its DAI for this function to succeed.
	/// @param amountIn The exact amount of DAI that will be swapped for WETH9.
	/// @return amountOut The amount of WETH9 received.
	function swapExactInputSingle(uint256 amountIn) external returns (uint256 amountOut) {
	// msg.sender must approve this contract

	// Transfer the specified amount of DAI to this contract.
	TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountIn);

	// Approve the router to spend DAI.
	TransferHelper.safeApprove(DAI, address(swapRouter), amountIn);
	
	...
パラメーター

スワップを実行するには、ExactInputSingleParamsというデータを渡す必要があります。
以下はパラメータの説明です。

  • tokenIn(入力トークン)
    • スワップの時に交換元として使用するトークンのコントラクトアドレス。
  • tokenOut(出力トークン)
    • スワップの結果として得られるトークンのコントラクトアドレス。
  • fee(手数料ティア)
    • スワップを実行する適切なプールコントラクトを特定する、プールの手数料情報。
  • recipient(受取アドレス)
    • スワップによって得られたトークンが送信されるアドレス。
  • deadline(有効期限)
    • スワップが失敗するunix時間。
    • これにより、トランザクションが長期間保留されたり、価格が急激に変動した場合にトランザクションが実行されないように保護します。
  • amountOutMinimum(最小出力トークン量)
    • この値は0に設定されていますが、本番環境では非常に重要。
    • この値はSDKやオンチェーンの価格オラクルを使用して計算され、スワップによって得られるトークン量が一定の最小量以上であることを確認するために使用されます。
    • これにより、フロントランニングや価格操作による悪意ある価格での取引から保護されます。
  • sqrtPriceLimitX96
    • この値も0に設定されていますが、本番環境では価格の制限を設定する。
    • 価格の影響を制御したり、価格に関連する様々なメカニズムを設定したりする時に役立ちます。
関数呼び出し
	...
	
	// Naively set amountOutMinimum to 0. In production, use an oracle or other data source to choose a safer value for amountOutMinimum.
	// We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount.
	ISwapRouter.ExactInputSingleParams memory params =
		ISwapRouter.ExactInputSingleParams({
			tokenIn: DAI,
			tokenOut: WETH9,
			fee: poolFee,
			recipient: msg.sender,
			deadline: block.timestamp,
			amountIn: amountIn,
			amountOutMinimum: 0,
			sqrtPriceLimitX96: 0
		});

	// The call to `exactInputSingle` executes the swap.
	amountOut = swapRouter.exactInputSingle(params);
	}
	
	...

正確なoutputスワップ

Exact Outputスワップは、取得したいトークン数量と最低限の入力トークン量を交換します。

例えば、ある数量の特定のトークンを得る必要がある場合、Exact Outputスワップを使用できます。
この場合、必要な出力トークン量を指定し、その出力量を確実に得るために最小でどれだけの入力トークンが必要かを計算します。
そして、その最小量以上の入力トークンを提供して、指定した出力量を確保します。

ただし、このスワップを使うと、スワップが実行された後に入力したトークンが余る可能性があるため、スワップの最後に余った入力トークンを呼び出し元のアドレスに返金します。
これにより、トークンの無駄を最小限に抑えることができます。

関数呼び出し
	...
	
	/// @notice swapExactOutputSingle swaps a minimum possible amount of DAI for a fixed amount of WETH.
	/// @dev The calling address must approve this contract to spend its DAI for this function to succeed. As the amount of input DAI is variable,
	/// the calling address will need to approve for a slightly higher amount, anticipating some variance.
	/// @param amountOut The exact amount of WETH9 to receive from the swap.
	/// @param amountInMaximum The amount of DAI we are willing to spend to receive the specified amount of WETH9.
	/// @return amountIn The amount of DAI actually spent in the swap.
	function swapExactOutputSingle(uint256 amountOut, uint256 amountInMaximum) external returns (uint256 amountIn) {
	// Transfer the specified amount of DAI to this contract.
	TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountInMaximum);

	// Approve the router to spend the specified `amountInMaximum` of DAI.
	// In production, you should choose the maximum amount to spend based on oracles or other data sources to achieve a better swap.
	TransferHelper.safeApprove(DAI, address(swapRouter), amountInMaximum);

	ISwapRouter.ExactOutputSingleParams memory params =
		ISwapRouter.ExactOutputSingleParams({
			tokenIn: DAI,
			tokenOut: WETH9,
			fee: poolFee,
			recipient: msg.sender,
			deadline: block.timestamp,
			amountOut: amountOut,
			amountInMaximum: amountInMaximum,
			sqrtPriceLimitX96: 0
		});

	// Executes the swap returning the amountIn needed to spend to receive the desired amountOut.
	amountIn = swapRouter.exactOutputSingle(params);

	// For exact output swaps, the amountInMaximum may not have all been spent.
	// If the actual amount spent (amountIn) is less than the specified maximum amount, we must refund the msg.sender and approve the swapRouter to spend 0.
	if (amountIn < amountInMaximum) {
	    TransferHelper.safeApprove(DAI, address(swapRouter), 0);
	    TransferHelper.safeTransfer(DAI, msg.sender, amountInMaximum - amountIn);
	}
}

スワップコントラクト

スワップコントラクトの完成版は以下になります。

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

import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';
import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol';

contract SwapExamples {
    // For the scope of these swap examples,
    // we will detail the design considerations when using
    // `exactInput`, `exactInputSingle`, `exactOutput`, and  `exactOutputSingle`.

    // It should be noted that for the sake of these examples, we purposefully pass in the swap router instead of inherit the swap router for simplicity.
    // More advanced example contracts will detail how to inherit the swap router safely.

    ISwapRouter public immutable swapRouter;

    // This example swaps DAI/WETH9 for single path swaps and DAI/USDC/WETH9 for multi path swaps.

    address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address public constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

    // For this example, we will set the pool fee to 0.3%.
    uint24 public constant poolFee = 3000;

    constructor(ISwapRouter _swapRouter) {
        swapRouter = _swapRouter;
    }

    /// @notice swapExactInputSingle swaps a fixed amount of DAI for a maximum possible amount of WETH9
    /// using the DAI/WETH9 0.3% pool by calling `exactInputSingle` in the swap router.
    /// @dev The calling address must approve this contract to spend at least `amountIn` worth of its DAI for this function to succeed.
    /// @param amountIn The exact amount of DAI that will be swapped for WETH9.
    /// @return amountOut The amount of WETH9 received.
    function swapExactInputSingle(uint256 amountIn) external returns (uint256 amountOut) {
        // msg.sender must approve this contract

        // Transfer the specified amount of DAI to this contract.
        TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountIn);

        // Approve the router to spend DAI.
        TransferHelper.safeApprove(DAI, address(swapRouter), amountIn);

        // Naively set amountOutMinimum to 0. In production, use an oracle or other data source to choose a safer value for amountOutMinimum.
        // We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount.
        ISwapRouter.ExactInputSingleParams memory params =
            ISwapRouter.ExactInputSingleParams({
                tokenIn: DAI,
                tokenOut: WETH9,
                fee: poolFee,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            });

        // The call to `exactInputSingle` executes the swap.
        amountOut = swapRouter.exactInputSingle(params);
    }

    /// @notice swapExactOutputSingle swaps a minimum possible amount of DAI for a fixed amount of WETH.
    /// @dev The calling address must approve this contract to spend its DAI for this function to succeed. As the amount of input DAI is variable,
    /// the calling address will need to approve for a slightly higher amount, anticipating some variance.
    /// @param amountOut The exact amount of WETH9 to receive from the swap.
    /// @param amountInMaximum The amount of DAI we are willing to spend to receive the specified amount of WETH9.
    /// @return amountIn The amount of DAI actually spent in the swap.
    function swapExactOutputSingle(uint256 amountOut, uint256 amountInMaximum) external returns (uint256 amountIn) {
        // Transfer the specified amount of DAI to this contract.
        TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountInMaximum);

        // Approve the router to spend the specifed `amountInMaximum` of DAI.
        // In production, you should choose the maximum amount to spend based on oracles or other data sources to acheive a better swap.
        TransferHelper.safeApprove(DAI, address(swapRouter), amountInMaximum);

        ISwapRouter.ExactOutputSingleParams memory params =
            ISwapRouter.ExactOutputSingleParams({
                tokenIn: DAI,
                tokenOut: WETH9,
                fee: poolFee,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountOut: amountOut,
                amountInMaximum: amountInMaximum,
                sqrtPriceLimitX96: 0
            });

        // Executes the swap returning the amountIn needed to spend to receive the desired amountOut.
        amountIn = swapRouter.exactOutputSingle(params);

        // For exact output swaps, the amountInMaximum may not have all been spent.
        // If the actual amount spent (amountIn) is less than the specified maximum amount, we must refund the msg.sender and approve the swapRouter to spend 0.
        if (amountIn < amountInMaximum) {
            TransferHelper.safeApprove(DAI, address(swapRouter), 0);
            TransferHelper.safeTransfer(DAI, msg.sender, amountInMaximum - amountIn);
        }
    }
}

マルチホップスワップ

以下はUniswapプロトコルのバージョン3(v3)で利用可能な複数のトークン間でスワップを行う方法を示すデモンストレーションコードです。

コントラクトのセットアップ

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

import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol';
import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';

contract SwapExamples {
	// For the scope of these swap examples,
	// we will detail the design considerations when using `exactInput`, `exactInputSingle`, `exactOutput`, and  `exactOutputSingle`.
	// It should be noted that for the sake of these examples we pass in the swap router as a constructor argument instead of inheriting it.
	// More advanced example contracts will detail how to inherit the swap router safely.
	// This example swaps DAI/WETH9 for single path swaps and DAI/USDC/WETH9 for multi path swaps.

	ISwapRouter public immutable swapRouter;
    
	address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
	address public constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
	address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

	// For this example, we will set the pool fee to 0.3%.
	uint24 public constant poolFee = 3000;

	constructor(ISwapRouter _swapRouter) {
		swapRouter = _swapRouter;
	}
	
	...

正確なinputマルチホップスワップ

以下のようなプロセスで実行されます。

  1. 特定の入力トークンに対して、指定された出力トークン量を得たいと考えます。

  2. スワップの進行に従い、中間的なステップとして、異なるトークン間のスワップが行われることがあります。
    これらの中間ステップでは、入力トークンから出力トークンへの変換が行われ、その都度手数料(fee)がかかります。

  3. 最終的に、最大の出力量を得ることを目指して、複数の中間ステップを経て、出力トークンが得られます。

パラメーター
  • path(パス)
    • このパスは(tokenAddress - fee - tokenAddress)のような形で、トークン間のスワップシーケンスを示します。
    • 各要素は、トークンのコントラクトアドレスや手数料などを指定し、スワップの途中でどのトークンをどのようにスワップするかを示します。
  • recipient(受取アドレス)
    • スワップで得た出力アセットを送信するアドレス。
  • deadline(有効期限)
    • トランザクションが失敗するunix時間。
    • これにより、トランザクションが長時間保留されたり、価格が急激に変動する場合にトランザクションが失敗するのを防ぎます。
  • amountIn(入力アセットの量)
    • 最初に提供される入力アセットの数量。
    • この量をスワップの入力として使用します。
  • amountOutMin(最小出力アセット量)
    • トランザクションが成功するための条件となる、最小の出力アセット量。
    • この値未満の場合、トランザクションは失敗します。
    • 本番環境では、期待される価格を示すためにSDKを使用したり、高度なセキュリティを持つシステムではオンチェーン価格オラクルを利用することが一般的です。
関数の呼び出し
	...
	
	/// @notice swapExactInputMultihop swaps a fixed amount of DAI for a maximum possible amount of WETH9 through an intermediary pool.
	/// For this example, we will swap DAI to USDC, then USDC to WETH9 to achieve our desired output.
	/// @dev The calling address must approve this contract to spend at least `amountIn` worth of its DAI for this function to succeed.
	/// @param amountIn The amount of DAI to be swapped.
	/// @return amountOut The amount of WETH9 received after the swap.
	function swapExactInputMultihop(uint256 amountIn) external returns (uint256 amountOut) {
	// Transfer `amountIn` of DAI to this contract.
	TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountIn);

	// Approve the router to spend DAI.
	TransferHelper.safeApprove(DAI, address(swapRouter), amountIn);

	// Multiple pool swaps are encoded through bytes called a `path`. A path is a sequence of token addresses and poolFees that define the pools used in the swaps.
	// The format for pool encoding is (tokenIn, fee, tokenOut/tokenIn, fee, tokenOut) where tokenIn/tokenOut parameter is the shared token across the pools.
	// Since we are swapping DAI to USDC and then USDC to WETH9 the path encoding is (DAI, 0.3%, USDC, 0.3%, WETH9).
	ISwapRouter.ExactInputParams memory params =
		ISwapRouter.ExactInputParams({
			path: abi.encodePacked(DAI, poolFee, USDC, poolFee, WETH9),
			recipient: msg.sender,
			deadline: block.timestamp,
			amountIn: amountIn,
			amountOutMinimum: 0
		});

	// Executes the swap.
	amountOut = swapRouter.exactInput(params);
	}
	
	...

正確なoutputマルチホップスワップ

任意の入力トークンを固定された出力トークン量と交換する方法です。
このスワップ方法は、マルチホップスワップの中では一般的ではなく、特定の状況で使用されます。コードのほとんどは他のスワップ方法と同じですが、1つ注目すべき違いがあります。
それは、パス(取引の経路)は逆順にエンコードされる点です。
出力トークン量が固定されていて入力トークン量が可変なため、入力トークン量を求めるために逆順にする必要があります。

パラメーター
  • path(取引の経路)

    • パスは、tokenAddress Fee tokenAddressの順序でエンコードされますが、実際の実行では逆順に処理されます。
    • これらの変数は、スワップシーケンス内の各プールコントラクトアドレスを計算するために必要です。
    • マルチホップスワップルーターコードは、これらの変数を使用して正しいプールを自動的に見つけ、スワップシーケンス内の各プールで必要なスワップを実行します。
  • recipient(受取アドレス)

    • 出力アセットの送り先アドレス。
    • スワップによって得たアセットがどこに送信されるかを指定します。
  • deadline(有効期限)

    • トランザクションが失敗するunix時間。
    • これにより、トランザクションが長時間保留されたり、価格が急激に変動する場合にトランザクションが失敗するのを防ぎます。
  • amountOut(出力アセットの希望量)

    • WETH9などの取得したいトークンの量。
    • このスワップは、指定された量のWETH9を得るために実行されます。
  • amountInMaximum(最大入力トークン量)

    • DAIWETH9の指定されたamountOutと交換するために必要となるDAIの最大量。
    • このパラメータは、トランザクションが大幅な価格変動によって失敗するのを防ぐために設定されます。
関数呼び出し
	...

	/// @notice swapExactOutputMultihop swaps a minimum possible amount of DAI for a fixed amount of WETH through an intermediary pool.
	/// For this example, we want to swap DAI for WETH9 through a USDC pool but we specify the desired amountOut of WETH9. Notice how the path encoding is slightly different in for exact output swaps.
	/// @dev The calling address must approve this contract to spend its DAI for this function to succeed. As the amount of input DAI is variable,
	/// the calling address will need to approve for a slightly higher amount, anticipating some variance.
	/// @param amountOut The desired amount of WETH9.
	/// @param amountInMaximum The maximum amount of DAI willing to be swapped for the specified amountOut of WETH9.
	/// @return amountIn The amountIn of DAI actually spent to receive the desired amountOut.
	function swapExactOutputMultihop(uint256 amountOut, uint256 amountInMaximum) external returns (uint256 amountIn) {
	// Transfer the specified `amountInMaximum` to this contract.
	TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountInMaximum);
	// Approve the router to spend  `amountInMaximum`.
	TransferHelper.safeApprove(DAI, address(swapRouter), amountInMaximum);

	// The parameter path is encoded as (tokenOut, fee, tokenIn/tokenOut, fee, tokenIn)
	// The tokenIn/tokenOut field is the shared token between the two pools used in the multiple pool swap. In this case USDC is the "shared" token.
	// For an exactOutput swap, the first swap that occurs is the swap which returns the eventual desired token.
	// In this case, our desired output token is WETH9 so that swap happens first, and is encoded in the path accordingly.
	ISwapRouter.ExactOutputParams memory params =
		ISwapRouter.ExactOutputParams({
			path: abi.encodePacked(WETH9, poolFee, USDC, poolFee, DAI),
			recipient: msg.sender,
			deadline: block.timestamp,
			amountOut: amountOut,
			amountInMaximum: amountInMaximum
		});

	// Executes the swap, returning the amountIn actually spent.
	amountIn = swapRouter.exactOutput(params);

	// If the swap did not require the full amountInMaximum to achieve the exact amountOut then we refund msg.sender and approve the router to spend 0.
	if (amountIn < amountInMaximum) {
		TransferHelper.safeApprove(DAI, address(swapRouter), 0);
		TransferHelper.safeTransferFrom(DAI, address(this), msg.sender, amountInMaximum - amountIn);
	}
}

マルチホップコントラクト

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

import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';
import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol';

contract SwapExamples {
    // For the scope of these swap examples,
    // we will detail the design considerations when using
    // `exactInput`, `exactInputSingle`, `exactOutput`, and  `exactOutputSingle`.

    // It should be noted that for the sake of these examples, we purposefully pass in the swap router instead of inherit the swap router for simplicity.
    // More advanced example contracts will detail how to inherit the swap router safely.

    ISwapRouter public immutable swapRouter;

    // This example swaps DAI/WETH9 for single path swaps and DAI/USDC/WETH9 for multi path swaps.

    address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address public constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

    // For this example, we will set the pool fee to 0.3%.
    uint24 public constant poolFee = 3000;

    constructor(ISwapRouter _swapRouter) {
        swapRouter = _swapRouter;
    }

    /// @notice swapInputMultiplePools swaps a fixed amount of DAI for a maximum possible amount of WETH9 through an intermediary pool.
    /// For this example, we will swap DAI to USDC, then USDC to WETH9 to achieve our desired output.
    /// @dev The calling address must approve this contract to spend at least `amountIn` worth of its DAI for this function to succeed.
    /// @param amountIn The amount of DAI to be swapped.
    /// @return amountOut The amount of WETH9 received after the swap.
    function swapExactInputMultihop(uint256 amountIn) external returns (uint256 amountOut) {
        // Transfer `amountIn` of DAI to this contract.
        TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountIn);

        // Approve the router to spend DAI.
        TransferHelper.safeApprove(DAI, address(swapRouter), amountIn);

        // Multiple pool swaps are encoded through bytes called a `path`. A path is a sequence of token addresses and poolFees that define the pools used in the swaps.
        // The format for pool encoding is (tokenIn, fee, tokenOut/tokenIn, fee, tokenOut) where tokenIn/tokenOut parameter is the shared token across the pools.
        // Since we are swapping DAI to USDC and then USDC to WETH9 the path encoding is (DAI, 0.3%, USDC, 0.3%, WETH9).
        ISwapRouter.ExactInputParams memory params =
            ISwapRouter.ExactInputParams({
                path: abi.encodePacked(DAI, poolFee, USDC, poolFee, WETH9),
                recipient: msg.sender,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: 0
            });

        // Executes the swap.
        amountOut = swapRouter.exactInput(params);
    }

    /// @notice swapExactOutputMultihop swaps a minimum possible amount of DAI for a fixed amount of WETH through an intermediary pool.
    /// For this example, we want to swap DAI for WETH9 through a USDC pool but we specify the desired amountOut of WETH9. Notice how the path encoding is slightly different in for exact output swaps.
    /// @dev The calling address must approve this contract to spend its DAI for this function to succeed. As the amount of input DAI is variable,
    /// the calling address will need to approve for a slightly higher amount, anticipating some variance.
    /// @param amountOut The desired amount of WETH9.
    /// @param amountInMaximum The maximum amount of DAI willing to be swapped for the specified amountOut of WETH9.
    /// @return amountIn The amountIn of DAI actually spent to receive the desired amountOut.
    function swapExactOutputMultihop(uint256 amountOut, uint256 amountInMaximum) external returns (uint256 amountIn) {
        // Transfer the specified `amountInMaximum` to this contract.
        TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountInMaximum);
        // Approve the router to spend  `amountInMaximum`.
        TransferHelper.safeApprove(DAI, address(swapRouter), amountInMaximum);

        // The parameter path is encoded as (tokenOut, fee, tokenIn/tokenOut, fee, tokenIn)
        // The tokenIn/tokenOut field is the shared token between the two pools used in the multiple pool swap. In this case USDC is the "shared" token.
        // For an exactOutput swap, the first swap that occurs is the swap which returns the eventual desired token.
        // In this case, our desired output token is WETH9 so that swap happpens first, and is encoded in the path accordingly.
        ISwapRouter.ExactOutputParams memory params =
            ISwapRouter.ExactOutputParams({
                path: abi.encodePacked(WETH9, poolFee, USDC, poolFee, DAI),
                recipient: msg.sender,
                deadline: block.timestamp,
                amountOut: amountOut,
                amountInMaximum: amountInMaximum
            });

        // Executes the swap, returning the amountIn actually spent.
        amountIn = swapRouter.exactOutput(params);

        // If the swap did not require the full amountInMaximum to achieve the exact amountOut then we refund msg.sender and approve the router to spend 0.
        if (amountIn < amountInMaximum) {
            TransferHelper.safeApprove(DAI, address(swapRouter), 0);
            TransferHelper.safeTransferFrom(DAI, address(this), msg.sender, amountInMaximum - amountIn);
        }
    }
}

流動性提供

コントラクトのセットアップ

Uniswap V3のカストディアルコントラクトポジションに関するサンプルです。
このカストディアルコントラクトを使用することで、以下の操作が可能になります。

  • ポジションの発行
    • Uniswap V3でトークンの取引を行うためのポジションを作成します。
  • ポジションへの流動性の追加
    • 作成したポジションに流動性(トークン)を追加して、取引の準備を整えます。
  • 流動性の減少
    • 一部またはすべての流動性を撤回して、ポジションからトークンを取り戻します。
  • 手数料の収集
    • 取引手数料を受け取ります。
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity =0.7.6;
pragma abicoder v2;

import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
import '@uniswap/v3-core/contracts/libraries/TickMath.sol';
import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol';
import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol';
import '@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol';
import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';
import '@uniswap/v3-periphery/contracts/base/LiquidityManagement.sol';

contract LiquidityExamples is IERC721Receiver {

	address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
	address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

	uint24 public constant poolFee = 3000;
	
	INonfungiblePositionManager public immutable nonfungiblePositionManager;

	...

ERC721との相互運用

各NFT(非代替トークン)は、ERC721スマートコントラクト内で一意のtokenIdによって識別されます。
ERC721トークンをデポジットするには以下の手順を実行します。

  1. Deposit(デポジット)構造体の作成
    デポジット情報を格納するための「Deposit」という構造体を作成します。

  2. NFTとDeposit構造体の紐付け
    次に、各NFTのtokenIdをキー、Deposit構造体を値にしたマッピング配列を作成します。

...

struct Deposit {
	address owner;
	uint128 liquidity;
	address token0;
	address token1;
}

mapping(uint256 => Deposit) public deposits;

...

Constructor

...

constructor(
	INonfungiblePositionManager _nonfungiblePositionManager,
	address _factory,
	address _WETH9
) PeripheryImmutableState(_factory, _WETH9) {
	nonfungiblePositionManager = _nonfungiblePositionManager;
}

...

ERC721トークンの保管を許可

コントラクトがERC721トークンを保管できるようにするには、継承した IERC721Receiver.solコントラクト内のonERC721Received関数を実装します。

引数のfromは使用されないので省略しても問題ないです。

...

function onERC721Received(
	address operator,
	address,
	uint256 tokenId,
	bytes calldata
) external override returns (bytes4) {
	// get position information
	_createDeposit(operator, tokenId);
	return this.onERC721Received.selector;
}

...

デポジットの作成

depositsマッピング配列にデータを追加するには、_createDepositという内部関数を作成し、nonfungiblePositionManager.solpositions構造体を取得します。
取得したtoken0token1、liquiditydeposits`マッピング配列に格納します。

function _createDeposit(address owner, uint256 tokenId) internal {
	(, , address token0, address token1, , , , uint128 liquidity, , , , ) = nonfungiblePositionManager.positions(tokenId);

	// set the owner and data for position
	// operator is msg.sender
	deposits[tokenId] = Deposit({owner: owner, liquidity: liquidity, token0: token0, token1: token1});
}

流動性提供コントラクト

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

import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
import '@uniswap/v3-core/contracts/libraries/TickMath.sol';
import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol';
import '../libraries/TransferHelper.sol';
import '../interfaces/INonfungiblePositionManager.sol';
import '../base/LiquidityManagement.sol';

contract LiquidityExamples is IERC721Receiver {
    address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

    uint24 public constant poolFee = 3000;

    INonfungiblePositionManager public immutable nonfungiblePositionManager;

    /// @notice Represents the deposit of an NFT
    struct Deposit {
        address owner;
        uint128 liquidity;
        address token0;
        address token1;
    }

    /// @dev deposits[tokenId] => Deposit
    mapping(uint256 => Deposit) public deposits;

    constructor(
        INonfungiblePositionManager _nonfungiblePositionManager
    ) {
        nonfungiblePositionManager = _nonfungiblePositionManager;
    }

    // Implementing `onERC721Received` so this contract can receive custody of erc721 tokens
    function onERC721Received(
        address operator,
        address,
        uint256 tokenId,
        bytes calldata
    ) external override returns (bytes4) {
        // get position information

        _createDeposit(operator, tokenId);

        return this.onERC721Received.selector;
    }

    function _createDeposit(address owner, uint256 tokenId) internal {
        (, , address token0, address token1, , , , uint128 liquidity, , , , ) =
            nonfungiblePositionManager.positions(tokenId);

        // set the owner and data for position
        // operator is msg.sender
        deposits[tokenId] = Deposit({owner: owner, liquidity: liquidity, token0: token0, token1: token1});
    }
}

新しいポジションの作成

パラメーター

新しいポジションを作成するには、nonFungiblePositionManagermint関数を呼び出す。

この例では、ミントするトークンの量をハードコーディングしています。

/// @notice Calls the mint function defined in periphery, mints the same amount of each token. For this example we are providing 1000 DAI and 1000 USDC in liquidity
/// @return tokenId The id of the newly minted ERC721
/// @return liquidity The amount of liquidity for the position
/// @return amount0 The amount of token0
/// @return amount1 The amount of token1
function mintNewPosition()
	external
	returns (
		uint256 tokenId,
		uint128 liquidity,
		uint256 amount0,
		uint256 amount1
	)
{
	// For this example, we will provide equal amounts of liquidity in both assets.
	// Providing liquidity in both assets means liquidity will be earning fees and is considered in-range.
	uint256 amount0ToMint = 1000;
	uint256 amount1ToMint = 1000;

ミント呼び出し

nonfungiblePositionManagerがコントラクトのトークンを使用できるように承認し、MintParams構造体を設定し、それをparams変数に割り当てます。
その後、mint関数を呼び出す時にこのparamsnonfungiblePositionManagerに渡します。

TickMath.MIN_TICKTickMath.MAX_TICKを使用することで、プール全範囲に流動性を提供しています。

amount0Minamount1Minはこの例で0に設定されていますが、本番環境では脆弱性の原因となります。
スリッページ保護のないmint呼び出しは、価格が正確でない状況でmint呼び出しを実行するフロントランニング攻撃に対して脆弱性を持つ可能性があります。

より安全な実装を行うためには、開発者はスリッページの推定プロセスを実装する必要があります。

	// Approve the position manager
	TransferHelper.safeApprove(DAI, address(nonfungiblePositionManager), amount0ToMint);
	TransferHelper.safeApprove(USDC, address(nonfungiblePositionManager), amount1ToMint);

	INonfungiblePositionManager.MintParams memory params =
		INonfungiblePositionManager.MintParams({
			token0: DAI,
			token1: USDC,
			fee: poolFee,
			tickLower: TickMath.MIN_TICK,
			tickUpper: TickMath.MAX_TICK,
			amount0Desired: amount0ToMint,
			amount1Desired: amount1ToMint,
			amount0Min: 0,
			amount1Min: 0,
			recipient: address(this),
			deadline: block.timestamp
		});

	// Note that the pool defined by DAI/USDC and fee tier 0.3% must already be created and initialized in order to mint
	(tokenId, liquidity, amount0, amount1) = nonfungiblePositionManager.mint(params);

Depositマッピングの更新と払い戻し

前章の「コントラクトのセットアップ」の内部関数を呼び出すことができます。
その後、ミント処理で残った流動性をmsg.senderに払い戻します。

	// Create a deposit
	_createDeposit(msg.sender, tokenId);

	// Remove allowance and refund in both assets.
	if (amount0 < amount0ToMint) {
		TransferHelper.safeApprove(DAI, address(nonfungiblePositionManager), 0);
		uint256 refund0 = amount0ToMint - amount0;
		TransferHelper.safeTransfer(DAI, msg.sender, refund0);
	}

	if (amount1 < amount1ToMint) {
		TransferHelper.safeApprove(USDC, address(nonfungiblePositionManager), 0);
		uint256 refund1 = amount1ToMint - amount1;
		TransferHelper.safeTransfer(USDC, msg.sender, refund1);
	}
}

コントラクトの例

:::details

/// @notice Calls the mint function defined in periphery, mints the same amount of each token. For this example we are providing 1000 DAI and 1000 USDC in liquidity
/// @return tokenId The id of the newly minted ERC721
/// @return liquidity The amount of liquidity for the position
/// @return amount0 The amount of token0
/// @return amount1 The amount of token1
function mintNewPosition()
external
returns (
	uint256 tokenId,
	uint128 liquidity,
	uint256 amount0,
	uint256 amount1
)
{
	// For this example, we will provide equal amounts of liquidity in both assets.
	// Providing liquidity in both assets means liquidity will be earning fees and is considered in-range.
	uint256 amount0ToMint = 1000;
	uint256 amount1ToMint = 1000;

	// Approve the position manager
	TransferHelper.safeApprove(DAI, address(nonfungiblePositionManager), amount0ToMint);
	TransferHelper.safeApprove(USDC, address(nonfungiblePositionManager), amount1ToMint);

	INonfungiblePositionManager.MintParams memory params =
		INonfungiblePositionManager.MintParams({
			token0: DAI,
			token1: USDC,
			fee: poolFee,
			tickLower: TickMath.MIN_TICK,
			tickUpper: TickMath.MAX_TICK,
			amount0Desired: amount0ToMint,
			amount1Desired: amount1ToMint,
			amount0Min: 0,
			amount1Min: 0,
			recipient: address(this),
			deadline: block.timestamp
		});

	// Note that the pool defined by DAI/USDC and fee tier 0.3% must already be created and initialized in order to mint
	(tokenId, liquidity, amount0, amount1) = nonfungiblePositionManager.mint(params);

	// Create a deposit
	_createDeposit(msg.sender, tokenId);

	// Remove allowance and refund in both assets.
	if (amount0 < amount0ToMint) {
		TransferHelper.safeApprove(DAI, address(nonfungiblePositionManager), 0);
		uint256 refund0 = amount0ToMint - amount0;
		TransferHelper.safeTransfer(DAI, msg.sender, refund0);
	}

	if (amount1 < amount1ToMint) {
		TransferHelper.safeApprove(USDC, address(nonfungiblePositionManager), 0);
		uint256 refund1 = amount1ToMint - amount1;
		TransferHelper.safeTransfer(USDC, msg.sender, refund1);
	}
}

:::

手数料の徴収

手数料の徴収

流動性の相互作用において、コントラクトが流動性ポジションNFTを保有している必要があります。
したがって、NFTのdepositが関数にコード化されていない例では、コントラクトがすでにNFTを所有していると見なされます。

オーナーポジションの手数料を徴収するには、呼び出し元のアドレスからNFTを転送し、NFTから関連する情報を関数内のローカル変数に代入し、それらの変数をthenonfungiblePositionManagerに渡してcollectを呼び出します。

この関数は、ポジションNFTの保管を維持しながら、すべての手数料を回収し、NFTの元の所有者に送ります。

/// @notice Collects the fees associated with provided liquidity
/// @dev The contract must hold the erc721 token before it can collect fees
/// @param tokenId The id of the erc721 token
/// @return amount0 The amount of fees collected in token0
/// @return amount1 The amount of fees collected in token1
function collectAllFees(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) {
// Caller must own the ERC721 position
// Call to safeTransfer will trigger `onERC721Received` which must return the selector else transfer will fail
nonfungiblePositionManager.safeTransferFrom(msg.sender, address(this), tokenId);

// set amount0Max and amount1Max to uint256.max to collect all fees
// alternatively can set recipient to msg.sender and avoid another transaction in `sendToOwner`
INonfungiblePositionManager.CollectParams memory params =
	INonfungiblePositionManager.CollectParams({
		tokenId: tokenId,
		recipient: address(this),
		amount0Max: type(uint128).max,
		amount1Max: type(uint128).max
	});

(amount0, amount1) = nonfungiblePositionManager.collect(params);

// send collected feed back to owner
	_sendToOwner(tokenId, amount0, amount1);
}

呼び出しアドレスに手数料送付

手数料またはポジション・トークンの形で、あらゆるトークンをNFTの所有者に送ります。

sendToOwnerでは、設定した手数料の額をsafeTransferの引数として渡し、手数料をオーナーに送金します。

/// @notice Transfers funds to owner of NFT
/// @param tokenId The id of the erc721
/// @param amount0 The amount of token0
/// @param amount1 The amount of token1
function _sendToOwner(
	uint256 tokenId,
	uint256 amount0,
	uint256 amount1
) internal {
	// get owner of contract
	address owner = deposits[tokenId].owner;

	address token0 = deposits[tokenId].token0;
	address token1 = deposits[tokenId].token1;
	// send collected fees to owner
	TransferHelper.safeTransfer(token0, owner, amount0);
	TransferHelper.safeTransfer(token1, owner, amount1);
}

流動性の低下

ポジションを全て引き出さずにポジションの流動性を減少させます。

この例では、コントラクトがすでにポジションNFTを保有していることを前提とし、呼び出すアドレスはポジションNFTをコントラクトにdepositしたアドレスと同じである必要があります。

流動性の低下

/// @notice A function that decreases the current liquidity by half. An example to show how to call the `decreaseLiquidity` function defined in periphery.
/// @param tokenId The id of the erc721 token
/// @return amount0 The amount received back in token0
/// @return amount1 The amount returned back in token1
function decreaseLiquidityInHalf(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) {
	// caller must be the owner of the NFT
	require(msg.sender == deposits[tokenId].owner, 'Not the owner');
	// get liquidity data for tokenId
	uint128 liquidity = deposits[tokenId].liquidity;
	uint128 halfLiquidity = liquidity / 2;

	// amount0Min and amount1Min are price slippage checks
	// if the amount received after burning is not greater than these minimums, transaction will fail
	INonfungiblePositionManager.DecreaseLiquidityParams memory params =
		INonfungiblePositionManager.DecreaseLiquidityParams({
			tokenId: tokenId,
			liquidity: halfLiquidity,
			amount0Min: 0,
			amount1Min: 0,
			deadline: block.timestamp
		});

	(amount0, amount1) = nonfungiblePositionManager.decreaseLiquidity(params);

	//send liquidity back to owner
	_sendToOwner(tokenId, amount0, amount1);
}

呼び出しアドレスに手数料送付

この関数は、手数料またはポジション・トークンの形で、あらゆるトークンをNFTの所有者に送ります。

sendToOwnerでは、設定した手数料の額をsafeTransferの引数として渡し、手数料をオーナーに送金します。

/// @notice Transfers funds to owner of NFT
/// @param tokenId The id of the erc721
/// @param amount0 The amount of token0
/// @param amount1 The amount of token1
function _sendToOwner(
	uint256 tokenId,
	uint256 amount0,
	uint256 amount1
) internal {
	// get owner of contract
	address owner = deposits[tokenId].owner;

	address token0 = deposits[tokenId].token0;
	address token1 = deposits[tokenId].token1;
	// send collected fees to owner
	TransferHelper.safeTransfer(token0, owner, amount0);
	TransferHelper.safeTransfer(token1, owner, amount1);
}

流動性の増加

範囲内の流動性増加

この例では、コントラクトがすでにNFTを保管していることを想定しています。

Uniswap v3プロトコルを使用して、指定された流動性ポジションの境界を変更することはできません。

/// @notice Increases liquidity in the current range
/// @dev Pool must be initialized already to add liquidity
/// @param tokenId The id of the erc721 token
/// @param amount0 The amount to add of token0
/// @param amount1 The amount to add of token1
function increaseLiquidityCurrentRange(
	uint256 tokenId,
	uint256 amountAdd0,
	uint256 amountAdd1
)
external
returns (
	uint128 liquidity,
	uint256 amount0,
	uint256 amount1
)
{
	INonfungiblePositionManager.IncreaseLiquidityParams memory params =
		INonfungiblePositionManager.IncreaseLiquidityParams({
			tokenId: tokenId,
			amount0Desired: amountAdd0,
			amount1Desired: amountAdd1,
			amount0Min: 0,
			amount1Min: 0,
			deadline: block.timestamp
		});

	(liquidity, amount0, amount1) = nonfungiblePositionManager.increaseLiquidity(params);
}

コントラクト全体

Uniswap V3のポジションNFTを保管し、手数料の徴収、流動性の増減、新規ポジションの作成によってポジションと流動性を操作できるコントラクトです。

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

import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
import '@uniswap/v3-core/contracts/libraries/TickMath.sol';
import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol';
import '../libraries/TransferHelper.sol';
import '../interfaces/INonfungiblePositionManager.sol';
import '../base/LiquidityManagement.sol';

contract LiquidityExamples is IERC721Receiver {
    address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

    uint24 public constant poolFee = 3000;

    INonfungiblePositionManager public immutable nonfungiblePositionManager;

    /// @notice Represents the deposit of an NFT
    struct Deposit {
        address owner;
        uint128 liquidity;
        address token0;
        address token1;
    }

    /// @dev deposits[tokenId] => Deposit
    mapping(uint256 => Deposit) public deposits;

    constructor(
        INonfungiblePositionManager _nonfungiblePositionManager
    ) {
        nonfungiblePositionManager = _nonfungiblePositionManager;
    }

    // Implementing `onERC721Received` so this contract can receive custody of erc721 tokens
    function onERC721Received(
        address operator,
        address,
        uint256 tokenId,
        bytes calldata
    ) external override returns (bytes4) {
        // get position information

        _createDeposit(operator, tokenId);

        return this.onERC721Received.selector;
    }

    function _createDeposit(address owner, uint256 tokenId) internal {
        (, , address token0, address token1, , , , uint128 liquidity, , , , ) =
            nonfungiblePositionManager.positions(tokenId);

        // set the owner and data for position
        // operator is msg.sender
        deposits[tokenId] = Deposit({owner: owner, liquidity: liquidity, token0: token0, token1: token1});
    }

    /// @notice Calls the mint function defined in periphery, mints the same amount of each token.
    /// For this example we are providing 1000 DAI and 1000 USDC in liquidity
    /// @return tokenId The id of the newly minted ERC721
    /// @return liquidity The amount of liquidity for the position
    /// @return amount0 The amount of token0
    /// @return amount1 The amount of token1
    function mintNewPosition()
        external
        returns (
            uint256 tokenId,
            uint128 liquidity,
            uint256 amount0,
            uint256 amount1
        )
    {
        // For this example, we will provide equal amounts of liquidity in both assets.
        // Providing liquidity in both assets means liquidity will be earning fees and is considered in-range.
        uint256 amount0ToMint = 1000;
        uint256 amount1ToMint = 1000;

        // transfer tokens to contract
        TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amount0ToMint);
        TransferHelper.safeTransferFrom(USDC, msg.sender, address(this), amount1ToMint);

        // Approve the position manager
        TransferHelper.safeApprove(DAI, address(nonfungiblePositionManager), amount0ToMint);
        TransferHelper.safeApprove(USDC, address(nonfungiblePositionManager), amount1ToMint);

        INonfungiblePositionManager.MintParams memory params =
            INonfungiblePositionManager.MintParams({
                token0: DAI,
                token1: USDC,
                fee: poolFee,
                tickLower: TickMath.MIN_TICK,
                tickUpper: TickMath.MAX_TICK,
                amount0Desired: amount0ToMint,
                amount1Desired: amount1ToMint,
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(this),
                deadline: block.timestamp
            });

        // Note that the pool defined by DAI/USDC and fee tier 0.3% must already be created and initialized in order to mint
        (tokenId, liquidity, amount0, amount1) = nonfungiblePositionManager.mint(params);

        // Create a deposit
        _createDeposit(msg.sender, tokenId);

        // Remove allowance and refund in both assets.
        if (amount0 < amount0ToMint) {
            TransferHelper.safeApprove(DAI, address(nonfungiblePositionManager), 0);
            uint256 refund0 = amount0ToMint - amount0;
            TransferHelper.safeTransfer(DAI, msg.sender, refund0);
        }

        if (amount1 < amount1ToMint) {
            TransferHelper.safeApprove(USDC, address(nonfungiblePositionManager), 0);
            uint256 refund1 = amount1ToMint - amount1;
            TransferHelper.safeTransfer(USDC, msg.sender, refund1);
        }
    }

    /// @notice Collects the fees associated with provided liquidity
    /// @dev The contract must hold the erc721 token before it can collect fees
    /// @param tokenId The id of the erc721 token
    /// @return amount0 The amount of fees collected in token0
    /// @return amount1 The amount of fees collected in token1
    function collectAllFees(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) {
        // Caller must own the ERC721 position, meaning it must be a deposit

        // set amount0Max and amount1Max to uint256.max to collect all fees
        // alternatively can set recipient to msg.sender and avoid another transaction in `sendToOwner`
        INonfungiblePositionManager.CollectParams memory params =
            INonfungiblePositionManager.CollectParams({
                tokenId: tokenId,
                recipient: address(this),
                amount0Max: type(uint128).max,
                amount1Max: type(uint128).max
            });

        (amount0, amount1) = nonfungiblePositionManager.collect(params);

        // send collected feed back to owner
        _sendToOwner(tokenId, amount0, amount1);
    }

    /// @notice A function that decreases the current liquidity by half. An example to show how to call the `decreaseLiquidity` function defined in periphery.
    /// @param tokenId The id of the erc721 token
    /// @return amount0 The amount received back in token0
    /// @return amount1 The amount returned back in token1
    function decreaseLiquidityInHalf(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) {
        // caller must be the owner of the NFT
        require(msg.sender == deposits[tokenId].owner, 'Not the owner');
        // get liquidity data for tokenId
        uint128 liquidity = deposits[tokenId].liquidity;
        uint128 halfLiquidity = liquidity / 2;

        // amount0Min and amount1Min are price slippage checks
        // if the amount received after burning is not greater than these minimums, transaction will fail
        INonfungiblePositionManager.DecreaseLiquidityParams memory params =
            INonfungiblePositionManager.DecreaseLiquidityParams({
                tokenId: tokenId,
                liquidity: halfLiquidity,
                amount0Min: 0,
                amount1Min: 0,
                deadline: block.timestamp
            });

        (amount0, amount1) = nonfungiblePositionManager.decreaseLiquidity(params);

        //send liquidity back to owner
        _sendToOwner(tokenId, amount0, amount1);
    }

    /// @notice Increases liquidity in the current range
    /// @dev Pool must be initialized already to add liquidity
    /// @param tokenId The id of the erc721 token
    /// @param amount0 The amount to add of token0
    /// @param amount1 The amount to add of token1
    function increaseLiquidityCurrentRange(
        uint256 tokenId,
        uint256 amountAdd0,
        uint256 amountAdd1
    )
        external
        returns (
            uint128 liquidity,
            uint256 amount0,
            uint256 amount1
        ) {

        TransferHelper.safeTransferFrom(deposits[tokenId].token0, msg.sender, address(this), amountAdd0);
        TransferHelper.safeTransferFrom(deposits[tokenId].token1, msg.sender, address(this), amountAdd1);

        TransferHelper.safeApprove(deposits[tokenId].token0, address(nonfungiblePositionManager), amountAdd0);
        TransferHelper.safeApprove(deposits[tokenId].token1, address(nonfungiblePositionManager), amountAdd1);

        INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager.IncreaseLiquidityParams({
            tokenId: tokenId,
            amount0Desired: amountAdd0,
            amount1Desired: amountAdd1,
            amount0Min: 0,
            amount1Min: 0,
            deadline: block.timestamp
        });

        (liquidity, amount0, amount1) = nonfungiblePositionManager.increaseLiquidity(params);

    }

    /// @notice Transfers funds to owner of NFT
    /// @param tokenId The id of the erc721
    /// @param amount0 The amount of token0
    /// @param amount1 The amount of token1
    function _sendToOwner(
        uint256 tokenId,
        uint256 amount0,
        uint256 amount1
    ) internal {
        // get owner of contract
        address owner = deposits[tokenId].owner;

        address token0 = deposits[tokenId].token0;
        address token1 = deposits[tokenId].token1;
        // send collected fees to owner
        TransferHelper.safeTransfer(token0, owner, amount0);
        TransferHelper.safeTransfer(token1, owner, amount1);
    }

    /// @notice Transfers the NFT to the owner
    /// @param tokenId The id of the erc721
    function retrieveNFT(uint256 tokenId) external {
        // must be the owner of the NFT
        require(msg.sender == deposits[tokenId].owner, 'Not the owner');
        // transfer ownership to original owner
        nonfungiblePositionManager.safeTransferFrom(address(this), msg.sender, tokenId);
        //remove information related to tokenId
        delete deposits[tokenId];
    }
}

流動性マイニング

概要

はじめに

DeFiプロジェクト、トークン作成者、その他の関係者として、Uniswap V3プールで範囲を指定しての流動性提供にインセンティブを与えたいと思うかもしれません。
ある特定のインセンティブ付与スキームについて説明していきます。

背景

用語を定義していきます。
ここでは、流動性にインセンティブを与えるプログラムを「Incentives」と呼びます。

  • rewardToken(報酬トークン)
    • 最も重要なパラメータは、インセンティブ提供者は、流動性提供の報酬として配布したいERC20トークンを選択します。
  • pool(プール)
    • 流動性を提供するUniswap V3プールのアドレス。
  • startTime(開始時間)
    • 報酬の配布が開始されるUNIXタイムスタンプ。
  • endTime(終了時間)
    • 報酬の配布が終了するUNIXタイムスタンプ。
  • refundee(返金受取アドレス)
    • Incentiveが終了した後に残った報酬を請求する権利を持つアドレス。

各インセンティブには関連する報酬があり、プログラムのライフサイクルの間に分配される報酬トークンの総量が割り当てられます。

報酬の数学

インセンティブがどのようなものか分かったところで、実際に参加者にどのように報酬が配分されるかを見ていきます。

インセンティブ作成者は、報酬額とプログラム期間を選びます。
これは、1秒あたりに配布するリワードトークンの量を選ぶことに直結します。
これをリワードレートと呼びます。
つまり、startTimeendTimeの間の1秒ごとに、一定量のトークンが、秒ごとに範囲内にあるある全ての流動性の間で比例配分されます。
重要なのは、これはプログラムへの参加をオプトインした流動性だけでなく、すべての流動性をカウントすることです。
つまり、インセンティブ作成者は、プログラム期間中、(潜在的に)全ての範囲内のLPに分配する価値があると判断した報酬レートを選ぶ必要があります。

ステーキング

インセンティブに参加するために、ユーザーは自分のポジションNFTをステーキングコントラクトに預け入れる必要があります。
ステーキングコントラクトはプログラムに参加するNFTから流動性が削除されないことを保証する必要があるためです。

いったんdepositされると、ユーザーのNFTが結び付けられているUniswap V3プールのアクティブなインセンティブに任意の数だけステークすることができます(これは最初の預託でアトミックに発生する可能性があることに注意してください)。ステークされたNFTは、上記のアルゴリズムに従って直ちに報酬を獲得し始めます。ユーザーは、プログラムが進行している間、獲得したリワードトークンを定期的に請求することも、オーバーヘッドを最小限に抑えるためにプログラムが終了するまで請求するのを待つこともできます。

ユーザーがIncentive(報酬プログラム) に参加する最初のステップは、自分のポジションNFTを特定のステーキングコントラクトアドレスに預けることです。
このアクションにより、一時的にNFTの管理権限がこのコントラクトに移ります。
これは、ステーキングコントラクトがNFTをプログラムに参加している間は流動性(取引可能な状態)が削除されないことを保証する必要があるためです。

一度NFTを預けたら、ユーザーはそのNFTをUniswap V3プールに関連付けられた任意のアクティブなIncentiveにステーキング(投資)することができます(初回の預け入れと同時にこれが行われることもあることに注意してください)。
ステーキングされたNFTは、上記で説明したアルゴリズムに従って、すぐに報酬を獲得し始めます。
ユーザーはプログラムが進行中の間に定期的に蓄積された報酬トークンを取り出すことができます。
また、オーバーヘッド(手数料や手間)を最小限に抑えるために、プログラムが終了するまで報酬を請求しない選択肢もあります。

プログラムの結論

*プログラムの終了条件

  1. block.timestamp >= endTime(ブロックのタイムスタンプが終了時刻を超えた場合)
    プログラムの期間が終了している必要があります。
    ただし、これはプログラムの正式な終了を意味しません。
    なぜなら、一部のユーザーは期限切れの直前やその後も報酬を最大限に得るために参加しているかもしれないからです。
    また、これは次の条件に直結します。

  2. すべてのNFTがステーク解除されること
    プログラムは、それに参加したすべてのNFTがステーク解除されたときにのみ終了できます。
    これを常に可能にするために、プログラムが終了した後、誰でもNFTをステーク解除できます(ただし、当然ながらNFTの所有者以外の未払いの報酬トークンを請求することはできません)。
    これにより、すべてのユーザーが自分自身をステーク解除しなくても、誰かが手動でステーク解除できるようになり、プログラムを終了できます。

未割り当ての報酬が残る場合

未割り当ての報酬が残る条件は、報酬率がすべての範囲内の流動性に対して同じであることを考えると理解しやすいです。
ただし、報酬トークンを請求できるのはプログラムの参加者のみであるため、すべてのプログラムにおいて請求できない報酬トークンが残る可能性があります。
したがって、refundees(返金申請者)はプログラムを終了させるインセンティブを持ちます。
この複雑な設計は、Uniswap V3の流動性に応じて報酬を一貫して割り当てる難しさに起因しています。

最後に、終了時刻を超えてプログラムに残っているステーカーは、報酬がわずかに増減する可能性があります。
これらの変化の大きさは、ステーカーが総アクティブ流動性のシェア、終了時刻後のステーク時間、およびステーク解除の順番に依存します。
2倍の期間では報酬の半分が残り、3倍の場合は1/3が残るように、報酬は期間に比例して減少することもあります。

しかし、ゲーム理論の観点からは、ステーカーは報酬を最大化するためにできるだけ早くステークを解除し、報酬を請求することを試みるはずです。
なぜなら、refundees(返金申請者)も残りの報酬を取り戻すことに関心があり、ステークを解除しないユーザーが多い場合、一斉にステークを解除しようとする可能性が高いからです。

フラッシュスワップ

はじめに

このスマートコントラクトは、V3プールで「フラッシュ」と呼ばれる操作を実行し、トークン0とトークン1を全額引き出します。
そして、同じトークンペアである別の手数料ティア(手数料率)を持つプールで、取り出したトークン0とトークン1を交換します。
交換が完了したら、最初のプールに元のトークンを返し、利益を元の呼び出し元アドレスに送金します。

フラッシュトランザクションの概要

フラッシュトランザクションは、トークンの送金条件が満たされる前にトークンの残高を移動させる方法です。
スワップの文脈では、これは入力が受け取られる前に出力がスワップから送信されることを意味します。

Uniswap V3では、Poolコントラクト内で新しい関数である「flash」が導入されています。
flashは、指定された量のトークン0とトークン1を指定した受け取りアドレスに引き出します。
引き出した量には、スワップ手数料も含まれます。
トランザクションの終了時には、引き出した量とスワップ手数料がプールに支払われることになります。
また、flashには「data」という4つ目のパラメータが含まれており、このパラメータを使用して、後で必要なデータを関数に渡し、後でデコードすることができます。

function flash(
	address recipient,
	uint256 amount0,
	uint256 amount1,
	bytes calldata data
) external override lock noDelegateCall {

フラッシュコールバック

トークンがflashによって引き出された後、それらのトークンがどのように返済されるかを理解するには、flash関数のコードを見る必要があります。
flash関数の一部に以下のようなコードがあります。

IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data);

この部分の処理では、msg.senderによってFlashCallback関数が呼び出されます。
これにより、プールに支払う残高を計算するために必要な手数料データが渡され、dataパラメータにエンコードされた任意のデータも渡されます。

Uniswap V3では、カスタムロジックでオーバーライド可能な以下の3つの異なるコールバック関数があります。

  • uniswapV3SwapCallback
  • uniswapV3MintCallback
  • uniswapV3FlashCallback

アービトラージコントラクトを書くためには、flashを呼び出し、uniswapV3FlashCallbackをオーバーライドして、トランザクションの実行を完了するために必要なステップを実行します。

V3コントラクトの継承

IUniswapV3FlashCallbackPeripheryPaymentsを継承してそれぞれの機能を利用します。
注意点としては、これらの継承元のスマートコントラクトも多くの他のスマートコントラクトを拡張していることがあり、例としてLowGasSafeMathを利用してuint256およびint256の型を拡張しています。

pragma solidity =0.7.6;
pragma abicoder v2;

import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3FlashCallback.sol';
import '@uniswap/v3-core/contracts/libraries/LowGasSafeMath.sol';

import '@uniswap/v3-periphery/contracts/base/PeripheryPayments.sol';
import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol';
import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol';
import '@uniswap/v3-periphery/contracts/libraries/CallbackValidation.sol';
import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';
import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol';

contract PairFlash is IUniswapV3FlashCallback, PeripheryPayments {
	using LowGasSafeMath for uint256;
	using LowGasSafeMath for int256;

	ISwapRouter public immutable swapRouter;

	constructor(
		ISwapRouter _swapRouter,
		address _factory,
		address _WETH9
	) PeripheryImmutableState(_factory, _WETH9) {
		swapRouter = _swapRouter;
	}

フラッシュ呼び出し

構造体のパラメーター

flashを呼び出すには、最初の呼び出しに必要なflashパラメータと、コールバック時に渡したいパラメータが必要になります。

FlashParams構造体には、プールから引き出したいトークンのアドレスと量、どのプールから引き出し、どのプールとスワップするかを決定するための3つのデータが含まれます。

struct FlashParams {
	address token0;
	address token1;
	uint24 fee1;
	uint256 amount0;
	uint256 amount1;
	uint24 fee2;
	uint24 fee3;
}

FlashCallbackData構造体には、コールバック時に送信するデータが格納されます。
これは、PoolAddressライブラリによって返された、手数料ティアが一致するソートされたトークンを表すpoolKeyが含まれます。

struct FlashCallbackData {
	uint256 amount0;
	uint256 amount1;
	address payer;
	PoolAddress.PoolKey poolKey;
	uint24 poolFee2;
	uint24 poolFee3;
}

プールキー

Flashparamsparamsとしてメモリ上に宣言してある)から関連するパラメータをpoolKeyに代入します。

function initFlash(FlashParams memory params) external {
	PoolAddress.PoolKey memory poolKey = PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee1});
}

次に、poolを[IUniswapV3Pool]型として宣言し、目的のプールのコントラクトでflashを呼び出せるようにします。

IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));

フラッシュ呼び出し

最後に、宣言したプールでflashを呼び出します。
最後のパラメーターに対して、FlashCallbackDataをabi.encodeを実行します。
このFlashCallbackDataはコールバックでデコードされ、トランザクションの次のステップを通知するために使用されます。

pool.flash(
	address(this),
	params.amount0,
	params.amount1,
	abi.encode(
		FlashCallbackData({
			amount0: params.amount0,
			amount1: params.amount1,
			payer: msg.sender,
			poolKey: poolKey,
			poolFee2: params.fee2,
			poolFee3: params.fee3
		})
	)
);

関数全体のコードは以下になります。

//fee1 is the fee of the pool from the initial borrow
//fee2 is the fee of the first pool to arb from
//fee3 is the fee of the second pool to arb from
struct FlashParams {
	address token0;
	address token1;
	uint24 fee1;
	uint256 amount0;
	uint256 amount1;
	uint24 fee2;
	uint24 fee3;
}

// fee2 and fee3 are the two other fees associated with the two other pools of token0 and token1
struct FlashCallbackData {
	uint256 amount0;
	uint256 amount1;
	address payer;
	PoolAddress.PoolKey poolKey;
	uint24 poolFee2;
	uint24 poolFee3;
}

function initFlash(FlashParams memory params) external {
	PoolAddress.PoolKey memory poolKey = PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee1});
	IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
	pool.flash(
		address(this),
		params.amount0,
		params.amount1,
		abi.encode(
			FlashCallbackData({
				amount0: params.amount0,
				amount1: params.amount1,
				payer: msg.sender,
				poolKey: poolKey,
				poolFee2: params.fee2,
				poolFee3: params.fee3
			})
		)
	);
}

フラッシュコールバック

フラッシュ・コールバックをカスタム・ロジックでオーバーライドして、必要なスワップを実行し、元のmsg.senderに利益を支払います。

uniswapV3FlashCallback関数を宣言し、オーバーライドします。

function uniswapV3FlashCallback(
	uint256 fee0,
	uint256 fee1,
	bytes calldata data
) external override {
	FlashCallbackData memory decoded = abi.decode(data, (FlashCallbackData));

各コールバックは、呼び出し元が本物のV3プールであることを検証する必要があります。
そうでない場合、プールコントラクトは、コールバック関数を操作するEOAを介した攻撃を受けやすくなります。

CallbackValidation.verifyCallback(factory, decoded.poolKey);

アドレス型のローカル変数をtoken0とtoken1に割り当てて、ルーターがフラッシュからトークンとやり取りできるようにします。

address token0 = decoded.poolKey.token0;
address token1 = decoded.poolKey.token1;

TransferHelper.safeApprove(token0, address(swapRouter), decoded.amount0);
TransferHelper.safeApprove(token1, address(swapRouter), decoded.amount1);

2つのスワップトランザクションで得られる最小のトークン量を計算します。
これらのトランザクションは、利益がない場合には実行しないようにするために、最低限の条件を設定しています。

  • amount1Min
    • トークン1の最小必要量。
  • amount0Min
    • トークン0の最小必要量。

これらの値は、計算されたトークンの数量に手数料を追加しています。
手数料はfee0fee1として表され、それぞれトークン0とトークン1の手数料を示します。
トークンの最小量を手数料を考慮に入れて計算することで、利益がないトランザクションが実行されないようになります。

uint256 amount1Min = LowGasSafeMath.add(decoded.amount1, fee1);
uint256 amount0Min = LowGasSafeMath.add(decoded.amount0, fee0);

スワップの開始

uint256 amountOut0 =
	swapRouter.exactInputSingle(
	ISwapRouter.ExactInputSingleParams({
		tokenIn: token1,
		tokenOut: token0,
		fee: decoded.poolFee2,
		recipient: address(this),
		deadline: block.timestamp + 200,
		amountIn: decoded.amount1,
		amountOutMinimum: amount0Min,
		sqrtPriceLimitX96: 0
	})
);

上記のコードは最初のスワップトランザクションを呼び出す部分です。
この呼び出しでは、ルーターインターフェースコントラクトのexactInputSingle関数を使用しています。
この呼び出しでは、amount0Inを最小の取引トークン量(最小アウトプット)として使用し、スワップの戻り値をamountOut0に格納しています。

以下の2つの新しい機能が導入されています。

  • sqrtPriceLimitX96

    • スワップがプールの価格を変更できる範囲を制限します。
    • 価格は常にトークン0をトークン1との比率で表す形式でプールコントラクトに格納されます。
    • この引数は、ユーザーが特定の価格までスワップを行いたい場合に便利です。
    • この例では、この引数を無効にするために0に設定します。
  • deadline

    • トランザクションがリバート(無効になる)するタイムスタンプです。
    • これにより、トランザクションが長時間保留された場合に価格環境が急激に変動することからトランザクションを保護します。
    • この例では、シンプルさのためにだいぶ先の将来のタイムスタンプに設定します。

このスワップトランザクションでは、最初に元のプールから引き出したamount1を、最大可能なアウトプット量に交換するための固定の入力として使用します。

この関数呼び出しは、前回のトークンペアで決定されたプールに対して実行されます。
ただし、手数料ティアと呼ばれるトレードプールの種類があり、このスワップでは手数料ティアのリストで次にある手数料ティアで実行されます。

uint256 amountOut1 = swapRouter.exactInputSingle(
	ISwapRouter.ExactInputSingleParams({
		tokenIn: token0,
		tokenOut: token1,
		fee: decoded.poolFee3,
		recipient: address(this),
		deadline: block.timestamp + 200,
		amountIn: decoded.amount0,
		amountOutMinimum: amount1Min,
		sqrtPriceLimitX96: 0
	})
);

上記のコードは、2つ目のスワップトランザクションを設定する部分です。
このスワップでは、最後の手数料ティア(fee tier)を使用し、元のプールから引き出したamount0をトークンの取引に使います。

プールからの返済

元のプールに対してflashトランザクションの返済を行うために、まずプールに支払う残高を計算し、ルーターがコントラクト内のトークンをプールに戻すために承認を行います。
以下のコードは、このプロセスを実行する部分です。

uint256 amount0Owed = LowGasSafeMath.add(decoded.amount0, fee0);
uint256 amount1Owed = LowGasSafeMath.add(decoded.amount1, fee1);

TransferHelper.safeApprove(token0, address(this), amount0Owed);
TransferHelper.safeApprove(token1, address(this), amount1Owed);

トークンに支払う残高がある場合、シンプルなロジックを使用してpay関数を呼び出します。
この時に注意すべきなのは、コールバック関数はinternalとしてマークされていますが、プール自体によって呼び出されているためpay関数を呼び出すことができるということです。

if (amount0Owed > 0) pay(token0, address(this), msg.sender, amount0Owed);
if (amount1Owed > 0) pay(token1, address(this), msg.sender, amount1Owed);

利益を支払うために、initFlash関数を実行し、flashトランザクションをトリガーし、その後コールバックの呼び出し元であるmsg.senderに対して利益を支払います。

if (amountOut0 > amount0Owed) {
	uint256 profit0 = LowGasSafeMath.sub(amountOut0, amount0Owed);

	TransferHelper.safeApprove(token0, address(this), profit0);
	pay(token0, address(this), decoded.payer, profit0);
}

if (amountOut1 > amount1Owed) {
	uint256 profit1 = LowGasSafeMath.sub(amountOut1, amount1Owed);
	TransferHelper.safeApprove(token0, address(this), profit1);
	pay(token1, address(this), decoded.payer, profit1);
}

関数全体のコード

uniswapV3FlashCallback
function uniswapV3FlashCallback(
	uint256 fee0,
	uint256 fee1,
	bytes calldata data
) external override {
	FlashCallbackData memory decoded = abi.decode(data, (FlashCallbackData));
	CallbackValidation.verifyCallback(factory, decoded.poolKey);

	address token0 = decoded.poolKey.token0;
	address token1 = decoded.poolKey.token1;

	TransferHelper.safeApprove(token0, address(swapRouter), decoded.amount0);
	TransferHelper.safeApprove(token1, address(swapRouter), decoded.amount1);

	// profitable check
	// exactInputSingle will fail if this amount not met
	uint256 amount1Min = LowGasSafeMath.add(decoded.amount1, fee1);
	uint256 amount0Min = LowGasSafeMath.add(decoded.amount0, fee0);

	// call exactInputSingle for swapping token1 for token0 in pool w/fee2
	uint256 amountOut0 =
	swapRouter.exactInputSingle(
		ISwapRouter.ExactInputSingleParams({
			tokenIn: token1,
			tokenOut: token0,
			fee: decoded.poolFee2,
			recipient: address(this),
			deadline: block.timestamp + 200,
			amountIn: decoded.amount1,
			amountOutMinimum: amount0Min,
			sqrtPriceLimitX96: 0
		})
	);

	// call exactInputSingle for swapping token0 for token 1 in pool w/fee3
	uint256 amountOut1 =
	swapRouter.exactInputSingle(
		ISwapRouter.ExactInputSingleParams({
			tokenIn: token0,
			tokenOut: token1,
			fee: decoded.poolFee3,
			recipient: address(this),
			deadline: block.timestamp + 200,
			amountIn: decoded.amount0,
			amountOutMinimum: amount1Min,
			sqrtPriceLimitX96: 0
		})
	);

	// end up with amountOut0 of token0 from first swap and amountOut1 of token1 from second swap
	uint256 amount0Owed = LowGasSafeMath.add(decoded.amount0, fee0);
	uint256 amount1Owed = LowGasSafeMath.add(decoded.amount1, fee1);

	TransferHelper.safeApprove(token0, address(this), amount0Owed);
	TransferHelper.safeApprove(token1, address(this), amount1Owed);

	if (amount0Owed > 0) pay(token0, address(this), msg.sender, amount0Owed);
	if (amount1Owed > 0) pay(token1, address(this), msg.sender, amount1Owed);

	// if profitable pay profits to payer
	if (amountOut0 > amount0Owed) {
		uint256 profit0 = LowGasSafeMath.sub(amountOut0, amount0Owed);

		TransferHelper.safeApprove(token0, address(this), profit0);
		pay(token0, address(this), decoded.payer, profit0);
	}
	if (amountOut1 > amount1Owed) {
		uint256 profit1 = LowGasSafeMath.sub(amountOut1, amount1Owed);
		TransferHelper.safeApprove(token0, address(this), profit1);
		pay(token1, address(this), decoded.payer, profit1);
	}
}

コントラクト全体

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

import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3FlashCallback.sol';
import '@uniswap/v3-core/contracts/libraries/LowGasSafeMath.sol';

import '@uniswap/v3-periphery/contracts/base/PeripheryPayments.sol';
import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol';
import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol';
import '@uniswap/v3-periphery/contracts/libraries/CallbackValidation.sol';
import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';
import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol';

/// @title Flash contract implementation
/// @notice An example contract using the Uniswap V3 flash function
contract PairFlash is IUniswapV3FlashCallback, PeripheryImmutableState, PeripheryPayments {
    using LowGasSafeMath for uint256;
    using LowGasSafeMath for int256;

    ISwapRouter public immutable swapRouter;

    constructor(
        ISwapRouter _swapRouter,
        address _factory,
        address _WETH9
    ) PeripheryImmutableState(_factory, _WETH9) {
        swapRouter = _swapRouter;
    }

    /// @param fee0 The fee from calling flash for token0
    /// @param fee1 The fee from calling flash for token1
    /// @param data The data needed in the callback passed as FlashCallbackData from `initFlash`
    /// @notice implements the callback called from flash
    /// @dev fails if the flash is not profitable, meaning the amountOut from the flash is less than the amount borrowed
    function uniswapV3FlashCallback(
        uint256 fee0,
        uint256 fee1,
        bytes calldata data
    ) external override {
        FlashCallbackData memory decoded = abi.decode(data, (FlashCallbackData));
        CallbackValidation.verifyCallback(factory, decoded.poolKey);

        address token0 = decoded.poolKey.token0;
        address token1 = decoded.poolKey.token1;

        TransferHelper.safeApprove(token0, address(swapRouter), decoded.amount0);
        TransferHelper.safeApprove(token1, address(swapRouter), decoded.amount1);

        // profitable check
        // exactInputSingle will fail if this amount not met
        uint256 amount1Min = LowGasSafeMath.add(decoded.amount1, fee1);
        uint256 amount0Min = LowGasSafeMath.add(decoded.amount0, fee0);

        // call exactInputSingle for swapping token1 for token0 in pool w/fee2
        uint256 amountOut0 =
            swapRouter.exactInputSingle(
                ISwapRouter.ExactInputSingleParams({
                    tokenIn: token1,
                    tokenOut: token0,
                    fee: decoded.poolFee2,
                    recipient: address(this),
                    deadline: block.timestamp,
                    amountIn: decoded.amount1,
                    amountOutMinimum: amount0Min,
                    sqrtPriceLimitX96: 0
                })
            );

        // call exactInputSingle for swapping token0 for token 1 in pool w/fee3
        uint256 amountOut1 =
            swapRouter.exactInputSingle(
                ISwapRouter.ExactInputSingleParams({
                    tokenIn: token0,
                    tokenOut: token1,
                    fee: decoded.poolFee3,
                    recipient: address(this),
                    deadline: block.timestamp,
                    amountIn: decoded.amount0,
                    amountOutMinimum: amount1Min,
                    sqrtPriceLimitX96: 0
                })
            );

        // end up with amountOut0 of token0 from first swap and amountOut1 of token1 from second swap
        uint256 amount0Owed = LowGasSafeMath.add(decoded.amount0, fee0);
        uint256 amount1Owed = LowGasSafeMath.add(decoded.amount1, fee1);

        TransferHelper.safeApprove(token0, address(this), amount0Owed);
        TransferHelper.safeApprove(token1, address(this), amount1Owed);

        if (amount0Owed > 0) pay(token0, address(this), msg.sender, amount0Owed);
        if (amount1Owed > 0) pay(token1, address(this), msg.sender, amount1Owed);

        // if profitable pay profits to payer
        if (amountOut0 > amount0Owed) {
            uint256 profit0 = LowGasSafeMath.sub(amountOut0, amount0Owed);

            TransferHelper.safeApprove(token0, address(this), profit0);
            pay(token0, address(this), decoded.payer, profit0);
        }
        if (amountOut1 > amount1Owed) {
            uint256 profit1 = LowGasSafeMath.sub(amountOut1, amount1Owed);
            TransferHelper.safeApprove(token0, address(this), profit1);
            pay(token1, address(this), decoded.payer, profit1);
        }
    }

    //fee1 is the fee of the pool from the initial borrow
    //fee2 is the fee of the first pool to arb from
    //fee3 is the fee of the second pool to arb from
    struct FlashParams {
        address token0;
        address token1;
        uint24 fee1;
        uint256 amount0;
        uint256 amount1;
        uint24 fee2;
        uint24 fee3;
    }
    // fee2 and fee3 are the two other fees associated with the two other pools of token0 and token1
    struct FlashCallbackData {
        uint256 amount0;
        uint256 amount1;
        address payer;
        PoolAddress.PoolKey poolKey;
        uint24 poolFee2;
        uint24 poolFee3;
    }

    /// @param params The parameters necessary for flash and the callback, passed in as FlashParams
    /// @notice Calls the pools flash function with data needed in `uniswapV3FlashCallback`
    function initFlash(FlashParams memory params) external {
        PoolAddress.PoolKey memory poolKey =
            PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee1});
        IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
        // recipient of borrowed amounts
        // amount of token0 requested to borrow
        // amount of token1 requested to borrow
        // need amount 0 and amount1 in callback to pay back pool
        // recipient of flash should be THIS contract
        pool.flash(
            address(this),
            params.amount0,
            params.amount1,
            abi.encode(
                FlashCallbackData({
                    amount0: params.amount0,
                    amount1: params.amount1,
                    payer: msg.sender,
                    poolKey: poolKey,
                    poolFee2: params.fee2,
                    poolFee3: params.fee3
                })
            )
        );
    }
}

最後に

今回の記事では、以下のUniswap V3について公式ドキュメントを翻訳・補足しながらまとめてきました。

https://docs.uniswap.org/contracts/v3/overview

いかがだったでしょうか?

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

https://chaldene.net/

https://qiita.com/cardene

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

DeCipher |"Read me" for All of Contracts

Discussion