Chapter 9-2: セキュリティを高めるための手法

blockchain.akifumifukaya.net

上記の記事で、コントラクトへの攻撃手法の説明をしました。

ここではセキュリティを高めるライブラリであるOpenZeppelinを紹介します。

OpenZeppelin

OpenZeppelinは、Solidityでセキュアなスマートコントラクトを作成する目的で作られた、基本的なスマートコントラクトのフォーマットを提供するライブラリ群です。

ERC20準拠トークン、クラウドセール、オーナー権限などのさまざまな雛形が用意されています。

スマートコントラクトはチューリング完全で自由度が高く、任意のプログラミングを作成して実行することが可能です。

しかし、自由度が高い反面、予期せぬ動作を発生させることにも繋がります。

OpenZeppelinに沿って作成することで、不具合や抜け漏れなどの障害を抑えることが可能になります。

開発元であるZeppelin Solutionsは、セキュリティに関する具体的な設計指針を「(Onward with Ethereum Smart Contract Security)https://blog.zeppelin.solutions/onward-with-ethereum-smart-contract-security-97a827e47702」として公開しています。

コントラクトの失敗は可能な限り早く失敗させ、かつ隠さない

assert, requireなどを使用して、チェックすることを心掛けましょう。

送金を引き出す機能を実装する

送金をコントラクト側で実行すると、fallback関数内で悪意のある処理を実行されてします。

Pull Payment Systemで実装しましょう。

状態確認、影響確認、動作の各機能に分割して実装する

「可能な限り早く失敗する」を実現する設計にも繋がります。

関数を実行する前に、関数を実行するための条件が揃っていることを確認します。

条件が揃っていなかったら処理を終了させます。

続いて、状態を更新し、更新が実行されなかったら場合、処理を中断してトランザクション前の状態に戻します。

最後に他のコントラクトやユーザーへのメッセージを返します。

EVMプラットフォームの制限に注意する

EVMプラットフォームの制限を把握しておきましょう。

配列が255以上になると、配列のループは必ずGasが枯渇して終了します。

また、開発者が予期しない操作をするのがユーザーです。

予期しない値も入力される前提にして、明示的に最適な型を指定しましょう。

EVMの仕様上、コールスタックにも1024回までという限界があります。

関数の処理にどの程度のスタックが必要なのか、常にコールスタックの限界を意識しましょう。

テストを書く

テストは必要以上にやっても損はありません。

リファクタリングや機能追加でも手助けになります。

テストは書くようにしましょう。

バグ報告機能と緊急停止機能を実装する

どんな開発者でもすべての脆弱性を把握することは困難でしょう。

バグを発見したユーザーに、報酬トークンを対価として支払ったり、コミュニティ内で何らかのインセンティブを付与するなど、報酬を払う仕組みを用意しておくと、バグ発見率が上がるはずです。

また、コントラクトは上書きできないので、緊急度が高いバグを発見した際は一時的にメンテナンスモードにするなどの要望もあります。

非常時のためにコントラクトをデプロイしたオーナーは、いつもでプログラムを停止状態にできる機能を実装しておくと、万が一のときに甚大な被害を回避することができます。

大量の資金を失うリスクを避けるため、デポジットの制限機能を用意する

コントラクト上に多くの資産を保有しているプログラムが狙われやすいです。

不用意に攻撃対象と認識されないために、コントラクトに預けるユーザーの資源量を制限する機能を実装するのは良い試みです。

ファイルを独立させてシンプルな実装にする

一般的なプログラムと同様に、モジュール構成でシンプルな実装を心掛けましょう。

シンオウるに小さくコードを記述することで、開発上の理解の向上に繋がります。

関数は短く、コードの依存関係を最小限にすることが重要です。

また変数や関数は理解しやすい命名に心掛けましょう。

ゼロから書き上げずに、OpenZeppelinのフォーマットを利用する

OpenZeppelinは、安全なコントラクトを作成するベストプラクティスを提供しています。

実績があり安全なコードがある場合は再利用することも選択肢の1つです。

Mythril

MythrilはSolidity用のセキュリティ分析ツールです。

コンパイル後のコードやSolidityのファイルから脆弱性を検知できます。

GethにデプロイしたコントラクトのアドレスをMythrilで指定することで、分析診断が可能です。

アセンブルしてスタックしたオペレーションを直接確認することができ、処理フローをグラフで表示することもできます。

スマートコントラクトの開発には念には念を入れたテストを実施し、攻撃は起き得るものと認識しておきましょう。

攻撃が発生した場合には、コントラクトを緊急停止して、セキュリティ分析ツールで調べるなどの準備をしておきましょう。

Chapter 9-1: スマートコントラクトへの攻撃手法と対策

9-1: スマートコントラクトへの攻撃手法と対策

スマートコントラクトは単なるプログラムです。

一旦、メインネットにデプロイされてしまえば、どのようなユーザーでも利用することができます。

コントラクトのソースコードは公開することが推奨されていますが、公開されていなくともetherscanにアドレスを問い合わせれば、誰でも参照可能です。

そのため、常に攻撃を受けるものと認識しましょう。

よく知られている攻撃手法を紹介します。

リエントラント(再入可能)

外部のコントラクトを呼び出す際、呼び出した関数が期待していないデータを変更してしまうことがあげられます。

TheDAO事件はこのバグを突かれています。

呼び出した関数の処理が終わらないうちに、他の関数を呼ばれてしまい、開発者の意図しない動作が発生してしまったのです。

残高を減らす前に送金してしまうコードが問題でした。

再現方法

攻撃者は送金処理を呼び出し、送金を受け取って、さらに送金処理を呼び出すコントラクトを作成します。

循環的に送金処理を行い払い出すEtherがコントラクト内になくなるか、設定されたGas Limitに達するかで、送金処理がエラーになるまで続きます。

TheDAO事件では、トークンの払い戻し処理の際に、攻撃するコントラクトからトークンの量を増やす処理を呼ぶことで、払い戻し処理をトークンの数量を増やしながら何度も行われました。

この悪意ある攻撃に対する解決策の1つとして、競合状態にある変数を不用意に変更されないようにミューテックスを利用する方法があります。

コントラクトを呼び出したいアカウントだけが変更可能なロック情報を作ることで、不意のメソッドが呼ばれるのを防ぐ方法です。

残高変更フラグでのロック状態を作るか、残高を先に変更してから送金処理を行うようにするなどの方法で対応しましょう。

トランザクションオーダー依存(TOD

トランザクションオーダー依存(TOD: Transaction Order Dependence)問題は、異なるユーザーがコントラクトを操作する場合、操作対象のトランザクションを送信した順序と、実際にブロックに取り込まれる順序が前後してしまい、意図通りに動作しないことです。

以下で、オークションコントラクト(AC)を例に考えてみます。

  1. 売り手が売りたいアイテムを1etherでACに登録

  2. 買い手はACにアイテムを購入するトランザクションを発行

  3. 売り手はアイテムの金額を2etherにするようにACの情報を更新

②→③の順番でトランザクションに取り込まれた場合、買い手はアイテムを1etherで購入できます。

しかし、ブロックチェーンは実時間とは違う時間が流れており、トランザクションは発行した時刻がどうであれ、ブロックに取り込まれる順番でしか前後関係を把握できません。

②→③の順番ではなく③→②の可能性もあります。

全てのトランザクションはGasを利用します。

Gasはブロック生成者の報酬となるため、各々のマイナーはGasが多いトランザクションを優先して取り込みます。

つまり、②で発生したトランザクションを売り手が検知することができれば、即座に③のトランザクションをGasを多めにして発行することで、買い手にアイテムを2etherで売ることが可能です。

そもそも、トランザクションの順序はマイナーに依存するため、この問題を防ぐ手立てはありません。

このケースでは、Webアプリケーションなどを前段に配置して、金額の更新は現在発行されている購入トランザクションがすべて完了後に実施するなどの対策を取る必要があるでしょう。

金融業界では同様の問題をフロントランニングと呼びます。

フロントランニングとは、顧客から売り注文が入った際に、売り注文が入ったことを証券会社が知ることで、事前に証券会社自らが売り抜けてしまうことを指します。

トランザクションオーダー依存の問題は、このフロントランニングの状態を引き起こしてしまいます。

また、トランザクションオーダー依存の問題は、トランザクションの送信アドレスが同一の場合には発生しません。

トランザクションが発行するたびに、アカウントに紐付くnonceは1つずつインクリメントされます。

同一アドレスからのトランザクションでは、このnonceで前後関係が判断されるため、意図しないトランザクションの順序入れ替えは発生しません。

タイムスタンプ依存

ブロックのタイムスタンプに依存する処理を記述する際、細心の注意が必要です。

ブロックのタイムスタンプは、ノードがブロックを生成した時刻です。

トランザクション発行時の時刻ではないので、各ノードに依存してしまいます。

タイムスタンプは、操作できる可能性があることを覚えておきましょう。

整数オーバーフロー

符号なし整数で表現される変数は注意しないと、オーバーフローする可能性があります。

オーバーフローを避けるためには、符号なし整数の型を大きくするか、または最大数に達するかどうかを判定する必要があります。

予期しないrevert

revertはコントラクトが失敗に終わった時に実行前に巻き戻すの処理です。

しかし、悪意あるユーザーによって、巻き戻しが必ず実行されるようになる可能性もあります。

ロジックに実行者の状態に依存する処理を含めることは避けましょう。

contract Auction {
address currentLeader;
uint highestBid;
function bid () payable {
require (msg.value > highestBid);
require (currentLeader.send(highestBid));
currentLeader = msg.sender;
highestBid = msg.value;
}
}
view raw Auction.sol hosted with ❤ by GitHub

上記のコード例は簡単なオークションのコントラクトです。

新しい入札者がbid()を呼び出した際の流れを説明します。

1つ目のrequireで現在の最高入札金額よりも新しい入札額が大きいかを確認します。

大きかった場合、2つ目のrequireで現在の入札者に現在の入札金額を返金します。

返金後は、新しい入札者は現在の入札者となり、新しい入札者が送金した金額が現在の最高入札金額になります。

このロジックでは、2つ目のrequireが問題となります。

悪意のあるコントラクト経由で入札しており、返金処理が必ず失敗する場合、このコントラクトは必ずトランザクション実行前に巻き戻ります。

つまり、誰も入札できない状態となります。

この状態を回避するために、「Pull Payment System」と呼ばれる返金方法に変更するべきです。

各々のユーザーに対する返金額を保持しておき、その金額を返却します。

contract Auction {
address currentLeader;
uint highestBid;
mapping (address => uint) usersBalance;
function bid () payable {
require (msg.value > highestBid);
// 現在の最高価格入札者の返金額を更新する
usersBalance[currentLeader] += highestBid;
currentLeader = msg.sender;
highestBid = msg.value;
}
function withdraw() {
require(usersBalance[msg.sender] > 0);
// 返金額を取得
uint amount = usersBalance[msg.sender];
// 送金
assert(msg.sender.send(amount));
}
}
view raw Auction.sol hosted with ❤ by GitHub

ブロックのGas Limit

ブロックにもGas Limitがあります。

トランザクションは実行された結果と共にブロックに入りますが、ブロックのGas Limitを超える場合は必ず失敗します。

例えばループ処理を実装していたとして、悪意のある攻撃者が必ずブロックのGas Limitを超えるようにしておけば、ループ処理は失敗します。

悪意のある攻撃者だけではなく、意図せず記述してしまう可能性もあるので、ループ処理には細心の注意を払いましょう。

強制的な送金

前述のリエントラントで説明したfallbackを使った脆弱性以外にも、ehterを送ることが可能です。

contract Vulnerable {
function () payable {
revert();
}
function doSomething() {
require(this.balance > 0);
// この下のコードが実行される可能性があります
}
}
view raw Vulnerable.sol hosted with ❤ by GitHub

上記のコードでは、送金時に動作するfallback関数内にrevert()を指定しているので、必ずトランザクション実行前の状態に戻ります。

このコントラクトが持つ残高がゼロの場合、doSomething()は残高をチェックしているので、それ以降の処理は実行されません。

しかし、このコントラクトに送金することは別経路でも可能です。

それはコントラクトを破棄するselfdestructを呼ぶことで実現できます。

selfdestructは引数にアドレスを指定し、その時点でコントラクトが所持しているetherをアドレス宛に送ります。

受け取り側がコントラクトでfallback関数を指定していたとしてもfallback関数は動作しません。

また、コントラクトがデプロイされる前に、コントラクトのアドレストォ予測して前もって送金することでも残高を増やすことができます。

このような可能性に留意してロジックを実装しましょう。

Chapter 8-3: ネットワークへのデプロイ

ネットワークへのデプロイ

ERC20準拠のトークンをTruffleからGethプライベートネット、Ropstenテストネットへデプロイします。

プライベートネットへのデプロイ

Gethを使用して、マイニングを実行します。

> miner.start(1)
null
> eth.mining
true

truffle.jsを以下のように書き換えてください。

module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "10"
},
}
};
view raw truffle.js hosted with ❤ by GitHub

上記のコードは、ネットワーク「development」を定義しています。

ローカルの8545ポートで動作しているnetwork_idが10番のネットワークを表しています。

$ truffle migrate --network development

上記のコマンドでデプロイします。

正常でデプロイが完了すると、以下のようなログが出力されます。

$ truffle migrate --network development
Using network 'development'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xa244887eacbe0ffff9d623a9b33ec898d05ae8e98ad9321eab71f8d1849ae51e
  Migrations: 0xc6606b19bee1ebe99a49c444d2c1d3999dabfdee
Saving successful migration to network...
  ... 0x34e038e5138525df91ef983bcc45aca4563e5ff884bb24ad569ff8685580d951
Saving artifacts...
Running migration: 2_deploy_dapps_token.js
  Deploying DappsToken...
  ... 0x00d2adab66b24ee0294b0dace1835f0ed796f3742802ef79da15741acb188373
  DappsToken: 0xb7b98eb1cfad29a8f19ef9f4e32aa4490409482f
Saving successful migration to network...
  ... 0x61997403b5875340f3528463a9b24ff472ca9d4c81cccad9e9632a356021a17d
Saving artifacts...

DappsToken: 0xb7b98eb1cfad29a8f19ef9f4e32aa4490409482fコントラクトのアドレスとなります。

下記のコマンドでdevelopmentネットワークに接続します。

$ truffle console --network development

動作確認

truffle(development)> d = DappsToken.at("0xb7b98eb1cfad29a8f19ef9f4e32aa4490409482f")
truffle(development)> d.name()
'DappsToken'
truffle(development)> d.symbol()
'DTKN'
truffle(development)> d.balanceOf(web3.eth.accounts[0])
{ [String: '1000'] s: 1, e: 3, c: [ 1000 ] }
truffle(development)> d.balanceOf(web3.eth.accounts[1])
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }
truffle(development)> d.balanceOf(web3.eth.accounts[2])
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }
truffle(development)> d.transfer(web3.eth.accounts[1], 100)
{ tx: '0x94bfb6e4c8446bdd2ee16e0aa33aac46f0767f43e7e129c1ea5ed3a259052f65',
  receipt:
   { blockHash: '0xb8c4bf57f251cf4322fec3f94c23eaddbdb33f0ca6017546e154f65944a32665',
     blockNumber: 619,
     contractAddress: null,
     cumulativeGasUsed: 51519,
     from: '0x81c8e5be811f32a90e6a7143ccfd6660cdd8d971',
     gasUsed: 51519,
     logs: [ [Object] ],
     logsBloom: '0x00200000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000100000000000000000000000000000000000000000000000000002000000000010000000000000000008000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000002000000000000004000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000',
     root: '0x936e27befec4794daec189579dc35d623b488bf47d76d1b37271aad7e089bd8a',
     to: '0xb7b98eb1cfad29a8f19ef9f4e32aa4490409482f',
     transactionHash: '0x94bfb6e4c8446bdd2ee16e0aa33aac46f0767f43e7e129c1ea5ed3a259052f65',
     transactionIndex: 0 },
  logs:
   [ { address: '0xb7b98eb1cfad29a8f19ef9f4e32aa4490409482f',
       blockNumber: 619,
       transactionHash: '0x94bfb6e4c8446bdd2ee16e0aa33aac46f0767f43e7e129c1ea5ed3a259052f65',
       transactionIndex: 0,
       blockHash: '0xb8c4bf57f251cf4322fec3f94c23eaddbdb33f0ca6017546e154f65944a32665',
       logIndex: 0,
       removed: false,
       event: 'Transfer',
       args: [Object] } ] }
truffle(development)> d.balanceOf(web3.eth.accounts[0])
{ [String: '900'] s: 1, e: 2, c: [ 900 ] }
truffle(development)> d.balanceOf(web3.eth.accounts[1])
{ [String: '100'] s: 1, e: 2, c: [ 100 ] }
truffle(development)> d.balanceOf(web3.eth.accounts[2])
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }

テストネットへのデプロイ

テストネットには様々な選択肢があります。

代表的なテストネットは、Ropsten、Kovan、Rinkebyの3種ですが、それぞれブロック生成のコンセンサスアルゴリズムが違います。

テストネット コンセンサスアルゴリズム
Ropsten Proof of Work
Kovan Proof of Authority
Rinkeby Proof of Authority

ここでは、Ropstenネットワークに対してデプロイを行っていきます。

MetaMaskのインストール

MetaMaskは、Google Chromeで利用できるイーサリアムのウォレットです。

MetaMaskを利用して、Ropstenネットワークへのデプロイ準備します。

ネットワークにコントラクトをデプロイするためには、Gasコストがかかるため、ある程度の残高が必要です。

Ropstenネットワークにコントラクトをデプロイするには、RopstenネットワークのEtherが必要です。

MetaMaskを使って、Etherを入手できるサイトで残高を増やしましょう。

MetaMaskはGoogle ChromeのExtensionとして提供されています。

Google Chromeを起動して、エクステンションを追加しましょう。

MetaMask - Chrome ウェブストア

Chromeのステータスバー右にキツネのアイコンが表示されているので、クリックしましょう。

パスワードを入力し、パスワードと復元用の秘密鍵は忘れないように保存しておきましょう。

Etherの入手(MetaMask Ether Faucetへの接続)

MetaMaskの設定が完了したので、RopstenネットワークのEtherを入手しましょう。

MetaMask Ether FaucetChromeで開きます。

MetaMaskのアイコンをクリックし、Repsten Test Networkネットワークに変更します。

キツネアイコンの右側をクリックすると、ネットワークの切り替えが行なえます。

f:id:akifumi-fukaya:20180417212334p:plain

「request 1 ether from faucet」ボタンをクリックしましょう。

トランザクションを発行され、MetaMaskから1.000ETH取得できたことが確認できます。

f:id:akifumi-fukaya:20180417212727p:plain

Infuraへの登録

Infura - Scalable Blockchain Infrastructure

Infuraは、Gethなどのイーサリアムクライアントを利用しなくとも、テストネットやメインネットにコントラクトをデプロイできる仕組みを提供しています。

Gethでデプロイすることはもちろん可能ですが、各ネットのブロックチェーンと同期する必要があるため、時間とディスク容量を大量に消費してしまいます。

https://infura.io/signup からアカウントを作成しましょう。

登録完了後、各ネットワークのURLが取得できます。

TruffleでのRopstenネットワークの設定

truffle-hdwallet-providerをインストールしましょう。

$ npm install truffle-hdwallet-provider

TruffleのプロジェクトにRopstenネットワークを設定します。

truffle.jsにネットワーク設定を追記しましょう。

var HDWalletProvider = require("truffle-hdwallet-provider");
var mnemonic = $MNEMONIC;
var accessToken = $INFURA_ACCESS_TOKEN;
module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "10"
},
ropsten: {
provider: function() {
return new HDWalletProvider(
mnemonic,
"https://ropsten.infura.io/" + accessToken
);
},
network_id: 3,
gas: 500000
}
}
};
view raw truffle.js hosted with ❤ by GitHub

$MNEMONICに、MertaMaskインストール時に保存したニーモニックを入力します。

$INFURA_ACCESS_TOKENは、Infuraにユーザー登録した際にメモしたURLの「https://ropsten.infura.io/」の後ろを入力します。

Gasを明示しないとデプロイに失敗するので、migrations/2_deploy_dapps_token.jsファイルでGasを明示しておきましょう。

var DappsToken = artifacts.require("./DappsToken.sol");
module.exports = function(deployer) {
var initialSupply = 1000e18;
deployer.deploy(DappsToken, initialSupply, {
gas: 2000000
});
};

Ropstenネットワークへのデプロイ

$ truffle migrate --network ropsten

上記のコマンドでRopstenネットワークへデプロイできます。

コントラクトのアドレスが出力されるので、メモしておきましょう。

デプロイ完了後、MetaMaskの画面を表示し、TOKENタブから「ADD TOKEN」ボタンをクリックします。

Token Contract Address, Token Symbol, Decimals of Precisionを入力して「Add」を押して登録しましょう。

MetaMaskのTOKENタブに1000DTKNが表示されて入れば、デプロイユーザーへの付与は完了しています。

Ropstenネットワークでトークンを送信してみましょう。

$ truffle console --network ropsten
truffle(ropsten)> d = DappsToken.at("0x7d20eaf8d2238eabec6fb98fb25f017e38b5d36f")
truffle(ropsten)> d.balanceOf("0x1297Cd103A77f87419a6bd68F56ecBD04d2978A2")
{ [String: '1e+21'] s: 1, e: 21, c: [ 10000000 ] }
truffle(ropsten)> d.balanceOf("0x1931Cb91567e1Ad51215A2a7364C1E263d7CCA85")
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }
truffle(ropsten)> d.transfer("0x1931Cb91567e1Ad51215A2a7364C1E263d7CCA85", 1e18)
{ tx: '0x4824274b48c424332c1b5f868fea0f98f6434fa7e1997d9324afed8dca0551ba',
  receipt:
   { blockHash: '0x5314ef977950aa4623fa96ce257871350900406f9c1dfbdfeb88c64724baf2a8',
     blockNumber: 3062029,
     contractAddress: null,
     cumulativeGasUsed: 82408,
     from: '0x1297cd103a77f87419a6bd68f56ecbd04d2978a2',
     gasUsed: 51839,
     logs: [ [Object] ],
     logsBloom: '0x00000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000020000001100010000000000000000000000000000000000000000000000000000400000000000000000000000010000000000000000000000000000000000000040000000000000000000000000002000000000000000000000000000000000000000800000000000000000000000000000008000000000000000000000000000000000000000000000000',
     status: '0x1',
     to: '0x7d20eaf8d2238eabec6fb98fb25f017e38b5d36f',
     transactionHash: '0x4824274b48c424332c1b5f868fea0f98f6434fa7e1997d9324afed8dca0551ba',
     transactionIndex: 1 },
  logs:
   [ { address: '0x7d20eaf8d2238eabec6fb98fb25f017e38b5d36f',
       blockNumber: 3062029,
       transactionHash: '0x4824274b48c424332c1b5f868fea0f98f6434fa7e1997d9324afed8dca0551ba',
       transactionIndex: 1,
       blockHash: '0x5314ef977950aa4623fa96ce257871350900406f9c1dfbdfeb88c64724baf2a8',
       logIndex: 1,
       removed: false,
       event: 'Transfer',
       args: [Object] } ] }
truffle(ropsten)> d.balanceOf("0x1297Cd103A77f87419a6bd68F56ecBD04d2978A2")
{ [String: '999000000000000000000'] s: 1, e: 20, c: [ 9990000 ] }
truffle(ropsten)> d.balanceOf("0x1931Cb91567e1Ad51215A2a7364C1E263d7CCA85")
{ [String: '1000000000000000000'] s: 1, e: 18, c: [ 10000 ] }
truffle(ropsten)>

MetaMask上でも、トークンの残高が変更されています。

Chapter 8-2: ERC20準拠のトークン作成

ERC20準拠のトーク

ERC20とは、イーサリアム上のトークンを標準化する仕様のことです。

ERC20に準拠するトークンをERC20準拠のトークンと呼びます。

ERC20に準拠することで、異なるトークン同士でのやり取りが簡単になり、ERC20対応のウォレットでの取り扱いが可能となります。

詳しくはこちらを参照してください。

ERC20 Token Standard - The Ethereum Wiki

プロジェクト作成

$ mkdir dapps-token
$ cd dapps-token
$ truffle init

OpenZeppelinのインストール

OpenZeppelinは、Solidity言語でセキュアにスマートコントラクトを開発できるオープンソースフレームワークです。

$ npm init -f
$ npm install zeppelin-solidity

トークコントラクトの作成

contracts/DappsToken.solファイルを作成し、トークコントラクトの実装を行います。

pragma solidity ^0.4.18;
import "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol";


contract DappsToken is StandardToken {
  string public name = "DappsToken";
  string public symbol = "DTKN";
  uint public decimals = 18;

  function DappsToken(uint initialSupply) public {
    totalSupply_ = initialSupply;
    balances[msg.sender] = initialSupply;
  }
}

dapps-token/DappsToken.sol at master · akifumi/dapps-token · GitHub

デプロイ時に動作するスクリプトを記述します。

migrations/2_deploy_dapps_token.jsファイルを作成します。

var DappsToken = artifacts.require("./DappsToken.sol");

module.exports = function(deployer) {
    var initialSupply = 1000;
    deployer.deploy(DappsToken, initialSupply);
};

dapps-token/2_deploy_dapps_token.js at master · akifumi/dapps-token · GitHub

テストコードの作成

tests/DappsToken.jsファイルを作成します。

var DappsToken = artifacts.require("./DappsToken.sol");

contract('DappsToken', function(accounts) {

  it("should put 1000 DappsToken in the first account", function() {
    return DappsToken.deployed().then(function(instance) {
      return instance.balanceOf.call(accounts[0]);
    }).then(function(balance) {
      assert.equal(balance.valueOf(), 1000, "1000 wasn't in the first account");
    });
  });
});

dapps-token/DappsToken.js at master · akifumi/dapps-token · GitHub

テストを実行します。

$ truffle develop
truffle(develop)> test

migrateコマンドでdevelopネットワークにデプロイし、設定した名前などを確認しましょう。

truffle(develop)> migrate
truffle(develop)> dappsToken = DappsToken.at(DappsToken.address)
truffle(develop)> dappsToken.name()
'DappsToken'
truffle(develop)> dappsToken.symbol()
'DTKN'
truffle(develop)> dappsToken.totalSupply()
{ [String: '1000'] s: 1, e: 3, c: [ 1000 ] }

アカウント別の発行量を確認します。

アカウントの1番目は発行者なので、トークンを全額保持しています。

アカウントの2番目は保持量が0です。

truffle(develop)> dappsToken.balanceOf(web3.eth.accounts[0])
{ [String: '1000'] s: 1, e: 3, c: [ 1000 ] }
truffle(develop)> dappsToken.balanceOf(web3.eth.accounts[1])
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }

送金してみましょう。

送金完了後に各アカウントが所持するトークン数を確認すると、トークンが正常に動作していることがわかります。

truffle(develop)> dappsToken.transfer(web3.eth.accounts[1], 100)
{ tx: '0xf001aa4460728b5b75b96e7afdc72e739d5a53cf71dffc4cb6323b83d1d5b2d0',
  receipt:
   { transactionHash: '0xf001aa4460728b5b75b96e7afdc72e739d5a53cf71dffc4cb6323b83d1d5b2d0',
     transactionIndex: 0,
     blockHash: '0x933600dadc8888729d6d538a27944088e37859b2d931af5f5e1a19a9f15e780f',
     blockNumber: 13,
     gasUsed: 51519,
     cumulativeGasUsed: 51519,
     contractAddress: null,
     logs: [ [Object] ],
     status: '0x01',
     logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000010000008000000000000000000010000000080000000000000000000000000000000000000000000000000000000000000000010000000000000000000010000000000000000000000000000000000000000010000000002000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000010000000000000' },
  logs:
   [ { logIndex: 0,
       transactionIndex: 0,
       transactionHash: '0xf001aa4460728b5b75b96e7afdc72e739d5a53cf71dffc4cb6323b83d1d5b2d0',
       blockHash: '0x933600dadc8888729d6d538a27944088e37859b2d931af5f5e1a19a9f15e780f',
       blockNumber: 13,
       address: '0x345ca3e014aaf5dca488057592ee47305d9b3e10',
       type: 'mined',
       event: 'Transfer',
       args: [Object] } ] }
truffle(develop)> dappsToken.balanceOf(web3.eth.accounts[0])
{ [String: '900'] s: 1, e: 2, c: [ 900 ] }
truffle(develop)> dappsToken.balanceOf(web3.eth.accounts[1])
{ [String: '100'] s: 1, e: 2, c: [ 100 ] }

Chapter 8-1: Truffleフレームワークの活用

Truffle

Truffle(トリュフ)は、スマートコントラクトの開発に必要となる、コンパイル、リンク、デプロイ、バイナリ管理の機能を持つ統合開発環境フレームワークです。

デプロイやマイグレーションスクリプトで管理することができます。

Solidityを使用したスマートコントラクト開発のデファクトスタンダートになり得ます(※2018/04/13時点)。

truffleframework.com

インストール方法

Truffleはnpmでインストールすることができます。

$ npm install -g truffle

プロジェクト作成

$ truffle init

を実行するとプロジェクトを作成することができます。

$ truffle init
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!

Commands:

  Compile:        truffle compile
  Migrate:        truffle migrate
  Test contracts: truffle test

コマンドの実行が完了すると、以下のディレクトリとファイルが作成されます。

名前 説明
contracts/ スマートコントラクトのディレクト
migrations/ スクリプトを記述できるデプロイメントファイルのディレクト
test/ テストファイルのディレクト
truffle-config.js Truffleの設定ファイル
truffle.js Truffleの設定ファイルの雛形

Ganache

イーサリアム開発用のパーソナルブロックチェーン

アプリケーションがブロックチェーンに与える影響をGUIで確認することができる。

Ganache | Truffle Suite

Truffle Develop

Truffleには、開発用のブロックチェーンとしてTruffle Developが組み込まれています。

$ truffle develop

で実行することができます。

インタラクティブなプロンプトが表示され、対話式でコマンドを実行することができます。

コントラクトのコンパイル

$ truffle compile

上記のコマンドでソースコードコンパイルできます。

truffle(develop)> compile

Truffle Developからコンパイルすることも可能です。

Truffleはコンパイル時間を短くするため、差分コンパイルが採用されています。

差分コンパイルではなく、全ファイルをコンパイルし直したい場合は、allオプションを付けます。

$ truffle compile --all

マイグレーション

マイグレーションファイルは、イーサリアムネットワークにコントラクトをデプロイする際に使用されます。

プロジェクトに新たなコントラクトを追加する際には、新規でスクリプトを追加します。

実行されたマイグレーションの履歴は、マイグレーションコントラクトによってブロックチェーン上に記録されます。

$ truffle migrate

上記のコマンドでマイグレーションが実行されます。

以前にマイグレーションが正常に実行されている場合、truffle migrateは最後に実行されたマイグレーションから実行を開始し、新たに作成されたマイグレーションのみを実行します。

新たなマイグレーションが存在しない場合は何も実行しません。

マイグレーションを最初から実行したい場合は、resetオプションを付けます。

$ truffle migrate --reset

公式サンプルMetaCoin

Truffleフレームワークには、独自コインを作成してアカウント間でやり取りするコントラクトのサンプルが用意されています。

サンプルプロジェクトのダウンロード

ディレクトリを作成して、その中でtruffle unboxコマンドを実行することでサンプルを取得することができます。

$ mkdir metacoin
$ cd metacoin
$ truffle unbox metacoin

truffle unboxコマンドは、Truffleフレームワークが定期ィオうするサンプルをローかつ環境に展開するコマンドです。

こちらに様々なサンプルが用意されています。

truffleframework.com

コンパイルマイグレーションの実行

$ truffle develop
truffle(develop)> migrate
Using network 'develop'.
Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xe993a882da8ba0d176a7feffe95e0f5fb2b417d0a8f07320bdfa722004360851
  Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
  ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying ConvertLib...
  ... 0xb097805eae8952a028f633d8eb76256fb00782cac3a4989c59615f571f83add8
  ConvertLib: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
  Linking ConvertLib to MetaCoin
  Deploying MetaCoin...
  ... 0x678ce1cfd0903e0fb7ec5d293858c32f428184c18ccaea408e64f20ef1194e82
  MetaCoin: 0xf25186b5081ff5ce73482ad761db0eb0d25abfbf
Saving successful migration to network...
  ... 0x059cf1bbc372b9348ce487de910358801bbbd1c89182853439bec0afaee6c7db
Saving artifacts...
truffle(develop)> m = MetaCoin.at("0xf25186b5081ff5ce73482ad761db0eb0d25abfbf")

マイグレーションの実行が完了すると、上記のようなログが出力されます。

実行環境によって、出力される値は違います。

MetaCoinコントラクトの参照

マイグレーション時に出力されたログのMetaCoin: 0xf25186b5081ff5ce73482ad761db0eb0d25abfbfというところから、MetaCoinのアドレスを取得することができます。

at()にMetaCoinのアドレスを指定することで、MetaCoinのコントラクトを変数から参照できます。

truffle(develop)> m = MetaCoin.at("0xf25186b5081ff5ce73482ad761db0eb0d25abfbf")

getBalanceの呼び出し

truffle(develop)> m.getBalance(web3.eth.accounts[0])
{ [String: '10000'] s: 1, e: 4, c: [ 10000 ] }
truffle(develop)> m.getBalance(web3.eth.accounts[1])
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }

sendCoinでコインを送る

truffle(develop)> m.sendCoin(web3.eth.accounts[1], 1000)
{ tx: '0x6b650d826e20aba52c82636f3f2947a263a66714f0684975253527e6f810d1ea',
  receipt:
   { transactionHash: '0x6b650d826e20aba52c82636f3f2947a263a66714f0684975253527e6f810d1ea',
     transactionIndex: 0,
     blockHash: '0xd27c08f5cf5efd1c47ba15b451ac78643dba5af026812de3b77e51eddf038e5a',
     blockNumber: 6,
     gasUsed: 51024,
     cumulativeGasUsed: 51024,
     contractAddress: null,
     logs: [ [Object] ],
     status: '0x01',
     logsBloom: '0x00000000000000000000000000000000010000000000000000000010000000000000000000000020000000000000000000000000000000000000000000000000000000000000000010000008000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000010000000000000000000010000000000000000000000000000000000000000010000000002000000000000000000000000000000000000002000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' },
  logs:
   [ { logIndex: 0,
       transactionIndex: 0,
       transactionHash: '0x6b650d826e20aba52c82636f3f2947a263a66714f0684975253527e6f810d1ea',
       blockHash: '0xd27c08f5cf5efd1c47ba15b451ac78643dba5af026812de3b77e51eddf038e5a',
       blockNumber: 6,
       address: '0xf25186b5081ff5ce73482ad761db0eb0d25abfbf',
       type: 'mined',
       event: 'Transfer',
       args: [Object] } ] }
truffle(develop)> m.getBalance(web3.eth.accounts[0])
{ [String: '9000'] s: 1, e: 3, c: [ 9000 ] }
truffle(develop)> m.getBalance(web3.eth.accounts[1])
{ [String: '1000'] s: 1, e: 3, c: [ 1000 ] }

残高を確認する

truffle(develop)> m.getBalance(web3.eth.accounts[0])
{ [String: '9000'] s: 1, e: 3, c: [ 9000 ] }
truffle(develop)> m.getBalance(web3.eth.accounts[1])
{ [String: '1000'] s: 1, e: 3, c: [ 1000 ] }
truffle(develop)> m.sendCoin(web3.eth.accounts[1], 10000)
{ tx: '0x175c04f4e568c865ef3735bc5ffd041b8e03b7832889ac4f9b7547cecdc2c3da',
  receipt:
   { transactionHash: '0x175c04f4e568c865ef3735bc5ffd041b8e03b7832889ac4f9b7547cecdc2c3da',
     transactionIndex: 0,
     blockHash: '0x529962199f9efb27c9111da7d4340df033b0eb659cb3a20a5f2949ca57cb7df4',
     blockNumber: 7,
     gasUsed: 23561,
     cumulativeGasUsed: 23561,
     contractAddress: null,
     logs: [],
     status: '0x01',
     logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' },
  logs: [] }
truffle(develop)> m.getBalance(web3.eth.accounts[0])
{ [String: '9000'] s: 1, e: 3, c: [ 9000 ] }
truffle(develop)> m.getBalance(web3.eth.accounts[1])
{ [String: '1000'] s: 1, e: 3, c: [ 1000 ] }

テストコード

テストコードは以下になります。

metacoin/metacoin.js at master · akifumi/metacoin · GitHub

truffle(develop)> test

testコマンドでテストを実行することができます。

テストを記述しておけば、正常に動作することをチェックすることができます。

お金を取り扱うコントラクトを作成するのであれば、十分すぎるほどテストを実行しておくことをおすすめします。

Truffleについて

Truffle

Truffle(トリュフ)は、スマートコントラクトの開発に必要となる、コンパイル、リンク、デプロイ、バイナリ管理の機能を持つ統合開発環境フレームワークです。

デプロイやマイグレーションスクリプトで管理することができます。

Solidityを使用したスマートコントラクト開発のデファクトスタンダートになりつつあります。

truffleframework.com

インストール方法

Truffleは、npmでインストールすることができます。

$ npm install -g truffle

プロジェクト作成

$ truffle init

を実行するとプロジェクトを作成することができます。

$ truffle init
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!

Commands:

  Compile:        truffle compile
  Migrate:        truffle migrate
  Test contracts: truffle test

コマンドの実行が完了すると、以下のディレクトリとファイルが作成されます。

名前 説明
contracts/ スマートコントラクトのディレクト
migrations/ スクリプトを記述できるデプロイメントファイルのディレクト
test/ テストファイルのディレクト
truffle-config.js Truffleの設定ファイル
truffle.js Truffleの設定ファイルの雛形

ちなみに、こんな感じのプロジェクトができました。

GitHub - akifumi/truffle-first

Truffle Develop

Truffleには、開発用のブロックチェーンとしてTruffle Developが組み込まれています。

$ truffle develop

で実行することができます。

インタラクティブなプロンプトが表示され、対話式でコマンドを実行することができます。

コントラクトのコンパイル

$ truffle compile

上記のコマンドでソースコードコンパイルできます。

truffle(develop)> compile

Truffle Developからコンパイルすることも可能です。

Truffleはコンパイル時間を短くするため、差分コンパイルが採用されています。

差分コンパイルではなく、全ファイルをコンパイルし直したい場合は、allオプションを付けます。

$ truffle compile --all

マイグレーション

マイグレーションファイルは、イーサリアムネットワークにコントラクトをデプロイする際に使用されます。

プロジェクトに新たなコントラクトを追加する際には、新規でスクリプトを追加します。

実行されたマイグレーションの履歴は、マイグレーションコントラクトによってブロックチェーン上に記録されます。

$ truffle migrate

上記のコマンドでマイグレーションが実行されます。

以前にマイグレーションが正常に実行されている場合、truffle migrateは最後に実行されたマイグレーションから実行を開始し、新たに作成されたマイグレーションのみを実行します。

新たなマイグレーションが存在しない場合は何も実行しません。

マイグレーションを最初から実行したい場合は、resetオプションを付けます。

$ truffle migrate --reset

最後に

Truffleを使って、早速アプリケーションを作成してみましょう!!

Chapter 1 - ブロックチェーンとは?

ブロックチェーンとは?

ブロックチェーン"インターネット依頼の発明"と言われるくらい注目されています。 暗号通貨システムとしてブロックチェーンの技術は発明されました。しかし、暗号通貨システム以外の様々な分野で応用される可能性が高まっています(ブロックチェーン2.0と呼ぶこともある)。

P2P方式

サービスを提供する「サーバ」とサービスを受ける「クライアント」が明確にその役割を分けている方式のことを、クライアントサーバ方式と言います。 世の中で動作している多くのサービスは、クライアントサーバ方式で動いています。

P2P方式は、すべての参加者がサーバにもクライアントにもなり、中央がいない方式です。 P2P方式は管理者がいないため、サーバ障害などサービスが止まることはありません。

DApps

DAppsとは、Decentralized Applications の略称です。 ブロックチェーン技術では、従来の中央集権型ではなく、自律分散型(非中央集約型)アプリケーション(DApps: Decentralized Applications)なシステムを実現できます。 ビットコインは世界で初めて登場したDAppsと言われています。

イーサリアム

イーサリアムは、誰でも自由にブロックチェーンにプログラムをデプロイし、新しいブロックチェーンアプリケーションを実装できるプラットフォームです。 イーサリアムによって、DAppsを開発する敷居はとても低くなりました。