Ethereum
aratanaDay 14

Truffleを使ってクラウドファンディングしてみる

aratana adventcalender 2017
この記事はaratana adventカレンダー14日目のエントリーです。
昨日は@aratana_tamutomoさんのCtagsでVimにタグジャンプ機能を追加するでした。
普段自分もVimを使っているのですが、タグジャンプ機能をもっとカスタマイズしていきたいなあと思いました。
本エントリーでは、最近趣味で勉強し始めたスマートコントラクトについて書きたいと思います。

はじめに

スマートコントラクトの開発には、Solidityを用いるのがデファクトスタンダードとなっていますが、Truffleというフレームワークを使うことでより効率的な開発ができるようになります。この記事では、クラウドファンディング機能を持ったコントラクトをEthereumネットワーク上にデプロイしアカウントを通して実行するところまでやっていきます。開発するコントラクトの持つ機能は以下のシンプルな機能に絞っています。
・プロジェクトへの出資
・集めた出資金の引き落とし(目標金額に達した場合)
・集めた出資金を出資者に返金(達しなかった場合)

コントラクトの作成

まずはプロジェクトディレクトリを作成しTruffleプロジェクトの初期化を行います。

mkdir sample-crowdfunding && cd sample-crowdfunding
truffle init

以下のディレクトリ及びファイルが生成されているかと思います。
・contracts/
・migrations/
・test/
・truffle.js
testディレクトリとtruffle.jsファイルには今回特に変更を加えません。(truffle.jsファイルはTruffleの設定ファイルです。)

ではcontractsディレクトリ以下にCrowdFunding.solファイルを作成し、以下のように中身を加えます。

CrowdFunding.sol
pragma solidity ^0.4.11;

contract CrowdFunding {
  struct Investor {
    address addr;
    uint amount;
  }

  address public owner;
  uint public numInvestors;
  uint public deadline;
  bool public goalReached;
  uint public goalAmount;
  uint public amountRaised;
  mapping (uint => Investor) public investors;

  // オーナーのみ実行可能にしたい処理が一部あるのでmodifierを設定
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }

  /**
  * コンストラクタ
  * @param _duration プロジェクトの期間を設定
  * @param _goalAmount 目標金額を設定
  */
  function CrowdFunding(uint _duration, uint _goalAmount) {
    owner = msg.sender;
    deadline = now + _duration;
    goalAmount = _goalAmount;
    goalReached = false;
    numInvestors = 0;
    amountRaised = 0;
  }

  /**
  * 出資を行う
  */
  function fund() payable {
    require(!goalReached);

    Investor investor = investors[numInvestors++];
    investor.addr = msg.sender;
    investor.amount = msg.value;
    amountRaised += investor.amount;
  }

  /**
  * 目標に達しているか確認し、達成した場合オーナーに出資金を送金
  * 達成していない場合は各出資者に出資金を返金
  */
  function checkGoalReached() public onlyOwner {
    require(!goalReached);
    require(now >= deadline);

    goalReached = true;
    if(amountRaised >= goalAmount) {
      owner.send(this.balance);
    } else {
      for(uint i = 0; i < numInvestors; i++) {
        investors[i].addr.send(investors[i].amount);
      }
    }
  }
}

作成したSolidityコードをコンパイル

次に作成したSolidityコードをEVMバイトコードへとコンパイルします。

truffle compile

コマンドを実行し、無事終了するとbuilds/contractsディレクトリ以下にコントラクトと同名のjson形式のファイルが作成されています。このファイルをartifactsと呼び、コントラクトのコンパイルやデプロイ時に上書きされるためユーザーが編集してはいけないようです。
参照 : COMPILING CONTRACTS

マイグレーションファイルの作成

次にマイグレーションを実行するためにmigrationsディレクトリ以下に2_deploy_crowd_funding.jsファイルを作成します。ファイル名の先頭の数字はマイグレーションが正常に実行されたか管理するために重要なものなので必ずつける必要があります。

2_deploy_funding.js
var CrowdFunding = artifacts.require('./CrowdFunding');

module.exports = function(deployer) {
  var _duration = 300;
  var _goalAmount = 100000;
  deployer.deploy(CrowdFunding, _duration, _goalAmount);
}

クラウドファンディングの期間を5分、目標金額を10etherに設定するため、コンストラクタに引数300、100000を渡しています。
実際にfund関数によってetherを送金する際にはBigNumber型に変換されるので変換後の値を目標金額としてセットしています。

マイグレーションの実行

truffle develop
truffle(develop)> migrate 

truffle developコマンドを実行してEthereumクライアントを立ち上げ、クライアント上でmigrateコマンドを実行することでマイグレーションが行われます。
(Truffle DevelopではなくGanacheなど他のEthereumクライアント上でデプロイしても問題ありません。)
無事に終了するとコントラクトのアカウントが表示されます。

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xcd244e4ca70b96163b78067a523661aebda5444219bef3ad113dace499b0bd17
  Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
  ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_crowd_funding.js
  Deploying CrowdFunding...
  ... 0xd932cba24140102c35180acb6f39a7f9a9aef3f145575e7695579d961527529e
  CrowdFunding: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
  ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...

クラウドファンディングしてみる

では、作成したコントラクトを実行してみます。
まず、

truffle(develop)> crowdFunding = CrowdFunding.at(CrowdFunding.address)

でEthereumネットワークにデプロイされたコントラクトオブジェクトをcrowdFundingという変数に格納します。
試しにcrowdFunding.amountRaisedを実行してみるとまだ誰も出資していないので0が確認できます。crowdFunding.goalReachedを実行するとこちらもfalseが確認できます。

truffle(develop)> crowdFunding.amountRaised() //現在の出資金の合計を返す
BigNumber { s: 1, e: 0, c: [ 0 ] }
truffle(develop)> crowdFunding.goalReached() //クラウドファンディングが終了した場合trueを返す
false

仮にaccounts[0]をクラウドファンディングのオーナー、accounts[1]とaccounts[2]を出資者1,2とみなし、それぞれの変数を定義します。
では出資者1から5ether、出資者2から6ether送金してクラウドファンディングを成功させてみます。

truffle(develop)> owner = web3.eth.accounts[0] //オーナーを定義
truffle(develop)> investor1 = web3.eth.accounts[1] //出資者1を定義
truffle(develop)> investor2 = web3.eth.accounts[2] //出資者2を定義
truffle(develop)> crowdFunding.fund.sendTransaction({from: investor1, gas: 5000000, value: web3.toWei(5, "ether")}) //出資者1から5ethの送金
truffle(develop)> crowdFunding.fund.sendTransaction({from: investor2, gas: 5000000, value: web3.toWei(6, "ether")}) //出資者2から6ethの送金

crowdFunding.amountRaisedを実行すると11etherが確認できます。
5分ほど待ってクラウドファンディングが終了したタイミングでcrowdFunding.checkGoalReachedを実行するとトランザクションが生成されているのが確認できます。

truffle(develop)> crowdFunding.amountRaised()
BigNumber { s: 1, e: 19, c: [ 110000 ] }
truffle(develop)> crowdFunding.checkGoalReached()
{ tx: '0xe29d615f72cd08bae138b4b3ee908687db557199c9b2abeb5451000233ae22e2',
  receipt: 
   { transactionHash: '0xe29d615f72cd08bae138b4b3ee908687db557199c9b2abeb5451000233ae22e2',
     transactionIndex: 0,
     blockHash: '0x031a9c20d049dd49678886e800149426359bb4f83da3f2f5154494532c5e96f2',
     blockNumber: 8,
     gasUsed: 51021,
     cumulativeGasUsed: 51021,
     contractAddress: null,
     logs: [] },
  logs: [] }

続いてcrowdFunding.goalReachedを確認するとtrueが返ってきます。
さらにアカウントごとの残高を確認するとオーナーは11etherを獲得し、出資者たちのアカウントからは出資額が引かれていることがわかります。
(各アカウントの残高はトランザクションで発生したGas分差し引かれているので実際にやりとりした金額より少ない数字になっています。)

truffle(develop)> crowdFunding.goalReached()
true
truffle(develop)> web3.fromWei(web3.eth.getBalance(owner), "ether").toNumber()
110.2338735
truffle(develop)> web3.fromWei(web3.eth.getBalance(investor1), "ether").toNumber()
94.9897128
truffle(develop)> web3.fromWei(web3.eth.getBalance(investor2), "ether").toNumber()
93.9927128

同様の手順で失敗ケースも確認できるかと思います。

おわりに

実際のサービス開発時には、セキュリティ等考慮することは他にもありますが簡単な機能だけならわずかなコード量でもスマートコントラクトを作成することができました。
Ethereumというプラットフォームを使ってどんなことができるのかと考えているとワクワクしますね。
初学者なので間違いを見つけた場合はコメントいただけるとありがたいです。

明日のaratana adventカレンダーは大先輩@YasuhiroKimesawaさんです、お楽しみに!

参考

Truffleドキュメント
Solidityドキュメント
Ethereum Foundationのオフィシャルサイトでは、独自トークンを発行してファンドを集めるクラウドセール(ICO)のサンプルプログラムが公開されています
Crowdsaleサンプルコード
堅牢なスマートコントラクト開発のためのブロックチェーン[技術]入門