bitbank

DeFi Smart Contract Solidity ブロックチェーン

UniswapV2の概要からコントラクトのコードについて1から丁寧にわかりやすく解説

かるでね

こんにちは!実務でPython、Djangoを使って、機械学習やWebアプリケーション開発などをしているかるでねです!
最近はSolidityを使ってスマートコントラクト開発やバグを見つけたりしています。

今回は「Uniswap」について解説していきます!

Uniswap」はEthereumブロックチェーンのスマートコントラクトを利用したDEXの1つです。

Solidityの基礎を学習して、「ERC20」と「ERC721」を学習した人が次に学習する内容として最適なのが「Uniswap」です。

スマートコントラクトへの理解やスキルレベルを一歩上げたい人はぜひこの記事で学んでいってください。

ERC20」と「ERC721」をまだしっかり理解できていない人は以下の記事で学んでください。

Uniswapとは

Uniswapは、Ethereumブロックチェーンのスマートコントラクトを利用したAMM型の分散型取引所(DEX)です。

聞き慣れない単語がいくつか出てきたので確認していきましょう。

DEX

DEXとは分散型取引所とも呼ばれ、中央集権的な管理者が存在せず、ユーザー同士で仮想通貨やトークンの交換を行うことができる取引所のことです。

DEXの定義を確認すると以下のように書かれています。

分散型取引所(DEX)は、暗号資産のトレーダー間で取引を直接実施するピアツーピアの市場です。DEXは暗号資産の中核を成す可能性のうちの1つを実現しています。銀行や証券会社などの仲介者が取り扱わない金融取引を促進するという可能性です。ユニスワップやスシスワップなどの人気があるDEXは、イーサリアムブロックチェーン上で運営されています。

https://www.coinbase.com/ja/learn/crypto-basics/what-is-a-dex

証券取引所のように中央に管理者が存在して仮想通貨取引の調整を行なっている取引所を中央集権型取引所(CEX)と言います。

一方、DEXは中央に管理者が存在しないため、スマートコントラクトを用いて暗号資産の価格が決まります。

DEXのメリット

より多くのトークンを取引可能

DEXでは、Ethereumベースのトークンであれば上場していなくてもトークンの取引をすることができます。

そのため、これから値上がりする可能性があるトークンを狙えるというメリットがあります。

仲介手数料が不要

中央に管理者がいないため、管理者に支払う余計な仲介手数料がわずかにしか発生しません。

そのためDEXを使用するユーザーは手数料を安く抑えることができます。

ハッキングリスクの軽減

DEXでは、ユーザーの資金を取引所ではなく各ユーザーのウォレットで保管しているため、ハッキングに強いです。

匿名性

DEXで取引を開始する際、個人情報の提出が不要です。

そのため誰でもDEXでの取引を開始できるのは大きなメリットです。

新興国での活用

新興国では銀行口座をもてない人が多くいます。

DEXでは個人情報の提出が不要であるため、スマホ1台あれば誰でも取引することができます。

DEXのデメリット

スマートコントラクトの脆弱性

スマートコントラクトに脆弱性があり、その脆弱性を突かれることで資金を抜かれてしまうリスクがあります。

メリットで述べた部分は各ユーザーがウォレットで管理している資金に限っての話なので、DEXに預けている資金はスマートコントラクトのハッキングリスクにさらされています。

悪意のあるトークン

メリットで上場していないトークンの取引が可能と述べました。

これらのトークンは審査を行なっていないため、トークン発行者が詐欺を行うことを目的に作成した可能性がないとは言えません。

そのため、トークンを取引する際はあらかじめホワイトペーパーや開発者のTwitter、Discordチャンネルを確認することが求められます。

ガス代が高い

UniswapはEthereumブロックチェーンに依存しているため、Uniswapで取引手数料を安く抑えることができても、決済時のガス代が高くなってしまう。

DeFiとは?

DeFiとは分散型金融と呼ばれ、中央管理者のいない金融仲介アプリケーションのことを指します。

以下の動画がわかりやすいです。

DeFiとDEXの違いとしては以下になります。

『DEFIというモノの中に、DEXという機能が存在している』

https://manabufan.com/2021/02/25/difi-dex/

DeFiとDEXは混同されがちなので、違いを理解しておくのは重要です。

AMM(Automated Market Maker)

ERC20トークン同士を直接交換することができます。

ERC20については以下の記事を参考にしてください。

あらかじめ設定されたアルゴリズム(一定のルール)に従って、自動的に取引を実行するシステムのことです。

通常の取引所のシステムでは、多くの人々に取引に参加してもらい、指値や約定を誘発し「好きな量を、好きな時に売買できる」状態を作り出せています。

このようにユーザーの売買希望金額がリアルタイムで表示されていて、その時点での相場を把握し、どの価格帯にフォーカスして注文を出すのが良いか判断できる取引をオーダーブック(板取引)といいます。

しかし、過去のDEXでは指値提示やキャンセルのたびにEthereumのガス代がかかり、流動性が不足し売買が活発にならないという問題がありました。

Ethereumは頻繁な取引に不向きであるため、オーダーブックを使用したDEXは廃れていきました。

その後に登場したのがAMMです。

スワップ(交換)に用いる仮想通貨をユーザー同士でプール(Uniswapに貸し出した資金を貯めておくところ)しておき、スワップ(交換)の際は他のユーザーと超苦節やり取りするのではなく、プールとの間で取引をします。

スワップ(交換)の際の価格はアルゴリズムによって自動的に計算されます。

これにより第3者の仲介がなくても、プールを介してユーザー同士で取引することができるようになります。

AMMについて以下の動画でわかりやすく解説されています。

UNI

UNIとは、Uniswapアプリケーション内で利用できる独自トークンのことです。

ガバナンストークン」として発行され、Uniswapプロジェクト運営に関わる投票時に利用可能です。

UNIの現在の価格は以下のサイトなどから確認できます。

Swap

仮想通貨・トークン同士を交換することをスワップと言います。

流動性マイニング(イールドファーミング)

自分が保有しているERC20トークンをUniswapに貸し出して「Pool」という場所に預け入れることで、UNIを一定額もらうことを流動性マイニング(イールドファーミング)といいます。

基本的に「ETH-DAI」、「ETH-USDT」、「ETH-WBTC」など暗号資産をペアで貸し出します。

トークンを預け入れることで流動性を提供することができ、「好きな量を、好きな時に売買できる」状態を作り出すことに貢献できます。

そのため、流動性マイニング(イールドファーミング)を行うことで、UNIを報酬としてもらうことができます。

保有している通貨を失うことなくUNIを得ることができるため、リスクが少ない投資法です。

Uniswapバージョン

Uniswapには、「UniswapV1」、「UniswapV2」、「UniswapV3」3つのバージョンが存在します。

この記事で取り上げるのは「UniswapV2」になります。

なぜ「UniswapV3」を取り上げないのかこれから説明していきます。

UniswapV1」は機能がシンプルすぎて、最新の機能の実装がなされていません。

UniswapV3」は「UniswapV2」の機能も備わっていますが、改良・最適化されているため複雑になっています。

そのため、「UniswapV3」を理解するには、まず「UniswapV2」を理解した方が効率よく理解することができるため、今回は「UniswapV2」を解説して行きます。

Uniswapのコントラクト構成

では次に「UniswapV2」のコントラクト構成のについて確認してきましょう。

UniswapV2」は4つのコントラクトで構成されていて、「Core」と「Periphery」の2つに分けることができます。

Core

Coreは以下の機能を備えています。

Coreの機能

  • トークンの保管
  • トークンの交換
  • トークンの追加
  • 報酬の獲得

Coreは以下の3つのコントラクトで構成されています。

Coreのコントラクト

  • UniswapV2Pair
  • UniswapV2Factory
  • UniswapV2ERC20

それぞれ以下のような機能を備えています。

UniswapV2Pair

2つのトークンのペアごとに作成されます。

UniswapV2Pair

  • トークンのスワップ(交換)
  • トークンのミント(発行)
  • トークンのバーン(焼却)

UniswapV2Factory

各トークンののペアごとにコントラクトを作成します。

UniswapV2ERC20

プールの所有権を管理するコントラクトです。

流動性提供者がプールに資金を追加すると、その報酬として「プール所有権トークン」を取得することができます。

流動性提供者がプールから資金を取り出したいときは、「プール所有権トークン」を渡し、資金と貯められた報酬を得ることができます。

このコントラクトで「プール所有権トークン」を追跡しています。

Periphery

Coreのコントラクトとやりとりするコントラクトです。

トークンのスワップや流動性提供などUniswapの機能はほとんどがこのコントラクトから実行されます。

Oracle

Uniswapは取引する2つのトークンの価格を随時取得する必要があります。

オンチェーン上で2つのトークンの価格を取得するために、オラクルが使用されます。

ただし、「特定の情報提供者に依存してしまう設計」は脆弱性を招くため、何かしらの対策が必要です。

そのため、Uniswapでは、直近数ブロック分のデータを利用することで、できるだけ現在の市場に近い価格を取得しつつ、意図的に不正確なデータを与える攻撃のコストを高めるような仕組みになっています。

UniswapV2Pair

まずは、UniswapV2Pair.solのコードから確認して行きましょう!

コードは以下になります。

1行ずつ確認して行きます。

読み込み

pragma solidity =0.5.16;

import './interfaces/IUniswapV2Pair.sol';
import './UniswapV2ERC20.sol';
import './libraries/Math.sol';
import './libraries/UQ112x112.sol';
import './interfaces/IERC20.sol';
import './interfaces/IUniswapV2Factory.sol';
import './interfaces/IUniswapV2Callee.sol';

まずはSolidityのバージョンを指定して、librariesinterfaceを読み込んでいます。

それぞれのコードはを置いておきます。

IUniswapV2Pair.sol

UniswapV2ERC20.sol

https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol

Math.sol

UQ112x112.sol

IERC20.sol

IUniswapV2Factory.sol

IUniswapV2Callee.sol

コントラクトとライブラリ定義

contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
    using SafeMath  for uint;
    using UQ112x112 for uint224;

この部分ではまずUniswapV2Pairコントラクトを定義しています。

IUniswapV2Pairコントラクトを定義してUniswapV2ERC20コントラクトを継承しています。

SafeMathライブラリは、オーバーフローとアンダーフローに対策するためのライブラリです。

オーバーフローとアンダーフローについては以下を参考にしてください。

UQ112x112ライブラリは、浮動小数点数をサポートするためのライブラリです。

Solidityでは、浮動小数点数をサポートしていないため、UQ112x112ライブラリを使用しています。

変数定義

uint public constant MINIMUM_LIQUIDITY = 10**3;
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));

address public factory;
address public token0;
address public token1;

uint112 private reserve0;           // uses single storage slot, accessible via getReserves
uint112 private reserve1;           // uses single storage slot, accessible via getReserves
uint32  private blockTimestampLast; // uses single storage slot, accessible via getReserves

uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event

uint private unlocked = 1;

MINIMUM_LIQUIDITY

流送性プールにトークンを預けることで、流動性プール内の最小数量(1e-18プールシェア)価値が時間の経過とともに上昇して行きます。

そのため少額の流動性を提供することが不可能になる問題が発生します。

少量であるため、ほとんどのトークンペアにとっては無視できるコストとなる。

https://ethereum.stackexchange.com/questions/132491/why-minimum-liquidity-is-used-in-dex-like-uniswap

SELECTOR

ERC20コントラクトのABI(Contract Application Binary Interface)セレクタです。

2つのERC20トークンを送る際に使用されます。

factory

プールを作成したUniswapV2Factoryコントラクトのアドレスです。

token0 & token1

このプール内で交換可能な2種類のERC20トークンのコントラクトアドレスです。

一定のアルゴリズムで各トークンのアドレスを用いて、token0token1に格納する順番を制御している。

reserve0 & reserve1

プール内に存在するtoken0token1の量を格納します。

reserve0reserve1は数字は異なりますが、価値は同じになります。

日本円とドルの関係と同じことをイメージすると理解しやすいです。

例)1$ = 132円

blockTimestampLast

トークンの交換が発生した直近のタイムスタンプを格納します。

ちなみにreserve0reserve1blockTimestampLastは同じスロットに格納されます。

reserve0 + reserve1 + blockTimestampLast = 112 + 112 + 32 = 256

スロットについては以下の記事を参考にしてください。

price0CumulativeLast & price1CumulativeLast

各トークンの累積コスト(直近のtoken0token1の価格)を保持する変数です。

一定期間の平均為替レートを計算するために使用します。

kLast

x * y = Kというアルゴリズムに沿って、reserve0 * reserve1の数値を格納する。

unlocked

ERC20トークンを送る際に、処理が終わる前に悪意を持ってERC20コントラクトを呼び出される可能性があります。

処理が開始された際にunlockedの値を0にすることで、別のERC20トークンの送付が起きないようにする。

以下の記事でより詳しく書いています。

修飾子定義

modifier lock() {
    require(unlocked == 1, 'UniswapV2: LOCKED');
    unlocked = 0;
    _;
    unlocked = 1;
}

先ほどのunlocked変数で述べた処理をここで行なっています。

2行目で他の処理が走っているか確認し、もし他の処理が走っていたらエラーを返すようにしています。

3~5行目では、他の処理が走っていない時にunlocked変数に0を格納(ロック)し、_の部分でlock修飾子を継承している関数の処理を実行したのち、unlocked変数に1を格納してロックを解除しています。

getReserves

function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
    _reserve0 = reserve0;
    _reserve1 = reserve1;
    _blockTimestampLast = blockTimestampLast;
}

reserve0reserve1blockTimestampLastの値を返す関数。

_safeTransfer

function _safeTransfer(address token, address to, uint value) private {
    (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
    require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
}

tokenで指定したERC20トークンをvalueで指定した量、toで指定したアドレスへ送る関数。

以下の2つの条件がどちらかがtrue出ない時、処理を元に戻します。

条件

  • ERC20トークンの送付の戻り値successtrue
  • 送付結果の戻り値dataに何かしらのデータが含まれていないか、dataをデコードした戻り値がtrue

Event系

event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(
    address indexed sender,
    uint amount0In,
    uint amount1In,
    uint amount0Out,
    uint amount1Out,
    address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);

4つのイベントが定義されています。

Mint

流動性提供者がERC20トークンを預ける時に発行される。

変数

  • sender = 関数実行アドレス。
  • amount0 = token0の量。
  • amount1 = token1の量。

Burn

流動性提供者がERC20トークンを引き出す時に発行される。

変数

  • sender = 関数実行アドレス。
  • amount0 = token0の量。
  • amount1 = token1の量。
  • to = ERC20トークンを受け取るアドレス。

Swap

ERC20トークンをスワップ(交換)した時に発行される。

Sync

ERC20トークンが追加される、または引き出されるたびに発行される。

最新の為替レートが提供される。

Constructor

constructor() public {
    factory = msg.sender;
}

コントラクトがデプロイされた時に一度だけ実行される関数。

favtory変数に、トークンのペアを作成したUniswapV2Factoryコントラクトのアドレスが格納されます。

initialize

function initialize(address _token0, address _token1) external {
    require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
    token0 = _token0;
    token1 = _token1;
}

作成されたこのUniswapV2Pairコントラクトが交換する2つのERC20トークンを指定する関数。

_update

function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
    require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
    uint32 blockTimestamp = uint32(block.timestamp % 2**32);
    uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
    if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
        // * never overflows, and + overflow is desired
        price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
        price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
    }
    reserve0 = uint112(balance0);
    reserve1 = uint112(balance1);require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
    blockTimestampLast = blockTimestamp;
    emit Sync(reserve0, reserve1);
}

ERC20トークンの入出金時に呼び出される関数。

require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');

balance0balance1がオーバーフローしないか確認しています。

uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
    // * never overflows, and + overflow is desired
    price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
    price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}

timeElapsed(前回のスワップ実行からの経過時間)が0でない場合、ブロック内での最初のスワップ(交換)であることを意味する。

この場合price0CumulativeLastprice1CumulativeLastの値を更新する。

更新する値は(_reserve1 / _reserve0 もしくは _reserve0 / _reserve1)x timeElapsedとなります。

reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);

最後にreserve0reserve1blockTimestampLastの値を更新して、Syncイベントを発行しています。

_mintFee

function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
    address feeTo = IUniswapV2Factory(factory).feeTo();
    feeOn = feeTo != address(0);
    uint _kLast = kLast; // gas savings
    if (feeOn) {
        if (_kLast != 0) {
            uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
            uint rootKLast = Math.sqrt(_kLast);
            if (rootK > rootKLast) {
                uint numerator = totalSupply.mul(rootK.sub(rootKLast));
                uint denominator = rootK.mul(5).add(rootKLast);
                uint liquidity = numerator / denominator;
                if (liquidity > 0) _mint(feeTo, liquidity);
            }
        }
    } else if (_kLast != 0) {
        kLast = 0;
    }
}

取引が行われるたびにUniswapV2Factoryコントラクトが指定したアドレスに支払われる手数料を計算する関数。

UniswapV2では、取引が行われるたびに0.3%の手数料を流動性提供者とUniswapV2Factoryコントラクトが指定したアドレスに支払います。

取引が行われるたびに手数料の1/6が支払われるため、その計算を行なっている。

address feeTo = IUniswapV2Factory(factory).feeTo();

ERC20トークンのペアを作成したUniswapV2Factoryコントラクトで指定した手数料の送付先アドレスを取得して、feeTo変数に格納しています。

uint _kLast = kLast; // gas savings

kLastの値を_kLast変数に格納しているのは、ストレージ変数からメモリ変数に格納することで、ガス代を節約しています。

if (feeOn) {
    if (_kLast != 0) {

送付先が0アドレスではなく、_kLastの値が0でない(プール内のどちらか片方のERC20トークンが0でない)時以下の処理を行います。

uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) {
    uint numerator = totalSupply.mul(rootK.sub(rootKLast));
    uint denominator = rootK.mul(5).add(rootKLast);
    uint liquidity = numerator / denominator;
    if (liquidity > 0) _mint(feeTo, liquidity);
}

ERC20トークンがプールに預けられていればrootKLastの値よりrootKの値の方が大きくなり、手数料を徴収することができます。

ここではだいぶ複雑な計算をしていますが、やっていることとしてはUniswapV2Factoryコントラクトが指定したアドレスに送るために、どのくらい流動性トークン(LP)を作成しなければいけないか計算しています。

kLast = 0;

手数料がないときにkLastの値を0に設定しています。

Uniswapが作成された当時は、不要なストレージをゼロにすると、Ethereumのサイズを小さ句なりガス代を返金してくれる機能があったため、このような処理が行われています。

より詳しくは以下の記事を参考にしてください。

mint

function mint(address to) external lock returns (uint liquidity) {
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    uint balance0 = IERC20(token0).balanceOf(address(this));
    uint balance1 = IERC20(token1).balanceOf(address(this));
    uint amount0 = balance0.sub(_reserve0);
    uint amount1 = balance1.sub(_reserve1);

    bool feeOn = _mintFee(_reserve0, _reserve1);
    uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
    if (_totalSupply == 0) {
        liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
       _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
    } else {
        liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
    }
    require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
    _mint(to, liquidity);

    _update(balance0, balance1, _reserve0, _reserve1);
    if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
    emit Mint(msg.sender, amount0, amount1);
}

流動性提供者がERC20トークンを貸し出した際に呼び出され、流動性トークンを作成する関数。

(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);

_reserve0_reserve1balance0balance1の値を取得し、各トークンがどれだけ追加されたかをamount0amount1に格納。

bool feeOn = _mintFee(_reserve0, _reserve1);

先ほど解説した_mintFee関数を実行し、徴収する手数料があれば計算して流動性トークンを作成します。

uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
if (_totalSupply == 0) {
    liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
   _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens

UniswapV2ERC20コントラクト内のtotalSupply変数を取得して、プール内のペアのERC20トークンを_totalSupplyに格納しています。

初めての入金の場合はMINIMUM_LIQUIDITY分のLPトークンを作成し、0アドレスに送ります。

このトークンは換金できないため、プール内が完全に空になることはありません。

最初の入金では2つのトークンの相対的な価値がわからないため、入金によって両方のトークンの価値が等しくなると仮定して平方根を取っています。

} else {
    liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}

2回目以降の入金であれば、2つのトークンの相対的な価値がわかっているため、流動性提供者が同等の価値の2つのトークンの貸し出しを期待します。

もし異なれば、罰として貸し出したトークンのうち価値の低い方を基準に流動性トークン(LP)を与える。

require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);

UniswapV2ERC20コントラクトの_mint関数を使用して、流動性トークン(LP)を作成し、toで指定したアドレスに送っています。

_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);

_reserve0_reserve1kLastの値を更新し、Mintイベントを発行しています。

burn

function burn(address to) external lock returns (uint amount0, uint amount1) {
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    address _token0 = token0;                                // gas savings
    address _token1 = token1;                                // gas savings
    uint balance0 = IERC20(_token0).balanceOf(address(this));
    uint balance1 = IERC20(_token1).balanceOf(address(this));
    uint liquidity = balanceOf[address(this)];

    bool feeOn = _mintFee(_reserve0, _reserve1);
    uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
    amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
    amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
    require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
    _burn(address(this), liquidity);
    _safeTransfer(_token0, to, amount0);
    _safeTransfer(_token1, to, amount1);
    balance0 = IERC20(_token0).balanceOf(address(this));
    balance1 = IERC20(_token1).balanceOf(address(this));

    _update(balance0, balance1, _reserve0, _reserve1);
    if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
    emit Burn(msg.sender, amount0, amount1, to);
}

流動性提供者が貸し出していたERC20トークンを引き出し、その分の流動性トークン(LP)を燃やす関数。

(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
address _token0 = token0;                                // gas savings
address _token1 = token1;                                // gas savings
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];

reserve0_reserve1balance0balance1、燃やすべき流動性トークンの量(LP)をliquidityに格納しています。

bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee

_mintFee関数を実行し、徴収する手数料があれば計算して流動性トークンを作成します。

UniswapV2ERC20コントラクト内のtotalSupply変数を取得して、プール内のペアのERC20トークンを_totalSupplyに格納しています。

amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');

amount0amount1に2つのトークンの引き出す量を計算して格納して、0以上か確認しています。

_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));

liquidity分の流動性トークン(LP)を燃やして、amount0分の_token0と、amount1分の_token1toに指定したアドレスへ送っています。

_token0_token1の残高をそれぞれbalance0balance1に格納しています。

_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);

_reserve0_reserve1kLastの値を更新し、Burnイベントを発行しています。

swap

function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
    require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

    uint balance0;
    uint balance1;
    { // scope for _token{0,1}, avoids stack too deep errors
    address _token0 = token0;
    address _token1 = token1;
    require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
    if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
    if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
    if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
    balance0 = IERC20(_token0).balanceOf(address(this));
    balance1 = IERC20(_token1).balanceOf(address(this));
    }
    uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
    uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
    require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
    { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
    uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
    uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
    require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
    }

    _update(balance0, balance1, _reserve0, _reserve1);
    emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}

プール内の1つのERC20トークンを預けて、もう片方のERC20トークンを引き出すPeripheryコントラクトから呼び出されることを想定している関数。

require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

uint balance0;
uint balance1;

引き出すトークンの量がどちらかが0以上であるか確認しています。

_reserve0_reserve1を取得して、現在プール内にあるERC20トークンの量がそれぞれamount0Outamount0Ou1より多いことを確認している。

{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens

token0token1の値を取得して、toに指定されているアドレスが不適切でないか確認しています。

引き出したいERC20トークンをamount0Out、もしくamount1Outtoで指定したアドレスに送っています。

if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);

data0以外の値を指定すれば、スワップについてトークンの受け取りアドレスに通知ができる。

balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}

_token0_token1の現在の残高を取得しています。

uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}

スワップによる損失がないかを確認しています。

スワップによって_reserve0 * _reserve1の値が減少することはありません。

それと同時に0.3%の手数料がスワップによって送られていることも確認しています。

balance0balance1にそれぞれ1000をかけて、金額を3倍したものを引きます。

これは残高から0.3%(3/1000 = 0.003 = 0.3%)引かれたことを意味します。

_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);

_reserve0_reserve1の値を更新し、Swapイベントを発行しています。

sync

function sync() external lock {
    _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}

実際の残高が、コントラクト側とずれてしまうことがあるため、reserve0reserve1の値を更新する関数。

ERC20トークンを引き出すことはまずいですが、入金する分には問題ないためERC20トークンを送っています。

skim

function skim(address to) external lock {
    address _token0 = token0; // gas savings
    address _token1 = token1; // gas savings
    _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
    _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}

実際の残高が、コントラクト側とずれてしまうことがあるため、reserve0reserve1の値を更新する関数。

こちらは余分なERC20トークンを引き出します。

誰がERC20トークンを預けたかわからないため、どのアカウントアドレスからでも呼び出すことができてしまいます。

UniswapV2Factory

次にUniswapV2Factoryコントラクトを見て行きます。

コードは以下になります。

1行ずつ確認して行きます。

読み込み

pragma solidity =0.5.16;

import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';

Solidityのバージョンを指定して、必要なinterfaceとコントラクトを読み込んでいます。

IUniswapV2Factory.sol

UniswapV2Pair.sol

コントラクト定義と継承

contract UniswapV2Factory is IUniswapV2Factory {

UniswapV2Factoryを定義して、IUniswapV2Factoryコントラクトを継承しています。

変数定義

address public feeTo;
address public feeToSetter;

mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;

feeTo

流動性トークンを貯めるアドレス。

feeToSetter

feeToで指定したアドレスを別のアドレスに変更するアドレス。

getPair

交換する2つのERC20トークンをもとにスワップ(交換)を実行するために必要なコントラクトのアドレスを保管。

getPair[tokenAのアドレス][tokenBのアドレス]で取得できます。

allPairs

このUniswapV2Factoryコントラクトで作成された、ERC20トークンペアのUniswapV2Pairコントラクトのアドレスを格納する配列。

Event系

event PairCreated(address indexed token0, address indexed token1, address pair, uint);

新しいERC20トークンのペアが作成された時に発行される。

Constructor

constructor(address _feeToSetter) public {
    feeToSetter = _feeToSetter;
}

コントラクトがデプロイされた時に一度だけ実行される関数。

_feeToSetterで指定したアドレスをfeeToSetterに格納。

allPairsLength

function allPairsLength() external view returns (uint) {
    return allPairs.length;
}

作成されたERC20トークンのペアの数を返す関数。

createPair

function createPair(address tokenA, address tokenB) external returns (address pair) {
    require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
    (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
    require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
    bytes memory bytecode = type(UniswapV2Pair).creationCode;
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    assembly {
        pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
    }
    IUniswapV2Pair(pair).initialize(token0, token1);
    getPair[token0][token1] = pair;
    getPair[token1][token0] = pair; // populate mapping in the reverse direction
    allPairs.push(pair);
    emit PairCreated(token0, token1, pair, allPairs.length);
}

2つのERC20トークンのペアコントラクトを作成する関数。

Uniswap側への許可は不要で、誰でも呼び出すことができます。

require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);

tokenAとtokenBが同じでないことを確認しています。

2つのトークンのアドレスを確認し、アドレスの数値が低い方がtoken0、大きい方がtoken1となるように、一貫した順番に並び替えている。

require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient

すでにERC20トークンのペアがないか確認しています。

bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
    pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}

新しいコントラクトを作成するためのオペコードの1つです。

他のコントラクトのアドレスとの衝突を防ぎます。

IUniswapV2Pair(pair).initialize(token0, token1);

作成したUniswapV2Pairコントラクトのinitialize関数を実行し、2つのトークンの情報を渡しています。

getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);

getPair配列に新しいERC20トークンのペア情報保存し、PairCreatedイベントを発行しています。

setFeeTo

function setFeeTo(address _feeTo) external {
    require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
    feeTo = _feeTo;
}

feeToSetterに格納されているアドレスのみが呼び出すことができ、feeTo_feeToで指定したアドレスを格納する関数。

手数料を受け取るアカウントを変更することができます。

setFeeToSetter

function setFeeToSetter(address _feeToSetter) external {
    require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
    feeToSetter = _feeToSetter;
}

feeToSetterに格納されているアドレスのみが呼び出すことができ、feeToSetter_feeToSetterで指定したアドレスを格納する関数。

UniswapV2ERC20

次にUniswapV2ERC20コントラクトを見て行きます。

コードは以下になります。

1行ずつ確認して行きます。

読み込み

pragma solidity =0.5.16;

import './interfaces/IUniswapV2ERC20.sol';
import './libraries/SafeMath.sol';

Solidityのバージョンを指定して、必要なinterfaceとコントラクトを読み込んでいます。

IUniswapV2ERC20.sol

SafeMath.sol

コントラクトとライブラリの定義

contract UniswapV2ERC20 is IUniswapV2ERC20 {
    using SafeMath for uint;

UniswapV2ERC20を定義して、IUniswapV2ERC20コントラクトを継承しています。

SafeMathライブラリは、オーバーフローとアンダーフローに対策するためのライブラリです。

オーバーフローとアンダーフローについては以下を参考にしてください。

変数定義

string public constant name = 'Uniswap V2';
string public constant symbol = 'UNI-V2';
uint8 public constant decimals = 18;
uint  public totalSupply;
mapping(address => uint) public balanceOf;
mapping(address => mapping(address => uint)) public allowance;

bytes32 public DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
mapping(address => uint) public nonces;

name

ERC20トークンの名前を格納。

symbol

ERC20トークンのシンボルを格納。

decimals

トークンが使用する小数点以下の桁数を格納。

totalSupply

トークンの総量を格納。

balanceOf

特定のアドレスがどれくらいのトークンを保持しているか格納する配列。

allowance

特定のアドレスAが、別のアドレスBにアドレスAが保有しているトークンを指定した量だけ操作できる許可を与える配列。

DOMAIN_SEPARATOR

ERC712標準の他のDappでは署名で使用できないような衝突が起きないことを保証した値。

Uniswapでは、承認を行う際にEthereumの秘密鍵でデータに署名するのは難しいため、安全性とウォレットの互換性を確保するために、コミュニティが広くサポートしている署名標準でであるERC712に依存している。

PERMIT_TYPEHASH

Solidityのコンパイル時に定数になるように設計される許可証。

DOMAIN_SEPARATORPERMIT_TYPEHASHについては筆者の理解が乏しいのでぜひ以下の記事などを参考にしてください。

nonces

受信者がデジタル署名を偽造することは実行不可能だが、同じトランザクションを2回送信することは簡単である。

以下の記事が参考になります。

これを防ぐために、アドレスをキーにした配列を作成し、値にはnonceと呼ばれる数値を使用して、前回使用したnonceよりも1つ多い値でないと実行が無効となる。

Event系

event Approval(address indexed owner, address indexed spender, uint value);
event Transfer(address indexed from, address indexed to, uint value);

Approval

ownerが所有するERC20トークンのうち、value分だけspenderに操作権限を与えたときに発行される。

Transfer

fromで指定したアドレスから、value分のERC20トークンをtoに指定したアドレスに送ったときに発行される。

Constructor

constructor() public {
    uint chainId;
    assembly {
        chainId := chainid
    }
    DOMAIN_SEPARATOR = keccak256(
        abi.encode(
            keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
            keccak256(bytes(name)),
            keccak256(bytes('1')),
            chainId,
            address(this)
        )
    );
}

コントラクトがデプロイされた時に一度だけ実行される関数。

constructor() public {
    uint chainId;
    assembly {
        chainId := chainid
    }

EthereumのchainIdを取得しています。

DOMAIN_SEPARATOR = keccak256(
    abi.encode(
        keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
        keccak256(bytes(name)),
        keccak256(bytes('1')),
        chainId,
        address(this)
    )
);

EIP712のドメインセパレーターを算出しています。

abi.encode で囲ったものを全てくっつけてハッシュ化しています。

以下の関数はERC20を確認していただくことで理解できるので、以下の記事を参考にしてください。

ERC20の関数

  • _mint
  • _burn
  • _approve
  • _transfer
  • _approve
  • transfer
  • trandferFrom

permit

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
    require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
    bytes32 digest = keccak256(
        abi.encodePacked(
            '\x19\x01',
            DOMAIN_SEPARATOR,
            keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
        )
    );
    address recoveredAddress = ecrecover(digest, v, r, s);
    require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
    _approve(owner, spender, value);
}

パーミッションを実装する関数。

require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');

期限を過ぎた取引は無効にされます。

bytes32 digest = keccak256(
    abi.encodePacked(
        '\x19\x01',
        DOMAIN_SEPARATOR,
        keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
    )
);

DOMAIN_SEPARATORを生成しています。

address recoveredAddress = ecrecover(digest, v, r, s);

ecrecoverという関数を使用して署名したアドレスを取得しています。

require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);

_approve関数を実行して、署名したアドレスが0アドレスでないことを確認して、ownerが指定したアドレスの所有しているvalue分のERC20トークンの操作権限をspenderで指定したアドレスに与えています。

UniswapV2Router02

最後にPeripheryコントラクトの中のUniswapV2Router02コントラクトについてまとめていきます。

Peripheryコントラクトは、他のコントラクトや分散型アプリケーションからの外部呼び出しに使用することができます。

Coreコントラクトを直接呼び出すことはもちろんできますが、複雑かつ、間違えて資金を失う可能性があります。

Coreコントラクトには、不正をしていないことを確認するためのテストが含まれているだけで、不具合がないかのチェックがありません。

Peripheryコントラクトであれば、不具合のチェックなども行なっているため、セキュリティ面でCoreコントラクトを直接触ることは推奨されません。

コードは以下になります。

読み込み

pragma solidity =0.6.6;

import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
import '@uniswap/lib/contracts/libraries/TransferHelper.sol';

import './interfaces/IUniswapV2Router02.sol';
import './libraries/UniswapV2Library.sol';
import './libraries/SafeMath.sol';
import './interfaces/IERC20.sol';
import './interfaces/IWETH.sol';

IUniswapV2Factory.sol

TransferHelper.sol

IUniswapV2Router02.sol

UniswapV2Library.sol

SafeMath.sol

IERC20.sol

IWETH.sol

UniswapV2はERC20トークンの任意のペアの交換ができますが、ETH自体はERC20トークンではありません。

ERC20トークン規格のコントラクトでETHを使用するためには、WETHが考案されました。

このコントラクトにETHを送ると、同量のWETHが生成されます。

WETHを燃やしてETHを取得することもできます。

コントラクトとライブラリの定義

contract UniswapV2Router02 is IUniswapV2Router02 {
    using SafeMath for uint;

UniswapV2Router02コントラクトを定義し、IUniswapV2Router02コントラクトを継承しています。

SafeMathライブラリは、オーバーフローとアンダーフローに対策するためのライブラリです。

オーバーフローとアンダーフローについては以下を参考にしてください。

変数の定義

address public immutable override factory;
address public immutable override WETH;

factoryコントラクトのアドレスとWETHコントラクトのアドレスを格納します。

immutableとついているように、一度設定すると変更はできません。

修飾子の定義

modifier ensure(uint deadline) {
    require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
    _;
}

このmodifierをつけた関数での取引では、時間制限を設けることができます。

deadlineという期限を現在時刻より後である必要があります。

Constructor

constructor(address _factory, address _WETH) public {
    factory = _factory;
    WETH = _WETH;
}

コントラクトがデプロイされた時に一度だけ実行される関数。

どのfactoryコントラクトを使用するか、どのWETHコントラクトを使用するか指定します。

receive

receive() external payable {
    assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
}

WETHコントラクトからERC20トークンをETHに戻す時に呼ばれる関数。

WETHコントラクトのみがERC20トークンをETHに戻すことができる。

_addLiquidity

function _addLiquidity(
    address tokenA,
    address tokenB,
    uint amountADesired,
    uint amountBDesired,
    uint amountAMin,
    uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
    // create the pair if it doesn't exist yet
    if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
        IUniswapV2Factory(factory).createPair(tokenA, tokenB);
    }
    (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
    if (reserveA == 0 && reserveB == 0) {
        (amountA, amountB) = (amountADesired, amountBDesired);
    } else {
        uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
        if (amountBOptimal <= amountBDesired) {
            require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
            (amountA, amountB) = (amountADesired, amountBOptimal);
        } else {
            uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
            assert(amountAOptimal <= amountADesired);
            require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
            (amountA, amountB) = (amountAOptimal, amountBDesired);
        }
    }
}

ERC20トークンのペアを貸し出し、プールに流動性を追加する関数。

address tokenA,
address tokenB,

ERC20トークンのアドレスです。

uint amountADesired,
uint amountBDesired,

流動性提供者が預け入れを希望するERC20トークンの価格です。

uint amountAMin,
uint amountBMin

預け入れ可能な最低金額です。

この金額以上の取引ができない場合は、取引を中止して元に戻します。

この機能が不要な場合は値に0を指定することもできます。

しかし、流動性提供者は通常預入の最低金額を指定します。

理由としては、取引を現在の価格に近いものに制限したいからです。

価格が大きく変動した際、預け入れの想定金額から大きくズレる可能性があります。

そのため許容できる範囲でのみ取引が実行されるように、基本的に預け入れの最低金額をせってしています。

) internal virtual returns (uint amountA, uint amountB) {

現在のERC20トークンの価格比率と等しくなるように、流動性提供者が預けるべき金額を返します。

// create the pair if it doesn't exist yet
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
    IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}

tokenAtokenBのペアが存在しない場合は新たに作成します。

(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);

指定したトークンペアの現在のプール内に預け入れられている量を取得します。

if (reserveA == 0 && reserveB == 0) {
    (amountA, amountB) = (amountADesired, amountBDesired);

まだ流動性提供されていなければ、預け入れる金額は流動性提供者が預け入れたい金額と同じになる。

} else {
    uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);

預け入れる金額がどのようになるかはUniswapV2Libraryコントラクトのquote関数を使用する。

現在の2つのERC20トークンの比率と同じになる。

if (amountBOptimal <= amountBDesired) {
    require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
    (amountA, amountB) = (amountADesired, amountBOptimal);

先ほど取得したamountBOptimalの値が流送性提供者が預けたい金額より小さい場合、tokenBは流動性提供者が考えているよりも現在価値が高いことを意味するため、より少ない金額が必要となる。

    } else {
        uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
        assert(amountAOptimal <= amountADesired);
        require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
        (amountA, amountB) = (amountAOptimal, amountBDesired);
    }
}

逆に、amountBOptimalの値が流送性提供者が預けたい金額より大きい場合、流動性提供shが考えているよりもtokenBの現在価値が低いことを意味するため、より多くの金額が必要となる。

しかし、預け入れたい希望金額よりも大きいため、tokenBを基準に計算できないため、代わりにtokenAを基準に預け入れに必要なトークンの数を計算する。

これらをまとめると以下のグラフのようになります。

1000tokenA(青線)と1000tokenB(赤線)を預けるとします。

X軸は為替レートでtokenA/tokenBとなっています。

x = 1の時、2つのトークンの価値は等しくなるため1000トークンずつ預け入れます。

x = 2の時、tokenAtokenBの2倍の価値があるため、1000tokenBに対して、500tokenAを預け入れれば良いです。

逆にx = 0.5の時、1000tokenAに対して、500tokenBを預け入れれば良いです。

Coreコントラクトに直接流動性を提供することもできるが、Coreコントラクトは自分が騙されていないことを確認するだけで、取引を送信してから実行されるまでに為替レートが変化すると、価値を失う危険性があります。

一方、Peripheryコントラクトを使用すると、預け入れるべき金額を計算後すぐに預け入れが可能なため、為替レートが変化せずに損をすることがなくなる。

addLiquidity

function addLiquidity(
    address tokenA,
    address tokenB,
    uint amountADesired,
    uint amountBDesired,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
    (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
    address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
    TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
    TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
    liquidity = IUniswapV2Pair(pair).mint(to);
}

流動性を提供するために呼び出される関数。

address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline

引数は_addLiquidity関数とほとんど同じです。

to

流動性を提供した際に受け取れる流動性トークン(LP)を受け取るアドレス。

deadline

トランザクションの制限時間。

) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);

_addLiquidity関数を実行して、実際に預け入れる金額を計算し、ガス代節約のために直接factoryに問い合わせずにUniswapV2LibraryコントラクトのpairFor関数を実行して、トークンの預け入れ先のアドレスを取得しています。

TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);

tokenAtokenBを先ほど取得したpairのアドレスへ、amountAamounB分預け入れます。

liquidity = IUniswapV2Pair(pair).mint(to);

流動性を提供した報酬として、流動性提供者に流動性トークン(LP)を渡しています。

預け入れたERC20トークンの量に応じて受け取れる流動性トークン(LP)も変わってきます。

addLiquidityETH

function addLiquidityETH(
    address token,
    uint amountTokenDesired,
    uint amountTokenMin,
    uint amountETHMin,
    address to,
    uint deadline
) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
    (amountToken, amountETH) = _addLiquidity(
        token,
        WETH,
        amountTokenDesired,
        msg.value,
        amountTokenMin,
        amountETHMin
    );
    address pair = UniswapV2Library.pairFor(factory, token, WETH);
    TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
    IWETH(WETH).deposit{value: amountETH}();
    assert(IWETH(WETH).transfer(pair, amountETH));
    liquidity = IUniswapV2Pair(pair).mint(to);
    // refund dust eth, if any
    if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}

流動性提供者がERC20トークンとETHのペアを預け入れた時にETHのラッピング処理を行い、流動性の提供を行う関数。

address token,
uint amountTokenDesired,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
(amountToken, amountETH) = _addLiquidity(
    token,
    WETH,
    amountTokenDesired,
    msg.value,
    amountTokenMin,
    amountETHMin
);
address pair = UniswapV2Library.pairFor(factory, token, WETH);
TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
IWETH(WETH).deposit{value: amountETH}();
assert(IWETH(WETH).transfer(pair, amountETH));

ほとんどaddLiquidity関数を処理は同じです。

異なる点としては、ETHをERC20トークンであるWETHに変換してtokenとペアにして預け入れを実行しています。

送付に失敗するとWETHの変換も無効になります。

liquidity = IUniswapV2Pair(pair).mint(to);
// refund dust eth, if any
if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);

流動性を提供した報酬として、流動性提供者に流動性トークン(LP)を渡しています。

預け入れたWETHに対して、ユーザー送ってきたETHの方が多い場合はその分をユーザーに戻しています。

removeLiquidity

function removeLiquidity(
    address tokenA,
    address tokenB,
    uint liquidity,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
    address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
    IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
    (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
    (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
    (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
    require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
    require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}

預け入れているトークンを引き出す関数。

address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {

ほとんどaddLiquidity関数を処理は同じです。

流動性提供者が引き出しを同意する各トークンの最低量を指定しているため、deadlineまでに実行されなければ無効になる。

address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);

預け入れている2つのトークンを燃やしています。

(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);

tokenAtokenBを適切に並び替え、片方だけ取得しています。

これは片方の値だけ取得できればよく、ガス代の節約になるからです。

(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);

先ほど取得したtoken0tokenAが同じであれば、amount0amountAに格納し、異なればamount1amountBに格納する。

require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');

流動性提供者が、引き出したい最低量のトークンよりも少ないトークンを引き出してしまっていないか確認しています。

先にトークンを流動性提供者に渡しても、正当でない場合状態を元に戻ることができます。

removeLiquidityETH

function removeLiquidityETH(
    address token,
    uint liquidity,
    uint amountTokenMin,
    uint amountETHMin,
    address to,
    uint deadline
) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
    (amountToken, amountETH) = removeLiquidity(
        token,
        WETH,
        liquidity,
        amountTokenMin,
        amountETHMin,
        address(this),
        deadline
    );
    TransferHelper.safeTransfer(token, to, amountToken);
    IWETH(WETH).withdraw(amountETH);
    TransferHelper.safeTransferETH(to, amountETH);
}

預け入れているトークンとETHのペアを引き出す関数。

行なっていることはremoveLiquidity関数とほとんど同じです。

違う点としては、WETHトークンを受け取ったのちにETHに変換してから流動性提供者に返しています。

removeLiquidityWithPermit

function removeLiquidityWithPermit(
    address tokenA,
    address tokenB,
    uint liquidity,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline,
    bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountA, uint amountB) {
    address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
    uint value = approveMax ? uint(-1) : liquidity;
    IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
    (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
}

事前承認を行わずremoveLiquidity関数を実行して、預け入れているERC20トークンを引き出す関数。

removeLiquidityETHWithPermit

function removeLiquidityETHWithPermit(
    address token,
    uint liquidity,
    uint amountTokenMin,
    uint amountETHMin,
    address to,
    uint deadline,
    bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountToken, uint amountETH) {
    address pair = UniswapV2Library.pairFor(factory, token, WETH);
    uint value = approveMax ? uint(-1) : liquidity;
    IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
    (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
}

事前承認を行わずremoveLiquidityETHWithPermit関数を実行して、預け入れているERC20トークンとETHを引き出す関数。

removeLiquidityETHSupportingFeeOnTransferTokens

function removeLiquidityETHSupportingFeeOnTransferTokens(
    address token,
    uint liquidity,
    uint amountTokenMin,
    uint amountETHMin,
    address to,
    uint deadline
) public virtual override ensure(deadline) returns (uint amountETH) {
    (, amountETH) = removeLiquidity(
        token,
        WETH,
        liquidity,
        amountTokenMin,
        amountETHMin,
        address(this),
        deadline
    );
    TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
    IWETH(WETH).withdraw(amountETH);
    TransferHelper.safeTransferETH(to, amountETH);
}

預け入れているERC20トークンとETHを引き出すときに、手数料が発生する場合に実行される関数。

removeLiquidity関数を実行して、まずはERC20トークンとWETHを引き出したのち、引き出したETHの量を取得して手数料を支払っています。

removeLiquidityETHWithPermitSupportingFeeOnTransferTokens

function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
    address token,
    uint liquidity,
    uint amountTokenMin,
    uint amountETHMin,
    address to,
    uint deadline,
    bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountETH) {
    address pair = UniswapV2Library.pairFor(factory, token, WETH);
    uint value = approveMax ? uint(-1) : liquidity;
    IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
    amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
        token, liquidity, amountTokenMin, amountETHMin, to, deadline
    );
}

事前承認を行わずremoveLiquidityETHSupportingFeeOnTransferTokens関数を実行して、預け入れているERC20トークンとETHを引き出して、手数料を支払う関数。

_swap

function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
    for (uint i; i < path.length - 1; i++) {
        (address input, address output) = (path[i], path[i + 1]);
        (address token0,) = UniswapV2Library.sortTokens(input, output);
        uint amountOut = amounts[i + 1];
        (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
        address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
        IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
            amount0Out, amount1Out, to, new bytes(0)
        );
    }
}

ERC20トークンのスワップ(交換)時に実行ユーザーに公開される機能に必要な内部の処理を行う関数。

for (uint i; i < path.length - 1; i++) {

トークンA、トークンB、トークンC、トークンDの4つのトークンがある時に、「A-B」、「B-C」、「C-D」のペアがあるとします。

この時パスの概念を使用して、「A-D」のスワップを、「A→B」、「B→C」、「C→D」の順に交換することで、「A-D」のスワップを直接行う必要がなくなる。

スワップの手順

  • 24.69トークンAを売って、25.30トークンBを取得する。
  • 24.69トークンBを売って、25.30トークンCを取得して、約0.61トークンBを利益として残す。
  • 24.69トークンCを売って、25.30トークンAを取得して、約0.61トークンCを利益として残す。
  • 0.61トークンAを利益として保有。(25.30トークンAから投資額の24.69トークンAを差し引いた額)

(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
uint amountOut = amounts[i + 1];

現在扱っているERC20トークンのペアを取得して並び替えています。

その後トークンの取得予定料を計算しています。

(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));

予想される出力金額をトークンをソートして取得しています。

address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;

最後のスワップ(交換)であれば、うけとったERC20トークンを受け取りアドレスに送ります。

IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
    amount0Out, amount1Out, to, new bytes(0)
);

実際にERC20トークンのスワップ(交換)を実行。

swapExactTokensForTokens

function swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
    amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
    require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, to);
}

ユーザーERC20トークンのスワップ(交換)するために直接使用される関数。

uint amountIn,
uint amountOutMin,
address[] calldata path,

path配列には、先ほど_swap関数の部分で説明したように現在所有しているトークンから目的のトークンを交換するために、いくつかのトークンペアを経由する必要があるため、その情報が格納されています。

	address to,
	uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
	amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
	require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');

各スワップ(交換)で購入する金額を計算しています。

もしその金額がユーザーが受け入れることができる最小値より小さい場合はエラーを起こし実行を無効にします。

TransferHelper.safeTransferFrom(
    path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);

最初のERC20トークンを最初のスワップ用のアカウントに送り、_swap関数を呼び出しています。

swapTokensForExactTokens

function swapTokensForExactTokens(
    uint amountOut,
    uint amountInMax,
    address[] calldata path,
    address to,
    uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
    amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
    require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, to);
}

swapExactTokensForTokens関数の逆のスワップを行う関数。

先ほど解説したswapExactTokensForTokens関数は、ユーザーが預け入れるトークンの正確な量と、その報酬として受け取るトークンの最小数を指定できました。

ユーザーが受け取りたいトークン数と、そのために支払う最大数の預け入れトークンを指定することができます。

swapExactETHForTokens

function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
    external
    virtual
    override
    payable
    ensure(deadline)
    returns (uint[] memory amounts)
{
    require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
    amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
    require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
    IWETH(WETH).deposit{value: amounts[0]}();
    assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
    _swap(amounts, path, to);
}

預け入れたいETHを指定して、ETHを預け入れてERC20トークンを受け取れる関数。

swapTokensForExactETH

function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
    external
    virtual
    override
    ensure(deadline)
    returns (uint[] memory amounts)
{
    require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
    amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
    require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, address(this));
    IWETH(WETH).withdraw(amounts[amounts.length - 1]);
    TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}

取得したいETHを指定して、ERC20トークンを預け入れしてETHを受け取れる関数。

swapExactTokensForETH

function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
    external
    virtual
    override
    ensure(deadline)
    returns (uint[] memory amounts)
{
    require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
    amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
    require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, address(this));
    IWETH(WETH).withdraw(amounts[amounts.length - 1]);
    TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}

預け入れたいERC20トークンを指定して、ERC20トークンを預け入れしてETHを受け取れる関数。

swapETHForExactTokens

function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
    external
    virtual
    override
    payable
    ensure(deadline)
    returns (uint[] memory amounts)
{
    require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
    amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
    require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
    IWETH(WETH).deposit{value: amounts[0]}();
    assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
    _swap(amounts, path, to);
    // refund dust eth, if any
    if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
}

取得したいERC20トークンを指定して、ETHを預け入れしてERC20トークンを受け取れる関数。

_swapSupportingFeeOnTransferTokens

function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
    for (uint i; i < path.length - 1; i++) {
        (address input, address output) = (path[i], path[i + 1]);
        (address token0,) = UniswapV2Library.sortTokens(input, output);
        IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
        uint amountInput;
        uint amountOutput;
        { // scope to avoid stack too deep errors
        (uint reserve0, uint reserve1,) = pair.getReserves();
        (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
        amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
        amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
        }
        (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
        address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
        pair.swap(amount0Out, amount1Out, to, new bytes(0));
    }
}

送金手数料や保管手数料がかかるERC20トークンをスワップ(交換)するための関数。

まずはユーザーからERC20トークンを預けてもらい、返ってきた値を元に手数料を計算して処理を行っています。

_swap関数を実行しても手数料処理を行うことはできます。

しかし、より多くのガス代が発生するのと、手数料が発生するトークンは稀なのでこの関数を実行した方が良いです。

swapExactTokensForTokensSupportingFeeOnTransferTokens

function swapExactTokensForTokensSupportingFeeOnTransferTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
) external virtual override ensure(deadline) {
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
    );
    uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
    _swapSupportingFeeOnTransferTokens(path, to);
    require(
        IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
        'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
    );
}

swapExactTokensForTokens関数と同じ処理を実行し、ERC20トークンの送付時に手数料を取る関数。

swapExactETHForTokensSupportingFeeOnTransferTokens

function swapExactETHForTokensSupportingFeeOnTransferTokens(
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
)
    external
    virtual
    override
    payable
    ensure(deadline)
{
    require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
    uint amountIn = msg.value;
    IWETH(WETH).deposit{value: amountIn}();
    assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
    uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
    _swapSupportingFeeOnTransferTokens(path, to);
    require(
        IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
        'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
    );
}

swapExactETHForTokens関数と同じ処理を実行し、transfer時に手数料を取る関数。

swapExactTokensForETHSupportingFeeOnTransferTokens

function swapExactTokensForETHSupportingFeeOnTransferTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
)
    external
    virtual
    override
    ensure(deadline)
{
    require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
    );
    _swapSupportingFeeOnTransferTokens(path, address(this));
    uint amountOut = IERC20(WETH).balanceOf(address(this));
    require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
    IWETH(WETH).withdraw(amountOut);
    TransferHelper.safeTransferETH(to, amountOut);
}

swapExactTokensForETH関数と同じ処理を実行し、transfer時に手数料を取る関数。

quote

function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
    return UniswapV2Library.quote(amountA, reserveA, reserveB);
}

UniswapV2Libraryコントラクトのquote関数を実行する関数。

特定のERC20トークンの量を与えられると、同等の価値を持つもう片方のトークン量を返します。

getAmountOut

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

UniswapV2LibraryコントラクトのgetAmountOut関数を実行する関数。

預け入れるトークンの金額が与えられると、もう片方のトークンの交換できる最大の金額が取得できます(手数料を考慮した値)。

getAmountIn

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

UniswapV2LibraryコントラクトのgetAmountIn関数を実行する関数。

指定したトークンの金額(手数料を考慮した値)の交換に必要な、もう片方の最小限のトークンの金額を取得します。

getAmountsOut

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

UniswapV2LibraryコントラクトのgetAmountsOut関数を実行する関数。

入力トークン量とトークンアドレスであるpath配列を与えて、path配列内のトークンアドレスのペアごとにgetReserves関数を呼び出して、取得できる最大のトークン量を計算してgetAmountOut関数を呼び出します。

getAmountsIn

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

UniswapV2LibraryコントラクトのgetAmountsIn関数を実行する関数。

取得したいトークン量とトークンアドレスであるpath配列を与えて、path配列内のトークンアドレスのペアごとにgetReserves関数を呼び出して、取得できる最大のトークン量を計算してgetAmountIn関数を呼び出します。

UniswapV2Library

前章でUniswapのメインのコントラクトは解説しました。

次にUniswapV2Library.solのコードを確認していきます。

コードは以下になります。

1行ずつ確認して行きます。

読み込み

pragma solidity >=0.5.0;

import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';

import "./SafeMath.sol";

まずはSolidityのバージョンを指定して、librariesを読み込んでいます。

それぞれのコードはを置いておきます。

IUniswapV2Pair.sol

Math.sol

コントラクトとライブラリ定義

library UniswapV2Library {
    using SafeMath for uint;

この部分ではまずUniswapV2Pairライブラリを定義しています。

UniswapV2Libraryコントラクトを定義しています。

SafeMathライブラリは、オーバーフローとアンダーフローに対策するためのライブラリです。

オーバーフローとアンダーフローについては以下を参考にしてください。

sortTokens

function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
    require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
    (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
}

2つのERC20トークンの順番を入れ替える関数。

同じ値が渡されると必ず同じ順番で返されます。

pairFor

function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
    (address token0, address token1) = sortTokens(tokenA, tokenB);
    pair = address(uint(keccak256(abi.encodePacked(
            hex'ff',
            factory,
            keccak256(abi.encodePacked(token0, token1)),
            hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
        ))));
}

2つのERC20トークンのスワップのためのPairコントラクトのアドレスを取得する関数。

使用するパラメータがわかれば、同じアルゴリズムでアドレスを計算することができるため、factoryコントラクト内で実行するよりもガス代が安く済む。

getReserves

function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
    (address token0,) = sortTokens(tokenA, tokenB);
    (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
    (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}

tokenAtokenBのを渡して、順番をソートして返す関数。

quote

function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
    require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
    require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
    amountB = amountA.mul(reserveB) / reserveA;
}

tokenAを預け入れると、どれくらいの量のtokenBを得ることができるか計算する関数。

為替レートが変化することを考慮しています。

getAmountOut

function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
    require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
    require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
    uint amountInWithFee = amountIn.mul(997);
    uint numerator = amountInWithFee.mul(reserveOut);
    uint denominator = reserveIn.mul(1000).add(amountInWithFee);
    amountOut = numerator / denominator;
}

あるトークンの預け入れ量に対して、ペアとなるもう片方のトークンを最大どれくらい受け取れるか計算する関数。

0.3%の手数料は引いた状態の値が返ってきます。

getAmountIn

function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
    require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
    require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
    uint numerator = reserveIn.mul(amountOut).mul(1000);
    uint denominator = reserveOut.sub(amountOut).mul(997);
    amountIn = (numerator / denominator).add(1);
}

あるトークンの受け取れる量を指定して、ペアとなるもう片方のトークンをどれくらい預け入れれば良いか計算する関数。

0.3%の手数料は引いた状態の値が返ってきます。

getAmountsOut

function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
    require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
    amounts = new uint[](path.length);
    amounts[0] = amountIn;
    for (uint i; i < path.length - 1; i++) {
        (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
        amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
    }
}

getAmountOut関数を複数回実行する際に実行される関数。

getAmountsIn

function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
    require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
    amounts = new uint[](path.length);
    amounts[amounts.length - 1] = amountOut;
    for (uint i = path.length - 1; i > 0; i--) {
        (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
        amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
    }
}

getAmountIn関数を複数回実行する際に実行される関数。

最後に

今回は「Uniswap」のコードについて解説してきました。

だいぶ複雑かつ長かったと思うので、ここまで読んでいただきありがとうございます。

一回では理解できない部分が多いと思うので、ぜひ自分でも調べながら辞書のような使い方をしてくれたら嬉しいです!

何か質問などがあれば以下のTwitterなどからDMください!

もし何か質問などがあれば以下のTwitterなどから連絡ください!

普段はSolidityやブロックチェーン、Web3についての情報発信をしています。

Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!

参考

-DeFi, Smart Contract, Solidity, ブロックチェーン
-, ,