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';すべて表示コピー
コントラクトは、これら(IUniswapV2Pair
やUniswapV2ERC20
)を実装していたり、これらを実装したコントラクトを呼び出すため、上記すべてのインターフェイスを認識する必要があります。
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.0
は2^112
、1.5
は2^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 getReserves2 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つの変数reserve0
、reserve1
、blockTimestampLast
すべて(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%のマーケット手数料も考慮していないので正確な数字ではありません。
イベント | 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;コピー
再入可能の悪用(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つご紹介します。
- 元に戻す。 外部コントラクトへのコールを元に戻すと、ブール値は
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);コピー
この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 );コピー
このイベントは、トレーダーがあるトークンを他のトレーダーとスワップしたときに発行されます。 ここでも、送信者と送信先が同じとは限りません。 各トークンは、取引所に送信されるか、取引所から受信します。
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 deployment2 function initialize(address _token0, address _token1) external {3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check4 token0 = _token0;5 token1 = _token1;6 }コピー
この関数では、ファクトリー(のみ)が、このペアが交換する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 {コピー
この関数は、トークンの入金や引き出しのたびに呼び出されます。
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 desired3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {コピー
経過時間がゼロとなっていない場合は、このブロックで最初の交換トランザクションとなります。 その場合、コストアキュムレータをアップデートする必要があります。
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 }コピー
各コストアキュムレータは、最新のコスト(他のトークンのリザーブまたはこのトークンのリザーブ)に経過時間(秒)を掛けてアップデートされます。 平均価格を求めるには、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 }コピー
最後に、グローバル変数をアップデートし、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 checks2 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 _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 tokensコピー
これが最初の入金の場合は、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);コピー
その後の入金では、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);コピー
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 }コピー
状態変数(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) {コピー
この関数は、流動性が引き出され、適切な流動性トークンをバーンする必要がある場合に呼び出されます。 また、ペリフェリーコントラクトからも呼び出されるようになっています。
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)];コピー
ペリフェリーコントラクトは、呼び出し前に、このコントラクトにバーンされる流動性を送信します。 これにより、バーンされる流動性の量を把握でき、確実にバーンすることができます。
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');コピー
流動性プロバイダーは、両方のトークンで同等の価値を受け取り、 交換レートを変更することもありません。
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 }11すべて表示コピー
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 {コピー
この関数は、 ペリフェリーコントラクトからも呼び出されることになっています。
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 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 tokens5 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 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');コピー
これは、スワップによる損失を確実に防ぐサニティチェックです。 スワップによって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 }コピー
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}すべて表示コピー
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;コピー
これらの状態変数はプロトコルフィーを実装するために必要です。詳細は、ホワイトペーパー(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 direction3 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 }56 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 := chainid5 }コピー
これはチェーン識別子(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;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';56import './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;34 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 contract3 }コピー
この関数は、トークンをWETHコントラクトからETHに引き換える際に呼び出されます。 私たちが使用しているWETHコントラクトだけが、これを行うことを許可されています。
流動性の追加
これらの関数は、ペア取引所にトークンを追加し、流動性プールを増加させます。
12 // **** 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で、流動性プロバイダーが次の値を指定するケースを想像してください。
パラメータ | 値 |
---|---|
amountADesired | 1000 |
amountBDesired | 1000 |
amountAMin | 900 |
amountBMin | 800 |
交換レートが0.9から1.25の間である限り、トランザクションは行われます。 交換レートがこの範囲から外れると、トランザクションはキャンセルされます。
この予防措置の理由は、トランザクションが即時ではなく、送信すると最終的にバリデータがそれらをブロックに含めるためです (ガス価格が非常に低い場合を除きます。その場合は、同じノンスでより高いガス価格で別のトランザクションを送信して上書きする必要があります) 。 送信およびトランザクションを含める処理の間で何が起こるかを制御することはできません。
1 ) internal virtual returns (uint amountA, uint amountB) {コピー
この関数は、リザーブ間の現在の比率と同等の比率を持てるよう、流動性プロバイダーが入金すべき量を返します。
1 // create the pair if it doesn't exist yet2 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 deadline5 ) 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 amountETHMin13 );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 any3 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 deadline10 ) 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 pair3 (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);コピー
コアコントラクトが返す量(下位アドレスのトークンが最初)から (tokenA
とtokenB
に応じて)ユーザが期待する量に置き換えます。
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 deadline8 ) 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 deadline17 );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 s10 ) 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 }161718 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 s26 ) 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)を持たないユーザーがプールから引き出せるようにします。
12 // **** 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 deadline10 ) 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 deadline19 );20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));21 IWETH(WETH).withdraw(amountETH);22 TransferHelper.safeTransferETH(to, amountETH);23 }24すべて表示コピー
この関数は、送金フィーまたはストレージフィーを持つトークンに使用できます。 トークンに当該フィーがある場合、 removeLiquidity
関数では、返金されるトークンの量を把握することができないため、最初に引き出してから残高を取得する必要があります。
123 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 s11 ) 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, deadline17 );18 }すべて表示コピー
最後の関数は、ストレージフィーとメタトランザクションを結び付けています。
取引
1 // **** SWAP ****2 // requires the initial amount to have already been sent to the first pair3 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つのペア取引所があるとします。
- 初期状態
- トレーダーは、Aトークンを24.695売り、Bトークンを25.305得ます。
- そのトレーダーは、Bトークンを24.695売り、Cトークンを25.305得ます。Bトークン約0.61を利益として保持します。
- そして、そのトレーダーは、Cトークンを24.695売って、Aトークンを25.305得ます。Cトークンの約0.61を利益として保持します。 そのトレーダーはまた、余分にAトークンを0.61持っています(トレーダーが最終的に得た25.305から、元の投資の24.695を差し引いたもの) 。
ステップ | A-B取引所 | B-C取引所 | A-C取引所 |
---|---|---|---|
1 | A:1000 B:1050 A/B=1.05 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
2 | A:1024.695 B:1024.695 A/B=1 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
3 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1050 C:1000 C/A=1.05 |
4 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A: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;コピー
最後の交換である場合、 交換するために受け取ったトークンを送信先に送ります。 そうでない場合は、次のペア取引所に送ります。
12 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
は読み取り専用です。
uint
やaddress
などのスカラー型では、コンパイラがストレージの選択をしますが、より長くてより高価な配列では、使用するストレージタイプを指定します。
1 address to,2 uint deadline3 ) 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 deadline7 ) 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 external3 virtual4 override5 payable6 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 }161718 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)19 external20 virtual21 override22 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 }35363738 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)39 external40 virtual41 override42 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 }555657 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)58 external59 virtual60 override61 payable62 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 any72 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 pair3 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 errors8 (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 }789 function swapExactTokensForTokensSupportingFeeOnTransferTokens(10 uint amountIn,11 uint amountOutMin,12 address[] calldata path,13 address to,14 uint deadline15 ) external virtual override ensure(deadline) {16 TransferHelper.safeTransferFrom(17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn18 );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 }262728 function swapExactETHForTokensSupportingFeeOnTransferTokens(29 uint amountOutMin,30 address[] calldata path,31 address to,32 uint deadline33 )34 external35 virtual36 override37 payable38 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 }515253 function swapExactTokensForETHSupportingFeeOnTransferTokens(54 uint amountIn,55 uint amountOutMin,56 address[] calldata path,57 address to,58 uint deadline59 )60 external61 virtual62 override63 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]), amountIn68 );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 }56 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)7 public8 pure9 virtual10 override11 returns (uint amountOut)12 {13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);14 }1516 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)17 public18 pure19 virtual20 override21 returns (uint amountIn)22 {23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);24 }2526 function getAmountsOut(uint amountIn, address[] memory path)27 public28 view29 virtual30 override31 returns (uint[] memory amounts)32 {33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);34 }3536 function getAmountsIn(uint amountOut, address[] memory path)37 public38 view39 virtual40 override41 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;23// a library for performing various math operations45library Math {6 function min(uint x, uint y) internal pure returns (uint z) {7 z = x < y ? x : y;8 }910 // 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)
このライブラリは、通常イーサリアムの算術の一部ではない小数を処理します。 数字xをx*2^112としてコード化して実行することで、 元の加算および減算オペコードをそのまま使用できます。
1pragma solidity =0.5.16;23// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format))45// range: [0, 2**112 - 1]6// resolution: 1 / 2**11278library UQ112x112 {9 uint224 constant Q112 = 2**112;すべて表示コピー
Q112
は、コード化の1つです。
1 // encode a uint112 as a UQ112x1122 function encode(uint112 y) internal pure returns (uint224 z) {3 z = uint224(y) * Q112; // never overflows4 }コピー
yは、uint112
であるため、大体の値は2^112-1です。 この数字は、UQ112x112
としてコード化することができます。
1 // divide a UQ112x112 by a uint112, returning a UQ112x1122 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;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';45import "./SafeMath.sol";67library UniswapV2Library {8 using SafeMath for uint;910 // returns sorted token addresses, used to handle return values from pairs sorted in this order11 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 calls2 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 hash9 ))));10 }すべて表示コピー
この関数は、2つのトークンのペア取引所のアドレスを計算します。 このコントラクトは、CREATE2オペコード(opens in a new tab)を使用して作成されるため、使用するパラメータがわかっていれば同じアルゴリズムを使用してアドレスを計算できます。 これはファクトリーよりも大幅に安くなります。
1 // fetches and sorts the reserves for a pair2 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 asset2 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 asset2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {コピー
上記のquote
関数は、ペア交換フィーがかからない場合に最適です。 ただし、0.3%の交換フィーがかかる場合、実際に得られる量は少なくなります。 この関数は、交換フィーを差し引いた量を計算します。
12 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 asset2 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 }コピー
この関数はほぼ同じことを行いますが、出力量を取得して入力を提供します。
12 // performs chained getAmountOut calculations on any number of pairs3 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 }1213 // performs chained getAmountIn calculations on any number of pairs14 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-later23pragma solidity >=0.6.0;45// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false6library TransferHelper {7 function safeApprove(8 address token,9 address to,10 uint256 value11 ) internal {12 // bytes4(keccak256(bytes('approve(address,uint256)')));13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));14すべて表示コピー
次の2つの方法のいずれかで異なるコントラクトを呼び出すことができます。
- インターフェイス定義を使い、関数呼び出しを作成。
- アプリケーションバイナリインターフェース (ABI)(opens in a new tab)を使って「手動」で呼び出しを作成。 これは、コードの作成者が行います。
1 require(2 success && (data.length == 0 || abi.decode(data, (bool))),3 'TransferHelper::safeApprove: approve failed'4 );5 }コピー
ERC-20標準以前に作成されたトークンとの後方互換性の便宜上、ERC-20の呼び出しは次のいずれかによって失敗することがあります。1つ目は、(success
がfalse
である場合) 元に戻すことによる失敗、2つ目は、(出力データがあり、それをブール値としてデコードするとfalse
になる場合) 成功したうえでfalse
値を返すことによる失敗です。
123 function safeTransfer(4 address token,5 address to,6 uint256 value7 ) 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)を実装しており、あるアカウントが別のアカウントから提供されたアローワンスを使うことができます。
12 function safeTransferFrom(3 address token,4 address from,5 address to,6 uint256 value7 ) 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)を実装しており、あるアカウントが別のアカウントから提供されたアローワンスを使うことができます。
12 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ページにおよびます。 ここまで読んでいただきありがとうございました。 (短いサンプルプログラムとは対照的に)実際のアプリケーションを作成する際の考慮事項を理解し、独自のユースケースにおいてコントラクトを作成できるようになったことを願っています。
ぜひ有用なコードを書いていただき、私たちを驚かせてください。
最終編集者: @wackerow(opens in a new tab), 2024年4月2日