Tai's website

UniswapでEtherとERC20トークンを交換する分散型アプリを作ってみる

October 03, 2019

はじめに

ブロックチェーンは、誰もが使えて誰も支配しないパブリックな台帳を歴史上初めて実現しました。しかし、実際には多くの中央集権的な交換所がハッシュパワーとプライベートキーを支配しているのが現状です。中央集権的な交換所は法定通貨と暗号通貨を交換するのには不可欠なのですが、その存在は当初のビットコインの哲学とは反しています。ブロックチェーンは、会社や政府などの機関から、個人へと力を移行させる技術のはずでした。

どうすれば本来の哲学に戻り、個人に力を移せるのか。そのために分散型なプロトコルの一つとして取引所の開発がすでに進められています。その中で僕が注目しているのが、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トークンの交換コントラクトに10ETH1000DAIが入っているとします。ここで定数は 10 x 100010000となります。次にこのコントラクトに1ETH送ると、コントラクトは11ETH持つことになります。ETHの量とDAIの量をかけた値は常に10000で一定でなければいけません。つまりDAIは10000 ÷ 11 ≒ 909DAIなければいけないということです。逆にいえば、コントラクトは1000DAI持っている必要はないので、1000 – 909 = 91DAIがトレーダーに渡されることになります。

この式は別名 x * y = k 式とも呼ばれ、xとyの値によって以下のような曲線を描きます。

x * y = k curve
x * y = k curve

Uniswapが素晴らしいのは、貯蓄システムと自動計算式によって売り手と買い手をマッチするコストを減らしたことにあります。このおかげでトレーダーは簡単かつ迅速に交換をすることができます。

セットアップ

では早速アプリを作るためのセットアップに進みます!これから作るアプリは、etherとdaiを交換できる簡単なNodeJSのアプリです。

GitHubでレポジトリを作ったので、先に軽く目を通しておくことをお勧めします。地図を持っていると道を進みやすいのと同じで、最終的にどういうコードになるのかわかった方が、コーディングが楽になります。僕が使っているOSはmacOS High Sierraです。

プライベートキーとアドレスの取得
vanity-ethからプライベートキーとアドレスを取ってきてください。Generateボタンを押すと一番下に生成されたプライベートキーとアドレスが出てきます。安全ではないので、テスト目的以外で使わないようにしましょう。

Rinkebyテストネットのetherを手に入れる
Rinkebyのetherを持っていない人は、faucetから取ってきてください。以下のようにツイートして、

Requesting faucet funds into YOUR_ADDRESS_HERE on the #Rinkeby #Ethereum test network.

このツイートのリンクをフォームに貼り付けるともらえます。時間がかかる場合は友達にお願いするか、もしくは@taisuke_mino_JPにツイートしていただければ少しは予備があるかもしれません。

残高はRinkeby Etherscanでアドレスを検索して確認することができます。0.1ETHあれば十分です。

Npmのプロジェクトを作る

mkdir eht-uniswap-demo  
cd eht-uniswap-demo/  
npm init --yes

*npmとnodejsが入っていない方はこちらからダウンロードできます。

gitをinitializeする

git init

web3のパッケージをインストールする

npm i --save web3@1.2.1

web3はEthereumのJavaScript APIと呼ばれています。テストネットを含めたEthereumのネットワークへのインターフェースです。JavaScriptのアプリからEthereumのノードとやりとりしたり、ブロックチェーン上にデプロイされているスマートコントラクトとやりとりすることもできます。

Infuraのエンドポイントを取得
EthereumのネットワークとやりとりするためのプロバイダーとしてInfuraを使います。Infuraに登録して新しいプロジェクトを作ってからRinkebyネットワークのエンドポイントを取得してください。URLは以下のようなフォーマットになります。

https://rinkeby.infura.io/v3/YOUR_PROJECT_ID

ethereumjs-txパッケージをインストールする

npm i --save ethereumjs-tx@1.3.7

ethereumjs-txは、トランズアクションオブジェクトを生成したり、サインするのに使います。

ETHからDAIへの交換

ではここからはコードを書いていきます。

ファイル作成
EthToDaiRinkeby.mjsというファイルを作成します。mjsは、ES6の文法で書くための拡張子です。

パッケージのインポート

import Web3 from "web3";  
import EthTx from "ethereumjs-tx";

交換コントラクトのアドレスとabiを宣言
Rinkebyテストネット上のDAIの交換コントラクトアドレスとabiを宣言します。こちらのリンクから引っ張ってきています。この定数は、コントラクトとやりとりする上で必要になります。

交換コントラクトアドレス:

const daiExchangeAddress = "0x77dB9C915809e7BE439D2AB21032B1b8B58F6891";

*Etherscanでも見ることができます。

交換コントラクトabi:

const daiExchangeAbi = '[{“name”: “TokenPurchase”undefined “inputs”: [{“type”: “address”undefined “name”: “buyer”undefined “indexed”: true}undefined {“type”: “uint256”undefined “name”: “eth_sold”undefined “indexed”: true}undefined {“type”: “uint256”undefined “name”: “tokens_bought”undefined “indexed”: true}]undefined “anonymous”: falseundefined “type”: “event”}undefined {“name”: “EthPurchase”undefined “inputs”: [{“type”: “address”undefined “name”: “buyer”undefined “indexed”: true}undefined {“type”: “uint256”undefined “name”: “tokens_sold”undefined “indexed”: true}undefined {“type”: “uint256”undefined “name”: “eth_bought”undefined “indexed”: true}]undefined “anonymous”: falseundefined “type”: “event”}undefined {“name”: “AddLiquidity”undefined “inputs”: [{“type”: “address”undefined “name”: “provider”undefined “indexed”: true}undefined {“type”: “uint256”undefined “name”: “eth_amount”undefined “indexed”: true}undefined {“type”: “uint256”undefined “name”: “token_amount”undefined “indexed”: true}]undefined “anonymous”: falseundefined “type”: “event”}undefined {“name”: “RemoveLiquidity”undefined “inputs”: [{“type”: “address”undefined “name”: “provider”undefined “indexed”: true}undefined {“type”: “uint256”undefined “name”: “eth_amount”undefined “indexed”: true}undefined {“type”: “uint256”undefined “name”: “token_amount”undefined “indexed”: true}]undefined “anonymous”: falseundefined “type”: “event”}undefined {“name”: “Transfer”undefined “inputs”: [{“type”: “address”undefined “name”: “_from”undefined “indexed”: true}undefined {“type”: “address”undefined “name”: “_to”undefined “indexed”: true}undefined {“type”: “uint256”undefined “name”: “_value”undefined “indexed”: false}]undefined “anonymous”: falseundefined “type”: “event”}undefined {“name”: “Approval”undefined “inputs”: [{“type”: “address”undefined “name”: “_owner”undefined “indexed”: true}undefined {“type”: “address”undefined “name”: “_spender”undefined “indexed”: true}undefined {“type”: “uint256”undefined “name”: “_value”undefined “indexed”: false}]undefined “anonymous”: falseundefined “type”: “event”}undefined {“name”: “setup”undefined “outputs”: []undefined “inputs”: [{“type”: “address”undefined “name”: “token_addr”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 175875}undefined {“name”: “addLiquidity”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “min_liquidity”}undefined {“type”: “uint256”undefined “name”: “max_tokens”}undefined {“type”: “uint256”undefined “name”: “deadline”}]undefined “constant”: falseundefined “payable”: trueundefined “type”: “function”undefined “gas”: 82605}undefined {“name”: “removeLiquidity”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}undefined {“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “amount”}undefined {“type”: “uint256”undefined “name”: “min_eth”}undefined {“type”: “uint256”undefined “name”: “min_tokens”}undefined {“type”: “uint256”undefined “name”: “deadline”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 116814}undefined {“name”: “__default__”undefined “outputs”: []undefined “inputs”: []undefined “constant”: falseundefined “payable”: trueundefined “type”: “function”}undefined {“name”: “ethToTokenSwapInput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “min_tokens”}undefined {“type”: “uint256”undefined “name”: “deadline”}]undefined “constant”: falseundefined “payable”: trueundefined “type”: “function”undefined “gas”: 12757}undefined {“name”: “ethToTokenTransferInput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “min_tokens”}undefined {“type”: “uint256”undefined “name”: “deadline”}undefined {“type”: “address”undefined “name”: “recipient”}]undefined “constant”: falseundefined “payable”: trueundefined “type”: “function”undefined “gas”: 12965}undefined {“name”: “ethToTokenSwapOutput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_bought”}undefined {“type”: “uint256”undefined “name”: “deadline”}]undefined “constant”: falseundefined “payable”: trueundefined “type”: “function”undefined “gas”: 50463}undefined {“name”: “ethToTokenTransferOutput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_bought”}undefined {“type”: “uint256”undefined “name”: “deadline”}undefined {“type”: “address”undefined “name”: “recipient”}]undefined “constant”: falseundefined “payable”: trueundefined “type”: “function”undefined “gas”: 50671}undefined {“name”: “tokenToEthSwapInput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_sold”}undefined {“type”: “uint256”undefined “name”: “min_eth”}undefined {“type”: “uint256”undefined “name”: “deadline”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 47503}undefined {“name”: “tokenToEthTransferInput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_sold”}undefined {“type”: “uint256”undefined “name”: “min_eth”}undefined {“type”: “uint256”undefined “name”: “deadline”}undefined {“type”: “address”undefined “name”: “recipient”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 47712}undefined {“name”: “tokenToEthSwapOutput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “eth_bought”}undefined {“type”: “uint256”undefined “name”: “max_tokens”}undefined {“type”: “uint256”undefined “name”: “deadline”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 50175}undefined {“name”: “tokenToEthTransferOutput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “eth_bought”}undefined {“type”: “uint256”undefined “name”: “max_tokens”}undefined {“type”: “uint256”undefined “name”: “deadline”}undefined {“type”: “address”undefined “name”: “recipient”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 50384}undefined {“name”: “tokenToTokenSwapInput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_sold”}undefined {“type”: “uint256”undefined “name”: “min_tokens_bought”}undefined {“type”: “uint256”undefined “name”: “min_eth_bought”}undefined {“type”: “uint256”undefined “name”: “deadline”}undefined {“type”: “address”undefined “name”: “token_addr”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 51007}undefined {“name”: “tokenToTokenTransferInput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_sold”}undefined {“type”: “uint256”undefined “name”: “min_tokens_bought”}undefined {“type”: “uint256”undefined “name”: “min_eth_bought”}undefined {“type”: “uint256”undefined “name”: “deadline”}undefined {“type”: “address”undefined “name”: “recipient”}undefined {“type”: “address”undefined “name”: “token_addr”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 51098}undefined {“name”: “tokenToTokenSwapOutput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_bought”}undefined {“type”: “uint256”undefined “name”: “max_tokens_sold”}undefined {“type”: “uint256”undefined “name”: “max_eth_sold”}undefined {“type”: “uint256”undefined “name”: “deadline”}undefined {“type”: “address”undefined “name”: “token_addr”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 54928}undefined {“name”: “tokenToTokenTransferOutput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_bought”}undefined {“type”: “uint256”undefined “name”: “max_tokens_sold”}undefined {“type”: “uint256”undefined “name”: “max_eth_sold”}undefined {“type”: “uint256”undefined “name”: “deadline”}undefined {“type”: “address”undefined “name”: “recipient”}undefined {“type”: “address”undefined “name”: “token_addr”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 55019}undefined {“name”: “tokenToExchangeSwapInput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_sold”}undefined {“type”: “uint256”undefined “name”: “min_tokens_bought”}undefined {“type”: “uint256”undefined “name”: “min_eth_bought”}undefined {“type”: “uint256”undefined “name”: “deadline”}undefined {“type”: “address”undefined “name”: “exchange_addr”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 49342}undefined {“name”: “tokenToExchangeTransferInput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_sold”}undefined {“type”: “uint256”undefined “name”: “min_tokens_bought”}undefined {“type”: “uint256”undefined “name”: “min_eth_bought”}undefined {“type”: “uint256”undefined “name”: “deadline”}undefined {“type”: “address”undefined “name”: “recipient”}undefined {“type”: “address”undefined “name”: “exchange_addr”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 49532}undefined {“name”: “tokenToExchangeSwapOutput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_bought”}undefined {“type”: “uint256”undefined “name”: “max_tokens_sold”}undefined {“type”: “uint256”undefined “name”: “max_eth_sold”}undefined {“type”: “uint256”undefined “name”: “deadline”}undefined {“type”: “address”undefined “name”: “exchange_addr”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 53233}undefined {“name”: “tokenToExchangeTransferOutput”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_bought”}undefined {“type”: “uint256”undefined “name”: “max_tokens_sold”}undefined {“type”: “uint256”undefined “name”: “max_eth_sold”}undefined {“type”: “uint256”undefined “name”: “deadline”}undefined {“type”: “address”undefined “name”: “recipient”}undefined {“type”: “address”undefined “name”: “exchange_addr”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 53423}undefined {“name”: “getEthToTokenInputPrice”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “eth_sold”}]undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 5542}undefined {“name”: “getEthToTokenOutputPrice”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_bought”}]undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 6872}undefined {“name”: “getTokenToEthInputPrice”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “tokens_sold”}]undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 5637}undefined {“name”: “getTokenToEthOutputPrice”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “uint256”undefined “name”: “eth_bought”}]undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 6897}undefined {“name”: “tokenAddress”undefined “outputs”: [{“type”: “address”undefined “name”: “out”}]undefined “inputs”: []undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 1413}undefined {“name”: “factoryAddress”undefined “outputs”: [{“type”: “address”undefined “name”: “out”}]undefined “inputs”: []undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 1443}undefined {“name”: “balanceOf”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “address”undefined “name”: “_owner”}]undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 1645}undefined {“name”: “transfer”undefined “outputs”: [{“type”: “bool”undefined “name”: “out”}]undefined “inputs”: [{“type”: “address”undefined “name”: “_to”}undefined {“type”: “uint256”undefined “name”: “_value”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 75034}undefined {“name”: “transferFrom”undefined “outputs”: [{“type”: “bool”undefined “name”: “out”}]undefined “inputs”: [{“type”: “address”undefined “name”: “_from”}undefined {“type”: “address”undefined “name”: “_to”}undefined {“type”: “uint256”undefined “name”: “_value”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 110907}undefined {“name”: “approve”undefined “outputs”: [{“type”: “bool”undefined “name”: “out”}]undefined “inputs”: [{“type”: “address”undefined “name”: “_spender”}undefined {“type”: “uint256”undefined “name”: “_value”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 38769}undefined {“name”: “allowance”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “address”undefined “name”: “_owner”}undefined {“type”: “address”undefined “name”: “_spender”}]undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 1925}undefined {“name”: “name”undefined “outputs”: [{“type”: “bytes32”undefined “name”: “out”}]undefined “inputs”: []undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 1623}undefined {“name”: “symbol”undefined “outputs”: [{“type”: “bytes32”undefined “name”: “out”}]undefined “inputs”: []undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 1653}undefined {“name”: “decimals”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: []undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 1683}undefined {“name”: “totalSupply”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: []undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 1713}]';

アドレスとプライベートキーの定数宣言

const addressFrom = "[YOUR_ADDRESS]";  
const privKey = "[YOUR_PRIVATE_KEY]";

Web3のセットアップ
Infuraをweb3プロバイダーとして使うためにセットアップします。

const web3 = new Web3(  
  new Web3.providers.HttpProvider("https://rinkeby.infura.io/v3/[YOUR_PROJECT_ID]"  
  )  
);

コントラクトをインスタンス化
web3のContractメソッドにアドレスとabiを渡して、コントラクトをインスタンス化します。

const daiExchangeContract = new web3.eth.Contract(JSON.parse(daiExchangeAbi)undefined daiExchangeAddress);

これでdai交換コントラクトの関数を呼んでetherとdaiを交換する準備ができました。

ETH_SOLD定数宣言
daiと交換したいETHの量をもつ定数を宣言します。

const ETH_SOLD = web3.utils.toHex(50000000000000000); // 0.05ETH

ethToTokenSwapInput関数のabiをエンコード
ethをdaiと交換するのに、ethToTokenSwapInput関数を使います。

function ethToTokenSwapInput(uint256 min_tokensundefined uint256 deadline) external payable returns (uint256 tokens_bought);

min_tokensdeadlineを引数にとります。min_tokensでは購入するdaiの最低額を指定します。deadlineではトランズアクションの期限を指定します。つまり期限以内に実行されなかったトランズアクションは失敗することになります。

これらの引数はフロントランニングを抑止するためのものです。仮に1ETHを交換する時の価格が200DAIだとしましょう。199DAI以下しかもらえない場合にはトランズアクションが失敗するように指定することで、フロントランナーが価格を操作したとしても大損することはありません。また期限を設定することで、フロントランナーが利益を得るためにトランズアクションを持ち続けることを抑制できます。フロントランニングについてはこちらの投稿が詳しいです。

min_tokens引数に渡すための定数を宣言します。

const MIN_TOKENS = web3.utils.toHex(0.2 * 10 ** 18); // 0.2 DAI

値が低すぎるとフロントランナーに簡単に利益を取られてしまい、逆に高すぎるとトランズアクションが失敗しやすいので、バランスを考える必要があります。

deadline引数に渡す別の定数を宣言します。以下の値は、UTC時間2019年10月1日午前12:00のUnixタイムスタンプです。すでに以下の日時を過ぎてしまっている場合は、こちらのオンラインのコンバーターを使ってください。

const DEADLINE = 1569888000; // 10/01/2019 @ 12:00am (UTC)

次にethToTokenSwapInput関数に2つの値を渡し、abiをエンコードします。

const exchangeEncodedABI = daiExchangeContract.methods.ethToTokenSwapInput(MIN_TOKENSundefined DEADLINE).encodeABI();

sendSignedTx関数の宣言
トランズアクションオブジェクトにサインして、それをEthereumネットワークにブロードキャストする関数を宣言します。

function sendSignedTx(transactionObjectundefined cb) {  
  let transaction = new EthTx(transactionObject);  
  const privateKey = new Buffer.from(privKeyundefined "hex");  
  transaction.sign(privateKey);  
  const serializedEthTx = transaction.serialize().toString("hex");  
  web3.eth.sendSignedTransaction(`0x${serializedEthTx}`undefined cb);  
}

トランズアクションオブジェクトを作成し、sendSignedTx関数を実行

web3.eth.getTransactionCount(addressFrom).then(transactionNonce => {  
  const transactionObject = {  
    chainId: 4undefined  
    nonce: web3.utils.toHex(transactionNonce)undefined  
    gasLimit: web3.utils.toHex(6000000)undefined  
    gasPrice: web3.utils.toHex(10000000000)undefined  
    to: daiExchangeAddressundefined  
    from: addressFromundefined  
    data: exchangeEncodedABIundefined  
    value: ETH_SOLD  
  };

sendSignedTx(transactionObjectundefined function(errorundefined result){  
    if(error) return console.log("error ===>"undefined error);  
    console.log("sent ===>"undefined result);  
  })  
}  
);

exchangeEncodedABIETH_SOLDをそれぞれdatavalueプロパティに渡しています。

ファイルを走らせる

npm run eth-to-dai-rinkeby

0xbbc617c97323301d7376683a0ae58db012b418f27963e54866eb92bec4839e10のようなトランズアクションハッシュを受け取ったら、etherscanで詳細を見ることができます。以下は僕のトランズアクションの例です。

[https://rinkeby.etherscan.io/tx/0xbbc617c97323301d7376683a0ae58db012b418f27963e54866eb92bec4839e10](https://rinkeby.etherscan.io/tx/0xbbc617c97323301d7376683a0ae58db012b418f27963e54866eb92bec4839e10) https://rinkeby.etherscan.io/tx/0xbbc617c97323301d7376683a0ae58db012b418f27963e54866eb92bec4839e10

トランズアクションが成功していれば、「Erc20 Token Txns」というタブメニューから自分のアドレスにdaiが入っていることが確認できるはずです。

これでEthとDaiを交換できました!

DAIからETHへの交換

次にDAIからETHに交換します。今回は少しトリッキーで、2つのトランズアクションが必要になります。まず最初にUniswapのdai交換コントラクトに自分のアカウントに代わってdaiを送る許可を与えます。次にtokenToEthSwapInput関数を呼んでDaiをEthと交換します。

1. UniswapのDAI交換コントラクトに許可を与える

別のapproveDaiExchangeRinkeby.mjsというファイルを作ります。今回は前のコードと重複している部分は省きます。最終的なコードはこちらを参考にしてください。

トークンコントラクトのアドレスとabiを宣言

Rinkebyテストネットでのdaiのトークンアドレスとabiを宣言します。

トークンコントラクトアドレス:

const daiTokenAddress = “0x2448eE2641d78CC42D7AD76498917359D961A783”;

*Etherscanでも確認できます。

トークンコントラクトabi:

const daiTokenAbi = ‘[{“name”: “Transfer”undefined “inputs”: [{“type”: “address”undefined “name”: “_from”undefined “indexed”: true}undefined {“type”: “address”undefined “name”: “_to”undefined “indexed”: true}undefined {“type”: “uint256”undefined “name”: “_value”undefined “indexed”: false}]undefined “anonymous”: falseundefined “type”: “event”}undefined {“name”: “Approval”undefined “inputs”: [{“type”: “address”undefined “name”: “_owner”undefined “indexed”: true}undefined {“type”: “address”undefined “name”: “_spender”undefined “indexed”: true}undefined {“type”: “uint256”undefined “name”: “_value”undefined “indexed”: false}]undefined “anonymous”: falseundefined “type”: “event”}undefined {“outputs”: []undefined “inputs”: [{“type”: “string”undefined “name”: “_name”}undefined {“type”: “string”undefined “name”: “_symbol”}undefined {“type”: “uint256”undefined “name”: “_decimals”}undefined {“type”: “uint256”undefined “name”: “_supply”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “constructor”}undefined {“name”: “transfer”undefined “outputs”: [{“type”: “bool”undefined “name”: “out”}]undefined “inputs”: [{“type”: “address”undefined “name”: “_to”}undefined {“type”: “uint256”undefined “name”: “_value”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 74020}undefined {“name”: “transferFrom”undefined “outputs”: [{“type”: “bool”undefined “name”: “out”}]undefined “inputs”: [{“type”: “address”undefined “name”: “_from”}undefined {“type”: “address”undefined “name”: “_to”}undefined {“type”: “uint256”undefined “name”: “_value”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 110371}undefined {“name”: “approve”undefined “outputs”: [{“type”: “bool”undefined “name”: “out”}]undefined “inputs”: [{“type”: “address”undefined “name”: “_spender”}undefined {“type”: “uint256”undefined “name”: “_value”}]undefined “constant”: falseundefined “payable”: falseundefined “type”: “function”undefined “gas”: 37755}undefined {“name”: “name”undefined “outputs”: [{“type”: “string”undefined “name”: “out”}]undefined “inputs”: []undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 6402}undefined {“name”: “symbol”undefined “outputs”: [{“type”: “string”undefined “name”: “out”}]undefined “inputs”: []undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 6432}undefined {“name”: “decimals”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: []undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 663}undefined {“name”: “totalSupply”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: []undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 693}undefined {“name”: “balanceOf”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “address”undefined “name”: “arg0”}]undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 877}undefined {“name”: “allowance”undefined “outputs”: [{“type”: “uint256”undefined “name”: “out”}]undefined “inputs”: [{“type”: “address”undefined “name”: “arg0”}undefined {“type”: “address”undefined “name”: “arg1”}]undefined “constant”: trueundefined “payable”: falseundefined “type”: “function”undefined “gas”: 1061}]’;

*ERC20トークンのabiはdai以外のトークンでも全て同じです。

daiExchangeAddress定数の宣言

const daiExchangeAddress = “0x77dB9C915809e7BE439D2AB21032B1b8B58F6891”;

このアドレスにdaiを自分の代わりに送金する許可を与えます。

daiトークンコントラクトのインスタンス化

const daiTokenContract = new web3.eth.Contract(  
  JSON.parse(daiTokenAbi)undefined  
  daiTokenAddress  
);

approve関数に渡す定数の宣言

const ADDRESS_SPENDER = daiExchangeAddress;  
const TOKENS = web3.utils.toHex(1 * 10 ** 18); // 1 DAI

daiExchangeAddressに1DAIを送る許可を与えます。

approve関数のabiのエンコード

const approveEncodedABI = daiTokenContract.methods  
  .approve(ADDRESS_SPENDERundefined TOKENS)  
  .encodeABI();

sendSignedTx関数の宣言
トランズアクションオブジェクトにサインし、それをEthereunネットワークにブロードキャストする関数を作成

function sendSignedTx(transactionObjectundefined cb) {  
  let transaction = new EthTx(transactionObject);  
  const privateKey = new Buffer.from(privKeyundefined "hex");  
  transaction.sign(privateKey);  
  const serializedEthTx = transaction.serialize().toString("hex");  
  web3.eth.sendSignedTransaction(`0x${serializedEthTx}`undefined cb);  
}

トランズアクションオブジェクトを作成し、sendSignedTx関数を実行

web3.eth.getTransactionCount(addressFrom).then(transactionNonce => {  
  const transactionObject = {  
    chainId: 4undefined  
    nonce: web3.utils.toHex(transactionNonce)undefined  
    gasLimit: web3.utils.toHex(42000)undefined  
    gasPrice: web3.utils.toHex(5000000)undefined  
    to: daiTokenAddressundefined  
    from: addressFromundefined  
    data: approveEncodedABI  
  };

sendSignedTx(transactionObjectundefined function(errorundefined result){  
    if(error) return console.log("error ===>"undefined error);  
    console.log("sent ===>"undefined result);  
  })  
}  
);

ファイルを走らせる

npm run approve-dai-exchange-rinkeby

0x1d16e0ccb3947ecbd34e16f9ae9378a17b16e11c701f1d0f44e5262d0e8f018aのようなトランズアクションハッシュを受け取ったら、etherscanで詳細を見ることができます。以下は僕のトランズアクションの例です。

[https://rinkeby.etherscan.io/tx/0x1d16e0ccb3947ecbd34e16f9ae9378a17b16e11c701f1d0f44e5262d0e8f018a](https://rinkeby.etherscan.io/tx/0x1d16e0ccb3947ecbd34e16f9ae9378a17b16e11c701f1d0f44e5262d0e8f018a) https://rinkeby.etherscan.io/tx/0x1d16e0ccb3947ecbd34e16f9ae9378a17b16e11c701f1d0f44e5262d0e8f018a

2. DAIをETHに交換する

次に実際にDAIをETHに交換する部分を実装します。今回も前のコードと重複している部分は省きます。DaiToEthRinkeby.mjsというファイルを作り、tokenToEthSwapInput関数を呼びます。

function tokenToEthSwapInput(uint256 tokens_soldundefined uint256 min_ethundefined uint256 deadline) external returns (uint256 eth_bought);

この関数は引数を3つとります。tokens_sold は交換するERC20トークンの量です。min_ethは交換するETHの最低額です。deadlineはトランズアクションの期限です。

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をエンコードします。

const tokenToEthEncodedABI = daiExchangeContract.methods  
  .tokenToEthSwapInput(TOKENS_SOLDundefined MIN_ETHundefined DEADLINE)  
  .encodeABI();

このtokenToEthEncodedABIをトランズアクションオブジェクトのdataプロパティに渡します。

ファイルを走らせます。

npm run dai-to-eth-rinkeby

0x6011a9f50b80dba7a2cb3f9dbf327f0f4c28ec6a4205c2b4002b3a008aa480f4のようなトランズアクションハッシュを受け取ったら、etherscanでその詳細を確認できます。以下は僕のトランズアクションの例です。

[https://rinkeby.etherscan.io/tx/0x6011a9f50b80dba7a2cb3f9dbf327f0f4c28ec6a4205c2b4002b3a008aa480f4](https://rinkeby.etherscan.io/tx/0x6011a9f50b80dba7a2cb3f9dbf327f0f4c28ec6a4205c2b4002b3a008aa480f4) https://rinkeby.etherscan.io/tx/0x6011a9f50b80dba7a2cb3f9dbf327f0f4c28ec6a4205c2b4002b3a008aa480f4

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

ETHが追加されていることがわかります。

DAI以外の他のERC20トークンを交換したい場合は、トークンのアドレスを変えるだけで実装できます。

この記事がまだここにないサービスを作る一助になれば幸いです。😎


Taisuke Mino

👋 三野泰佑です。新しい金融経済のインフラを作ることがミッションです。その第一歩として、Goyemonを創りました(現在プライベートベータ)。暗号通貨を使った様々な金融サービスにアクセスできるウォレットです。簡単なインターフェースで貸借、交換、くじなどのサービスを使うことが可能です。Twitterやってます 🙂