
分散型取引のためのプロトコル「Uniswap(ユニスワップ)」を使うと、非常に分散的でありながら優れたユーザー体験を実現できます。この記事では、EtherとERC20トークンを交換する簡単なアプリ開発をとおしてUniswapを理解し、その実装ノウハウを学びます。
はじめに
ブロックチェーンは、誰もが使えて誰も支配しないパブリックな台帳を歴史上初めて実現しました。しかし、実際には多くの中央集権的な交換所がハッシュパワーとプライベートキーを支配しているのが現状です。中央集権的な交換所は法定通貨と暗号通貨を交換するのには不可欠なのですが、その存在は当初のビットコインの哲学とは反しています。ブロックチェーンは、会社や政府などの機関から、個人へと力を移行させる技術のはずでした。
どうすれば本来の哲学に戻り、個人に力を移せるのか。そのために分散型なプロトコルの1つとして取引所の開発がすでに進められています。そのなかで私が注目しているのが「Uniswap」という交換プロトコルです。分散型取引所は実装によってどの程度分散的であるかが異なりますが、私がUniswapに注目しているのは、非常に分散的でありながらユーザー体験が優れているからです。
既存の仕組みを代替するには、ユーザーが使いたいと思う、よりよいサービスを創るしかないでしょう。これはプログラマーであればコードを書き、実際にモノを創ることを意味します。この記事でやろうとしているのは、まさにそれです。具体的には、EtherとERC20トークンを交換する簡単なアプリを作ります。
この記事を読み終えるころには、Uniswapプロトコルを使ってDappやウォレットのなかでEtherとERC20トークンを交換する機能を追加できるようになります。
まずはプロトコルの仕組みを簡単に説明してから、アプリを作っていきます。
Uniswapの簡単な説明
Uniswapは、EtherとERC20トークンのための分散的な交換プロトコルです。交換のプロセスはすべてブロックチェーン上で行われ、間に誰も介入しません。第三者が介入しない代わりに、それぞれのERC20トークンごとに別々のスマートコントラクトがあります。これらの交換コントラクトは、Etherと特定のERC20トークンを備えています。トレーダーはこの貯蓄を使っていつでも交換することができます。Etherをコントラクトに送れば特定のERC20トークンを得ることができ、逆に特定のERC20トークンを送ればEtherを得ることができます。
この貯蓄はリクイディティ・プロバイダーによって貯められ、彼らは見返りとして手数料を交換が行われるごとに取ります。「リクイディティ」というのは流動性のことで、EtherとERC20トークンの流動性を提供するので「リクイディティ・プロバイダー」と呼ばれます。ERC20トークン同士を交換することも可能です。
交換するときの価格は、Constant Product Market Maker Modelという価格設定の式によって自動で決められます。交換コントラクト内のEtherの量とERC20トークンの量が常に一定になるように価格が計算されます。この式はERC20トークン同士の交換でも同様に適応され、交換コントラクト内のERC20トークンAの量とERC20トークンBの量が常に一定になるように価格が計算されます。
例えば、DaiというERC20トークンの交換コントラクトに10ETH
と1000DAI
が入っているとします。ここで定数は 10 x 1000
で10000
となります。次にこのコントラクトに1ETH
送ると、コントラクトは11ETH
持つことになります。Etherの量とDaiの量をかけた値は常に10000
で一定でなければいけません。つまりDaiは10000 ÷ 11 ≒ 909DAI
でなければならないということです。逆にいえば、コントラクトは1000DAI
を持っている必要はないので、1000 – 909 = 91DAI
がトレーダーに渡されることになります。
この式は別名 x * y = k
式とも呼ばれ、xとyの値によって以下のような曲線を描きます。
Uniswapがすばらしいのは、貯蓄システムと自動計算式によって売り手と買い手をマッチするコストを減らしたことにあります。このおかげでトレーダーは簡単かつ迅速に交換できます。
セットアップ
では早速アプリを作るためのセットアップに進みましょう! これから作るアプリは、EtherとDaiを交換できる簡単なNodeJSのアプリです。
GitHubでレポジトリを作ったので、先に軽く目を通しておくことをお勧めします。地図を持っていると道を進みやすいのと同じで、最終的にどういうコードになるのかわかった方が、コーディングが楽になります。私が使っているOSはmacOS High Sierraです。
プライベートキーとアドレスの取得
vanity-ethからプライベートキーとアドレスを取ってきてください。Generateボタンを押すと一番下に生成されたプライベートキーとアドレスが出てきます。安全ではないので、テスト目的以外で使わないようにしましょう。
RinkebyテストネットのEtherを手に入れる
RinkebyのEtherを持っていない人は、faucetから取ってきてください。以下のようにツイートして、
1 |
Requesting faucet funds into YOUR_ADDRESS_HERE on the #Rinkeby #Ethereum test network. |
このツイートのリンクをフォームに貼り付けるともらえます。時間がかかる場合は友達にお願いするか、もしくは@taisuke_mino_JPにツイートしていただければ少しは予備があるかもしれません。
残高はRinkeby Etherscanでアドレスを検索して確認することができます。0.1ETHあれば十分です。
Npmのプロジェクトを作る
1 2 3 |
mkdir eht-uniswap-demo cd eht-uniswap-demo/ npm init --yes |
※npmとnodejsが入っていない方はこちらからダウンロードできます。
gitをinitializeする
1 |
git init |
web3のパッケージをインストールする
1 |
npm i --save web3@1.2.1 |
web3はEthereumのJavaScript APIと呼ばれています。テストネットを含めたEthereumのネットワークへのインターフェースです。JavaScriptのアプリからEthereumのノードとやりとりしたり、ブロックチェーン上にデプロイされているスマートコントラクトとやりとりすることもできます。
Infuraのエンドポイントを取得
EthereumのネットワークとやりとりするためのプロバイダーとしてInfuraを使います。Infuraに登録して新しいプロジェクトを作ってからRinkebyネットワークのエンドポイントを取得してください。URLは以下のようなフォーマットになります。
1 |
https://rinkeby.infura.io/v3/YOUR_PROJECT_ID |
ethereumjs-txパッケージをインストールする
1 |
npm i --save ethereumjs-tx@1.3.7 |
ethereumjs-txは、トランザクションオブジェクトを生成したり、サインするのに使います。
EtherからDaiへの交換
ではここからはコードを書いていきます。
ファイル作成
EthToDaiRinkeby.mjsというファイルを作成します。mjsは、ES6の文法で書くための拡張子です。
パッケージのインポート
1 2 |
import Web3 from "web3"; import EthTx from "ethereumjs-tx"; |
交換コントラクトのアドレスとabiを宣言
Rinkebyテストネット上のDaiの交換コントラクトアドレスとabiを宣言します。こちらのリンクから引っ張ってきています。この定数は、コントラクトとやりとりする上で必要になります。
交換コントラクトアドレス:
1 |
const daiExchangeAddress = "0x77dB9C915809e7BE439D2AB21032B1b8B58F6891"; |
*Etherscanでも見ることができます。
交換コントラクトabi:
1 |
const daiExchangeAbi = '[{“name”: “TokenPurchase”, “inputs”: [{“type”: “address”, “name”: “buyer”, “indexed”: true}, {“type”: “uint256”, “name”: “eth_sold”, “indexed”: true}, {“type”: “uint256”, “name”: “tokens_bought”, “indexed”: true}], “anonymous”: false, “type”: “event”}, {“name”: “EthPurchase”, “inputs”: [{“type”: “address”, “name”: “buyer”, “indexed”: true}, {“type”: “uint256”, “name”: “tokens_sold”, “indexed”: true}, {“type”: “uint256”, “name”: “eth_bought”, “indexed”: true}], “anonymous”: false, “type”: “event”}, {“name”: “AddLiquidity”, “inputs”: [{“type”: “address”, “name”: “provider”, “indexed”: true}, {“type”: “uint256”, “name”: “eth_amount”, “indexed”: true}, {“type”: “uint256”, “name”: “token_amount”, “indexed”: true}], “anonymous”: false, “type”: “event”}, {“name”: “RemoveLiquidity”, “inputs”: [{“type”: “address”, “name”: “provider”, “indexed”: true}, {“type”: “uint256”, “name”: “eth_amount”, “indexed”: true}, {“type”: “uint256”, “name”: “token_amount”, “indexed”: true}], “anonymous”: false, “type”: “event”}, {“name”: “Transfer”, “inputs”: [{“type”: “address”, “name”: “_from”, “indexed”: true}, {“type”: “address”, “name”: “_to”, “indexed”: true}, {“type”: “uint256”, “name”: “_value”, “indexed”: false}], “anonymous”: false, “type”: “event”}, {“name”: “Approval”, “inputs”: [{“type”: “address”, “name”: “_owner”, “indexed”: true}, {“type”: “address”, “name”: “_spender”, “indexed”: true}, {“type”: “uint256”, “name”: “_value”, “indexed”: false}], “anonymous”: false, “type”: “event”}, {“name”: “setup”, “outputs”: [], “inputs”: [{“type”: “address”, “name”: “token_addr”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 175875}, {“name”: “addLiquidity”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “min_liquidity”}, {“type”: “uint256”, “name”: “max_tokens”}, {“type”: “uint256”, “name”: “deadline”}], “constant”: false, “payable”: true, “type”: “function”, “gas”: 82605}, {“name”: “removeLiquidity”, “outputs”: [{“type”: “uint256”, “name”: “out”}, {“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “amount”}, {“type”: “uint256”, “name”: “min_eth”}, {“type”: “uint256”, “name”: “min_tokens”}, {“type”: “uint256”, “name”: “deadline”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 116814}, {“name”: “__default__”, “outputs”: [], “inputs”: [], “constant”: false, “payable”: true, “type”: “function”}, {“name”: “ethToTokenSwapInput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “min_tokens”}, {“type”: “uint256”, “name”: “deadline”}], “constant”: false, “payable”: true, “type”: “function”, “gas”: 12757}, {“name”: “ethToTokenTransferInput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “min_tokens”}, {“type”: “uint256”, “name”: “deadline”}, {“type”: “address”, “name”: “recipient”}], “constant”: false, “payable”: true, “type”: “function”, “gas”: 12965}, {“name”: “ethToTokenSwapOutput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_bought”}, {“type”: “uint256”, “name”: “deadline”}], “constant”: false, “payable”: true, “type”: “function”, “gas”: 50463}, {“name”: “ethToTokenTransferOutput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_bought”}, {“type”: “uint256”, “name”: “deadline”}, {“type”: “address”, “name”: “recipient”}], “constant”: false, “payable”: true, “type”: “function”, “gas”: 50671}, {“name”: “tokenToEthSwapInput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_sold”}, {“type”: “uint256”, “name”: “min_eth”}, {“type”: “uint256”, “name”: “deadline”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 47503}, {“name”: “tokenToEthTransferInput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_sold”}, {“type”: “uint256”, “name”: “min_eth”}, {“type”: “uint256”, “name”: “deadline”}, {“type”: “address”, “name”: “recipient”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 47712}, {“name”: “tokenToEthSwapOutput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “eth_bought”}, {“type”: “uint256”, “name”: “max_tokens”}, {“type”: “uint256”, “name”: “deadline”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 50175}, {“name”: “tokenToEthTransferOutput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “eth_bought”}, {“type”: “uint256”, “name”: “max_tokens”}, {“type”: “uint256”, “name”: “deadline”}, {“type”: “address”, “name”: “recipient”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 50384}, {“name”: “tokenToTokenSwapInput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_sold”}, {“type”: “uint256”, “name”: “min_tokens_bought”}, {“type”: “uint256”, “name”: “min_eth_bought”}, {“type”: “uint256”, “name”: “deadline”}, {“type”: “address”, “name”: “token_addr”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 51007}, {“name”: “tokenToTokenTransferInput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_sold”}, {“type”: “uint256”, “name”: “min_tokens_bought”}, {“type”: “uint256”, “name”: “min_eth_bought”}, {“type”: “uint256”, “name”: “deadline”}, {“type”: “address”, “name”: “recipient”}, {“type”: “address”, “name”: “token_addr”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 51098}, {“name”: “tokenToTokenSwapOutput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_bought”}, {“type”: “uint256”, “name”: “max_tokens_sold”}, {“type”: “uint256”, “name”: “max_eth_sold”}, {“type”: “uint256”, “name”: “deadline”}, {“type”: “address”, “name”: “token_addr”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 54928}, {“name”: “tokenToTokenTransferOutput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_bought”}, {“type”: “uint256”, “name”: “max_tokens_sold”}, {“type”: “uint256”, “name”: “max_eth_sold”}, {“type”: “uint256”, “name”: “deadline”}, {“type”: “address”, “name”: “recipient”}, {“type”: “address”, “name”: “token_addr”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 55019}, {“name”: “tokenToExchangeSwapInput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_sold”}, {“type”: “uint256”, “name”: “min_tokens_bought”}, {“type”: “uint256”, “name”: “min_eth_bought”}, {“type”: “uint256”, “name”: “deadline”}, {“type”: “address”, “name”: “exchange_addr”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 49342}, {“name”: “tokenToExchangeTransferInput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_sold”}, {“type”: “uint256”, “name”: “min_tokens_bought”}, {“type”: “uint256”, “name”: “min_eth_bought”}, {“type”: “uint256”, “name”: “deadline”}, {“type”: “address”, “name”: “recipient”}, {“type”: “address”, “name”: “exchange_addr”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 49532}, {“name”: “tokenToExchangeSwapOutput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_bought”}, {“type”: “uint256”, “name”: “max_tokens_sold”}, {“type”: “uint256”, “name”: “max_eth_sold”}, {“type”: “uint256”, “name”: “deadline”}, {“type”: “address”, “name”: “exchange_addr”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 53233}, {“name”: “tokenToExchangeTransferOutput”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_bought”}, {“type”: “uint256”, “name”: “max_tokens_sold”}, {“type”: “uint256”, “name”: “max_eth_sold”}, {“type”: “uint256”, “name”: “deadline”}, {“type”: “address”, “name”: “recipient”}, {“type”: “address”, “name”: “exchange_addr”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 53423}, {“name”: “getEthToTokenInputPrice”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “eth_sold”}], “constant”: true, “payable”: false, “type”: “function”, “gas”: 5542}, {“name”: “getEthToTokenOutputPrice”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_bought”}], “constant”: true, “payable”: false, “type”: “function”, “gas”: 6872}, {“name”: “getTokenToEthInputPrice”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “tokens_sold”}], “constant”: true, “payable”: false, “type”: “function”, “gas”: 5637}, {“name”: “getTokenToEthOutputPrice”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “uint256”, “name”: “eth_bought”}], “constant”: true, “payable”: false, “type”: “function”, “gas”: 6897}, {“name”: “tokenAddress”, “outputs”: [{“type”: “address”, “name”: “out”}], “inputs”: [], “constant”: true, “payable”: false, “type”: “function”, “gas”: 1413}, {“name”: “factoryAddress”, “outputs”: [{“type”: “address”, “name”: “out”}], “inputs”: [], “constant”: true, “payable”: false, “type”: “function”, “gas”: 1443}, {“name”: “balanceOf”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “address”, “name”: “_owner”}], “constant”: true, “payable”: false, “type”: “function”, “gas”: 1645}, {“name”: “transfer”, “outputs”: [{“type”: “bool”, “name”: “out”}], “inputs”: [{“type”: “address”, “name”: “_to”}, {“type”: “uint256”, “name”: “_value”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 75034}, {“name”: “transferFrom”, “outputs”: [{“type”: “bool”, “name”: “out”}], “inputs”: [{“type”: “address”, “name”: “_from”}, {“type”: “address”, “name”: “_to”}, {“type”: “uint256”, “name”: “_value”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 110907}, {“name”: “approve”, “outputs”: [{“type”: “bool”, “name”: “out”}], “inputs”: [{“type”: “address”, “name”: “_spender”}, {“type”: “uint256”, “name”: “_value”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 38769}, {“name”: “allowance”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “address”, “name”: “_owner”}, {“type”: “address”, “name”: “_spender”}], “constant”: true, “payable”: false, “type”: “function”, “gas”: 1925}, {“name”: “name”, “outputs”: [{“type”: “bytes32”, “name”: “out”}], “inputs”: [], “constant”: true, “payable”: false, “type”: “function”, “gas”: 1623}, {“name”: “symbol”, “outputs”: [{“type”: “bytes32”, “name”: “out”}], “inputs”: [], “constant”: true, “payable”: false, “type”: “function”, “gas”: 1653}, {“name”: “decimals”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [], “constant”: true, “payable”: false, “type”: “function”, “gas”: 1683}, {“name”: “totalSupply”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [], “constant”: true, “payable”: false, “type”: “function”, “gas”: 1713}]'; |
アドレスとプライベートキーの定数宣言
1 2 |
const addressFrom = "[YOUR_ADDRESS]"; const privKey = "[YOUR_PRIVATE_KEY]"; |
Web3のセットアップ
Infuraをweb3プロバイダーとして使うためにセットアップします。
1 2 3 4 |
const web3 = new Web3( new Web3.providers.HttpProvider("https://rinkeby.infura.io/v3/[YOUR_PROJECT_ID]" ) ); |
コントラクトをインスタンス化
web3のContract
メソッドにアドレスとabiを渡して、コントラクトをインスタンス化します。
1 |
const daiExchangeContract = new web3.eth.Contract(JSON.parse(daiExchangeAbi), daiExchangeAddress); |
これでDai交換コントラクトの関数を呼んでEtherとDaiを交換する準備ができました。
ETH_SOLD
定数宣言
Daiと交換したいEtherの量をもつ定数を宣言します。
1 |
const ETH_SOLD = web3.utils.toHex(50000000000000000); // 0.05ETH |
ethToTokenSwapInput
関数のabiをエンコード
EtherをDaiと交換するのに、ethToTokenSwapInput
関数を使います。
1 |
function ethToTokenSwapInput(uint256 min_tokens, uint256 deadline) external payable returns (uint256 tokens_bought); |
min_tokens
とdeadline
を引数にとります。min_tokens
では購入するDaiの最低額を指定します。deadline
ではトランザクションの期限を指定します。つまり期限以内に実行されなかったトランザクションは失敗することになります。
これらの引数はフロントランニングを抑止するためのものです。仮に1ETH
を交換する時の価格が200DAI
だとしましょう。199DAI
以下しかもらえない場合にはトランザクションが失敗するように指定することで、フロントランナーが価格を操作したとしても大損することはありません。また期限を設定することで、フロントランナーが利益を得るためにトランザクションを持ち続けることを抑制できます。フロントランニングについてはこちらの投稿が詳しいです。
min_tokens
引数に渡すための定数を宣言します。
1 |
const MIN_TOKENS = web3.utils.toHex(0.2 * 10 ** 18); // 0.2 DAI |
値が低すぎるとフロントランナーに簡単に利益を取られてしまい、逆に高すぎるとトランザクションが失敗しやすいので、バランスを考える必要があります。
deadline
引数に渡す別の定数を宣言します。以下の値は、UTC時間2019年10月1日午前12:00のUnixタイムスタンプです。すでに以下の日時を過ぎてしまっている場合は、こちらのオンラインのコンバーターを使ってください。
1 |
const DEADLINE = 1569888000; // 10/01/2019 @ 12:00am (UTC) |
次にethToTokenSwapInput
関数に2つの値を渡し、abiをエンコードします。
1 |
const exchangeEncodedABI = daiExchangeContract.methods.ethToTokenSwapInput(MIN_TOKENS, DEADLINE).encodeABI(); |
sendSignedTx
関数の宣言
トランザクションオブジェクトにサインして、それをEthereumネットワークにブロードキャストする関数を宣言します。
1 2 3 4 5 6 7 |
function sendSignedTx(transactionObject, cb) { let transaction = new EthTx(transactionObject); const privateKey = new Buffer.from(privKey, "hex"); transaction.sign(privateKey); const serializedEthTx = transaction.serialize().toString("hex"); web3.eth.sendSignedTransaction(`0x${serializedEthTx}`, cb); } |
トランザクションオブジェクトを作成し、sendSignedTx
関数を実行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
web3.eth.getTransactionCount(addressFrom).then(transactionNonce => { const transactionObject = { chainId: 4, nonce: web3.utils.toHex(transactionNonce), gasLimit: web3.utils.toHex(6000000), gasPrice: web3.utils.toHex(10000000000), to: daiExchangeAddress, from: addressFrom, data: exchangeEncodedABI, value: ETH_SOLD }; sendSignedTx(transactionObject, function(error, result){ if(error) return console.log("error ===>", error); console.log("sent ===>", result); }) } ); |
exchangeEncodedABI
とETH_SOLD
をそれぞれdata
とvalue
プロパティに渡しています。
ファイルを走らせる
1 |
npm run eth-to-dai-rinkeby |
0xbbc617c97323301d7376683a0ae58db012b418f27963e54866eb92bec4839e10
のようなトランザクションハッシュを受け取ったら、Etherscanで詳細を見ることができます。以下は私のトランザクションの例です。
トランザクションが成功していれば、「Erc20 Token Txns」というタブメニューから自分のアドレスにDaiが入っていることが確認できるはずです。
これでEtherとDaiを交換できました!
DaiからEtherへの交換
次にDaiからEtherに交換します。今回は少しトリッキーで、2つのトランザクションが必要になります。最初にUniswapのDai交換コントラクトに自分のアカウントに代わってDaiを送る許可を与えます。次にtokenToEthSwapInput
関数を呼んでDaiをEtherと交換します。
1. UniswapのDai交換コントラクトに許可を与える
別のapproveDaiExchangeRinkeby.mjsというファイルを作ります。今回は前のコードと重複している部分は省きます。最終的なコードはこちらを参考にしてください。
トークンコントラクトのアドレスとabiを宣言
RinkebyテストネットでのDaiのトークンアドレスとabiを宣言します。
トークンコントラクトアドレス:
1 |
const daiTokenAddress = “0x2448eE2641d78CC42D7AD76498917359D961A783”; |
*Etherscanでも確認できます。
トークンコントラクトabi:
1 |
const daiTokenAbi = ‘[{“name”: “Transfer”, “inputs”: [{“type”: “address”, “name”: “_from”, “indexed”: true}, {“type”: “address”, “name”: “_to”, “indexed”: true}, {“type”: “uint256”, “name”: “_value”, “indexed”: false}], “anonymous”: false, “type”: “event”}, {“name”: “Approval”, “inputs”: [{“type”: “address”, “name”: “_owner”, “indexed”: true}, {“type”: “address”, “name”: “_spender”, “indexed”: true}, {“type”: “uint256”, “name”: “_value”, “indexed”: false}], “anonymous”: false, “type”: “event”}, {“outputs”: [], “inputs”: [{“type”: “string”, “name”: “_name”}, {“type”: “string”, “name”: “_symbol”}, {“type”: “uint256”, “name”: “_decimals”}, {“type”: “uint256”, “name”: “_supply”}], “constant”: false, “payable”: false, “type”: “constructor”}, {“name”: “transfer”, “outputs”: [{“type”: “bool”, “name”: “out”}], “inputs”: [{“type”: “address”, “name”: “_to”}, {“type”: “uint256”, “name”: “_value”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 74020}, {“name”: “transferFrom”, “outputs”: [{“type”: “bool”, “name”: “out”}], “inputs”: [{“type”: “address”, “name”: “_from”}, {“type”: “address”, “name”: “_to”}, {“type”: “uint256”, “name”: “_value”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 110371}, {“name”: “approve”, “outputs”: [{“type”: “bool”, “name”: “out”}], “inputs”: [{“type”: “address”, “name”: “_spender”}, {“type”: “uint256”, “name”: “_value”}], “constant”: false, “payable”: false, “type”: “function”, “gas”: 37755}, {“name”: “name”, “outputs”: [{“type”: “string”, “name”: “out”}], “inputs”: [], “constant”: true, “payable”: false, “type”: “function”, “gas”: 6402}, {“name”: “symbol”, “outputs”: [{“type”: “string”, “name”: “out”}], “inputs”: [], “constant”: true, “payable”: false, “type”: “function”, “gas”: 6432}, {“name”: “decimals”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [], “constant”: true, “payable”: false, “type”: “function”, “gas”: 663}, {“name”: “totalSupply”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [], “constant”: true, “payable”: false, “type”: “function”, “gas”: 693}, {“name”: “balanceOf”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “address”, “name”: “arg0”}], “constant”: true, “payable”: false, “type”: “function”, “gas”: 877}, {“name”: “allowance”, “outputs”: [{“type”: “uint256”, “name”: “out”}], “inputs”: [{“type”: “address”, “name”: “arg0”}, {“type”: “address”, “name”: “arg1”}], “constant”: true, “payable”: false, “type”: “function”, “gas”: 1061}]’; |
*ERC20トークンのabiはDai以外のトークンでもすべて同じです。
daiExchangeAddress定数の宣言
1 |
const daiExchangeAddress = “0x77dB9C915809e7BE439D2AB21032B1b8B58F6891”; |
このアドレスにDaiを自分の代わりに送金する許可を与えます。
Daiトークンコントラクトのインスタンス化
1 2 3 4 |
const daiTokenContract = new web3.eth.Contract( JSON.parse(daiTokenAbi), daiTokenAddress ); |
approve
関数に渡す定数の宣言
1 2 |
const ADDRESS_SPENDER = daiExchangeAddress; const TOKENS = web3.utils.toHex(1 * 10 ** 18); // 1 DAI |
daiExchangeAddress
に1DAIを送る許可を与えます。
approve
関数のabiのエンコード
1 2 3 |
const approveEncodedABI = daiTokenContract.methods .approve(ADDRESS_SPENDER, TOKENS) .encodeABI(); |
sendSignedTx
関数の宣言
トランザクションオブジェクトにサインし、それをEthereunネットワークにブロードキャストする関数を作成
1 2 3 4 5 6 7 |
function sendSignedTx(transactionObject, cb) { let transaction = new EthTx(transactionObject); const privateKey = new Buffer.from(privKey, "hex"); transaction.sign(privateKey); const serializedEthTx = transaction.serialize().toString("hex"); web3.eth.sendSignedTransaction(`0x${serializedEthTx}`, cb); } |
トランザクションオブジェクトを作成し、sendSignedTx
関数を実行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
web3.eth.getTransactionCount(addressFrom).then(transactionNonce => { const transactionObject = { chainId: 4, nonce: web3.utils.toHex(transactionNonce), gasLimit: web3.utils.toHex(42000), gasPrice: web3.utils.toHex(5000000), to: daiTokenAddress, from: addressFrom, data: approveEncodedABI }; sendSignedTx(transactionObject, function(error, result){ if(error) return console.log("error ===>", error); console.log("sent ===>", result); }) } ); |
ファイルを走らせる
1 |
npm run approve-dai-exchange-rinkeby |
0x1d16e0ccb3947ecbd34e16f9ae9378a17b16e11c701f1d0f44e5262d0e8f018a
のようなトランザクションハッシュを受け取ったら、etherscanで詳細を見ることができます。以下は私のトランザクションの例です。

2. DaiをEtherに交換する
次に実際にDaiをEtherに交換する部分を実装します。今回も前のコードと重複している部分は省きます。DaiToEthRinkeby.mjsというファイルを作り、tokenToEthSwapInput関数を呼びます。
1 |
function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256 eth_bought); |
この関数は引数を3つとります。tokens_sold
は交換するERC20トークンの量です。min_eth
は交換するEtherの最低額です。deadline
はトランザクションの期限です。
1 2 3 |
const TOKENS_SOLD = web3.utils.toHex(0.4 * 10 ** 18); // 0.4DAI const MIN_ETH = web3.utils.toHex(5000000000000000); // 0.005ETH const DEADLINE = 1569888000; // 10/01/2019 @ 12:00am (UTC) |
tokenToEthSwapInput
関数に3つの定数を引数として渡し、abiをエンコードします。
1 2 3 |
const tokenToEthEncodedABI = daiExchangeContract.methods .tokenToEthSwapInput(TOKENS_SOLD, MIN_ETH, DEADLINE) .encodeABI(); |
このtokenToEthEncodedABI
をトランザクションオブジェクトのdata
プロパティに渡します。
ファイルを走らせます。
1 |
npm run dai-to-eth-rinkeby |
0x6011a9f50b80dba7a2cb3f9dbf327f0f4c28ec6a4205c2b4002b3a008aa480f4
のようなトランザクションハッシュを受け取ったら、etherscanでその詳細を確認できます。以下は私のトランザクションの例です。

以下のようにDaiが自分のアドレスから引かれ、

Etherが追加されていることがわかります。
Dai以外の他のERC20トークンを交換したい場合は、トークンのアドレスを変えるだけで実装できます。
この記事がまだ存在していない新たなサービスを作るための一助になれば幸いです。😎
Profile
解説:三野泰佑
クリプトエンジニア
2013年頃からブロックチェーンに関わり、スマートコントラクトのベストプラクティスを閲覧できるサイトやSolidityの監査をするサービスを開発。現在はICOVOのエンジニアとして開発や執筆をしながら、ステーブルコインに特化したモバイルウォレットも開発している。