こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58
今回は「Uniswap」について解説していきます!
「Uniswap」はEthereumブロックチェーンのスマートコントラクトを利用したDEXの1つです。
Solidityの基礎を学習して、「ERC20」と「ERC721」を学習した人が次に学習する内容として最適なのが「Uniswap」です。
スマートコントラクトへの理解やスキルレベルを一歩上げたい人はぜひこの記事で学んでいってください。
「ERC20」と「ERC721」をまだしっかり理解できていない人は以下の記事で学んでください。
Uniswapとは
Uniswapは、Ethereumブロックチェーンのスマートコントラクトを利用したAMM型の分散型取引所(DEX)です。
聞き慣れない単語がいくつか出てきたので確認していきましょう。
DEX
DEXとは分散型取引所とも呼ばれ、中央集権的な管理者が存在せず、ユーザー同士で仮想通貨やトークンの交換を行うことができる取引所のことです。
DEXの定義を確認すると以下のように書かれています。
分散型取引所(DEX)は、暗号資産のトレーダー間で取引を直接実施するピアツーピアの市場です。DEXは暗号資産の中核を成す可能性のうちの1つを実現しています。銀行や証券会社などの仲介者が取り扱わない金融取引を促進するという可能性です。ユニスワップやスシスワップなどの人気があるDEXは、イーサリアムブロックチェーン上で運営されています。
https://www.coinbase.com/ja/learn/crypto-basics/what-is-a-dex
証券取引所のように中央に管理者が存在して仮想通貨取引の調整を行なっている取引所を中央集権型取引所(CEX)と言います。
一方、DEXは中央に管理者が存在しないため、スマートコントラクトを用いて暗号資産の価格が決まります。
DEXのメリット
より多くのトークンを取引可能
DEXでは、Ethereumベースのトークンであれば上場していなくてもトークンの取引をすることができます。
そのため、これから値上がりする可能性があるトークンを狙えるというメリットがあります。
仲介手数料が不要
中央に管理者がいないため、管理者に支払う余計な仲介手数料がわずかにしか発生しません。
そのためDEXを使用するユーザーは手数料を安く抑えることができます。
ハッキングリスクの軽減
DEXでは、ユーザーの資金を取引所ではなく各ユーザーのウォレットで保管しているため、ハッキングに強いです。
匿名性
DEXで取引を開始する際、個人情報の提出が不要です。
そのため誰でもDEXでの取引を開始できるのは大きなメリットです。
新興国での活用
新興国では銀行口座をもてない人が多くいます。
DEXでは個人情報の提出が不要であるため、スマホ1台あれば誰でも取引することができます。
DEXのデメリット
スマートコントラクトの脆弱性
スマートコントラクトに脆弱性があり、その脆弱性を突かれることで資金を抜かれてしまうリスクがあります。
メリットで述べた部分は各ユーザーがウォレットで管理している資金に限っての話なので、DEXに預けている資金はスマートコントラクトのハッキングリスクにさらされています。
悪意のあるトークン
メリットで上場していないトークンの取引が可能と述べました。
これらのトークンは審査を行なっていないため、トークン発行者が詐欺を行うことを目的に作成した可能性がないとは言えません。
そのため、トークンを取引する際はあらかじめホワイトペーパーや開発者のTwitter、Discordチャンネルを確認することが求められます。
ガス代が高い
UniswapはEthereumブロックチェーンに依存しているため、Uniswapで取引手数料を安く抑えることができても、決済時のガス代が高くなってしまう。
DeFiとは?
DeFiとは分散型金融と呼ばれ、中央管理者のいない金融仲介アプリケーションのことを指します。
以下の動画がわかりやすいです。
DeFiとDEXの違いとしては以下になります。
『DEFIというモノの中に、DEXという機能が存在している』
https://manabufan.com/2021/02/25/difi-dex/
DeFiとDEXは混同されがちなので、違いを理解しておくのは重要です。
AMM(Automated Market Maker)
ERC20トークン同士を直接交換することができます。
ERC20については以下の記事を参考にしてください。
あらかじめ設定されたアルゴリズム(一定のルール)に従って、自動的に取引を実行するシステムのことです。
通常の取引所のシステムでは、多くの人々に取引に参加してもらい、指値や約定を誘発し「好きな量を、好きな時に売買できる」状態を作り出せています。
このようにユーザーの売買希望金額がリアルタイムで表示されていて、その時点での相場を把握し、どの価格帯にフォーカスして注文を出すのが良いか判断できる取引をオーダーブック(板取引)といいます。
しかし、過去のDEXでは指値提示やキャンセルのたびにEthereumのガス代がかかり、流動性が不足し売買が活発にならないという問題がありました。
Ethereumは頻繁な取引に不向きであるため、オーダーブックを使用したDEXは廃れていきました。
その後に登場したのがAMMです。
スワップ(交換)に用いる仮想通貨をユーザー同士でプール(Uniswapに貸し出した資金を貯めておくところ)しておき、スワップ(交換)の際は他のユーザーと超苦節やり取りするのではなく、プールとの間で取引をします。
スワップ(交換)の際の価格はアルゴリズムによって自動的に計算されます。
これにより第3者の仲介がなくても、プールを介してユーザー同士で取引することができるようになります。
AMMについて以下の動画でわかりやすく解説されています。
UNI
UNIとは、Uniswapアプリケーション内で利用できる独自トークンのことです。
「ガバナンストークン」として発行され、Uniswapプロジェクト運営に関わる投票時に利用可能です。
UNIの現在の価格は以下のサイトなどから確認できます。
https://coinmarketcap.com/ja/currencies/uniswap/
Swap
仮想通貨・トークン同士を交換することをスワップと言います。
流動性マイニング(イールドファーミング)
自分が保有しているERC20トークンをUniswapに貸し出して「Pool」という場所に預け入れることで、UNIを一定額もらうことを流動性マイニング(イールドファーミング)といいます。
基本的に「ETH-DAI」、「ETH-USDT」、「ETH-WBTC」など暗号資産をペアで貸し出します。
トークンを預け入れることで流動性を提供することができ、「好きな量を、好きな時に売買できる」状態を作り出すことに貢献できます。
そのため、流動性マイニング(イールドファーミング)を行うことで、UNIを報酬としてもらうことができます。
保有している通貨を失うことなくUNIを得ることができるため、リスクが少ない投資法です。
Uniswapバージョン
Uniswapには、「UniswapV1」、「UniswapV2」、「UniswapV3」3つのバージョンが存在します。
この記事で取り上げるのは「UniswapV2」になります。
なぜ「UniswapV3」を取り上げないのかこれから説明していきます。
「UniswapV1」は機能がシンプルすぎて、最新の機能の実装がなされていません。
「UniswapV3」は「UniswapV2」の機能も備わっていますが、改良・最適化されているため複雑になっています。
そのため、「UniswapV3」を理解するには、まず「UniswapV2」を理解した方が効率よく理解することができるため、今回は「UniswapV2」を解説して行きます。
Uniswapのコントラクト構成
では次に「UniswapV2」のコントラクト構成のについて確認してきましょう。
「UniswapV2」は4つのコントラクトで構成されていて、「Core」と「Periphery」の2つに分けることができます。
Core
Coreは以下の機能を備えています。
Coreの機能
- トークンの保管
- トークンの交換
- トークンの追加
- 報酬の獲得
Coreは以下の3つのコントラクトで構成されています。
Coreのコントラクト
- UniswapV2Pair
- UniswapV2Factory
- UniswapV2ERC20
それぞれ以下のような機能を備えています。
UniswapV2Pair
2つのトークンのペアごとに作成されます。
UniswapV2Pair
- トークンのスワップ(交換)
- トークンのミント(発行)
- トークンのバーン(焼却)
UniswapV2Factory
各トークンののペアごとにコントラクトを作成します。
UniswapV2ERC20
プールの所有権を管理するコントラクトです。
流動性提供者がプールに資金を追加すると、その報酬として「プール所有権トークン」を取得することができます。
流動性提供者がプールから資金を取り出したいときは、「プール所有権トークン」を渡し、資金と貯められた報酬を得ることができます。
このコントラクトで「プール所有権トークン」を追跡しています。
Periphery
Coreのコントラクトとやりとりするコントラクトです。
トークンのスワップや流動性提供などUniswapの機能はほとんどがこのコントラクトから実行されます。
Oracle
Uniswapは取引する2つのトークンの価格を随時取得する必要があります。
オンチェーン上で2つのトークンの価格を取得するために、オラクルが使用されます。
ただし、「特定の情報提供者に依存してしまう設計」は脆弱性を招くため、何かしらの対策が必要です。
そのため、Uniswapでは、直近数ブロック分のデータを利用することで、できるだけ現在の市場に近い価格を取得しつつ、意図的に不正確なデータを与える攻撃のコストを高めるような仕組みになっています。
UniswapV2Pair
まずは、UniswapV2Pair.sol
のコードから確認して行きましょう!
コードは以下になります。
https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol
1行ずつ確認して行きます。
読み込み
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Pair.sol';
import './UniswapV2ERC20.sol';
import './libraries/Math.sol';
import './libraries/UQ112x112.sol';
import './interfaces/IERC20.sol';
import './interfaces/IUniswapV2Factory.sol';
import './interfaces/IUniswapV2Callee.sol';
まずはSolidityのバージョンを指定して、libraries
やinterface
を読み込んでいます。
それぞれのコードはを置いておきます。
IUniswapV2Pair.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/interfaces/IUniswapV2Pair.sol
UniswapV2ERC20.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol
Math.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/libraries/Math.sol
UQ112x112.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/libraries/UQ112x112.sol
IERC20.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/interfaces/IERC20.sol
IUniswapV2Factory.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/interfaces/IUniswapV2Factory.sol
IUniswapV2Callee.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/interfaces/IUniswapV2Callee.sol
コントラクトとライブラリ定義
contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
using SafeMath for uint;
using UQ112x112 for uint224;
この部分ではまずUniswapV2Pairコントラクトを定義しています。
IUniswapV2Pair
コントラクトを定義してUniswapV2ERC20
コントラクトを継承しています。
SafeMath
ライブラリは、オーバーフローとアンダーフローに対策するためのライブラリです。
オーバーフローとアンダーフローについては以下を参考にしてください。
UQ112x112
ライブラリは、浮動小数点数をサポートするためのライブラリです。
Solidityでは、浮動小数点数をサポートしていないため、UQ112x112
ライブラリを使用しています。
変数定義
uint public constant MINIMUM_LIQUIDITY = 10**3;
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
address public factory;
address public token0;
address public token1;
uint112 private reserve0; // uses single storage slot, accessible via getReserves
uint112 private reserve1; // uses single storage slot, accessible via getReserves
uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event
uint private unlocked = 1;
MINIMUM_LIQUIDITY
流送性プールにトークンを預けることで、流動性プール内の最小数量(1e-18プールシェア)価値が時間の経過とともに上昇して行きます。
そのため少額の流動性を提供することが不可能になる問題が発生します。
これを緩和するために、1e-15 (0.000000000000001)
プールシェアを焼却している。
少量であるため、ほとんどのトークンペアにとっては無視できるコストとなる。
SELECTOR
ERC20コントラクトのABI(Contract Application Binary Interface)セレクタです。
2つのERC20トークンを送る際に使用されます。
factory
プールを作成したUniswapV2Factory
コントラクトのアドレスです。
token0 & token1
このプール内で交換可能な2種類のERC20トークンのコントラクトアドレスです。
一定のアルゴリズムで各トークンのアドレスを用いて、token0
とtoken1
に格納する順番を制御している。
reserve0 & reserve1
プール内に存在するtoken0
とtoken1
の量を格納します。
reserve0
とreserve1
は数字は異なりますが、価値は同じになります。
日本円とドルの関係と同じことをイメージすると理解しやすいです。
例)1$ = 132円
blockTimestampLast
トークンの交換が発生した直近のタイムスタンプを格納します。
ちなみにreserve0
とreserve1
とblockTimestampLast
は同じスロットに格納されます。
reserve0
+ reserve1
+ blockTimestampLast
= 112
+ 112
+ 32
= 256
スロットについては以下の記事を参考にしてください。
price0CumulativeLast & price1CumulativeLast
各トークンの累積コスト(直近のtoken0
とtoken1
の価格)を保持する変数です。
一定期間の平均為替レートを計算するために使用します。
kLast
x * y = K
というアルゴリズムに沿って、reserve0
* reserve1
の数値を格納する。
unlocked
ERC20トークンを送る際に、処理が終わる前に悪意を持ってERC20コントラクトを呼び出される可能性があります。
処理が開始された際にunlocked
の値を0
にすることで、別のERC20トークンの送付が起きないようにする。
以下の記事でより詳しく書いています。
修飾子定義
modifier lock() {
require(unlocked == 1, 'UniswapV2: LOCKED');
unlocked = 0;
_;
unlocked = 1;
}
先ほどのunlocked
変数で述べた処理をここで行なっています。
2行目で他の処理が走っているか確認し、もし他の処理が走っていたらエラーを返すようにしています。
3~5行目では、他の処理が走っていない時にunlocked
変数に0
を格納(ロック)し、_
の部分でlock
修飾子を継承している関数の処理を実行したのち、unlocked
変数に1
を格納してロックを解除しています。
getReserves
function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
reserve0
とreserve1
とblockTimestampLast
の値を返す関数。
_safeTransfer
function _safeTransfer(address token, address to, uint value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
}
token
で指定したERC20トークンをvalue
で指定した量、to
で指定したアドレスへ送る関数。
以下の2つの条件がどちらかがtrue出ない時、処理を元に戻します。
条件
- ERC20トークンの送付の戻り値
success
がtrue
。 - 送付結果の戻り値
data
に何かしらのデータが含まれていないか、data
をデコードした戻り値がtrue
Event系
event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);
4つのイベントが定義されています。
Mint
流動性提供者がERC20トークンを預ける時に発行される。
変数
sender
= 関数実行アドレス。amount0
=token0
の量。amount1
=token1
の量。
Burn
流動性提供者がERC20トークンを引き出す時に発行される。
変数
sender
= 関数実行アドレス。amount0
=token0
の量。amount1
=token1
の量。to
= ERC20トークンを受け取るアドレス。
Swap
ERC20トークンをスワップ(交換)した時に発行される。
Sync
ERC20トークンが追加される、または引き出されるたびに発行される。
最新の為替レートが提供される。
Constructor
constructor() public {
factory = msg.sender;
}
コントラクトがデプロイされた時に一度だけ実行される関数。
favtory変数に、トークンのペアを作成したUniswapV2Factory
コントラクトのアドレスが格納されます。
initialize
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
token0 = _token0;
token1 = _token1;
}
作成されたこのUniswapV2Pair
コントラクトが交換する2つのERC20トークンを指定する関数。
_update
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// * never overflows, and + overflow is desired
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}
ERC20トークンの入出金時に呼び出される関数。
require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
balance0
とbalance1
がオーバーフローしないか確認しています。
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// * never overflows, and + overflow is desired
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
timeElapsed
(前回のスワップ実行からの経過時間)が0
でない場合、ブロック内での最初のスワップ(交換)であることを意味する。
この場合price0CumulativeLast
とprice1CumulativeLast
の値を更新する。
更新する値は(_reserve1 / _reserve0 もしくは _reserve0 / _reserve1)x timeElapsed
となります。
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
最後にreserve0
とreserve1
とblockTimestampLast
の値を更新して、Sync
イベントを発行しています。
_mintFee
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
address feeTo = IUniswapV2Factory(factory).feeTo();
feeOn = feeTo != address(0);
uint _kLast = kLast; // gas savings
if (feeOn) {
if (_kLast != 0) {
uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) {
uint numerator = totalSupply.mul(rootK.sub(rootKLast));
uint denominator = rootK.mul(5).add(rootKLast);
uint liquidity = numerator / denominator;
if (liquidity > 0) _mint(feeTo, liquidity);
}
}
} else if (_kLast != 0) {
kLast = 0;
}
}
取引が行われるたびにUniswapV2Factory
コントラクトが指定したアドレスに支払われる手数料を計算する関数。
UniswapV2では、取引が行われるたびに0.3%
の手数料を流動性提供者とUniswapV2Factory
コントラクトが指定したアドレスに支払います。
取引が行われるたびに手数料の1/6
が支払われるため、その計算を行なっている。
address feeTo = IUniswapV2Factory(factory).feeTo();
ERC20トークンのペアを作成したUniswapV2Factory
コントラクトで指定した手数料の送付先アドレスを取得して、feeTo
変数に格納しています。
uint _kLast = kLast; // gas savings
kLast
の値を_kLast
変数に格納しているのは、ストレージ変数からメモリ変数に格納することで、ガス代を節約しています。
if (feeOn) {
if (_kLast != 0) {
送付先が0アドレスではなく、_kLast
の値が0
でない(プール内のどちらか片方のERC20トークンが0
でない)時以下の処理を行います。
uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) {
uint numerator = totalSupply.mul(rootK.sub(rootKLast));
uint denominator = rootK.mul(5).add(rootKLast);
uint liquidity = numerator / denominator;
if (liquidity > 0) _mint(feeTo, liquidity);
}
ERC20トークンがプールに預けられていればrootKLast
の値よりrootK
の値の方が大きくなり、手数料を徴収することができます。
ここではだいぶ複雑な計算をしていますが、やっていることとしてはUniswapV2Factory
コントラクトが指定したアドレスに送るために、どのくらい流動性トークン(LP)を作成しなければいけないか計算しています。
kLast = 0;
手数料がないときにkLast
の値を0
に設定しています。
Uniswapが作成された当時は、不要なストレージをゼロにすると、Ethereumのサイズを小さ句なりガス代を返金してくれる機能があったため、このような処理が行われています。
より詳しくは以下の記事を参考にしてください。
https://learnblockchain.cn/article/3987
mint
function mint(address to) external lock returns (uint liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
}
流動性提供者がERC20トークンを貸し出した際に呼び出され、流動性トークンを作成する関数。
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
_reserve0
と_reserve1
、balance0
、balance1
の値を取得し、各トークンがどれだけ追加されたかをamount0
とamount1
に格納。
bool feeOn = _mintFee(_reserve0, _reserve1);
先ほど解説した_mintFee
関数を実行し、徴収する手数料があれば計算して流動性トークンを作成します。
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
UniswapV2ERC20
コントラクト内のtotalSupply
変数を取得して、プール内のペアのERC20トークンを_totalSupply
に格納しています。
初めての入金の場合はMINIMUM_LIQUIDITY
分のLPトークンを作成し、0アドレスに送ります。
このトークンは換金できないため、プール内が完全に空になることはありません。
最初の入金では2つのトークンの相対的な価値がわからないため、入金によって両方のトークンの価値が等しくなると仮定して平方根を取っています。
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
2回目以降の入金であれば、2つのトークンの相対的な価値がわかっているため、流動性提供者が同等の価値の2つのトークンの貸し出しを期待します。
もし異なれば、罰として貸し出したトークンのうち価値の低い方を基準に流動性トークン(LP)を与える。
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
UniswapV2ERC20
コントラクトの_mint
関数を使用して、流動性トークン(LP)を作成し、toで指定したアドレスに送っています。
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
_reserve0
と_reserve1
とkLast
の値を更新し、Mint
イベントを発行しています。
burn
function burn(address to) external lock returns (uint amount0, uint amount1) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);
}
流動性提供者が貸し出していたERC20トークンを引き出し、その分の流動性トークン(LP)を燃やす関数。
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];
reserve0
と_reserve1
、balance0
、balance1
、燃やすべき流動性トークンの量(LP)をliquidity
に格納しています。
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
_mintFee
関数を実行し、徴収する手数料があれば計算して流動性トークンを作成します。
UniswapV2ERC20
コントラクト内のtotalSupply
変数を取得して、プール内のペアのERC20トークンを_totalSupply
に格納しています。
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
amount0
とamount1
に2つのトークンの引き出す量を計算して格納して、0
以上か確認しています。
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
liquidity
分の流動性トークン(LP)を燃やして、amount0
分の_token0
と、amount1
分の_token1
をto
に指定したアドレスへ送っています。
_token0
と_token1
の残高をそれぞれbalance0
とbalance1
に格納しています。
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);
_reserve0
と_reserve1
とkLast
の値を更新し、Burn
イベントを発行しています。
swap
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
uint balance0;
uint balance1;
{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
プール内の1つのERC20トークンを預けて、もう片方のERC20トークンを引き出すPeriphery
コントラクトから呼び出されることを想定している関数。
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
uint balance0;
uint balance1;
引き出すトークンの量がどちらかが0
以上であるか確認しています。
_reserve0
と_reserve1
を取得して、現在プール内にあるERC20トークンの量がそれぞれamount0Out
とamount0Ou1
より多いことを確認している。
{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
token0
とtoken1
の値を取得して、to
に指定されているアドレスが不適切でないか確認しています。
引き出したいERC20トークンをamount0Out
、もしくamount1Out
分to
で指定したアドレスに送っています。
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
data
に0
以外の値を指定すれば、スワップについてトークンの受け取りアドレスに通知ができる。
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
_token0
と_token1
の現在の残高を取得しています。
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}
スワップによる損失がないかを確認しています。
スワップによって_reserve0 * _reserve1
の値が減少することはありません。
それと同時に0.3%
の手数料がスワップによって送られていることも確認しています。
balance0
とbalance1
にそれぞれ1000
をかけて、金額を3
倍したものを引きます。
これは残高から0.3%(3/1000 = 0.003 = 0.3%)
引かれたことを意味します。
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
_reserve0
と_reserve1
の値を更新し、Swap
イベントを発行しています。
sync
function sync() external lock {
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}
実際の残高が、コントラクト側とずれてしまうことがあるため、reserve0
とreserve1
の値を更新する関数。
ERC20トークンを引き出すことはまずいですが、入金する分には問題ないためERC20トークンを送っています。
skim
function skim(address to) external lock {
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
_safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
_safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}
実際の残高が、コントラクト側とずれてしまうことがあるため、reserve0
とreserve1
の値を更新する関数。
こちらは余分なERC20トークンを引き出します。
誰がERC20トークンを預けたかわからないため、どのアカウントアドレスからでも呼び出すことができてしまいます。
UniswapV2Factory
次にUniswapV2Factoryコントラクトを見て行きます。
コードは以下になります。
https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Factory.sol
1行ずつ確認して行きます。
読み込み
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';
Solidityのバージョンを指定して、必要なinterfaceとコントラクトを読み込んでいます。
IUniswapV2Factory.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/interfaces/IUniswapV2Factory.sol
UniswapV2Pair.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol
コントラクト定義と継承
contract UniswapV2Factory is IUniswapV2Factory {
UniswapV2Factory
を定義して、IUniswapV2Factory
コントラクトを継承しています。
変数定義
address public feeTo;
address public feeToSetter;
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
feeTo
流動性トークンを貯めるアドレス。
feeToSetter
feeToで指定したアドレスを別のアドレスに変更するアドレス。
getPair
交換する2つのERC20トークンをもとにスワップ(交換)を実行するために必要なコントラクトのアドレスを保管。
getPair[tokenAのアドレス][tokenBのアドレス]
で取得できます。
allPairs
このUniswapV2Factory
コントラクトで作成された、ERC20トークンペアのUniswapV2Pair
コントラクトのアドレスを格納する配列。
Event系
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
新しいERC20トークンのペアが作成された時に発行される。
Constructor
constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}
コントラクトがデプロイされた時に一度だけ実行される関数。
_feeToSetter
で指定したアドレスをfeeToSetter
に格納。
allPairsLength
function allPairsLength() external view returns (uint) {
return allPairs.length;
}
作成されたERC20トークンのペアの数を返す関数。
createPair
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
IUniswapV2Pair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
2つのERC20トークンのペアコントラクトを作成する関数。
Uniswap側への許可は不要で、誰でも呼び出すことができます。
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
tokenAとtokenBが同じでないことを確認しています。
2つのトークンのアドレスを確認し、アドレスの数値が低い方がtoken0
、大きい方がtoken1
となるように、一貫した順番に並び替えている。
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
すでにERC20トークンのペアがないか確認しています。
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
新しいコントラクトを作成するためのオペコードの1つです。
他のコントラクトのアドレスとの衝突を防ぎます。
https://docs.openzeppelin.com/cli/2.8/deploying-with-create2
IUniswapV2Pair(pair).initialize(token0, token1);
作成したUniswapV2Pair
コントラクトのinitialize
関数を実行し、2つのトークンの情報を渡しています。
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
getPair
配列に新しいERC20トークンのペア情報保存し、PairCreated
イベントを発行しています。
setFeeTo
function setFeeTo(address _feeTo) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeTo = _feeTo;
}
feeToSetter
に格納されているアドレスのみが呼び出すことができ、feeTo
に_feeTo
で指定したアドレスを格納する関数。
手数料を受け取るアカウントを変更することができます。
setFeeToSetter
function setFeeToSetter(address _feeToSetter) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeToSetter = _feeToSetter;
}
feeToSetter
に格納されているアドレスのみが呼び出すことができ、feeToSetter
に_feeToSetter
で指定したアドレスを格納する関数。
UniswapV2ERC20
次にUniswapV2ERC20コントラクトを見て行きます。
コードは以下になります。
https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol
1行ずつ確認して行きます。
読み込み
pragma solidity =0.5.16;
import './interfaces/IUniswapV2ERC20.sol';
import './libraries/SafeMath.sol';
Solidityのバージョンを指定して、必要なinterfaceとコントラクトを読み込んでいます。
IUniswapV2ERC20.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/interfaces/IUniswapV2ERC20.sol
SafeMath.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/libraries/Math.sol
コントラクトとライブラリの定義
contract UniswapV2ERC20 is IUniswapV2ERC20 {
using SafeMath for uint;
UniswapV2ERC20
を定義して、IUniswapV2ERC20
コントラクトを継承しています。
SafeMath
ライブラリは、オーバーフローとアンダーフローに対策するためのライブラリです。
オーバーフローとアンダーフローについては以下を参考にしてください。
変数定義
string public constant name = 'Uniswap V2';
string public constant symbol = 'UNI-V2';
uint8 public constant decimals = 18;
uint public totalSupply;
mapping(address => uint) public balanceOf;
mapping(address => mapping(address => uint)) public allowance;
bytes32 public DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
mapping(address => uint) public nonces;
name
ERC20トークンの名前を格納。
symbol
ERC20トークンのシンボルを格納。
decimals
トークンが使用する小数点以下の桁数を格納。
totalSupply
トークンの総量を格納。
balanceOf
特定のアドレスがどれくらいのトークンを保持しているか格納する配列。
allowance
特定のアドレスAが、別のアドレスBにアドレスAが保有しているトークンを指定した量だけ操作できる許可を与える配列。
DOMAIN_SEPARATOR
ERC712標準の他のDappでは署名で使用できないような衝突が起きないことを保証した値。
Uniswapでは、承認を行う際にEthereumの秘密鍵でデータに署名するのは難しいため、安全性とウォレットの互換性を確保するために、コミュニティが広くサポートしている署名標準でであるERC712に依存している。
PERMIT_TYPEHASH
Solidityのコンパイル時に定数になるように設計される許可証。
DOMAIN_SEPARATORとPERMIT_TYPEHASHについては筆者の理解が乏しいのでぜひ以下の記事などを参考にしてください。
https://eips.ethereum.org/EIPS/eip-712
nonces
受信者がデジタル署名を偽造することは実行不可能だが、同じトランザクションを2回送信することは簡単である。
以下の記事が参考になります。
これを防ぐために、アドレスをキーにした配列を作成し、値にはnonceと呼ばれる数値を使用して、前回使用したnonceよりも1つ多い値でないと実行が無効となる。
Event系
event Approval(address indexed owner, address indexed spender, uint value);
event Transfer(address indexed from, address indexed to, uint value);
Approval
owner
が所有するERC20トークンのうち、value
分だけspender
に操作権限を与えたときに発行される。
Transfer
from
で指定したアドレスから、value
分のERC20トークンをto
に指定したアドレスに送ったときに発行される。
Constructor
constructor() public {
uint chainId;
assembly {
chainId := chainid
}
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
keccak256(bytes(name)),
keccak256(bytes('1')),
chainId,
address(this)
)
);
}
コントラクトがデプロイされた時に一度だけ実行される関数。
constructor() public {
uint chainId;
assembly {
chainId := chainid
}
EthereumのchainId
を取得しています。
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
keccak256(bytes(name)),
keccak256(bytes('1')),
chainId,
address(this)
)
);
EIP712のドメインセパレーターを算出しています。
abi.encode
で囲ったものを全てくっつけてハッシュ化しています。
以下の関数はERC20を確認していただくことで理解できるので、以下の記事を参考にしてください。
ERC20の関数
_mint
_burn
_approve
_transfer
_approve
transfer
trandferFrom
permit
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}
パーミッションを実装する関数。
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
期限を過ぎた取引は無効にされます。
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
DOMAIN_SEPARATOR
を生成しています。
address recoveredAddress = ecrecover(digest, v, r, s);
ecrecover
という関数を使用して署名したアドレスを取得しています。
https://coders-errand.com/ecrecover-signature-verification-ethereum/
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
_approve
関数を実行して、署名したアドレスが0アドレスでないことを確認して、owner
が指定したアドレスの所有しているvalue
分のERC20トークンの操作権限をspender
で指定したアドレスに与えています。
UniswapV2Router02
最後にPeripheryコントラクトの中のUniswapV2Router02コントラクトについてまとめていきます。
Peripheryコントラクトは、他のコントラクトや分散型アプリケーションからの外部呼び出しに使用することができます。
Coreコントラクトを直接呼び出すことはもちろんできますが、複雑かつ、間違えて資金を失う可能性があります。
Coreコントラクトには、不正をしていないことを確認するためのテストが含まれているだけで、不具合がないかのチェックがありません。
Peripheryコントラクトであれば、不具合のチェックなども行なっているため、セキュリティ面でCoreコントラクトを直接触ることは推奨されません。
コードは以下になります。
https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol
読み込み
pragma solidity =0.6.6;
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
import './interfaces/IUniswapV2Router02.sol';
import './libraries/UniswapV2Library.sol';
import './libraries/SafeMath.sol';
import './interfaces/IERC20.sol';
import './interfaces/IWETH.sol';
IUniswapV2Factory.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/interfaces/IUniswapV2Factory.sol
TransferHelper.sol
https://github.com/Uniswap/solidity-lib/blob/master/contracts/libraries/TransferHelper.sol
IUniswapV2Router02.sol
https://github.com/Uniswap/v2-periphery/blob/master/contracts/interfaces/IUniswapV2Router02.sol
UniswapV2Library.sol
https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol
SafeMath.sol
https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/SafeMath.sol
IERC20.sol
https://github.com/Uniswap/v2-periphery/blob/master/contracts/interfaces/IERC20.sol
IWETH.sol
https://github.com/Uniswap/v2-periphery/blob/master/contracts/interfaces/IWETH.sol
UniswapV2はERC20トークンの任意のペアの交換ができますが、ETH自体はERC20トークンではありません。
ERC20トークン規格のコントラクトでETHを使用するためには、WETHが考案されました。
このコントラクトにETHを送ると、同量のWETHが生成されます。
WETHを燃やしてETHを取得することもできます。
コントラクトとライブラリの定義
contract UniswapV2Router02 is IUniswapV2Router02 {
using SafeMath for uint;
UniswapV2Router02
コントラクトを定義し、IUniswapV2Router02
コントラクトを継承しています。
SafeMath
ライブラリは、オーバーフローとアンダーフローに対策するためのライブラリです。
オーバーフローとアンダーフローについては以下を参考にしてください。
変数の定義
address public immutable override factory;
address public immutable override WETH;
factoryコントラクトのアドレスとWETHコントラクトのアドレスを格納します。
immutable
とついているように、一度設定すると変更はできません。
修飾子の定義
modifier ensure(uint deadline) {
require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
_;
}
このmodifier
をつけた関数での取引では、時間制限を設けることができます。
deadline
という期限を現在時刻より後である必要があります。
Constructor
constructor(address _factory, address _WETH) public {
factory = _factory;
WETH = _WETH;
}
コントラクトがデプロイされた時に一度だけ実行される関数。
どのfactoryコントラクトを使用するか、どのWETHコントラクトを使用するか指定します。
receive
receive() external payable {
assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
}
WETHコントラクトからERC20トークンをETHに戻す時に呼ばれる関数。
WETHコントラクトのみがERC20トークンをETHに戻すことができる。
_addLiquidity
function _addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
// create the pair if it doesn't exist yet
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}
ERC20トークンのペアを貸し出し、プールに流動性を追加する関数。
address tokenA,
address tokenB,
ERC20トークンのアドレスです。
uint amountADesired,
uint amountBDesired,
流動性提供者が預け入れを希望するERC20トークンの価格です。
uint amountAMin,
uint amountBMin
預け入れ可能な最低金額です。
この金額以上の取引ができない場合は、取引を中止して元に戻します。
この機能が不要な場合は値に0を指定することもできます。
しかし、流動性提供者は通常預入の最低金額を指定します。
理由としては、取引を現在の価格に近いものに制限したいからです。
価格が大きく変動した際、預け入れの想定金額から大きくズレる可能性があります。
そのため許容できる範囲でのみ取引が実行されるように、基本的に預け入れの最低金額をせってしています。
) internal virtual returns (uint amountA, uint amountB) {
現在のERC20トークンの価格比率と等しくなるように、流動性提供者が預けるべき金額を返します。
// create the pair if it doesn't exist yet
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
tokenA
とtokenB
のペアが存在しない場合は新たに作成します。
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
指定したトークンペアの現在のプール内に預け入れられている量を取得します。
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
まだ流動性提供されていなければ、預け入れる金額は流動性提供者が預け入れたい金額と同じになる。
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
預け入れる金額がどのようになるかはUniswapV2Library
コントラクトのquote
関数を使用する。
現在の2つのERC20トークンの比率と同じになる。
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
先ほど取得したamountBOptimal
の値が流送性提供者が預けたい金額より小さい場合、tokenBは流動性提供者が考えているよりも現在価値が高いことを意味するため、より少ない金額が必要となる。
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
逆に、amountBOptimal
の値が流送性提供者が預けたい金額より大きい場合、流動性提供shが考えているよりもtokenBの現在価値が低いことを意味するため、より多くの金額が必要となる。
しかし、預け入れたい希望金額よりも大きいため、tokenB
を基準に計算できないため、代わりにtokenA
を基準に預け入れに必要なトークンの数を計算する。
これらをまとめると以下のグラフのようになります。
1000tokenA
(青線)と1000tokenB
(赤線)を預けるとします。
X軸は為替レートでtokenA/tokenB
となっています。
x = 1
の時、2つのトークンの価値は等しくなるため1000
トークンずつ預け入れます。
x = 2
の時、tokenA
はtokenB
の2倍の価値があるため、1000tokenB
に対して、500tokenA
を預け入れれば良いです。
逆にx = 0.5
の時、1000tokenA
に対して、500tokenB
を預け入れれば良いです。
Coreコントラクトに直接流動性を提供することもできるが、Coreコントラクトは自分が騙されていないことを確認するだけで、取引を送信してから実行されるまでに為替レートが変化すると、価値を失う危険性があります。
一方、Peripheryコントラクトを使用すると、預け入れるべき金額を計算後すぐに預け入れが可能なため、為替レートが変化せずに損をすることがなくなる。
addLiquidity
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
}
流動性を提供するために呼び出される関数。
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
引数は_addLiquidity
関数とほとんど同じです。
to
流動性を提供した際に受け取れる流動性トークン(LP)を受け取るアドレス。
deadline
トランザクションの制限時間。
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
_addLiquidity
関数を実行して、実際に預け入れる金額を計算し、ガス代節約のために直接factoryに問い合わせずにUniswapV2Library
コントラクトのpairFor
関数を実行して、トークンの預け入れ先のアドレスを取得しています。
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
tokenA
とtokenB
を先ほど取得したpairのアドレスへ、amountA
とamounB
分預け入れます。
liquidity = IUniswapV2Pair(pair).mint(to);
流動性を提供した報酬として、流動性提供者に流動性トークン(LP)を渡しています。
預け入れたERC20トークンの量に応じて受け取れる流動性トークン(LP)も変わってきます。
addLiquidityETH
function addLiquidityETH(
address token,
uint amountTokenDesired,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
(amountToken, amountETH) = _addLiquidity(
token,
WETH,
amountTokenDesired,
msg.value,
amountTokenMin,
amountETHMin
);
address pair = UniswapV2Library.pairFor(factory, token, WETH);
TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
IWETH(WETH).deposit{value: amountETH}();
assert(IWETH(WETH).transfer(pair, amountETH));
liquidity = IUniswapV2Pair(pair).mint(to);
// refund dust eth, if any
if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}
流動性提供者がERC20トークンとETHのペアを預け入れた時にETHのラッピング処理を行い、流動性の提供を行う関数。
address token,
uint amountTokenDesired,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
(amountToken, amountETH) = _addLiquidity(
token,
WETH,
amountTokenDesired,
msg.value,
amountTokenMin,
amountETHMin
);
address pair = UniswapV2Library.pairFor(factory, token, WETH);
TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
IWETH(WETH).deposit{value: amountETH}();
assert(IWETH(WETH).transfer(pair, amountETH));
ほとんどaddLiquidity
関数を処理は同じです。
異なる点としては、ETHをERC20トークンであるWETHに変換してtoken
とペアにして預け入れを実行しています。
送付に失敗するとWETHの変換も無効になります。
liquidity = IUniswapV2Pair(pair).mint(to);
// refund dust eth, if any
if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
流動性を提供した報酬として、流動性提供者に流動性トークン(LP)を渡しています。
預け入れたWETHに対して、ユーザー送ってきたETHの方が多い場合はその分をユーザーに戻しています。
removeLiquidity
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}
預け入れているトークンを引き出す関数。
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
ほとんどaddLiquidity
関数を処理は同じです。
流動性提供者が引き出しを同意する各トークンの最低量を指定しているため、deadline
までに実行されなければ無効になる。
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
預け入れている2つのトークンを燃やしています。
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
tokenA
とtokenB
を適切に並び替え、片方だけ取得しています。
これは片方の値だけ取得できればよく、ガス代の節約になるからです。
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
先ほど取得したtoken0
とtokenA
が同じであれば、amount0
をamountA
に格納し、異なればamount1
をamountB
に格納する。
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
流動性提供者が、引き出したい最低量のトークンよりも少ないトークンを引き出してしまっていないか確認しています。
先にトークンを流動性提供者に渡しても、正当でない場合状態を元に戻ることができます。
removeLiquidityETH
function removeLiquidityETH(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
(amountToken, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
TransferHelper.safeTransfer(token, to, amountToken);
IWETH(WETH).withdraw(amountETH);
TransferHelper.safeTransferETH(to, amountETH);
}
預け入れているトークンとETHのペアを引き出す関数。
行なっていることはremoveLiquidity
関数とほとんど同じです。
違う点としては、WETHトークンを受け取ったのちにETHに変換してから流動性提供者に返しています。
removeLiquidityWithPermit
function removeLiquidityWithPermit(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
(amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
}
事前承認を行わずremoveLiquidity
関数を実行して、預け入れているERC20トークンを引き出す関数。
removeLiquidityETHWithPermit
function removeLiquidityETHWithPermit(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountToken, uint amountETH) {
address pair = UniswapV2Library.pairFor(factory, token, WETH);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
(amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
}
事前承認を行わずremoveLiquidityETHWithPermit
関数を実行して、預け入れているERC20トークンとETHを引き出す関数。
removeLiquidityETHSupportingFeeOnTransferTokens
function removeLiquidityETHSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountETH) {
(, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
IWETH(WETH).withdraw(amountETH);
TransferHelper.safeTransferETH(to, amountETH);
}
預け入れているERC20トークンとETHを引き出すときに、手数料が発生する場合に実行される関数。
removeLiquidity関数を実行して、まずはERC20トークンとWETHを引き出したのち、引き出したETHの量を取得して手数料を支払っています。
removeLiquidityETHWithPermitSupportingFeeOnTransferTokens
function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountETH) {
address pair = UniswapV2Library.pairFor(factory, token, WETH);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
token, liquidity, amountTokenMin, amountETHMin, to, deadline
);
}
事前承認を行わずremoveLiquidityETHSupportingFeeOnTransferTokens
関数を実行して、預け入れているERC20トークンとETHを引き出して、手数料を支払う関数。
_swap
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
for (uint i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
uint amountOut = amounts[i + 1];
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}
ERC20トークンのスワップ(交換)時に実行ユーザーに公開される機能に必要な内部の処理を行う関数。
for (uint i; i < path.length - 1; i++) {
トークンA、トークンB、トークンC、トークンDの4つのトークンがある時に、「A-B」、「B-C」、「C-D」のペアがあるとします。
この時パスの概念を使用して、「A-D」のスワップを、「A→B」、「B→C」、「C→D」の順に交換することで、「A-D」のスワップを直接行う必要がなくなる。
スワップの手順
24.69トークンA
を売って、25.30トークンB
を取得する。24.69トークンB
を売って、25.30トークンC
を取得して、約0.61トークンB
を利益として残す。24.69トークンC
を売って、25.30トークンA
を取得して、約0.61トークンC
を利益として残す。- 約
0.61トークンA
を利益として保有。(25.30トークンA
から投資額の24.69トークンA
を差し引いた額)
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
uint amountOut = amounts[i + 1];
現在扱っているERC20トークンのペアを取得して並び替えています。
その後トークンの取得予定料を計算しています。
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
予想される出力金額をトークンをソートして取得しています。
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
最後のスワップ(交換)であれば、うけとったERC20トークンを受け取りアドレスに送ります。
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
実際にERC20トークンのスワップ(交換)を実行。
swapExactTokensForTokens
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
ユーザーERC20トークンのスワップ(交換)するために直接使用される関数。
uint amountIn,
uint amountOutMin,
address[] calldata path,
path
配列には、先ほど_swap
関数の部分で説明したように現在所有しているトークンから目的のトークンを交換するために、いくつかのトークンペアを経由する必要があるため、その情報が格納されています。
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
各スワップ(交換)で購入する金額を計算しています。
もしその金額がユーザーが受け入れることができる最小値より小さい場合はエラーを起こし実行を無効にします。
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
最初のERC20トークンを最初のスワップ用のアカウントに送り、_swap
関数を呼び出しています。
swapTokensForExactTokens
function swapTokensForExactTokens(
uint amountOut,
uint amountInMax,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
swapExactTokensForTokens
関数の逆のスワップを行う関数。
先ほど解説したswapExactTokensForTokens
関数は、ユーザーが預け入れるトークンの正確な量と、その報酬として受け取るトークンの最小数を指定できました。
ユーザーが受け取りたいトークン数と、そのために支払う最大数の預け入れトークンを指定することができます。
swapExactETHForTokens
function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
external
virtual
override
payable
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
IWETH(WETH).deposit{value: amounts[0]}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
_swap(amounts, path, to);
}
預け入れたいETHを指定して、ETHを預け入れてERC20トークンを受け取れる関数。
swapTokensForExactETH
function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
external
virtual
override
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, address(this));
IWETH(WETH).withdraw(amounts[amounts.length - 1]);
TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}
取得したいETHを指定して、ERC20トークンを預け入れしてETHを受け取れる関数。
swapExactTokensForETH
function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
external
virtual
override
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, address(this));
IWETH(WETH).withdraw(amounts[amounts.length - 1]);
TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}
預け入れたいERC20トークンを指定して、ERC20トークンを預け入れしてETHを受け取れる関数。
swapETHForExactTokens
function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
external
virtual
override
payable
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
IWETH(WETH).deposit{value: amounts[0]}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
_swap(amounts, path, to);
// refund dust eth, if any
if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
}
取得したいERC20トークンを指定して、ETHを預け入れしてERC20トークンを受け取れる関数。
_swapSupportingFeeOnTransferTokens
function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
for (uint i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
uint amountInput;
uint amountOutput;
{ // scope to avoid stack too deep errors
(uint reserve0, uint reserve1,) = pair.getReserves();
(uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
}
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
pair.swap(amount0Out, amount1Out, to, new bytes(0));
}
}
送金手数料や保管手数料がかかるERC20トークンをスワップ(交換)するための関数。
まずはユーザーからERC20トークンを預けてもらい、返ってきた値を元に手数料を計算して処理を行っています。
_swap
関数を実行しても手数料処理を行うことはできます。
しかし、より多くのガス代が発生するのと、手数料が発生するトークンは稀なのでこの関数を実行した方が良いです。
swapExactTokensForTokensSupportingFeeOnTransferTokens
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) {
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
);
uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
_swapSupportingFeeOnTransferTokens(path, to);
require(
IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
);
}
swapExactTokensForTokens
関数と同じ処理を実行し、ERC20トークンの送付時に手数料を取る関数。
swapExactETHForTokensSupportingFeeOnTransferTokens
function swapExactETHForTokensSupportingFeeOnTransferTokens(
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
)
external
virtual
override
payable
ensure(deadline)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
uint amountIn = msg.value;
IWETH(WETH).deposit{value: amountIn}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
_swapSupportingFeeOnTransferTokens(path, to);
require(
IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
);
}
swapExactETHForTokens
関数と同じ処理を実行し、transfer
時に手数料を取る関数。
swapExactTokensForETHSupportingFeeOnTransferTokens
function swapExactTokensForETHSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
)
external
virtual
override
ensure(deadline)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
);
_swapSupportingFeeOnTransferTokens(path, address(this));
uint amountOut = IERC20(WETH).balanceOf(address(this));
require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
IWETH(WETH).withdraw(amountOut);
TransferHelper.safeTransferETH(to, amountOut);
}
swapExactTokensForETH
関数と同じ処理を実行し、transfer
時に手数料を取る関数。
quote
function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
return UniswapV2Library.quote(amountA, reserveA, reserveB);
}
UniswapV2Library
コントラクトのquote
関数を実行する関数。
特定のERC20トークンの量を与えられると、同等の価値を持つもう片方のトークン量を返します。
getAmountOut
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
public
pure
virtual
override
returns (uint amountOut)
{
return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
}
UniswapV2Library
コントラクトのgetAmountOut
関数を実行する関数。
預け入れるトークンの金額が与えられると、もう片方のトークンの交換できる最大の金額が取得できます(手数料を考慮した値)。
getAmountIn
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
public
pure
virtual
override
returns (uint amountIn)
{
return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
}
UniswapV2Library
コントラクトのgetAmountIn
関数を実行する関数。
指定したトークンの金額(手数料を考慮した値)の交換に必要な、もう片方の最小限のトークンの金額を取得します。
getAmountsOut
function getAmountsOut(uint amountIn, address[] memory path)
public
view
virtual
override
returns (uint[] memory amounts)
{
return UniswapV2Library.getAmountsOut(factory, amountIn, path);
}
UniswapV2Library
コントラクトのgetAmountsOut
関数を実行する関数。
入力トークン量とトークンアドレスであるpath
配列を与えて、path
配列内のトークンアドレスのペアごとにgetReserves
関数を呼び出して、取得できる最大のトークン量を計算してgetAmountOut
関数を呼び出します。
getAmountsIn
function getAmountsIn(uint amountOut, address[] memory path)
public
view
virtual
override
returns (uint[] memory amounts)
{
return UniswapV2Library.getAmountsIn(factory, amountOut, path);
}
UniswapV2Library
コントラクトのgetAmountsIn
関数を実行する関数。
取得したいトークン量とトークンアドレスであるpath
配列を与えて、path
配列内のトークンアドレスのペアごとにgetReserves
関数を呼び出して、取得できる最大のトークン量を計算してgetAmountIn
関数を呼び出します。
UniswapV2Library
前章でUniswapのメインのコントラクトは解説しました。
次にUniswapV2Library.sol
のコードを確認していきます。
コードは以下になります。
https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol
1行ずつ確認して行きます。
読み込み
pragma solidity >=0.5.0;
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
import "./SafeMath.sol";
まずはSolidityのバージョンを指定して、libraries
を読み込んでいます。
それぞれのコードはを置いておきます。
IUniswapV2Pair.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/interfaces/IUniswapV2Pair.sol
Math.sol
https://github.com/Uniswap/v2-core/blob/master/contracts/libraries/Math.sol
コントラクトとライブラリ定義
library UniswapV2Library {
using SafeMath for uint;
この部分ではまずUniswapV2Pair
ライブラリを定義しています。
UniswapV2Library
コントラクトを定義しています。
SafeMath
ライブラリは、オーバーフローとアンダーフローに対策するためのライブラリです。
オーバーフローとアンダーフローについては以下を参考にしてください。
sortTokens
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
}
2つのERC20トークンの順番を入れ替える関数。
同じ値が渡されると必ず同じ順番で返されます。
pairFor
function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
pair = address(uint(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
))));
}
2つのERC20トークンのスワップのためのPairコントラクトのアドレスを取得する関数。
使用するパラメータがわかれば、同じアルゴリズムでアドレスを計算することができるため、factoryコントラクト内で実行するよりもガス代が安く済む。
getReserves
function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
(address token0,) = sortTokens(tokenA, tokenB);
(uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}
tokenA
とtokenB
のを渡して、順番をソートして返す関数。
quote
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
amountB = amountA.mul(reserveB) / reserveA;
}
tokenA
を預け入れると、どれくらいの量のtokenB
を得ることができるか計算する関数。
為替レートが変化することを考慮しています。
getAmountOut
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}
あるトークンの預け入れ量に対して、ペアとなるもう片方のトークンを最大どれくらい受け取れるか計算する関数。
0.3%
の手数料は引いた状態の値が返ってきます。
getAmountIn
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint numerator = reserveIn.mul(amountOut).mul(1000);
uint denominator = reserveOut.sub(amountOut).mul(997);
amountIn = (numerator / denominator).add(1);
}
あるトークンの受け取れる量を指定して、ペアとなるもう片方のトークンをどれくらい預け入れれば良いか計算する関数。
0.3%
の手数料は引いた状態の値が返ってきます。
getAmountsOut
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[0] = amountIn;
for (uint i; i < path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}
getAmountOut
関数を複数回実行する際に実行される関数。
getAmountsIn
function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[amounts.length - 1] = amountOut;
for (uint i = path.length - 1; i > 0; i--) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
}
}
getAmountIn
関数を複数回実行する際に実行される関数。
最後に
今回は「Uniswap」のコードについて解説してきました。
だいぶ複雑かつ長かったと思うので、ここまで読んでいただきありがとうございます。
一回では理解できない部分が多いと思うので、ぜひ自分でも調べながら辞書のような使い方をしてくれたら嬉しいです!
何か質問などがあれば以下のTwitterなどからDMください!
もし何か質問などがあれば以下のTwitterなどから連絡ください!
普段はSolidityやブロックチェーン、Web3についての情報発信をしています。
Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!
参考
https://diamond.jp/crypto/defi/dex/
https://www.coinbase.com/ja/learn/crypto-basics/what-is-a-dex
https://diamond.jp/crypto/defi/uniswap/
https://www.caica.jp/media/crypto/uniswap-about/
https://zuu.co.jp/media/cryptocurrency/what-is-uniswap
https://hashhub-research.com/articles/2020-08-21-oracle
https://betterprogramming.pub/uniswap-smart-contract-breakdown-ea20edf1a0ff
https://betterprogramming.pub/uniswap-smart-contract-breakdown-part-2-b9ea2fca65d1
https://docs.uniswap.org/contracts/v2/overview
https://ethereum.org/ja/developers/tutorials/uniswap-v2-annotated-code/