Neutrinoで働くブロックチェーンエンジニアのブログ

渋谷の専門特化コワークNeutrinoに入居してブロックチェーンエンジニアとして働いています。元丸の内。(Neutrino運営企業とは直接関係ありません)

DEXを動かす〜Etherdeltaのコントラクトを実行する〜

前回、Etherdelta(DEX)のコントラクトをropstenネットワークにデプロイを試しました。

今回はデプロイ済のコントラクトのメソッドを呼び出して動かしてみます。

www.blockchainengineer.tokyo

この記事では、

  • macOS High Sierra 10.13.4
  • node.js:v9.4.0
  • web3: 0.17.0-alpha (etherdeltaで利用していた時のバージョンに合わせたため相当古い!)

を使っています。

前準備

Etherdeltaのコントラクトがデプロイされていることを前提とします。

www.blockchainengineer.tokyo

network: ropsten デプロイ済コントラクト: 0xa8397C75E3dE9AB2be026c3Eb1Cd16942A4b917E

Etherdeltaのコントラクトはこちらに公開されており、これがオリジナルになります。

https://github.com/etherdelta/smart_contract

Etherdeltaのおさらい

分散型取引所であるDEX(Decentralized EXchange)です。 特徴としては以下のようなものがあります。

  • ETH、ERC20のトークンを交換できる
  • 取引、注文、預け入れなどをコントラクト上で実行する
  • GOXしないかというと、預け入れた分についてはコードがハッキングされてGOXする可能性がある

トークンの保有情報は、Ethereum上にマッピングを作って管理します。

//大外で指定するKeyであるaddressはトークンのアドレス。内側のmappingで指定するのは(保有者、残高)の組
mapping (address => mapping (address => uint)) public tokens; 

その際、トークンは発行コントラクトアドレスで指定して管理されています。

TRX(Tron)なら"0xf230b790e05390fc8295f4d3f60332c93bed42e2"など。

Ethereumは特殊なため、0で管理しています。

ETH:"0x0000000000000000000000000000000000000000"

コントラクトコードの解説

コアとなる5つのイベント

ブロックチェーンに書き込むのはeventを通して行われます。基本的にはコアとなる5つのイベント「Order,Cancel,Trade,Deposit,Withdraw」に基づいて実行されます。

特に重要なのはDeposit,Order,Tradeです。

大きな流れとしては以下になりまして、図にしています。

  1. Depositでユーザーはコントラクトに預金を行います
  2. Orderでどのトークンとどのトークン(あるいはETH)を交換するかの注文発行をします
  3. TradeでOrder同士を確認して交換を行います。マッチング処理自体はオフチェーンでやってい流ようです

f:id:naomasabit:20180731083503j:plain

各イベントの記録内容は以下の通りです。

注文発行イベント

event Order(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce, address user);

引数(記録内容)

  • tokenGet:欲しいトークンのアドレス
  • amountGet:欲しい量
  • tokenGive:渡すトークンのアドレス
  • amountGive:渡す量
  • expires:有効期間(ブロック高)
  • nonce:注文番号
  • user:注文者

注文キャンセルイベント

event Cancel(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce, address user, uint8 v, bytes32 r, bytes32 s);

  • tokenGet:欲しいトークンのアドレス
  • amountGet:欲しい量
  • tokenGive:渡すトークンのアドレス
  • amountGive:渡す量
  • expires:有効期間(ブロック高)
  • nonce:注文番号
  • user:注文
  • v r s :ecrecoverで公開鍵を復元するための楕円曲線暗号要素

取引イベント(マッチしたらトレード実行する)

event Trade(address tokenGet, uint amountGet, address tokenGive, uint amountGive, address get, address give);

  • tokenGet:欲しいトークンのアドレス
  • amountGet:欲しい量
  • tokenGive:渡すトークンのアドレス
  • amountGive:渡す量
  • get:取得アカウント
  • give:渡すアカウント

預金イベント

event Deposit(address token, address user, uint amount, uint balance);

  • token:預金するトークンのアドレス
  • user:預金するユーザーのアドレス
  • balance:預金量
  • balance:預金後の残高

引き出しイベント

event Withdraw(address token, address user, uint amount, uint balance);

  • token:引き出すトークンのアドレス
  • user:引き出すユーザーのアドレス
  • amount:引き出す量
  • balance:引き出した後の残高

node.jsでコントラクトを動かす

web3のproviderを初期化する

準備としてweb3のproviderを初期化します。

//WEB3を呼び出す
const Web3 = require('web3');
//infuraのアクセストークンを設定
var accessToken = "your access token"; // 変更のこと
//ropstenテストネットに接続するためのproviderを初期化
const provider = new Web3.providers.HttpProvider(
   "https://ropsten.infura.io/" + accessToken
    )
//秘密鍵を設定
const privStr = 'your private key' // 変更のこと
//WEB3にproviderをセットして初期化
//ropstenのhttpproviderをセットする
const web3 = new Web3(provider);
//デプロイ済のコントラクトを設定する
const contractAccount = "0xa8397C75E3dE9AB2be026c3Eb1Cd16942A4b917E";
// ABIを設定する。ABIはremixから抽出
var abi = [{"constant":false,"inputs":[{"name"....}]
const contract = web3.eth.contract(abi).at(contractAccount);

ETHを預金するdeposit functionを呼び出す

まずはdeposit関数を呼び出します。 deposit関数はpayable属性がついていて、送金できる関数です。送金した額が預金されます。

function deposit() payable {
    //ETHの預金額を記録する tokenアドレス0はETH
    tokens[0][msg.sender] = safeAdd(tokens[0][msg.sender], msg.value);
    //event Deposit(address token, address user, uint amount, uint balance)の呼び出し
    //address tokenは0, msg.sender=送信者, msg.value=送金額,tokens[0][msg.sender]=送金後の送金者の残高
    Deposit(0, msg.sender, msg.value, tokens[0][msg.sender]);
  }

以下の順番で実施する流れにしています。

  1. トランザクションの作成(作成時に呼び出すメソッドを指定) // setTransaction()
  2. トランザクションの署名 // signTx()
  3. トランザクションの送金 // sendTransaction()

呼び出すコード例を書きました。

//トランザクションの作成
function setTransaction(data, fromAccount, toAccount, value){
  // アドレスの何個目かのトランザクションかをnonceを作成
  var count = web3.eth.getTransactionCount(fromAccount);
  // gasPriceを取得
  var gasPrice = web3.eth.gasPrice;
  // gasLimitはRopstenの上限である4700000に設定
  var gasLimit = 4700000;
  // トランザクションのフォーマットに沿って、gasや宛先など、detaも当てはめ
  //トランザクションの作成
  var rawTransaction = {
    "from": fromAccount,
    "nonce": web3.toHex(count),
    "gasPrice": web3.toHex(gasPrice),
    "gasLimit": web3.toHex(gasLimit),
    "to": toAccount,
    "value": value,
    "data": data,
    "chainId": 0x03 //ropsten
  };

  return rawTransaction;
}

//トランザクションの送金
function sendTransaction(rawTx){
   //送金前に署名する
   var signedTx = signTx(rawTx);
   web3.eth.sendRawTransaction('0x' + signedTx.toString('hex'), function(err, hash) {
   if (!err)
      console.log(hash);
   else
      console.log(err);
  });
}

// トランザクションに秘密鍵で署名する
function signTx(rawTx){
  var privKey = new Buffer(privStr, 'hex');
  var tx = new Tx(rawTx);
  tx.sign(privKey);
  var serializedTx = tx.serialize();
  return serializedTx;
}

//トランザクションの預金
function execDepositEth(from_account, to_account, value){
   //"データ"と言われる実行メソッドを指定するためのバイナリ(MethodIDと言われるもの)を作成
   var data = contract.deposit.getData();
   //"データ"をトランザクションに設定する
   var rawTx = setTransaction(data, from_account, to_account, value);
   //トランザクションを送る
   sendTransaction(rawTx);
}

//預金の実行
//fromAccount:送金元アドレス, contractAccount:コントラクトアドレス, value:0.1ETH(10weiの17乗)
execDepositEth(fromAccount, contractAccount, 100000000000000000);

0.1ETHが送られていることが分かります。

f:id:naomasabit:20180731211352p:plain

またExplorerで確認するとちゃんとdepositが呼ばれています。

f:id:naomasabit:20180731211548p:plain

なお、トークンを預金するにはdepositTokenを利用します。

orderで注文を発行する

注文の発行をします。こちらも"data"として指定する呼び出すメソッド、引数を変更するだけでdepositと基本は同じです。

// tokenGet:欲しいトークン amountGet:欲しい量 tokenGive:渡すトークンアドレス:渡す金額 expires:注文期間(block height)
  function order(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce) {
    //引数をSHA256でハッシュ化
    bytes32 hash = sha256(this, tokenGet, amountGet, tokenGive, amountGive, expires, nonce);
    //注文データベースを有効として記録
    orders[msg.sender][hash] = true;
    //  event Order(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce, address user);
    Order(tokenGet, amountGet, tokenGive, amountGive, expires, nonce, msg.sender);
  }

ただ、ropstenネットワークでトークンを持っていなかったため、手抜きとしてETH100weiの購入をETH200weiでするというわけのわからない注文をサンプルコードで発行しています。

function execOrder(from_account, to_account, value, tokenGet, amountGet, tokenGive, amountGive, expires, nonce){
  // "order" functionを指定してバイナリ(MethodIDと言われるもの)を作成
  // getData内に引数を入れることで、トランザクションに引数として設定できる
  var data = contract.order.getData(tokenGet, amountGet, tokenGive, amountGive, expires, nonce);
  // "データ"をトランザクションに設定する
  var rawTx = setTransaction(data, from_account, to_account, value);
  //トランザクションを送る
  sendTransaction(rawTx);
}

// ETHのトークンアドレスは0を設定
var ethAddress = "0x0000000000000000000000000000000000000000";
var fromAccount = "0x120f7826d29e414d8a47c19d5d2576c72f44eafe";

//fromAccount:送金元にfromAccount設定,送金先アカウントにコントラクトを設定,送金額を0,注文するtokenにethアドレスを設定,注文額を100wei,有効期限に4000000ブロック高を設定,売却額を200wei,nonceにはorder番号を設定
execOrder(fromAccount, contractAccount, 0, ethAddress, 100, ethAddress, 200, 4000000, 1)

トランザクションが生まれています。

f:id:naomasabit:20180731211650p:plain

トランザクションを選択して、eventLogを見ると、Orderの引数もみられます。

f:id:naomasabit:20180731211659p:plain

tradeで注文を実行する

マッチングが行われたら、trade関数で注文の実行を行います。

コントラクト側ではマッチするorderがあるかを確認します。

別アカウントで対応するorderを作成しないといけないのですが、今回そこまでできなかったので割愛します。

基本的にはdeposit,orderと同様に引数で"データ"を作成して、マッチさせるだけのため、大きく変わりないはずです。

まとめ

一通り、WEB3とproviderを使ってコントラクトを動かす手段をみてきました。

DEXはマッチングに対してアクションを起こすという、アプリケーションの基本的なロジックを抑えたコントラクトです。

Dappsを動かすというとき、DEXからお手本として触ってみるのが良いのではないかと私は思っています。

今回使ったコードは以下リポジトリにアップしています。 ※ただし、パッケージなどはetherdeltaの当時のバージョンを利用しているため、古い可能性が高いです。利用じはご注意ください。

github.com