DAICOオープンソースフレームワーク #05 DaicoPool.sol の実装
21st, August, 2018

この回では、調達した資金を管理するDaicoPoolについて解説する。
各TokenSaleコントラクトはfinalizeされる際にこのDaicoPoolに調達資金を送金する。ICOで調達したすべての資金はこのコントラクトに集められ、プロジェクト開始時からTAP値に従って徐々にリリースされる。コンストラクタは以下の通りだ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* DaicoPool.sol */ // _votingTokenAddr = 発行するトークン=投票に使うトークンのアドレス // tap_amount = TAPの初期値 // _initialRelease = プロジェクト開始時にPoolからリリースされる一時金(wei) constructor(address _votingTokenAddr, uint256 tap_amount, uint256 _initialRelease) public { require(_votingTokenAddr != 0x0); require(tap_amount > 0); //0は不可。永久に資金がロックされることになる initialTap = tap_amount; votingTokenAddr = _votingTokenAddr; status = Status.Initializing; initialRelease = _initialRelease; votingAddr = new Voting(ERC20Interface(_votingTokenAddr), address(this)); } |
tap_amountには、TAPの初期値[wei/sec]を与える。これは、あらかじめホワイトペーパーに宣言している値を与えるべきである。TAP値は公開情報であるため、ホワイトペーパーと異なる場合は参加者によって指摘され、信用を失う結果となる。
_initialReleaseについても同様で、ホワイトペーパーに記載した資金の初期リリース量を設定する。
DaicoPoolのコンストラクタ内でVotingコントラクトがデプロイされ、DaicoPoolのTAP値とSelfDestructionはVotingコントラクトに管理されることになる。
状態遷移
DiacoPoolは3つのstateを持つ。
- Initializing(プロジェクト開始前)
このときはまだ資金はリリースされない。TokenSaleから調達資金を受け取る。 - ProjectInProgress(プロジェクト進行中)
TAP値に従って資金がリリースされる。 - Destructed(SelfDestruction済み)
投票の結果SelfDestructionされた状態。TAPによる資金リリースは停止し、トークン保有者は返金を受けることができる。
変数
コントラクト内には以下の変数を持つ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/* DaicoPool.sol */ //現在のTAP値[wei/sec] uint256 public tap; //TAPの初期値[wei/sec] uint256 public initialTap; //プロジェクト開始時の初期資金リリース量 uint256 public initialRelease; //これまでにリリースされた金額[wei] 引き出されても減少しない点に注意。 uint256 public releasedBalance; //これまでに引き出された金額[wei] uint256 public withdrawnBalance; //releasedBalanceを最後に計算したタイムスタンプ uint256 public lastUpdatedTime; //調達した資金額(wei) uint256 public fundRaised; //Self Destruction時に支払われる手仕舞い資金の日数(TAP値*日数が支払われる) uint256 public closingRelease = 30 days; |
TokenSaleManagerとの関連付け
TokenSaleManagerをデプロイする際に、DaicoPoolのsetTokenSaleContract()が呼ばれることにより、DaicoPoolとTokenSaleManagerが関連付けられる。setTokenSaleConrractr()は一度しか実行できない。
DaicoPoolに関連付けられたTokenSaleManagerは、資金のリリースを開始するトリガーstartProject()を実行することができる。
1 2 3 4 5 6 7 8 |
/* DaicoPool.sol */ function setTokenSaleContract(address _tokenSaleAddr) external { /* Can be set only once */ require(tokenSaleAddr == address(0x0)); require(_tokenSaleAddr != address(0x0)); tokenSaleAddr = _tokenSaleAddr; } |
資金リリースの開始
TokenSaleManagerは、トークンセールが終わってfinalizeされる際に、DaicoPoolのstartProject()を実行する。
これにより、DaicoPoolのstateがInitializingからProjectInProgressに遷移し、初期資金がリリースされ、TAP値に従った資金リリースが開始する。
1 2 3 4 5 6 7 8 9 10 11 |
/* DaicoPool.sol */ function startProject() external onlyTokenSaleContract { require(status == Status.Initializing); status = Status.ProjectInProgress; //stateをプロジェクト進行中に変更 lastUpdatedTime = block.timestamp; //releaseBalanceの更新日時 releasedBalance = initialRelease; //初期の一時金をリリース updateTap(initialTap); //初期のTAP値を設定 fundRaised = address(this).balance; //セールによる調達資金額を確定 } |
資金の引き出し
プロジェクトの起案者(DaicoPoolのowner)は、時間の経過に応じてリリースされた資金をwithdraw()によって引き出すことができる。
withdraw()が実行されると、updateReleasedBalance()を呼び出してリリース済み残高を確認する。その後getAvailableBalance()によって引き出し可能残高(リリース済み残高−引き出し済み残高)を計算し、その額を上限に引き出しを行う。引数_amountで指定した金額、もしくは引き出し可能上限のどちらか小さいほうの金額が引き出される。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* DaicoPool.sol */ function withdraw(uint256 _amount) public onlyOwner { require(_amount > 0); uint256 amount = _amount; updateReleasedBalance(); //リリース済み残高を更新 uint256 available_balance = getAvailableBalance(); //引き出し可能残高 if (amount > available_balance) { amount = available_balance; //引き出し可能残高を超えない } withdrawnBalance = withdrawnBalance.add(amount); //引き出し済残高更新 owner.transfer(amount); //送金 emit WithdrawalHistory(“ETH”, amount); } |
残高の更新・確認関連のfunctionは以下の3つだ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/* DaicoPool.sol */ //リリース済み残高を更新 function updateReleasedBalance() internal { releasedBalance = getReleasedBalance(); //TAPによるリリース額を計算 lastUpdatedTime = block.timestamp; //計算時刻を更新 } //TAP値に従ってリリース済資金を計算 function getReleasedBalance() public view returns(uint256) { uint256 time_elapsed = block.timestamp.sub(lastUpdatedTime); //時間経過を計算 return releasedBalance.add(time_elapsed.mul(tap)); //TAP値 * 経過時間をリリース済み残高に加算 } //引き出し可能残高を計算 function getAvailableBalance() public view returns(uint256) { uint256 available_balance = getReleasedBalance().sub(withdrawnBalance); //リリース済残高-引出し済み残高 if (available_balance > address(this).balance) { available_balance = address(this).balance; //Poolが持つETH残高を超えない } return available_balance; } |
TAP値の更新
DaicoPoolは、Votingコントラクトの指示によりTAP値を更新することがある。VotingでRaiseTapに関する提案が可決された場合に、以下のraiseTap()が実行され、TAP値が更新される。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* DaicoPool.sol */ function raiseTap(uint256 tapMultiplierRate) external onlyVoting { updateReleasedBalance(); //これまでのTAP値に基づきリリース済み残高を更新 updateTap(tap.mul(tapMultiplierRate).div(100)); TAP値を更新 } //tapMultiplierRate = TAP変動率。単位は%である。(200を与えると200%=2倍になる) function updateTap(uint256 new_tap) private { //コントラクト内部からのみ呼び出し可能 tap = new_tap; emit TapHistory(new_tap); } |
DaicoPoolのraiseTap()としては、TAP値は増加させることも減少させることも可能だが、現在はVoting側で101〜200%の範囲に制限を入れている。これは、投票に不正があった場合の急激な変化を防ぐためである。
SelfDestruction
Votingコントラクトは、投票の結果、SelfDestructionの提案が可決されると、DaicoPoolのselfDestruction()を実行し、資金プールを「破壊」する。SelfDestructionが行われると、起案者にTAP値に基づいた30日分の資金をリリースし、TAPを閉じる。その後トークン保有者は返金を受けることが可能になる。
1 2 3 4 5 6 7 8 9 10 11 |
/* DaicoPool.sol */ function selfDestruction() external onlyVoting { status = Status.Destructed; updateReleasedBalance(); releasedBalance.add(closingRelease.mul(tap)); //30日分の資金をリリース updateTap(0); //TAPを閉じる uint256 _totalSupply = ERC20Interface(votingTokenAddr).totalSupply(); refundRateNano = address(this).balance.sub(getAvailableBalance()).mul(10**9).div(_totalSupply); //トークンに対する資金の返金レートを確定 } |
30日分の資金をリリースするのは、プロジェクトの手仕舞い(もしくは縮小)には費用がかかるためだ。たとえば、契約していたオフィスから退去するためには元の状態への復帰をする必要があるし、人件費をはじめとした各種経費も即日解約できる契約とは限らない。
返金
トークン保有者は、プールがSelfDestructionされていれば、トークンを返却することでETHの返金を受けることができる。返金される資金は、以下の式で表される。
返金される ETH = SelfDestruction後にプールに残った資金 ✕ (トークン返却数 / トークンの総発行量)
返金を受けるには、あらかじめトークンのapprove()によってDaicoPoolにトークンの引出し権限を付与しておく。
1 2 3 4 |
function approve(address _spender, uint256 _value) external returns (bool); // _spender = トークンの引出し権限を受け取るアドレス // _value = 引出し可能にするトークンの量 |
refund()を実行し、トークンを返却すると、トークンの量に応じて資金が返金される。
1 2 3 4 5 6 7 8 9 10 11 |
/* DaicoPool.sol */ function refund(uint256 tokenAmount) external poolDestructed { require(ERC20Interface(votingTokenAddr).transferFrom(msg.sender, this, tokenAmount)); //function実行者からトークンを引き出す uint256 refundingEther = tokenAmount.mul(refundRateNano).div(10**9); //返金額を計算 emit Refund(msg.sender, tokenAmount); msg.sender.transfer(refundingEther); //返金 } // tokenAmount = 返却するトークンの量 |
DaicoPoolの動作はこれですべてである。
Profile
解説:小幡 拓弥
ICOVO AGのCOOであり、ブロックチェーンエンジニア。2016年Blockchain HackathonにてMVP。自然言語処理や機械学習、チェスライクゲームAI開発の分野での経験を持つ。