メインコンテンツへスキップ

Uniswap-v2コントラクトの手順

Solidityuniswap
中級
Ori Pomerantz
2021年5月1日
92 分の読書 minute read

はじめに

Uniswap v2(opens in a new tab)では、任意の 2 つの ERC-20 トークン同士の取引が可能になります。 本記事では、このプロトコルを実装したコントラクトのソースコードをチェックして、このように書かれている理由を理解していきます。

Uniswap の役割

基本的には、流動性プロバイダーとトレーダーの 2 種類のユーザーが存在します。

liquidity providers(流動性プロバイダー)は、交換可能な 2 つのトークン(ここではToken0Token1と呼びます)をプールに提供します。 その見返りとして、liquidity token(流動性トークン)と呼ばれるプールの一部所有権を表す第 3 のトークンを受け取ります。

Traders(トレーダー)は、あるトークンをプールに送り、流動性プロバイダーが提供するプールから他のトークン(例えば、Token0を送ってToken1を受け取る)を受け取ります。 交換レートは、プールが持つToken0Token1の相対的な数で決定されます。 加えて、流動性プールの報酬としてプールに数パーセントのフィーを取られます。

流動性プロバイダーが元のトークンを取り戻したい場合は、プールトークンをバーンして、報酬を含めた元のトークンを受け取ることができます。

詳しい説明はこちら(opens in a new tab)をご覧ください。

なぜ v2? v3 じゃないの?

Uniswap v3(opens in a new tab)は v2 を非常に複雑化したアップグレード版です。 まず v2 を学んでから v3 に進む方が簡単です。

コアコントラクト vs ペリフェリーコントラクト

Uniswap v2 は、コアコントラクトとペリフェリーコントラクトの 2 つのコンポーネントに分かれています。 コアコントラクトはアセットを保有するため安全性の保証が重要となりますが、この分割によって監査を簡略化することができます。 ペリフェリーコントラクトはトレーダーが必要とするすべての機能を提供しています。

データおよび制御フロー

Uniswap の 3 大アクションを実行したときに発生するデータフローと制御フローは以下のとおりです。

  1. 異なるトークン間でスワップを実行する
  2. 市場に流動性を与え、ERC-20 流動性トークンのペア取引で報酬を得る
  3. ERC-20 流動性トークンをバーンし、ペア取引によってトレーダーが取引できる ERC-20 トークンを取り戻す

スワップ

トレーダーが使用する最も一般的なフローです。

呼び出し元

  1. スワップされた量のアローワンスをペリフェリーアカウントへ提供します。
  2. ペリフェリーコントラクトには多数のスワップ関数がありますが、そのうち 1 つを呼び出します(ETH が関係しているかどうか、入金するトークンの量や取り戻すトークンの量をトレーダーが指定するかどうかなどによって、呼び出す関数は異なります)。 どのスワップ関数もpath、つまり経由する取引所の配列を受け取ります。

ペリフェリーコントラクト内の処理 (UniswapV2Router02.sol)

  1. パスに沿って、各取引所で取引する量を特定します。
  2. パスを繰り返し処理します。 経路にある各取引所に入力トークンを送信し、取引所のスワップ関数を呼び出します。 通常、トークンの送信先アドレスはパス上にある次のペア取引所です。 最後の取引は、トレーダーが提供したアドレスとなります。

コアコントラクト内の処理(UniswapV2Pair.sol)

  1. コアコントラクトで不正がされていないこと、スワップ後も十分な流動性を維持できることを検証します。
  2. 既存のリザーブ量と追加のトークン数を確認します。 追加のトークン量は、交換用に受け取った入力トークンの数です。
  3. 出力トークンを送信先に送ります。
  4. _updateを呼び出し、リザーブ量をアップデートします。

ペリフェリーコントラクト(UniswapV2Router02.sol)に戻る

  1. 必要なクリーンアップを行います(例えば、WETH トークンをバーンして ETH へ戻し、トレーダーに送るなど)。

流動性の追加

呼び出し元

  1. 流動性プールに追加される量のアローワンスをペリフェリーアカウントに提供します。
  2. ペリフェリーコントラクトのaddLiquidity関数の 1 つを呼び出します。

ペリフェリーコントラクト内の処理(UniswapV2Router02.sol)

  1. 必要に応じて新しいペア取引所を作成します。
  2. 既存のペア取引所がある場合は、追加するトークンの量を計算します。 両方のトークンは同じ値であることが想定されるため、新規トークンと既存トークンの比率は同じになるはずです。
  3. 受け取り可能なトークンの量かどうか確認します(実行者は流動性の追加を希望しない最低量を指定することができます) 。
  4. コアコントラクトを呼び出します。

コアコントラクト内の処理(UniswapV2Pair.sol)

  1. 流動性トークンをミントして、呼び出し元に送信します。
  2. _updateを呼び出し、リザーブ量をアップデートします。

流動性の削除

呼び出し元

  1. ペリフェリーアカウントに、基礎トークンと引き換えにバーンされた流動性トークンのアローワンスを提供します。
  2. ペリフェリーコントラクトのremoveLiquidity関数の内の 1 つを呼び出します。

ペリフェリーコントラクト内の処理(UniswapV2Router02.sol)

  1. ペア取引所に流動性トークンを送ります。

コアコントラクト(UniswapV2Pair.sol)

  1. バーンされたトークンの量に応じて、基礎トークンを送信先アドレスに送ります。 例えば、プールに A トークンが 1000 個、B トークンが 500 個、流動性トークンが 90 個あるとします。流動性トークン 9 個を受け取ってバーンすると、流動性トークンの 10%をバーンすることになり、ユーザーに 100 個の A トークンと 50 個の B トークンが返されることになります。
  2. 流動性トークンをバーンします。
  3. _updateを呼び出し、リザーブ量をアップデートします。

コアコントラクト

流動性を保持する安全なコントラクトです。

UniswapV2Pair.sol

このコントラクト(opens in a new tab)は、トークンを交換する実際のプールを実装しています。 これは Uniswap の中核機能です。

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Pair.sol';
4import './UniswapV2ERC20.sol';
5import './libraries/Math.sol';
6import './libraries/UQ112x112.sol';
7import './interfaces/IERC20.sol';
8import './interfaces/IUniswapV2Factory.sol';
9import './interfaces/IUniswapV2Callee.sol';
すべて表示
コピー

コントラクトは、これら(IUniswapV2PairUniswapV2ERC20)を実装していたり、これらを実装したコントラクトを呼び出すため、上記すべてのインターフェイスを認識する必要があります。

1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
コピー

このコントラクトは、流動性トークンの ERC-20 関数を提供するUniswapV2ERC20を継承しています。

1 using SafeMath for uint;
コピー

このSafeMath ライブラリ(opens in a new tab)は、オーバーフローとアンダーフローを回避するために使用されます。 このライブラリがないと、-1であるべき値が2^256-1になってしまうことがあるため重要な役割を担っています。

1 using UQ112x112 for uint224;
コピー

通常、プールコントラクトの計算では小数を使いますが、 EVM で小数はサポートされていません。 そこで、Uniswap が考えた解決策は、224 ビット値を使用することです。整数部分に 112 ビット、小数部分に 112 ビットを使います つまり、1.02^1121.52^112 + 2^111のように表します。

このライブラリの詳細は、後述するドキュメントに掲載されています。

変数

1 uint public constant MINIMUM_LIQUIDITY = 10**3;
コピー

ゼロ除算のケースを回避するため、流動性トークンには常に最小数が存在します(ただし、ゼロアカウントが所有しています。) その数はMINIMUM_LIQUIDITYであり、1000 です。

1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
コピー

これは、ERC-20 送金関数の ABI セレクターです。 2 つのトークンアカウント間で ERC-20 トークンを送金するために使用されます。

1 address public factory;
コピー

これは、このプールを作成したファクトリコントラクトです。 すべてのプールは 2 つの ERC-20 トークン間の取引所であり、ファクトリーはこれらすべてのプールをつなぐ中心点です。

1 address public token0;
2 address public token1;
コピー

このプールで交換可能な 2 種類の ERC-20 トークンのコントラクトアドレスがあります。

1 uint112 private reserve0; // uses single storage slot, accessible via getReserves
2 uint112 private reserve1; // uses single storage slot, accessible via getReserves
コピー

プールが保有する各種トークンのリザーブです。 2 つが同じ値であると仮定すると、各 token0 は reserve1/reserve0 token1 に相当します。

1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
コピー

交換が行われた最後のブロックのタイムスタンプで、時間の経過とともに交換レートを追跡する際に使用されます。

イーサリアムのコントラクトで最も高額なガス代の 1 つはストレージで、コントラクトコールから次のコールまで持続します。 各ストレージセルは 256 ビットとなっているため、 3 つの変数reserve0reserve1blockTimestampLastすべて(112+112+32=256)を 1 つのストレージ値に含めるように割り当てます。

1 uint public price0CumulativeLast;
2 uint public price1CumulativeLast;
コピー

これらの変数は、各トークンの累積コストを(お互いの価値で)保有し、 一定期間の平均交換レートを算出する際に使用できます。

1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event
コピー

token0 と token1 の交換レートは、取引時の両リザーブの倍数を一定に維持するようペア取引所が決定します。 kLastがその値です。 流動性プロバイダーがトークンを入出金するとこのレートは変化し、マーケット手数料 0.3%によって若干増えます。

下記にシンプルな例をご紹介します。 ただし、簡略化のため表は小数点以下 3 桁までとし、0.3%のマーケット手数料も考慮していないので正確な数字ではありません。

イベントreserve0reserve1reserve0 * reserve1平均交換レート(token1/token0)
初期設定1,000.0001,000.0001,000,000
トレーダー A が 50 の token0 を 47.619 の token1 とスワップ1,050.000952.3811,000,0000.952
トレーダー B が 10 の token0 を 8.984 の token1 と スワップ1,060.000943.3961,000,0000.898
トレーダー C が 40 の token0 を 34.305 の token1 とスワップ1,100.000909.0901,000,0000.858
トレーダー D が 100 の token1 を 109.01 の token0 とスワップ990.9901,009.0901,000,0000.917
トレーダー E が 10 の token0 を 10.079 の token1 とスワップ1,000.990999.0101,000,0001.008

トレーダーが提供する token0 の量が増えるほど、需要と供給により token1 の相対的価値も上がり、逆の場合も同じことが言えます。

ロック

1 uint private unlocked = 1;
コピー

再入可能の悪用(opens in a new tab)によるセキュリティ脆弱性の一種があります。 Uniswap は、任意の ERC-20 トークンを転送することになっているため、Uniswap マーケットを悪用しようとする ERC-20 コントラクトを呼び出す可能性があります。 コントラクトの一部にunlocked変数を使うことで、(同じトランザクション内で)実行中に関数が呼び出されるのを防ぐことができます。

1 modifier lock() {
コピー

この関数はmodifier(opens in a new tab)で、通常の関数をラップして何らかの方法で挙動を変更する関数です。

1 require(unlocked == 1, 'UniswapV2: LOCKED');
2 unlocked = 0;
コピー

unlockedが 1 の場合は 0 に設定します。 すでに 0 の場合は呼び出しを元に戻して、失敗させます。

1 _;
コピー

modifier の_;は、(すべてのパラメータを含む)元の関数呼び出しです。 これは、関数が呼び出されたときにunlockedが 1 であり、実行中にunlockedの値が 0 になった場合のみ、関数呼び出しが発生することを意味します。

1 unlocked = 1;
2 }
コピー

main 関数に戻った後、ロックを解除します。

その他の 関数

1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
2 _reserve0 = reserve0;
3 _reserve1 = reserve1;
4 _blockTimestampLast = blockTimestampLast;
5 }
コピー

この関数が呼び出されると、取引所の現在の状態を返します。 Solidity の関数は、複数の値を返せることに(opens in a new tab)注意してください。

1 function _safeTransfer(address token, address to, uint value) private {
2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
コピー

この内部関数は、ERC20 トークン量を取引所から第三者に転送します。 SELECTORは、呼び出している関数がtransfer(address,uint)(上記の定義を参照)であることを指定しています。

トークン関数のインターフェイスのインポートを回避するため、いずれかのABI 関数(opens in a new tab)を使い、「手動」で呼び出しを作成します。

1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
2 }
コピー

ERC-20 転送コールが失敗を報告する例を下記に 2 つご紹介します。

  1. 元に戻す。 外部コントラクトへのコールを元に戻すと、ブール値はfalseを返します。
  2. 正常に終了したが、失敗を報告する。 この場合、戻り値のバッファの長さがゼロ以外で、ブール値としてデコードするとfalseになります。

いずれかの状態が発生した場合は、元に戻します。

イベント

1 event Mint(address indexed sender, uint amount0, uint amount1);
2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
コピー

この2つのイベントは、流動性プロバイダーが流動性を入金 (Mint)または、引き出した(Burn)場合に発行されます。 いずれの場合も、預け入れまたは引き出された token0 と token1 の量と、呼び出したアカウントのアイデンティティ(sender) は、イベントに含まれます。 引き出しの場合、イベントにはトークンを受け取るターゲット(to)も含みますが、送信者と同じとは限りません。

1 event Swap(
2 address indexed sender,
3 uint amount0In,
4 uint amount1In,
5 uint amount0Out,
6 uint amount1Out,
7 address indexed to
8 );
コピー

このイベントは、トレーダーがあるトークンを他のトレーダーとスワップしたときに発行されます。 ここでも、送信者と送信先が同じとは限りません。 各トークンは、取引所に送信されるか、取引所から受信します。

1 event Sync(uint112 reserve0, uint112 reserve1);
コピー

最後に、Syncは、トークンが追加または引き出されるたびに発行され、その理由に関係なく、最新のリザーブ情報(交換レート)を提供します。

setup 関数

これらの関数は、新しいペア取引所がセットアップされたときに 1 度だけ呼び出されます。

1 constructor() public {
2 factory = msg.sender;
3 }
コピー

コンストラクタは、ペアを作成したファクトリのアドレスを確実に追跡できるようにします。 この情報は、initializeと(ファクトリが存在する場合は)ファクトリフィーに必要となります。

1 // called once by the factory at time of deployment
2 function initialize(address _token0, address _token1) external {
3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
4 token0 = _token0;
5 token1 = _token1;
6 }
コピー

この関数では、ファクトリー(のみ)が、このペアが交換する 2 つの ERC-20 トークンを指定できます。

内部の update 関数

_update
1 // update reserves and, on the first call per block, price accumulators
2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
コピー

この関数は、トークンの入金や引き出しのたびに呼び出されます。

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

balance0 または balance1(uint256)が、uint112(-1) (=2^112-1)よりも大きい場合、(uint112 に変換された場合、オーバーフローで 0 に戻ってしまうため)オーバーフローを防止するために_update の続行が拒否されます。 通常のトークンは 10^18 単位に分割できるため、各取引所で各トークンが約 5.1*10^15 に制限されます。 今のところ、問題は発生していません。

1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);
2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
コピー

経過時間がゼロとなっていない場合は、このブロックで最初の交換トランザクションとなります。 その場合、コストアキュムレータをアップデートする必要があります。

1 // * never overflows, and + overflow is desired
2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
4 }
コピー

各コストアキュムレータは、最新のコスト(他のトークンのリザーブまたはこのトークンのリザーブ)に経過時間(秒)を掛けてアップデートされます。 平均価格を求めるには、2 つの時点の累積価格を読み、その時間差で割ります。 例えば、次の一連のイベントを想定してください。

イベントreserve0reserve1タイムスタンプ限界交換率(reserve1/reserve0)price0CumulativeLast
初期設定1,000.0001,000.0005,0001.0000
トレーダー A が token0 を 50 入金し、token1 を 47.619 戻す1,050.000952.3815,0200.90720
トレーダー B が token0 を 10 入金し、token1 を 8.984 戻す1,060.000943.3965,0300.89020+10*0.907 = 29.07
トレーダー C が token0 を 40 入金し、token1 を 34.305 戻す1,100.000909.0905,1000.82629.07+70*0.890 = 91.37
トレーダー D が token1 を 100 入金し、token0 を 109.01 戻す990.9901,009.0905,1101.01891.37+10*0.826 = 99.63
トレーダー E が token0 を 10 入金し、token1 を 10.079 戻す1,000.990999.0105,1500.99899.63+40*1.1018 = 143.702

タイムスタンプ 5,030 から 5,150 の間のToken0の平均価格を計算してみましょう。 price0Cumulativeの値の差は、143.702-29.07=114.632 です。 これは 2 分間(120 秒)の平均値です。 つまり、平均価格は 114.632/120 = 0.955 となります。

古いリザーブサイズを把握しておく必要があるのは、この価格計算のためです。

1 reserve0 = uint112(balance0);
2 reserve1 = uint112(balance1);
3 blockTimestampLast = blockTimestamp;
4 emit Sync(reserve0, reserve1);
5 }
コピー

最後に、グローバル変数をアップデートし、Syncイベントを発行します。

_mintFee
1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
コピー

Uniswap 2.0 で、トレーダーは、マーケットの利用料として 0.30%のフィーを払います。 フィーの大半は、流動性プロバイダーに支払われます(取引の 0.25%)。 残りの 0.05%は、流動性プロバイダーまたは、プロトコルフィーとしてファクトリで指定されたアドレスへ送られます。プロトコルフィーは、Uniswap の開発のために支払われます。

計算量(それに伴うガス代)を削減するため、このフィーは、トランザクションごとではなく、プールから流動性が追加または削除されるときだけ計算されます。

1 address feeTo = IUniswapV2Factory(factory).feeTo();
2 feeOn = feeTo != address(0);
コピー

上記コードのファクトリーのフィー送信先を読んでみましょう。 ゼロの場合、プロトコルフィーはかからず、フィーを計算する必要もありません。

1 uint _kLast = kLast; // gas savings
コピー

kLast状態変数はストレージに位置しているため、コントラクトへの異なる呼び出し間で値を持つことになります。 ストレージへのアクセスは、コントラクトへの関数呼び出しが終了したときにリリースされる揮発メモリへのアクセスよりも非常に高価となるため、内部変数を使ってガス代を節約します。

1 if (feeOn) {
2 if (_kLast != 0) {
コピー

流動性プロバイダーは、流動性トークンの価値が上昇するだけで取り分をもらえます。 一方、プロトコルフィーは、新しい流動性トークンをミントし、feeToアドレスに提供する必要があります。

1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
2 uint rootKLast = Math.sqrt(_kLast);
3 if (rootK > rootKLast) {
コピー

プロトコルフィーを徴収する新しい流動性がある場合の 平方根関数については、この記事の後半で解説します。

1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));
2 uint denominator = rootK.mul(5).add(rootKLast);
3 uint liquidity = numerator / denominator;
コピー

このフィーの複雑な計算方法については、ホワイトペーパー(opens in a new tab)の 5 ページ目で説明されています。 (流動性が実際に変化する前に、流動性が追加または削除されるたびにこの計算を実行するため、)kLastが計算された時間から現在までの間に流動性が追加または削除されていないことがわかります。そのため、reserve0 * reserve1の変更は、トランザクションフィーに起因する必要があります。 (流動性の追加または削除がなければ、reserve0 * reserve1を一定に保ちます)。

1 if (liquidity > 0) _mint(feeTo, liquidity);
2 }
3 }
コピー

UniswapV2ERC20._mint関数を利用して、追加の流動性トークンを作成し、feeToに割り当てます。

1 } else if (_kLast != 0) {
2 kLast = 0;
3 }
4 }
コピー

フィーがない場合、kLastがゼロでなければゼロに設定します。 このコントラクトが書かれたとき、ガス払い戻し機能(opens in a new tab)がありました。この機能は、コントラクトによって必要のないストレージをゼロにすることで、イーサリアム全体のサイズを縮小するよう促したものです。 この機能により、可能な場合はコードは払い戻しを受けます。

外部アクセス可能な関数

どのトランザクションまたはコントラクトでも、これらの関数を呼び出すことはできますが、ペリフェリーコントラクトから呼び出されるように設計されていることに注意してください。 直接呼び出すと、ペア取引所で不正行為はできませんが、誤って価値を失ってしまう可能性があります。

mint
1 // this low-level function should be called from a contract which performs important safety checks
2 function mint(address to) external lock returns (uint liquidity) {
コピー

この関数は、流動性プロバイダーが流動性をプールへ追加するときに呼び出されます。 報酬として追加の流動性トークンをミントします。 同じトランザクションで流動性を追加した後に呼び出すペリフェリーコントラクトから呼び出されます。(そうすることで、誰もが正当な所有者より前に、新しい流動性を要求するトランザクションの送信ができなくなります。)

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
コピー

これは、Solidity 関数が返す複数の戻り値を読み取る方法です。 最後に返された値のブロックのタイムスタンプは必要ないため、破棄します。

1 uint balance0 = IERC20(token0).balanceOf(address(this));
2 uint balance1 = IERC20(token1).balanceOf(address(this));
3 uint amount0 = balance0.sub(_reserve0);
4 uint amount1 = balance1.sub(_reserve1);
コピー

現在の残高を取得し、各トークンタイプで追加された値を確認します。

1 bool feeOn = _mintFee(_reserve0, _reserve1);
コピー

プロトコルフィーを計算して収集し、それに応じて流動性トークンをミントします。 _mintFeeのパラメータは古いリザーブ値であるため、フィーによるプールの変更にのみ基づいてフィーは正確に計算されます

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

これが最初の入金の場合は、MINIMUM_LIQUIDITYトークンを作成し、それらをロックするためにゼロアドレスに送信します。 このトークンは引き換えることができないため、プールが完全に空になることはありません(これによりゼロ除算を防ぎます) 。 MINIMUM_LIQUIDITYの値は、1000 です。これは、ETH が wei 単位に分割されるように、ほとんどの ERC-20 がトークンの 10 の 18 乗の単位に分割されることを考慮して、単一トークンの値の 10^-15 となっており、 高コストではありません。

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

裁定取引で価値の喪失を防ぎ、同等の価値を提供することが入金者の利益になるため、信頼することができます。 例えば、2 つのトークンの価値が同等であるものの、入金者がToken0の 4 倍のToken1を入金したとします。 トレーダーは、価値を抽出するために、ペア取引所がToken0の価値の方が高いと考えている事実を利用することができます。

イベントreserve0reserve1reserve0 * reserve1プールの値(reserve0 + reserve1)
初期設定83225640
トレーダーはToken0トークンを 8 入金し、Token1を 16 戻す161625632

上記のように、トレーダーは、プールの価値の減少から追加の 8 トークンを獲得し、それを所有する入金者に損害を与えました。

1 } else {
2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
コピー

その後の入金では、2 つのアセットの交換レートがすでにわかっており、流動性プロバイダーが両方のアセットで同等の価値を提供することが期待できます。 そうしなかった場合、罰として、提供された流動性より低い価値の流動性トークンが与えられます。

初回の入金であれ、その後の入金であれ、提供する流動性トークンの数は、 reserve0*reserve1の変化の平方根に等しくなり、(「罰金」の対象となる両方のトークンの種類で同等の価値ではない入金をしないかぎり) 流動性トークンの価値は変化しません。 もう 1 つ、同等の価値を持つ 2 つのトークンの例をご紹介します。3 つが良い入金で、1 つが(1 種類のトークンのみを入金するため、流動性トークンは生成されない)悪い入金です。

イベントreserve0reserve1reserve0 * reserve1プール値(reserve0 + reserve1)この入金でミントされた流動性トークン流動性トークンの合計各流動性トークンの価値
初期設定8.0008.0006416.000882.000
各種 4 つずつ入金12.00012.00014424.0004122.000
各種 2 つずつ入金14.00014.00019628.0002142.000
等しくない値を入金18.00014.00025232.000014~2.286
裁定取引後~15.874~15.874252~31.748014~2.267
1 }
2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
3 _mint(to, liquidity);
コピー

UniswapV2ERC20._mint関数を使用して、追加の流動性トークンを実際に作成し、正しいアカウントに付与します。

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

状態変数(reserve0reserve1、必要に応じてkLast)をアップデートし、必要であれば適切なイベントを発行します。

burn
1 // this low-level function should be called from a contract which performs important safety checks
2 function burn(address to) external lock returns (uint amount0, uint amount1) {
コピー

この関数は、流動性が引き出され、適切な流動性トークンをバーンする必要がある場合に呼び出されます。 また、ペリフェリーコントラクトからも呼び出されるようになっています。

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
2 address _token0 = token0; // gas savings
3 address _token1 = token1; // gas savings
4 uint balance0 = IERC20(_token0).balanceOf(address(this));
5 uint balance1 = IERC20(_token1).balanceOf(address(this));
6 uint liquidity = balanceOf[address(this)];
コピー

ペリフェリーコントラクトは、呼び出し前に、このコントラクトにバーンされる流動性を送信します。 これにより、バーンされる流動性の量を把握でき、確実にバーンすることができます。

1 bool feeOn = _mintFee(_reserve0, _reserve1);
2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
コピー

流動性プロバイダーは、両方のトークンで同等の価値を受け取り、 交換レートを変更することもありません。

1 _burn(address(this), liquidity);
2 _safeTransfer(_token0, to, amount0);
3 _safeTransfer(_token1, to, amount1);
4 balance0 = IERC20(_token0).balanceOf(address(this));
5 balance1 = IERC20(_token1).balanceOf(address(this));
6
7 _update(balance0, balance1, _reserve0, _reserve1);
8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
9 emit Burn(msg.sender, amount0, amount1, to);
10 }
11
すべて表示
コピー

burn関数の残りの部分は、mint関数が逆(ミラーイメージ)になったものです。

swap
1 // this low-level function should be called from a contract which performs important safety checks
2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
コピー

この関数は、 ペリフェリーコントラクトからも呼び出されることになっています。

1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
4
5 uint balance0;
6 uint balance1;
7 { // scope for _token{0,1}, avoids stack too deep errors
コピー

ローカル変数は、メモリに保存するか、少ない場合は直接スタック上に保存できます。 数を制限できれば、ガスの使用量が少ないスタックを使用することができます。 詳細については、正式なイーサリアム仕様であるイエロー ペーパー(opens in a new tab)の 26 ページ目に記載されている等式 298 をご覧ください。

1 address _token0 = token0;
2 address _token1 = token1;
3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
コピー

この送金は、すべての条件が満たされていることを確認する前に行っているため、楽観的です。 これはイーサリアムでは、問題ありません。呼び出しの後半で、条件を満たしていなければ、作成されたすべての変更が戻されるからです。

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

リクエストされた場合、受信者にスワップについて通知します。

1 balance0 = IERC20(_token0).balanceOf(address(this));
2 balance1 = IERC20(_token1).balanceOf(address(this));
3 }
コピー

現在の残高を取得します。 ペリフェリーコントラクトは、スワップを呼び出す前にトークンを送信するため、 コントラクトで不正行為がされていないことを簡単に確認できるようになります。このチェックは、ペリフェリーコントラクト以外のエンティティから呼び出される可能性があるため、コアコントラクトで実行しなければなりません

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

これは、スワップによる損失を確実に防ぐサニティチェックです。 スワップによってreserve0*reserve1が減少することはありません。 これは、スワップで 0.3%のフィーが送信されることを保証する場所でもあります。K 値のサニティチェックをする前に、両方の残高に 1000 を掛け、3 を掛けた金額を引きます。これは現在のリザーブの K 値と比較する前に、残高から 0.3%(3/1000 = 0.003 = 0.3%)が差し引かれることを意味します。

1 }
2
3 _update(balance0, balance1, _reserve0, _reserve1);
4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
5 }
コピー

reserve0reserve1をアップデートし、必要に応じて価格アキュムレータとタイムスタンプもアップデートして、イベントを発行します。

sync または skim

実際の残高とペア取引所が持っているとされるリザーブとが一致しない可能性があります。 コントラクトの同意なしにトークンを引き出すことはできませんが、入金は可能です。 アカウントは、 mintまたはswapどちらかを呼び出すことなくトークンを取引所に送信することができます。

このケースでは次の 2 つの解決策があります。

  • syncでリザーブを現在の残高にアップデートします。
  • skimで余分な金額を引き出します。 誰がトークンを入金したか分からないため、どのアカウントでもskimの呼び出しが許可されていることに注意してください。 この情報はイベント内で発行されますが、イベントはブロックチェーンからはアクセスできません。
1 // force balances to match reserves
2 function skim(address to) external lock {
3 address _token0 = token0; // gas savings
4 address _token1 = token1; // gas savings
5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
7 }
8
9
10
11 // force reserves to match balances
12 function sync() external lock {
13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
14 }
15}
すべて表示
コピー

UniswapV2Factory.sol

このコントラクト(opens in a new tab)では、ペア取引所を作ります。

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Factory.sol';
4import './UniswapV2Pair.sol';
5
6contract UniswapV2Factory is IUniswapV2Factory {
7 address public feeTo;
8 address public feeToSetter;
コピー

これらの状態変数はプロトコルフィーを実装するために必要です。詳細は、ホワイトペーパー(opens in a new tab)の 5 ページ目をご覧ください。 feeToは、プロトコルフィーのための流動性トークンを蓄積するアドレスで、feeToSetterは、feeToを別のアドレスに変更できるアドレスです。

1 mapping(address => mapping(address => address)) public getPair;
2 address[] public allPairs;
コピー

これらの変数は、ペアと 2 種類のトークン間の取引を追跡します。

最初のgetPairは、交換する 2 つの ERC-20 トークンに基づいてペア取引所コントラクトを識別するマッピングです。 ERC-20 トークンは、それらを実装するコントラクトのアドレスによって特定されるため、キーと値はすべてアドレスです。 tokenAからtokenBに交換するペア取引所のアドレスを取得するために、 getPair[<tokenA address>][<tokenB address>]を使います(逆の場合も同様) 。

2 つ目の変数allPairsは、このファクトリーによって作られたすべてのペア取引所のアドレスを含む配列です。 イーサリアムでは、マッピング内容のイテレートやすべてのキーのリストを取得することはできないので、この変数が、ファクトリーが管理する取引所を知る唯一の方法となります。

注: マッピングのすべてのキーをイテレートできない理由は、コントラクトのデータストレージが高額であるため、使用量が少ないほど良く、変更頻度が少ないほど良いからです。 イテレーションをサポートするマッピング(opens in a new tab)の作成もできますが、キーのリストのために追加のストレージが必要です。 通常、ほとんどのアプリケーションで必要ありません。

1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);
コピー

このイベントは、新しいペア取引所が作成されたときに発行されます。 トークンのアドレス、ペア取引所のアドレス、ファクトリーによって管理されている全取引所の数が含まれます。

1 constructor(address _feeToSetter) public {
2 feeToSetter = _feeToSetter;
3 }
コピー

コンストラクタが行う唯一のことは、feeToSetterを指定することです。 ファクトリーはフィーなしで開始し、変更できるのはfeeSetterのみとなります。

1 function allPairsLength() external view returns (uint) {
2 return allPairs.length;
3 }
コピー

この関数は、取引ペアの数を返します。

1 function createPair(address tokenA, address tokenB) external returns (address pair) {
コピー

これはファクトリーのメイン関数で、ERC-20 トークン間のペア取引所を作成します。 誰でもこの関数を呼び出せることに注意してください。 新しいペア取引所を作成するのに Uniswap からの許可は必要ありません。

1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
2 (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
コピー

新しい取引所のアドレスを決定的にできるよう、事前にオフチェーンで計算できるようになっており (これはレイヤー 2 トランザクションで役立ちます) 、 そうするためには、受け取った順序に関わらずトークンアドレスの順序に一貫性を持たせる必要があります。並べ替えているのは、こうした理由からです。

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

大きな流動性プールは価格がより安定するため、小さなものよりも優れています。 トークンのペアに対して流動性プールを 1 つ以上持たないようにします。 すでに取引所が存在する場合、同じペアに対して別の取引所を作る必要がないからです。

1 bytes memory bytecode = type(UniswapV2Pair).creationCode;
コピー

新しいコントラクトを作成するには、作成するコードが必要です(コンストラクタ関数と、実際のコントラクトの EVM バイトコードをメモリに書き込むコードの両方) 。 通常、Solidity でaddr = new <name of contract>(<constructor parameters>)を使うだけで、コンパイラは全ての処理を実行しますが、決定的なコントラクトアドレスを取得するには、CREATE2 オペコード(opens in a new tab)を使用する必要があります。 このコードが書かれた時点では、このオペコードは Solidity ではサポートされていなかったため、コードを手動で実行する必要がありましたが、 現在 Solidity は CREATE2 をサポートしている(opens in a new tab)ため、問題ありません。

1 bytes32 salt = keccak256(abi.encodePacked(token0, token1));
2 assembly {
3 pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
4 }
コピー

オペコードが Solidity によってサポートされていなかった時は、 インラインアセンブリ(opens in a new tab)を使って呼び出していました。

1 IUniswapV2Pair(pair).initialize(token0, token1);
コピー

initialize関数を呼び出して、新しい取引所に交換する 2 つのトークンの種類を伝えます。

1 getPair[token0][token1] = pair;
2 getPair[token1][token0] = pair; // populate mapping in the reverse direction
3 allPairs.push(pair);
4 emit PairCreated(token0, token1, pair, allPairs.length);
5 }
コピー

状態変数に新しいペアの情報を保存し、イベントを発行することで、新しいペア取引所を全世界に周知します。

1 function setFeeTo(address _feeTo) external {
2 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
3 feeTo = _feeTo;
4 }
5
6 function setFeeToSetter(address _feeToSetter) external {
7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
8 feeToSetter = _feeToSetter;
9 }
10}
すべて表示
コピー

これらの 2 つの関数は、 feeSetterにフィーの受取人を(必要に応じて)制御し、 feeSetterを新しいアドレスに変更できます。

UniswapV2ERC20.sol

このコントラクト(opens in a new tab)は、ERC-20 流動性トークンを実装しています。 OpenZeppelin ERC-20 コントラクトと類似しているので、異なる箇所であるpermitの機能についてのみ説明します。

イーサリアムでのトランザクションでは、現実のお金に相当するイーサ(ETH)のコストがかかります。 ETH ではない ERC-20 トークンを持っていても、トランザクションを送信することができないため役に立ちません。 この問題を回避する解決策の 1 つが、メタトランザクション(opens in a new tab)です。 トークンの所有者がトランザクションに署名することで、第三者がチェーンからトークンを引き出したり、インターネットを使って受信者へ送信したりすることができます。 ETH を持っている受信者が、所有者の代わりに許可を送信します 。

1 bytes32 public DOMAIN_SEPARATOR;
2 // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
3 bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
コピー

このハッシュは、 トランザクションタイプのための識別子(opens in a new tab)です。 ここでサポートされているのは、これらのパラメータを持ったPermitだけです。

1 mapping(address => uint) public nonces;
コピー

受信者が電子署名を偽造することはできませんが、 同じトランザクションを 2 回送信することは簡単にできてしまいます(これはリプレイ攻撃(opens in a new tab)の形です)。 これを防ぐために、ノンス(opens in a new tab)を使います。 新しいPermitのノンスが、最後に使用されたノンスに 1 を足した数でない場合は無効と見なします。

1 constructor() public {
2 uint chainId;
3 assembly {
4 chainId := chainid
5 }
コピー

これはチェーン識別子(opens in a new tab)を取得するためのコードで、 Yul(opens in a new tab)と呼ばれる EVM アセンブリ方言を使用します。 Yul の現在のバージョンでは、chainidではなくchainid()を使用する必要があることに注意してください。

1 DOMAIN_SEPARATOR = keccak256(
2 abi.encode(
3 keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
4 keccak256(bytes(name)),
5 keccak256(bytes('1')),
6 chainId,
7 address(this)
8 )
9 );
10 }
すべて表示
コピー

EIP-712 のドメインセパレータ(opens in a new tab)を計算します。

1 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
コピー

これは、パーミッション(permission)を実装する関数です。 パラメータとして関連するフィールドと署名(opens in a new tab)のために 3 つのスカラー値 (v、r、s) を受け取ります。

1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
コピー

期限が過ぎると、トランザクションは受け付けません。

1 bytes32 digest = keccak256(
2 abi.encodePacked(
3 '\x19\x01',
4 DOMAIN_SEPARATOR,
5 keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
6 )
7 );
コピー

abi.encodePacked(...)は、受信を予定しているメッセージです。 私たちはノンスが何であるべきかを認識しているので、パラメータとして取得する必要はありません。

イーサリアム署名アルゴリズムは、256 ビットで署名することになっているため、 keccak256ハッシュ関数を使用します。

1 address recoveredAddress = ecrecover(digest, v, r, s);
コピー

ダイジェストと署名から、ecrecover(opens in a new tab)を使用して署名したアドレスを取得できます。

1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
2 _approve(owner, spender, value);
3 }
4
コピー

すべて問題なければ、これをERC-20 承認(opens in a new tab)として処理します。

ペリフェリーコントラクト

ペリフェリーコントラクトは、Uniswap の API(アプリケーションプログラムインターフェイス) です。 他のコントラクトや分散型アプリケーションから、外部呼び出しで利用可能です。 コアコントラクトは直接呼び出すことができますが、より複雑で間違えると価値を失ってしまう可能性があります。 コアコントラクトには、他者に対する不正行為がないことを確認するためのテストのみが含まれており、サニティチェックは含まれていません。 ペリフェリーコントラクトには含まれており、必要に応じてアップデートすることができます。

UniswapV2Router01.sol

このコントラクト(opens in a new tab)には問題があるため、使用してはいけません(opens in a new tab)。 幸いなことに、ペリフェリーコントラクトはステートレスでアセットを保有していないため、非推奨にするのは簡単です。代わりに、UniswapV2Router02を使用するよう提案します。

UniswapV2Router02.sol

通常はこのコントラクト(opens in a new tab)を通して Uniswap を使用します。 こちら(opens in a new tab)で使用方法を確認できます。

1pragma solidity =0.6.6;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
5
6import './interfaces/IUniswapV2Router02.sol';
7import './libraries/UniswapV2Library.sol';
8import './libraries/SafeMath.sol';
9import './interfaces/IERC20.sol';
10import './interfaces/IWETH.sol';
すべて表示
コピー

これらの大半はすでに目にしていると思います。そうでなくても、非常にわかりやすいものです。 唯一の例外は、IWETH.solです。 Uniswap v2 は、どの ERC-20 トークンのペアでも交換可能ですが、イーサ(ETH)自体は ERC-20 トークンではありません。 ETH は、標準より前から存在しており、独自のメカニズムによって送金されます。 ERC-20 トークンが適用されるコントラクトで ETH を利用できるようにするため、ラップドイーサ(WETH)(opens in a new tab)が考案されました。 このコントラクトに ETH を送ると、同じ量の WETH がミントされます。 WETH をバーンすると、ETH を取り戻すことができます。

1contract UniswapV2Router02 is IUniswapV2Router02 {
2 using SafeMath for uint;
3
4 address public immutable override factory;
5 address public immutable override WETH;
コピー

ルーターは、どのファクトリーを使用するか、WETH を必要とするトランザクションについてはどの WETH コントラクトを使用するかを認識する必要があります。 これらの値は、不変(opens in a new tab)であり、コンストラクタでのみ設定できます。 これにより、ユーザーは、不正なコントラクトに変更されることはないという確信を得ることができます。

1 modifier ensure(uint deadline) {
2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
3 _;
4 }
コピー

この修飾子は、時間制限のあるトランザクション(「可能なとき Y 時の前に X を実行」) が、時間制限後に発生しないようにするものです。

1 constructor(address _factory, address _WETH) public {
2 factory = _factory;
3 WETH = _WETH;
4 }
コピー

このコンストラクタは、不変な状態変数を設定しています。

1 receive() external payable {
2 assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
3 }
コピー

この関数は、トークンを WETH コントラクトから ETH に引き換える際に呼び出されます。 私たちが使用している WETH コントラクトだけが、これを行うことを許可されています。

流動性の追加

これらの関数は、ペア取引所にトークンを追加し、流動性プールを増加させます。

1
2 // **** ADD LIQUIDITY ****
3 function _addLiquidity(
コピー

この関数は、ペア取引所に入金する A トークンと B トークンの量を計算するために使用されます。

1 address tokenA,
2 address tokenB,
コピー

これらは、ERC-20 トークンコントラクトのアドレスです。

1 uint amountADesired,
2 uint amountBDesired,
コピー

これらは流動性プロバイダーが入金を希望する量であり、 A と B が入金できる最大量でもあります。

1 uint amountAMin,
2 uint amountBMin
コピー

これらは受け入れ可能な最低入金量です。 この量以上でトランザクションを実行できない場合は、元に戻します。 この機能を使用しない場合は、ゼロを指定してください。

流動性プロバイダーは通常、トランザクションを現在の交換レートに近いレートに制限したいため、最小値を指定します。 交換レートの変動が大きすぎると、本来の価値を変更するニュースを意味する可能性があり、こうした状況下では、流動性プロバイダーは手動で対応方法を決定したいと考えます。

例えば、交換レートが 1 対 1 で、流動性プロバイダーが次の値を指定するケースを想像してください。

パラメータ
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

交換レートが 0.9 から 1.25 の間である限り、トランザクションは行われます。 交換レートがこの範囲から外れると、トランザクションはキャンセルされます。

この予防措置の理由は、トランザクションが即時ではなく、送信すると最終的にバリデータがそれらをブロックに含めるためです (ガス価格が非常に低い場合を除きます。その場合は、同じノンスでより高いガス価格で別のトランザクションを送信して上書きする必要があります) 。 送信およびトランザクションを含める処理の間で何が起こるかを制御することはできません。

1 ) internal virtual returns (uint amountA, uint amountB) {
コピー

この関数は、リザーブ間の現在の比率と同等の比率を持てるよう、流動性プロバイダーが入金すべき量を返します。

1 // create the pair if it doesn't exist yet
2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);
4 }
コピー

このトークンペアの取引所がまだない場合は、作成します。

1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
コピー

ペアの現在のリザーブを取得します。

1 if (reserveA == 0 && reserveB == 0) {
2 (amountA, amountB) = (amountADesired, amountBDesired);
コピー

現在のリザーブが空の場合、新しいペア取引所であることがわかります。 流動性プロバイダーが提供したい量とまったく同じ量を入金しなければなりません。

1 } else {
2 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
コピー

量を予想する必要がある場合は、この関数(opens in a new tab)を使って最適量を取得します。 現在のリザーブと同じ比率が必要です。

1 if (amountBOptimal <= amountBDesired) {
2 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
3 (amountA, amountB) = (amountADesired, amountBOptimal);
コピー

amountBOptimalが、流動性プロバイダーが希望する入金の量より小さい場合、つまり現在のトークン B の価値が流動性プロバイダーが考えているものよりも高くなるため、量を減らす必要があります。

1 } else {
2 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
3 assert(amountAOptimal <= amountADesired);
4 require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
5 (amountA, amountB) = (amountAOptimal, amountBDesired);
コピー

B の最適量が B の希望量よりも大きい場合、現在の B トークンは流動性入金者が考えるよりも価値が低いということになり、量を増やす必要がありますが、 希望量は最大値となっているため、これはできません。 代わりに、B トークンの希望量に対する A トークンの最適数を計算します。

まとめると、このようなグラフになります。 A トークンを 1000(青線)と B トークンを 1000(赤線)を入金しようとしたとします。 X 軸は交換レート、A/B です。 x=1 の場合、両者は同じ価値であり、それぞれ 1000 ずつ入金しています。 x=2 の場合、A は B の 2 倍の価値があるので(A トークン 1 つにつき、B トークン 2 つが得られる) 、B トークン 1000 を入金しても、A トークンは 500 にしかなりません。 x=0.5 の場合は逆に、A トークンが 1000、B トークンが 500 となります。

グラフ

(UniswapV2Pair::mint(opens in a new tab)を使用して、)直接コアコントラクトへ流動性を入金することもできますが、コアコントラクトはそれ自体に不正がないかだけを確認するものであるため、トランザクションの送信から実行までの間に交換レートが変わった場合、価値を喪失するリスクがあります。 ペリフェリーコントラクトを使った場合、即時に入金すべき量を計算し入金します。これにより、交換レートは変わらず何も失うことはありません。

1 function addLiquidity(
2 address tokenA,
3 address tokenB,
4 uint amountADesired,
5 uint amountBDesired,
6 uint amountAMin,
7 uint amountBMin,
8 address to,
9 uint deadline
すべて表示
コピー

この関数は、流動性を入金するトランザクションによって呼び出されます。 ほとんどのパラメータは上記の_addLiquidityと同じですが、下記に 2 つの例外をご紹介します。

. toはミントされた新しい流動性トークンを取得するアドレスで、流動性プロバイダーのプールの取り分を示します。 deadlineは、トランザクションの制限時間です。

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

実際に入金する量を計算し、流動性プールのアドレスを見つけます。 ガスを節約するために、ファクトリーに依頼するのではなく、ライブラリ関数pairForを使います (ライブラリ内の下記を参照) 。

1 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
2 TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
コピー

適正量のトークンをユーザーからペア取引所に送信します。

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 }
コピー

代わりに、プールの部分的な所有権のために流動性トークンをtoアドレスに提供します。 コアコントラクトのmint関数は、(最後に流動性が変更されたときと比較して)追加のトークンがいくつあるかを確認し、それに応じて流動性をミントします。

1 function addLiquidityETH(
2 address token,
3 uint amountTokenDesired,
コピー

流動性プロバイダーが、トークン/ETH のペア取引所に流動性を提供したい場合は、いくつかの相違点があります。 コントラクトは、ラッピングされた ETH で流動性プロバイダーを扱います。 ユーザーはトランザクション時に ETH を送金するだけで良いため(量は msg.valueで確認可能)、入金したい ETH の数を指定する必要はありません。

1 uint amountTokenMin,
2 uint amountETHMin,
3 address to,
4 uint deadline
5 ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
6 (amountToken, amountETH) = _addLiquidity(
7 token,
8 WETH,
9 amountTokenDesired,
10 msg.value,
11 amountTokenMin,
12 amountETHMin
13 );
14 address pair = UniswapV2Library.pairFor(factory, token, WETH);
15 TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
16 IWETH(WETH).deposit{value: amountETH}();
17 assert(IWETH(WETH).transfer(pair, amountETH));
すべて表示
コピー

ETH を入金するには、コントラクトはまずそれを WETH にラップし、次に WETH をペアに送金します。 送金がassertでラップされていることに注意してください。 つまり、送金が失敗すると、コントラクトの呼び出しも失敗するので、ラッピングが実際に発生しないということです。

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 // refund dust eth, if any
3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
4 }
コピー

ユーザーは、ETH をすでに送金しているので、(一方のトークンがユーザーが考えるより価値が低いため) 余りが出てしまう場合は、払い戻しを行う必要があります。

流動性の削除

これらの関数は流動性を削除し、流動性プロバイダーに返却します。

1 // **** REMOVE LIQUIDITY ****
2 function removeLiquidity(
3 address tokenA,
4 address tokenB,
5 uint liquidity,
6 uint amountAMin,
7 uint amountBMin,
8 address to,
9 uint deadline
10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
すべて表示
コピー

これは、流動性を削除する最もシンプルなケースです。 各トークンには、流動性プロバイダーが受け入れに同意する最低量があり、期限までに行う必要があります。

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

コアコントラクトのburn関数は、ユーザーへトークンを返却する処理をします。

1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
コピー

関数が複数の値を返した時、私たちが必要なのはその一部なので、必要な値のみを取得する方法はこうなります。 値を読み取ってそれをまったく使用しないよりも、ガス代を抑えることができます。

1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
コピー

コアコントラクトが返す量(下位アドレスのトークンが最初)から (tokenAtokenBに応じて)ユーザが期待する量に置き換えます。

1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
3 }
コピー

最初に送金して、その正当性を確認することは問題ありません。正当でなければ、すべての状態変更は元に戻されるからです。

1 function removeLiquidityETH(
2 address token,
3 uint liquidity,
4 uint amountTokenMin,
5 uint amountETHMin,
6 address to,
7 uint deadline
8 ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
9 (amountToken, amountETH) = removeLiquidity(
10 token,
11 WETH,
12 liquidity,
13 amountTokenMin,
14 amountETHMin,
15 address(this),
16 deadline
17 );
18 TransferHelper.safeTransfer(token, to, amountToken);
19 IWETH(WETH).withdraw(amountETH);
20 TransferHelper.safeTransferETH(to, amountETH);
21 }
すべて表示
コピー

WETH トークンを受け取り、ETH と交換して流動性プロバイダーに戻す点を除いて、ETH の流動性を削除する方法はほぼ同じです。

1 function removeLiquidityWithPermit(
2 address tokenA,
3 address tokenB,
4 uint liquidity,
5 uint amountAMin,
6 uint amountBMin,
7 address to,
8 uint deadline,
9 bool approveMax, uint8 v, bytes32 r, bytes32 s
10 ) external virtual override returns (uint amountA, uint amountB) {
11 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
12 uint value = approveMax ? uint(-1) : liquidity;
13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
14 (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
15 }
16
17
18 function removeLiquidityETHWithPermit(
19 address token,
20 uint liquidity,
21 uint amountTokenMin,
22 uint amountETHMin,
23 address to,
24 uint deadline,
25 bool approveMax, uint8 v, bytes32 r, bytes32 s
26 ) external virtual override returns (uint amountToken, uint amountETH) {
27 address pair = UniswapV2Library.pairFor(factory, token, WETH);
28 uint value = approveMax ? uint(-1) : liquidity;
29 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
30 (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
31 }
すべて表示
コピー

これらの関数はメタトランザクションをリレーし、許可メカニズムを使用して、イーサ(ETH)を持たないユーザーがプールから引き出せるようにします。

1
2 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****
3 function removeLiquidityETHSupportingFeeOnTransferTokens(
4 address token,
5 uint liquidity,
6 uint amountTokenMin,
7 uint amountETHMin,
8 address to,
9 uint deadline
10 ) public virtual override ensure(deadline) returns (uint amountETH) {
11 (, amountETH) = removeLiquidity(
12 token,
13 WETH,
14 liquidity,
15 amountTokenMin,
16 amountETHMin,
17 address(this),
18 deadline
19 );
20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
21 IWETH(WETH).withdraw(amountETH);
22 TransferHelper.safeTransferETH(to, amountETH);
23 }
24
すべて表示
コピー

この関数は、送金フィーまたはストレージフィーを持つトークンに使用できます。 トークンに当該フィーがある場合、 removeLiquidity関数では、返金されるトークンの量を把握することができないため、最初に引き出してから残高を取得する必要があります。

1
2
3 function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
4 address token,
5 uint liquidity,
6 uint amountTokenMin,
7 uint amountETHMin,
8 address to,
9 uint deadline,
10 bool approveMax, uint8 v, bytes32 r, bytes32 s
11 ) external virtual override returns (uint amountETH) {
12 address pair = UniswapV2Library.pairFor(factory, token, WETH);
13 uint value = approveMax ? uint(-1) : liquidity;
14 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
15 amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
16 token, liquidity, amountTokenMin, amountETHMin, to, deadline
17 );
18 }
すべて表示
コピー

最後の関数は、ストレージフィーとメタトランザクションを結び付けています。

取引

1 // **** SWAP ****
2 // requires the initial amount to have already been sent to the first pair
3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
コピー

この関数は、トレーダーへ公開される関数で必要な内部処理を実行します。

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

この記事を執筆している時点で、388,160 種の ERC-20 トークン(opens in a new tab)が存在しています。 各トークンのペアごとにペア取引所がある場合、1500 億を超えるペア取引所が存在することになります。 現時点では、チェーン全体で、ERC-20 トークンのアカウント数の 0.1%しか存在していません(opens in a new tab)。 代わりに、スワップ関数はパスの概念をサポートしています。 トレーダーは、A を B に、B を C に、C を D に交換できるため、A-D ペアを直接交換する必要はありません。

これらのマーケットの価格は同期する傾向にあります。同期していないと、裁定取引の機会が生じてしまうからです。 例えば、3 つのトークン A、B、C で、各ペアに 1 つずつ合計 3 つのペア取引所があるとします。

  1. 初期状態
  2. トレーダーは、A トークンを 24.695 売り、B トークンを 25.305 得ます。
  3. そのトレーダーは、B トークンを 24.695 売り、C トークンを 25.305 得ます。B トークン約 0.61 を利益として保持します。
  4. そして、そのトレーダーは、C トークンを 24.695 売って、A トークンを 25.305 得ます。C トークンの約 0.61 を利益として保持します。 そのトレーダーはまた、余分に A トークンを 0.61 持っています(トレーダーが最終的に得た 25.305 から、元の投資の 24.695 を差し引いたもの) 。
ステップA-B 取引所B-C 取引所A-C 取引所
1A:1000 B:1050 A/B=1.05B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.05
2A:1024.695 B:1024.695 A/B=1B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.05
3A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1050 C:1000 C/A=1.05
4A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1024.695 C:1024.695 C/A=1
1 (address input, address output) = (path[i], path[i + 1]);
2 (address token0,) = UniswapV2Library.sortTokens(input, output);
3 uint amountOut = amounts[i + 1];
コピー

現在扱っているペアを取得し、(ペアで使用するために)ソートし、期待される出力量を取得します。

1 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
コピー

期待される出力量を取得し、ペア取引所が期待する方法でソートされます。

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

最後の交換である場合、 交換するために受け取ったトークンを送信先に送ります。 そうでない場合は、次のペア取引所に送ります。

1
2 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
3 amount0Out, amount1Out, to, new bytes(0)
4 );
5 }
6 }
コピー

実際に、ペア取引所を呼び出してトークンをスワップします。 取引所について伝えるコールバックは必要ないため、フィールドにバイトは送信しません。

1 function swapExactTokensForTokens(
コピー

この関数は、トークンを別のトークンにスワップする際にトレーダーが直接使用します。

1 uint amountIn,
2 uint amountOutMin,
3 address[] calldata path,
コピー

このパラメータには、ERC-20 コントラクトのアドレスが含まれています。 上記で説明したように、所有しているアセットから希望するアセットを得るために、いくつかのペア取引所を経由しなければならないため、配列になっています。

Solidity の関数パラメータは、memoryまたはcalldataのいずれかに格納できます。 関数がコントラクトへのエントリポイントである場合、ユーザーによって(トランザクションを使用して)直接別のコントラクトから呼び出されます。 その後、calldata から直接パラメータの値を取ることができます。 上記_swapのように、関数が内部で呼び出されている場合、パラメータはmemoryに保存する必要があります。 呼び出されたコントラクトの観点から、 calldataは読み取り専用です。

uintaddressなどのスカラー型では、コンパイラがストレージの選択をしますが、より長くてより高価な配列では、使用するストレージタイプを指定します。

1 address to,
2 uint deadline
3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
コピー

返り値は、常にメモリ内で返されます。

1 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
2 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
コピー

各スワップで購入される量を計算します。 結果的にトレーダーが受け入れる最小値を下回った場合は、トランザクションを取り消します。

1 TransferHelper.safeTransferFrom(
2 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
3 );
4 _swap(amounts, path, to);
5 }
コピー

最後に、開始時の ERC-20 トークンを最初のペア取引所のアカウントに送金し、_swapを呼び出します。 これは、すべて同じトランザクション内で発生しているため、ペア取引所は予期しないトークンがこの送金の一部にあることを認識しています。

1 function swapTokensForExactTokens(
2 uint amountOut,
3 uint amountInMax,
4 address[] calldata path,
5 address to,
6 uint deadline
7 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
8 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
9 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
10 TransferHelper.safeTransferFrom(
11 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
12 );
13 _swap(amounts, path, to);
14 }
すべて表示
コピー

前にある関数swapTokensForTokensでは、トレーダーが与える入力トークンの正確な数と、トレーダーが受け取りたい出力トークンの最小数を指定することができます。 この関数では、リバーススワップを行います。トレーダーが希望する出力トークンの数と、支払いを希望する入力トークンの最大数を指定できます。

どちらの場合も、トレーダーは最初に、このペリフェリーコントラクトに送金できるようにアローワンスを与えなければなりません。

1 function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
2 external
3 virtual
4 override
5 payable
6 ensure(deadline)
7 returns (uint[] memory amounts)
8 {
9 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
10 amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
11 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
12 IWETH(WETH).deposit{value: amounts[0]}();
13 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
14 _swap(amounts, path, to);
15 }
16
17
18 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
19 external
20 virtual
21 override
22 ensure(deadline)
23 returns (uint[] memory amounts)
24 {
25 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
26 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
27 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
28 TransferHelper.safeTransferFrom(
29 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
30 );
31 _swap(amounts, path, address(this));
32 IWETH(WETH).withdraw(amounts[amounts.length - 1]);
33 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
34 }
35
36
37
38 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
39 external
40 virtual
41 override
42 ensure(deadline)
43 returns (uint[] memory amounts)
44 {
45 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
46 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
47 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
48 TransferHelper.safeTransferFrom(
49 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
50 );
51 _swap(amounts, path, address(this));
52 IWETH(WETH).withdraw(amounts[amounts.length - 1]);
53 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
54 }
55
56
57 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
58 external
59 virtual
60 override
61 payable
62 ensure(deadline)
63 returns (uint[] memory amounts)
64 {
65 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
66 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
67 require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
68 IWETH(WETH).deposit{value: amounts[0]}();
69 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
70 _swap(amounts, path, to);
71 // refund dust eth, if any
72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
73 }
すべて表示
コピー

これらの 4 つの変数はすべて、ETH とトークン間の取引に関連しています。 唯一の違いは、トレーダーから ETH を受け取り WETH をミントするために使うか、パスの最終取引所から WETH を受け取り、それをバーンして残った ETH をトレーダーに送り返すかです。

1 // **** SWAP (supporting fee-on-transfer tokens) ****
2 // requires the initial amount to have already been sent to the first pair
3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
コピー

これは、送金フィーやストレージフィーがあるトークンをスワップするための内部関数です(詳細は、問題点(issue)(opens in a new tab)をご覧ください) 。

1 for (uint i; i < path.length - 1; i++) {
2 (address input, address output) = (path[i], path[i + 1]);
3 (address token0,) = UniswapV2Library.sortTokens(input, output);
4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
5 uint amountInput;
6 uint amountOutput;
7 { // scope to avoid stack too deep errors
8 (uint reserve0, uint reserve1,) = pair.getReserves();
9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
すべて表示
コピー

送金フィーがあるため、 各送金で得られるトークン量を知らせてくれるgetAmountsOut関数に依存することができません(元の_swapを呼び出す前に行う方法) 。 代わりに、最初に送金し、戻ったトークンの数を確認する必要があります。

注: 理論的には、_swapの代わりにこの関数を使うことができますが、特定のケースにおいては、ガス代が高くつくことになります(例えば、 必要最低量に満たないため、最終的に送金処理が元に戻された場合) 。 送金フィートークンは極めてレアなため、受け入れる必要はありますが、少なくとも 1 つのスワップを経由すると仮定した場合、すべてのスワップで受け入れる必要はありません。

1 }
2 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
3 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
4 pair.swap(amount0Out, amount1Out, to, new bytes(0));
5 }
6 }
7
8
9 function swapExactTokensForTokensSupportingFeeOnTransferTokens(
10 uint amountIn,
11 uint amountOutMin,
12 address[] calldata path,
13 address to,
14 uint deadline
15 ) external virtual override ensure(deadline) {
16 TransferHelper.safeTransferFrom(
17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
18 );
19 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
20 _swapSupportingFeeOnTransferTokens(path, to);
21 require(
22 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
23 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
24 );
25 }
26
27
28 function swapExactETHForTokensSupportingFeeOnTransferTokens(
29 uint amountOutMin,
30 address[] calldata path,
31 address to,
32 uint deadline
33 )
34 external
35 virtual
36 override
37 payable
38 ensure(deadline)
39 {
40 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
41 uint amountIn = msg.value;
42 IWETH(WETH).deposit{value: amountIn}();
43 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
44 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
45 _swapSupportingFeeOnTransferTokens(path, to);
46 require(
47 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
48 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
49 );
50 }
51
52
53 function swapExactTokensForETHSupportingFeeOnTransferTokens(
54 uint amountIn,
55 uint amountOutMin,
56 address[] calldata path,
57 address to,
58 uint deadline
59 )
60 external
61 virtual
62 override
63 ensure(deadline)
64 {
65 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
66 TransferHelper.safeTransferFrom(
67 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
68 );
69 _swapSupportingFeeOnTransferTokens(path, address(this));
70 uint amountOut = IERC20(WETH).balanceOf(address(this));
71 require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
72 IWETH(WETH).withdraw(amountOut);
73 TransferHelper.safeTransferETH(to, amountOut);
74 }
すべて表示
コピー

これらは通常のトークンに使用されるものと類似した変数ですが、代わりに _swapSupportingFeeOnTransferTokensを呼び出します。

1 // **** LIBRARY FUNCTIONS ****
2 function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
3 return UniswapV2Library.quote(amountA, reserveA, reserveB);
4 }
5
6 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
7 public
8 pure
9 virtual
10 override
11 returns (uint amountOut)
12 {
13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
14 }
15
16 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
17 public
18 pure
19 virtual
20 override
21 returns (uint amountIn)
22 {
23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
24 }
25
26 function getAmountsOut(uint amountIn, address[] memory path)
27 public
28 view
29 virtual
30 override
31 returns (uint[] memory amounts)
32 {
33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);
34 }
35
36 function getAmountsIn(uint amountOut, address[] memory path)
37 public
38 view
39 virtual
40 override
41 returns (uint[] memory amounts)
42 {
43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);
44 }
45}
すべて表示
コピー

これらの関数は、UniswapV2 ライブラリ関数を呼び出すプロキシにすぎません。

UniswapV2Migrator.sol

このコントラクトは、取引所を以前の v1 から v2 へ移行するために使用されました。 現在は、移行済みのため使われません。

ライブラリ

SafeMath ライブラリ(opens in a new tab)は、十分にドキュメント化されているため、ここでドキュメント化する必要はありません。

数学

このライブラリには、Solidity コードで通常は必要としない、言語に含まれていない数学関数があります。

1pragma solidity =0.5.16;
2
3// a library for performing various math operations
4
5library Math {
6 function min(uint x, uint y) internal pure returns (uint z) {
7 z = x < y ? x : y;
8 }
9
10 // babylonian method (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
11 function sqrt(uint y) internal pure returns (uint z) {
12 if (y > 3) {
13 z = y;
14 uint x = y / 2 + 1;
すべて表示
コピー

平方根よりも高い推定値である x から始めます(これが、1 ~ 3 を特殊なケースとして扱う必要がある理由です)。

1 while (x < z) {
2 z = x;
3 x = (y / x + x) / 2;
コピー

前回の推定値とその平方根を求めようとしている数の平均値を、前回の推定値で割って、より正確な推定値を求めます。 新しい推定値が既存の推定値より低くなくなるまで繰り返します。 詳細については、こちら(opens in a new tab)をご覧ください。

1 }
2 } else if (y != 0) {
3 z = 1;
コピー

ゼロの平方根は必要ありません。 1、2、3 の平方根はおおよそ 1 です(整数を使用するため、小数部分は無視します) 。

1 }
2 }
3}
コピー

固定点小数(UQ112x112)

このライブラリは、通常イーサリアムの算術の一部ではない小数を処理します。 数字xx*2^112としてコード化して実行することで、 元の加算および減算オペコードをそのまま使用できます。

1pragma solidity =0.5.16;
2
3// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format))
4
5// range: [0, 2**112 - 1]
6// resolution: 1 / 2**112
7
8library UQ112x112 {
9 uint224 constant Q112 = 2**112;
すべて表示
コピー

Q112は、コード化の 1 つです。

1 // encode a uint112 as a UQ112x112
2 function encode(uint112 y) internal pure returns (uint224 z) {
3 z = uint224(y) * Q112; // never overflows
4 }
コピー

y は、uint112であるため、大体の値は 2^112-1 です。 この数字は、UQ112x112としてコード化することができます。

1 // divide a UQ112x112 by a uint112, returning a UQ112x112
2 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
3 z = x / uint224(y);
4 }
5}
コピー

2 つのUQ112x112の値を割ると、結果は 2^112 で乗算されなくなります。 そのため、代わりに分母に整数を使用します。 乗算を行うには同様のトリックを使用する必要がありますが、UQ112x112の値の乗算を行う必要はありません。

UniswapV2 ライブラリ

このライブラリは、ペリフェリーコントラクトにのみ使用されます。

1pragma solidity >=0.5.0;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
4
5import "./SafeMath.sol";
6
7library UniswapV2Library {
8 using SafeMath for uint;
9
10 // returns sorted token addresses, used to handle return values from pairs sorted in this order
11 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
12 require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
13 (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
14 require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
15 }
すべて表示
コピー

2 つのトークンをアドレス順に並び替えて、それらのペア取引所のアドレスを取得できるようにします。 これをしないと、パラメーター A、B の場合とパラメーター B、A の場合の 2 つの可能性が生じ、1 つではなく、2 つの交換になってしまうため、必ず行ってください。

1 // calculates the CREATE2 address for a pair without making any external calls
2 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
3 (address token0, address token1) = sortTokens(tokenA, tokenB);
4 pair = address(uint(keccak256(abi.encodePacked(
5 hex'ff',
6 factory,
7 keccak256(abi.encodePacked(token0, token1)),
8 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
9 ))));
10 }
すべて表示
コピー

この関数は、2 つのトークンのペア取引所のアドレスを計算します。 このコントラクトは、CREATE2 オペコード(opens in a new tab)を使用して作成されるため、使用するパラメータがわかっていれば同じアルゴリズムを使用してアドレスを計算できます。 これはファクトリーよりも大幅に安くなります。

1 // fetches and sorts the reserves for a pair
2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
3 (address token0,) = sortTokens(tokenA, tokenB);
4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
6 }
コピー

この関数は、ペア取引所が持っている 2 つのトークンのリザーブを返します。 どちらの順序でもトークンを受け取る可能性があり、内部使用のためにソートすることに注意してください。

1 // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 amountB = amountA.mul(reserveB) / reserveA;
6 }
コピー

この関数は、フィーがかからない場合に、トークン A と引き換えに得られるトークン B の量を示します。 この計算では、送金によって交換レートが変わることが考慮されています。

1 // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
コピー

上記のquote関数は、ペア交換フィーがかからない場合に最適です。 ただし、0.3%の交換フィーがかかる場合、実際に得られる量は少なくなります。 この関数は、交換フィーを差し引いた量を計算します。

1
2 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
4 uint amountInWithFee = amountIn.mul(997);
5 uint numerator = amountInWithFee.mul(reserveOut);
6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);
7 amountOut = numerator / denominator;
8 }
コピー

Solidity は小数をネイティブに扱うことができないため、単に 0.997 を掛けることはできません。 代わりに、分子に 997 を、分母に 1000 を掛けて同じ結果を得ます。

1 // given an output amount of an asset and pair reserves, returns a required input amount of the other asset
2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 uint numerator = reserveIn.mul(amountOut).mul(1000);
6 uint denominator = reserveOut.sub(amountOut).mul(997);
7 amountIn = (numerator / denominator).add(1);
8 }
コピー

この関数はほぼ同じことを行いますが、出力量を取得して入力を提供します。

1
2 // performs chained getAmountOut calculations on any number of pairs
3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
5 amounts = new uint[](path.length);
6 amounts[0] = amountIn;
7 for (uint i; i < path.length - 1; i++) {
8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
10 }
11 }
12
13 // performs chained getAmountIn calculations on any number of pairs
14 function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
15 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
16 amounts = new uint[](path.length);
17 amounts[amounts.length - 1] = amountOut;
18 for (uint i = path.length - 1; i > 0; i--) {
19 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
20 amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
21 }
22 }
23}
すべて表示
コピー

これらの 2 つの関数は、複数のペア取引所を経由する必要がある場合に、値を特定します。

送金ヘルパー

このライブラリ(opens in a new tab)は、元の状態に戻せるよう、ERC-20 およびイーサリアム送金関連の成功チェックを追加し、同様の方法でfalse値も返します。

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3pragma solidity >=0.6.0;
4
5// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false
6library TransferHelper {
7 function safeApprove(
8 address token,
9 address to,
10 uint256 value
11 ) internal {
12 // bytes4(keccak256(bytes('approve(address,uint256)')));
13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
14
すべて表示
コピー

次の 2 つの方法のいずれかで異なるコントラクトを呼び出すことができます。

1 require(
2 success && (data.length == 0 || abi.decode(data, (bool))),
3 'TransferHelper::safeApprove: approve failed'
4 );
5 }
コピー

ERC-20 標準以前に作成されたトークンとの後方互換性の便宜上、ERC-20 の呼び出しは次のいずれかによって失敗することがあります。1 つ目は、(successfalseである場合) 元に戻すことによる失敗、2 つ目は、(出力データがあり、それをブール値としてデコードするとfalseになる場合) 成功したうえでfalse値を返すことによる失敗です。

1
2
3 function safeTransfer(
4 address token,
5 address to,
6 uint256 value
7 ) internal {
8 // bytes4(keccak256(bytes('transfer(address,uint256)')));
9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
10 require(
11 success && (data.length == 0 || abi.decode(data, (bool))),
12 'TransferHelper::safeTransfer: transfer failed'
13 );
14 }
すべて表示
コピー

この関数は、ERC-20 の送金機能(opens in a new tab)を実装しており、あるアカウントが別のアカウントから提供されたアローワンスを使うことができます。

1
2 function safeTransferFrom(
3 address token,
4 address from,
5 address to,
6 uint256 value
7 ) internal {
8 // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
10 require(
11 success && (data.length == 0 || abi.decode(data, (bool))),
12 'TransferHelper::transferFrom: transferFrom failed'
13 );
14 }
すべて表示
コピー

この関数は、ERC-20 の transferFrom 機能(opens in a new tab)を実装しており、あるアカウントが別のアカウントから提供されたアローワンスを使うことができます。

1
2 function safeTransferETH(address to, uint256 value) internal {
3 (bool success, ) = to.call{value: value}(new bytes(0));
4 require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');
5 }
6}
コピー

この関数は、アカウントにイーサ(ETH)を送金します。 異なるコントラクトへのすべての呼び出しで、イーサ(ETH)の送信ができます。 実際には関数を呼び出す必要がないため、この呼び出しでデータを送信することはありません。

まとめ

この記事は、約 50 ページにおよびます。 ここまで読んでいただきありがとうございました。 (短いサンプルプログラムとは対照的に)実際のアプリケーションを作成する際の考慮事項を理解し、独自のユースケースにおいてコントラクトを作成できるようになったことを願っています。

ぜひ有用なコードを書いていただき、私たちを驚かせてください。

最終編集者: @HiroyukiNaito(opens in a new tab), 2024年2月23日

このチュートリアルは役に立ちましたか?