Uniswap-v2コントラクトの手順
はじめに
Uniswap v2(opens in a new tab)では、任意の 2 つの ERC-20 トークン同士の取引が可能になります。 本記事では、このプロトコルを実装したコントラクトのソースコードをチェックして、このように書かれている理由を理解していきます。
Uniswap の役割
基本的には、流動性プロバイダーとトレーダーの 2 種類のユーザーが存在します。
liquidity providers(流動性プロバイダー)は、交換可能な 2 つのトークン(ここではToken0とToken1と呼びます)をプールに提供します。 その見返りとして、liquidity token(流動性トークン)と呼ばれるプールの一部所有権を表す第 3 のトークンを受け取 ります。
Traders(トレーダー)は、あるトークンをプールに送り、流動性プロバイダーが提供するプールから他のトークン(例えば、Token0を送ってToken1を受け取る)を受け取ります。 交換レートは、プールが持つToken0とToken1の相対的な数で決定されます。 加えて、流動性プールの報酬としてプールに数パーセントのフィーを取られます。
流動性プロバイダーが元のトークンを取り戻したい場合は、プールトークンをバーンして、報酬を含めた元のトークンを受け取ることができます。
詳しい説明はこちら(opens in a new tab)をご覧ください。
なぜ v2? v3 じゃないの?
Uniswap v3(opens in a new tab)は v2 を非常に複雑化したアップグレード版です。 まず v2 を学んでから v3 に進む方が簡単です。
コアコントラクト vs ペリフェリーコントラクト
Uniswap v2 は、コアコントラクトとペリフェリーコントラクトの 2 つのコンポーネントに分かれています。 コアコントラクトはアセットを保有するため安全性の保証が重要となりますが、この分割によって監査を簡略化することができます。 ペリフェリーコントラクトはトレーダーが必要とするすべての機能を提供しています。
データおよび制御フロー
Uniswap の 3 大アクションを実行したときに発生するデータフローと制御フローは以下のとおりです。
- 異なるトークン間でスワップを実行する
- 市場に流動性を与え、ERC-20 流動性トークンのペア取引で報酬を得る
- ERC-20 流動性トークンをバーンし、ペア取引によってトレーダーが取引できる ERC-20 トークンを取り戻す
スワップ
トレーダーが使用する最も一般的なフローです。
呼び出し元
- スワップされた量のアローワンスをペリフェリーアカウントへ提供します。
- ペリフェリーコントラクトには多数のスワップ関数がありますが、そのうち 1 つを呼び出します(ETH が関係しているかどうか、入金するトークンの量や取り戻すトークンの量をトレーダーが指定するかどうかなどによって、呼び出す関数は異なります)。 どのスワップ関数も
path
、つまり経由する取引所の配列を受け取ります。
ペリフェリーコントラクト内の処理 (UniswapV2Router02.sol)
- パスに沿って、各取引所で取引する量を特定します。
- パスを繰り返し処理します。 経路にある各取引所に入力トークンを送信し、取引所の
スワップ
関数を呼び出します。 通常、トークンの送信先アドレスはパス上にある次のペア取引所です。 最後の取引は、トレーダーが提供したアドレスとなります。
コアコントラクト内の処理(UniswapV2Pair.sol)
- コアコントラクトで不正がされていないこと、スワップ後も十分な流動性を維持できることを検証します。
- 既存のリザーブ量と追加のトークン数を確認します。 追加のトークン量は、交換用に受け取った入力トークンの数です。
- 出力トークンを送信先に送ります。
_update
を呼び出し、リザーブ量をアップデートします。
ペリフェリーコントラクト(UniswapV2Router02.sol)に戻る
- 必要なクリーンアップを行います(例えば、WETH トークンをバーンして ETH へ戻し、トレーダーに送るなど)。
流動性の追加
呼び出し元
- 流動性プールに追加される量のアローワンスをペリフェリーアカウントに提供します。
- ペリフェリーコントラクトの
addLiquidity
関数の 1 つを呼び出します。
ペリフェリーコントラクト内の処理(UniswapV2Router02.sol)
- 必要に応じて新しいペア取引所を作成します。
- 既存のペア取引所がある場合は、追加するトークンの量を計算します。 両方のトークンは同じ値であることが想定されるため、新規トークンと既存トークンの比率は同じになる はずです。
- 受け取り可能なトークンの量かどうか確認します(実行者は流動性の追加を希望しない最低量を指定することができます) 。
- コアコントラクトを呼び出します。
コアコントラクト内の処理(UniswapV2Pair.sol)
- 流動性トークンをミントして、呼び出し元に送信します。
_update
を呼び出し、リザーブ量をアップデートします。
流動性の削除
呼び出し元
- ペリフェリーアカウントに、基礎トークンと引き換えにバーンされた流動性トークンのアローワンスを提供します。
- ペリフェリーコントラクトの
removeLiquidity
関数の内の 1 つを呼び出します。
ペリフェリーコントラクト内の処理(UniswapV2Router02.sol)
- ペア取引所に流動性トークンを送ります。
コアコントラクト(UniswapV2Pair.sol)
- バーンされたトークンの量に応じて、基礎トークンを送信先アドレスに送ります。 例えば、プールに A トークンが 1000 個、B トークンが 500 個、流動性トークンが 90 個あるとします。流動性トークン 9 個を受け取ってバーンすると、流動性トークンの 10%をバーンすることになり、ユーザーに 100 個の A トークンと 50 個の B トークンが返されることになります。
- 流動性トークンをバーンします。
_update
を呼び出し、リザーブ量をアップデートします。
コアコントラクト
流動性を保持する安全なコントラクトです。
UniswapV2Pair.sol
このコントラクト(opens in a new tab)は、トークンを交換する実際のプールを実装しています。 これは Uniswap の中核機能です。
1pragma solidity =0.5.16;23import './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';10すべて表示コピー
コントラクトは、これら(IUniswapV2Pair
やUniswapV2ERC20
)を実装していたり、これらを実装したコントラクトを呼び出すため、上記すべてのインターフェイスを認識する必要があります。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {2コピー
このコントラクトは、流動性トークンの ERC-20 関数を提供するUniswapV2ERC20
を継承しています。
1 using SafeMath for uint;2コピー
このSafeMath ライブラリ(opens in a new tab)は、オーバーフローとアンダーフローを回避するために使用されます。 このライブラリがないと、-1
であるべき値が2^256-1
になってしまうことがあるため重要な役割を担っています。
1 using UQ112x112 for uint224;2コピー
通常、プールコントラクトの計算では小数を使いますが、 EVM で小数はサポートされていません。 そこで、Uniswap が考えた解決策は、224 ビット値を使用することです。整数部分に 112 ビット、小数部分に 112 ビットを使います つまり、1.0
は2^112
、1.5
は2^112 + 2^111
のように表します。
このライブラリの詳細は、後述するドキュメントに掲載されています。
変数
1 uint public constant MINIMUM_LIQUIDITY = 10**3;2コピー
ゼロ除算のケースを回避するため、流動性トークンには常に最小数が存在します(ただし、ゼロアカウントが所有しています。) その数はMINIMUM_LIQUIDITYであり、1000 です。
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));2コピー
これは、ERC-20 送金関数の ABI セレクターです。 2 つのトークンアカウント間で ERC-20 トークンを送金するために使用されます。
1 address public factory;2コピー
これは、このプールを作成したファクトリコントラクトです。 すべてのプールは 2 つの ERC-20 トークン間の取引所であり、ファクトリーはこれらすべてのプールをつなぐ中心点です。
1 address public token0;2 address public token1;3コピー
このプールで交換可能な 2 種類の ERC-20 トークンのコントラクトアドレスがあります。
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves3コピー
プールが保有する各種トークンのリザーブです。 2 つが同じ値であると仮定すると、各 token0 は reserve1/reserve0 token1 に相当します。
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves2コピー
交換が行われた最後のブロックのタイムスタンプで、時間の経過とともに交換レートを追跡する際に使用されます。
イーサリアムのコントラクトで最も高額なガス代の 1 つはストレージで、コントラクトコールから次のコールまで 持続します。 各ストレージセルは 256 ビットとなっているため、 3 つの変数reserve0
、reserve1
、blockTimestampLast
すべて(112+112+32=256)を 1 つのストレージ値に含めるように割り当てます。
1 uint public price0CumulativeLast;2 uint public price1CumulativeLast;3コピー
これらの変数は、各トークンの累積コストを(お互いの価値で)保有し、 一定期間の平均交換レートを算出する際に使用できます。
1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event2コピー
token0 と token1 の交換レートは、取引時の両リザーブの倍数を一定に維持するようペア取引所が決定します。 kLast
がその値です。 流動性プロバイダーがトークンを入出金するとこのレートは変化し、マーケット手数料 0.3%によって若干増えます。
下記にシンプルな例をご紹介します。 ただし、簡略化のため表は小数点以下 3 桁までとし、0.3%のマーケット手数料も考慮していないので正確な数字ではありません。
イベント | reserve0 | reserve1 | reserve0 * reserve1 | 平均交換レート(token1/token0) |
---|---|---|---|---|
初期設定 | 1,000.000 | 1,000.000 | 1,000,000 | |
トレーダー A が 50 の token0 を 47.619 の token1 とスワップ | 1,050.000 | 952.381 | 1,000,000 | 0.952 |
トレーダー B が 10 の token0 を 8.984 の token1 と スワップ | 1,060.000 | 943.396 | 1,000,000 | 0.898 |
トレーダー C が 40 の token0 を 34.305 の token1 とスワップ | 1,100.000 | 909.090 | 1,000,000 | 0.858 |
トレーダー D が 100 の token1 を 109.01 の token0 とスワップ | 990.990 | 1,009.090 | 1,000,000 | 0.917 |
トレーダー E が 10 の token0 を 10.079 の token1 とスワップ | 1,000.990 | 999.010 | 1,000,000 | 1.008 |
トレーダーが提供する token0 の量が増えるほど、需要と供給により token1 の相対的価値も上がり、逆の場合も同じことが言えます。
ロック
1 uint private unlocked = 1;2コピー
再入可能の悪用(opens in a new tab)によるセキュリティ脆弱性の一種があります。 Uniswap は、任意の ERC-20 トークンを転送することになっているため、Uniswap マーケットを悪用しようとする ERC-20 コントラクトを呼び出す可能性があります。 コントラクトの一部にunlocked
変数を使うことで、(同じトランザクション内で)実行中に関数が呼び出されるのを防ぐことができます。
1 modifier lock() {2コピー
この関数はmodifier(opens in a new tab)で、通常の関数をラップして何らかの方法で挙動を変更する関数です。
1 require(unlocked == 1, 'UniswapV2: LOCKED');2 unlocked = 0;3コピー
unlocked
が 1 の場合は 0 に設定します。 すでに 0 の場合は呼び出しを元に戻して、失敗させます。
1 _;2コピー
modifier の_;
は、(すべてのパラメータを含む)元の関数呼び出しです。 これは、関数が呼び出されたときにunlocked
が 1 であり、実行中にunlocked
の値が 0 になった場合のみ、関数呼び出しが発生することを意味します。
1 unlocked = 1;2 }3コピー
main 関数に戻った後、ロックを解除します。
その他の 関数
1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {2 _reserve0 = reserve0;3 _reserve1 = reserve1;4 _blockTimestampLast = blockTimestampLast;5 }6コピー
この関数が呼び出されると、取引所の現在の状態を返します。 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));3コピー
この内部関数は、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 }3コピー
ERC-20 転送コールが失敗を報告する例を下記に 2 つご紹介します。
- 元に戻す。 外部コントラクトへのコールを元に戻すと、ブール値は
false
を返します。 - 正常に終了したが、失敗を報告する。 この場合、戻り値のバッファの長さがゼロ以外で、ブール値としてデコードすると
false
になります。
いずれかの状態が発生した場合は、元に戻します。
イベント
1 event Mint(address indexed sender, uint amount0, uint amount1);2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);3コピー
この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 to8 );9コピー
このイベントは、トレーダーがあるトークンを他のトレーダーとスワップしたときに発行されます。 ここでも、送信者と送信先が同じとは限りません。 各トークンは、取引所に送信されるか、取引所から受信します。
1 event Sync(uint112 reserve0, uint112 reserve1);2コピー
最後に、Sync
は、トークンが追加または引き出されるたびに発行され、その理由に関係なく、最新のリザーブ情報(交換レート)を提供します。
setup 関数
これらの関数は、新しいペア取引所がセットアップされたときに 1 度だけ呼び出されます。
1 constructor() public {2 factory = msg.sender;3 }4コピー
コ ンストラクタは、ペアを作成したファクトリのアドレスを確実に追跡できるようにします。 この情報は、initialize
と(ファクトリが存在する場合は)ファクトリフィーに必要となります。
1 // called once by the factory at time of deployment2 function initialize(address _token0, address _token1) external {3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check4 token0 = _token0;5 token1 = _token1;6 }7コピー
この関数では、ファクトリー(のみ)が、このペアが交換する 2 つの ERC-20 トークンを指定できます。
内部の update 関数
_update
1 // update reserves and, on the first call per block, price accumulators2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {3コピー
この関数は、トークンの入金や引き出しのたびに呼び出されます。
1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');2コピー
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 desired3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {4コピー
経過時間がゼロとなっていない場合は、このブロックで最初の交換トランザクションとなります。 その場合、コストアキュムレータをアップデートする必要があります。
1 // * never overflows, and + overflow is desired2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;4 }5コピー
各コストアキュムレータは、最新のコスト(他のトークンのリザーブまたはこのトークンのリザーブ)に経過時間(秒)を掛けてアップデートされます。 平均価格を求めるには、2 つの時点の累積価格を読み、その時間差で割ります。 例えば、次の一連のイベントを想定してください。
イベント | reserve0 | reserve1 | タイムスタンプ | 限界交換率(reserve1/reserve0) | price0CumulativeLast |
---|---|---|---|---|---|
初期設定 | 1,000.000 | 1,000.000 | 5,000 | 1.000 | 0 |
トレーダー A が token0 を 50 入金し、token1 を 47.619 戻す | 1,050.000 | 952.381 | 5,020 | 0.907 | 20 |
トレーダー B が token0 を 10 入金し、token1 を 8.984 戻す | 1,060.000 | 943.396 | 5,030 | 0.890 | 20+10*0.907 = 29.07 |
トレーダー C が token0 を 40 入金し、token1 を 34.305 戻す | 1,100.000 | 909.090 | 5,100 | 0.826 | 29.07+70*0.890 = 91.37 |
トレーダー D が token1 を 100 入金し、token0 を 109.01 戻す | 990.990 | 1,009.090 | 5,110 | 1.018 | 91.37+10*0.826 = 99.63 |
トレーダー E が token0 を 10 入金し、token1 を 10.079 戻す | 1,000.990 | 999.010 | 5,150 | 0.998 | 99.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 }6コピー
最後に、グローバル変数をアップデートし、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) {3コピー
Uniswap 2.0 で、トレーダーは、マーケットの利用料として 0.30%のフィーを払います。 フィーの大半は、流動性プロバイダーに支払われます(取引の 0.25%)。 残りの 0.05%は、流動性プロバイダーまたは、プロトコルフィーとしてファクトリで指定されたアドレスへ送られます。プロトコルフィーは、Uniswap の開発のために支払われます。
計算量(それに伴うガス代)を削減するため、このフィーは、トランザクションごとではなく、プールから流動性が追加または 削除されるときだけ計算されます。
1 address feeTo = IUniswapV2Factory(factory).feeTo();2 feeOn = feeTo != address(0);3コピー
上記コードのファクトリーのフィー送信先を読んでみましょう。 ゼロの場合、プロトコルフィーはかからず、フィーを計算する必要もありません。
1 uint _kLast = kLast; // gas savings2コピー
kLast
状態変数はストレージに位置しているため、コントラクトへの異なる呼び出し間で値を持つことになります。 ストレージへのアクセスは、コントラクトへの関数呼び出しが終了したときにリリースされる揮発メモリへのアクセスよりも非常に高価となるため、内部変数を使ってガス代を節約します。
1 if (feeOn) {2 if (_kLast != 0) {3コピー
流動性プロバイダーは、流動性トークンの価値が上昇するだけで取り分をもらえます。 一方、プロトコルフィーは、新しい流動性トークンをミントし、feeTo
アドレスに提供する必要があります。
1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));2 uint rootKLast = Math.sqrt(_kLast);3 if (rootK > rootKLast) {4コピー
プロトコルフィーを徴収する新しい流動性がある場合の 平方根関数については、この記事の後半で解説します。
1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));2 uint denominator = rootK.mul(5).add(rootKLast);3 uint liquidity = numerator / denominator;4コピー
このフィーの複雑な計算方法については、ホワイトペーパー(opens in a new tab)の 5 ページ目で説明されています。 (流動性が実際に変化する前に、流動性が追加または削除されるたびにこの計算を実行するため、)kLast
が計算された時間から現在までの間に流動性が追加または削除されていないことがわかります。そのため、reserve0 * reserve1
の変更は、トランザクションフィーに起因する必要があります。 (流動性の追加または削除がなければ、reserve0 * reserve1
を一定に保ちます)。
1 if (liquidity > 0) _mint(feeTo, liquidity);2 }3 }4コピー
UniswapV2ERC20._mint
関数を利用して、追加の流動性トークンを作成し、feeTo
に割り当てます。
1 } else if (_kLast != 0) {2 kLast = 0;3 }4 }5コピー
フィーがない場合、kLast
がゼロでなければゼロに設定します。 このコントラクトが書かれたとき、ガス払い戻し機能(opens in a new tab)がありました。この機能は、コントラクトによって必要のないストレージをゼロにすることで、イーサリアム全体のサイズを縮小するよう促したものです。 この機能により、可能な場合はコードは払い戻しを受けます。
外部アクセス可能な関数
どのトランザクションまたはコントラクトでも、これらの関数を呼び出すことはできますが、ペリフェリーコントラクトから呼び出されるように設計されていることに注意してください。 直接呼び出すと、ペア取引所で不正行為はできませんが、誤って価値を失ってしまう可能性があります。
mint
1 // this low-level function should be called from a contract which performs important safety checks2 function mint(address to) external lock returns (uint liquidity) {3コピー
この関数は、流動性プロバイダーが流動性をプールへ追加するときに呼び出されます。 報酬として追加の流動性トークンをミントします。 同じトランザクションで流動性を追加した後に呼び出すペリフェリーコントラクトから呼び出されます。(そうすることで、誰もが正当な所有者より前に、新しい流動性を要求するトランザクションの送信ができなくなります。)
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings2コピー
これは、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);5コピー
現在の残高を取得し、各トークンタイプで追加された値を確認します。
1 bool feeOn = _mintFee(_reserve0, _reserve1);2コピー
プロトコルフィーを計算して収集し、それに応じて流動性トークンをミントします。 _mintFee
のパラメータは古いリザーブ値であるため、フィーによるプールの変更にのみ基づいてフィーは正確に計算されます
1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee2 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 tokens5コピー
これが最初の入金の場合は、MINIMUM_LIQUIDITY
トークンを作成し、それらをロックするためにゼロアドレスに送信します。 このトークンは引き換えることができないため、プールが完全に空になることはありません(これによりゼロ除算を防ぎます) 。 MINIMUM_LIQUIDITY
の値は、1000 です。これは、ETH が wei 単位に分割されるように、ほとんどの ERC-20 がトークンの 10 の 18 乗の単位に分割されることを考慮して、単一トークンの値の 10^-15 となっており、 高コストではありません。
最初の入金の時点では、2 つのトークンの相対的価値はわからないため、金額を掛けて平方根をとります。入金によって両方のトークンの価値が等しくなったと仮定します。
裁定取引 で価値の喪失を防ぎ、同等の価値を提供することが入金者の利益になるため、信頼することができます。 例えば、2 つのトークンの価値が同等であるものの、入金者がToken0の 4 倍のToken1を入金したとします。 トレーダーは、価値を抽出するために、ペア取引所がToken0の価値の方が高いと考えている事実を利用することができます。
イベント | reserve0 | reserve1 | reserve0 * reserve1 | プールの値(reserve0 + reserve1) |
---|---|---|---|---|
初期設定 | 8 | 32 | 256 | 40 |
トレーダーはToken0トークンを 8 入金し、Token1を 16 戻す | 16 | 16 | 256 | 32 |
上記のように、トレーダーは、プールの価値の減少から追加の 8 トークンを獲得し、それを所有する入金者に損害を与えました。
1 } else {2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);3コピー
その後の入金では、2 つのアセットの交換レートがすでにわかっており、流動性プロバイダーが両方のアセットで同等の価値を提供することが期待できます。 そうしなかった場合、罰として、提供された流動性より低い価値の流動性トークンが与えられます。
初回の入金であれ、その後の入金であれ、提供する流動性トークンの数は、 reserve0*reserve1
の変化の平方根に等しくなり、(「罰金」の対象となる両方のトークンの種類で同等の価値ではない入金をしないかぎり) 流動性トークンの価値は変化しません。 もう 1 つ、同等の価値を持つ 2 つのトークンの例をご紹介します。3 つが良い入金で、1 つが(1 種類のトークンのみを入金するため、流動性トークンは生成されない)悪い入金です。
イベント | reserve0 | reserve1 | reserve0 * reserve1 | プール値(reserve0 + reserve1) | この入金でミントされた流動性トークン | 流動性トークンの合計 | 各流動性トークンの価値 |
---|---|---|---|---|---|---|---|
初期設定 | 8.000 | 8.000 | 64 | 16.000 | 8 | 8 | 2.000 |
各種 4 つずつ入金 | 12.000 | 12.000 | 144 | 24.000 | 4 | 12 | 2.000 |
各種 2 つずつ入金 | 14.000 | 14.000 | 196 | 28.000 | 2 | 14 | 2.000 |
等しくない値を入金 | 18.000 | 14.000 | 252 | 32.000 | 0 | 14 | ~2.286 |
裁定取引後 | ~15.874 | ~15.874 | 252 | ~31.748 | 0 | 14 | ~2.267 |
1 }2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');3 _mint(to, liquidity);4コピー
UniswapV2ERC20._mint
関数を使用して、追加の流動性トークンを実際に作成し、正しいアカウントに付与します。
12 _update(balance0, balance1, _reserve0, _reserve1);3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date4 emit Mint(msg.sender, amount0, amount1);5 }6コピー
状態変数(reserve0
、reserve1
、必要に応じてkLast
)をアップデートし、必要であれば適切なイベントを発行します。
burn
1 // this low-level function should be called from a contract which performs important safety checks2 function burn(address to) external lock returns (uint amount0, uint amount1) {3コピー
この関数は、流動性が引き出され、適切な流動性トークンをバーンする必要がある場合に呼び出されます。 また、ペリフェリーコントラクトからも呼び出されるようになっています。
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings2 address _token0 = token0; // gas savings3 address _token1 = token1; // gas savings4 uint balance0 = IERC20(_token0).balanceOf(address(this));5 uint balance1 = IERC20(_token1).balanceOf(address(this));6 uint liquidity = balanceOf[address(this)];7コピー
ペリフェリーコントラクトは、呼び出し前に、このコントラクトにバーンされる流動性を送信します。 これにより、バーンされる流動性の量を把握でき、確実にバーンすることができます。
1 bool feeOn = _mintFee(_reserve0, _reserve1);2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');6コピー
流動性プロバイダーは、両方のトークンで同等の価値を受け取り、 交換レートを変更することもありません。
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));67 _update(balance0, balance1, _reserve0, _reserve1);8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date9 emit Burn(msg.sender, amount0, amount1, to);10 }1112すべて表示コピー
burn
関数の残りの部分は、mint
関数が逆(ミラーイメージ)になったものです。
swap
1 // this low-level function should be called from a contract which performs important safety checks2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {3コピー
この関数は、 ペリフェリーコントラクトからも呼び出されることになっています。
1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');45 uint balance0;6 uint balance1;7 { // scope for _token{0,1}, avoids stack too deep errors8コピー
ローカル変数は、メモリに保存するか、少ない場合は直接スタック上に保存できます。 数を制限できれば、ガスの使用量が少ないスタックを使用することができます。 詳細については、正式なイーサリアム仕様であるイエロー ペーパー(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 tokens5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens6コピー
この送金は、すべての条件が満たされていることを確認する前に行っているため、楽観的です。 これはイーサリアムでは、問題ありません。呼び出しの後半で、条件を満たしていなければ、作成されたすべての変更が戻されるからです。
1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);2コピー
リクエストされた場合、受信者にスワップについて通知します。
1 balance0 = IERC20(_token0).balanceOf(address(this));2 balance1 = IERC20(_token1).balanceOf(address(this));3 }4コピー
現在の残高を取得します。 ペリフェリーコントラクトは、スワップを呼び出す前にトークンを送信するため、 コントラクトで不正行為がされていないことを簡単に確認できるようになります。このチェックは、ペリフェリーコントラクト以外のエンティティから呼び出される可能性があるため、コアコントラクトで実行しなければなりません。
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 errors5 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');8コピー
これは、スワップによる損失を確実に防ぐサニティチェックです。 スワップによってreserve0*reserve1
が減少することはありません。 これは、スワップで 0.3%のフィーが送信されることを保証する場所でもあります。K 値のサ ニティチェックをする前に、両方の残高に 1000 を掛け、3 を掛けた金額を引きます。これは現在のリザーブの K 値と比較する前に、残高から 0.3%(3/1000 = 0.003 = 0.3%)が差し引かれることを意味します。
1 }23 _update(balance0, balance1, _reserve0, _reserve1);4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);5 }6コピー
reserve0
とreserve1
をアップデートし、必要に応じて価格アキュムレータとタイムスタンプもアップデートして、イベントを発行します。
sync または skim
実際の残高とペア取引所が持っているとされるリザーブとが一致しない可能性があります。 コントラクトの同意なしにトークンを引き出すことはできませんが、入金は可能です。 アカウントは、 mint
またはswap
どちらかを呼び出すことなくトークンを取引所に送信することができます。
このケースでは次の 2 つの解決策があります。
sync
でリザーブを現在の残高にアップデートします。skim
で余分な金額を引き出します。 誰がトークンを入金したか分からないため、どのアカウントでもskim
の呼び出しが許可されていることに注意してください。 この情報はイベント内で発行されますが、イベントはブロックチェーンからはアクセスできません。
1 // force balances to match reserves2 function skim(address to) external lock {3 address _token0 = token0; // gas savings4 address _token1 = token1; // gas savings5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));7 }891011 // force reserves to match balances12 function sync() external lock {13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);14 }15}16すべて表示コピー
UniswapV2Factory.sol
このコントラクト(opens in a new tab)では、ペア取引所を作ります。
1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Factory.sol';4import './UniswapV2Pair.sol';56contract UniswapV2Factory is IUniswapV2Factory {7 address public feeTo;8 address public feeToSetter;9コピー
これらの状態変数はプロトコルフィーを実装するために必要です。詳細は、ホワイトペーパー(opens in a new tab)の 5 ページ目をご覧ください。 feeTo
は、プロトコルフィーのための流動性トークンを蓄積するアドレスで、feeToSetter
は、feeTo
を別のアドレスに変更できるアドレスです。
1 mapping(address => mapping(address => address)) public getPair;2 address[] public allPairs;3コピー
これらの変数は、ペアと 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);2コピー
このイベントは、新しいペア取引所が作成されたときに発行されます。 トークンのアドレス、ペア取引所のアドレス、ファクトリーによって管理されている全取引所の数が含まれます。
1 constructor(address _feeToSetter) public {2 feeToSetter = _feeToSetter;3 }4コピー
コンストラクタが行う唯一のことは、feeToSetter
を指定することです。 ファクトリーはフィーなしで開始し、変更できるのはfeeSetter
のみとなります。
1 function allPairsLength() external view returns (uint) {2 return allPairs.length;3 }4