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

第1回、第2回はDAICOVOの全体構成とデプロイ・運用フローについて解説した。第3回では、最も基本的なコントラクトであるトークンについて解説する。
トークンの要件
DAICOVOでは、ERC20トークンにERC223の特徴を追加したDaicovoStandardTokenのテンプレートを提供している。このテンプレートを使って簡単にトークンを発行することが可能だが、これ以外のトークンを用いてDAICOVOでICOを行うこともできる。その場合のトークンの要件は、次の通りである。
ERC20 インターフェイスを持つこと
1 2 3 4 5 6 7 8 9 10 |
/* ERC20インタフェース */ function totalSupply() external view returns (uint256); function balanceOf(address who) external view returns (uint256); function transfer(address to, uint256 value) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); function transferFrom(address from, address to, uint256 value) external returns (bool); function approve(address spender, uint256 value) external returns (bool); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); |
Mintable インターフェイスを持つこと
1 2 3 4 5 6 7 |
/* Mintableインタフェース */ bool public mintingFinished = false; function mint(address _to, uint256 _amount) onlyOwner canMint public returns (bool) function finishMinting() onlyOwner canMint public returns (bool) event Mint(address indexed to, uint256 amount); event MintFinished(); |
Ownable インターフェイスを持つこと
1 2 3 4 5 |
/* Ownableインタフェース */ address public owner; function transferOwnership(address newOwner) public onlyOwner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); |
DAICOVOでは、TokenはTokenControllerを通して発行(mint)される。TokenControllerからmint(address _to, uint256 _amount)がコールされて新規トークンを発行する形だ。mintを実行できるのはownerのみである。このため、トークンのownerをTokenControllerに変更しておく必要がある。
DaicovoStandardTokenの構成
DaicovoStandardTokenはERC20に準拠したトークンテンプレートだ。そこへ、コントラクトへのトークンの誤送信を防止するための規格ERC223から、一部のfunctionをオーバーライドしている。さらに、既知の問題を解消したり、機能に柔軟性を持たせたりするために、いくつかのfunctionや変数を拡張している。DaicovoStandardTokenのメンバー構成を表にまとめると以下のようになる。

ERC20のメンバーについては解説不要だろう。もし詳細な情報が必要なら、解説記事がたくさんあるので検索してほしい。
なお、DaicovoStandardTokenはOpen Zeppelinの提供するERC20のコードと、Dexaranの提供するERC223のコードを一部改変して使用している。どちらもMITライセンスのOSSである。
ERC20の課題
DaicovoStandardTokenがERC223の一部を導入している理由は、ERC20の抱える問題の1つを解決するためだ。その問題とは、「コントラクトへトークンのtransferを行うと、そのトークンは誰も取り出せなくなることがある」というものだ。
ERC20トークンを誰かに送りたい場合、transfer()のfunctionを使用して、残高を自分から相手へ移動する。このとき、受け取る側がEOA(Externaly Owned Account)の場合はユーザーがコントロールしているため、トークンを受け取ったことを知ることができる。
しかし、受け取る側がコントラクトの場合、コントラクトは自発的にトランザクションをチェックすることがないため、受け取ったことに気付かない。また、transfer()に同期してコントラクトのfunctionを実行する仕組みもない。そのうえ、そのコントラクトにトークンを取り出すfunctionが備わっていなければ、永久にそのコントラクト内に残高がロックされてしまう。
1 2 3 4 5 6 7 8 9 10 11 12 |
/* ERC20Standard.sol */ function transfer(address _to, uint256 _value) external returns (bool) { require(_to != address(0)); require(_value <= balances[msg.sender]); //残高を移し替えるだけなので、受け取る側はtransferに同期して動作しない balances[msg.sender] = balances[msg.sender].sub(_value); balances[_to] = balances[_to].add(_value); emit Transfer(msg.sender, _to, _value); return true; } |
この問題により、これまで何十億ドルもの価値に相当するトークンが失われたと言われている。
このことが特に起こりやすいのは、「トークンを送ることと引き換えに何かの動作をさせるコントラクト」を使う場合である。たとえば、「Aトークンを10個集めるとBトークンに交換できる」というfunctionを持つコントラクトCがあったとする。この交換を行うためにユーザーXがCにAトークン10個をtransfer()したとすると、先述の通りCはトークンを受け取ったことを検知できないため、先の問題が起こる。正しい手順は、次の2トランザクションで行う。
- トークンAのapprove()を使って、XからCにAトークンを10引き出す権限を付与する。
- Cの「A→Bへのトークン交換」functionを呼び出す。そのfunctionの中で、CはトークンAのtransferFrom()により、XからAトークンを10引き出す。これはapprove()により引出し権限を与えられているから可能になっている。
この手順が必要であると知らずに、単にCにトークンをtransferしてしまう例が多いため、多くのトークンが失われている。
ERC223の仕組み
上記のERC20の課題を解決するために提案されたのがERC223だ。こちらのリポジトリにサンプルコードと共に公開されている。ERC20(EIP20)と異なり、こちらは現時点でEIPに正式採用されてはいない。
ERC223は、誤ってコントラクトにトークンを送ってしまうことを防ぐという趣旨で提案された。その仕組みは、以下の通りである。
- 1. transfer()時に送り先がEOAかコントラクトかチェックする
- 2-a. 送り先がEOAの場合はそのままtransfer()する。
- 2-b. 送り先がコントラクトの場合、そのコントラクトのtokenFallback()をコールする。
- 3-a. tokenFallbackを持っていなければエラーとなりtransfer失敗となる。
- 3-b. tokenFallbackを持っていれば、その中でトークンの受け取り処理(先の例であれば、Aトークン10個に対してBトークン1個を発行する)を行う。
ERC223提案者による実装例は次の通りだ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* ERC223_token.sol */ function transfer(address _to, uint _value, bytes _data) { uint codeLength; //EOAかコントラクトかチェック assembly { codeLength := extcodesize(_to) } //残高移動 balances[msg.sender] = balances[msg.sender].sub(_value); balances[_to] = balances[_to].add(_value); if(codeLength>0) { //送り先がコントラクトの場合はtokenFallbackを呼ぶ ERC223ReceivingContract receiver = ERC223ReceivingContract(_to); receiver.tokenFallback(msg.sender, _value, _data); } emit Transfer(msg.sender, _to, _value, _data); } |
この手順により、ERC223のtransferは、EOAまたは明示的にトークンの受け取り方を定義しているコントラクトのみにトークンを送る仕組みになっている。
ちなみに、あるアドレスがEOAかコントラクトかを判別するには、以下のようなアセンブリコードでそのアドレスの保有するコードサイズを調べれば良い。コードサイズが0の場合はEOAである。
1 2 3 |
assembly { codeLength := extcodesize(_to) } |
ERC223のインターフェイス
ERC223では、ERC20のapprove関連のfunction(以下3つ)はすべて削除されている。
1 2 3 4 5 |
function allowance(address owner, address spender) external view returns (uint256); function transferFrom(address from, address to, uint256 value) external returns (bool); function approve(address spender, uint256 value) external returns (bool); |
これは、ERC20でコントラクトにトークンを送るための、先述した2STEPの手順が不要になったためだ。しかし、approveにはそれ以外の使い方1※もあるため、これを削除したERC223がERC20に対し機能的に上位互換であるとは言えない。
※1 たとえば、approveで自分の予備アドレスにトークンの引き出し権限を付与しておけば、万が一メインアドレスのプライベートキーを紛失するなどのトラブルがあっても予備アドレスからトークンを引き出すことが可能だ。
追加されたfunctionは、transfer(address _to, uint _value, bytes _data)だ。ERC20の2引数のtransfer(address _to, uint _value)に加え、3引数のこのfunctionがあり、どちらもトークンの送信に使える。
3番目の引数 dataは、tokenFallbackに付与され、トークン送信先コントラクトで処理を行う際に使われる引数データである。非固定長のbyteデータとなっているため、処理に必要な引数データは全てエンコードしてこの1つの引数にまとめる必要がある。
ユーザーがコントラクト側の仕様に沿ってデータをエンコードする必要があること、コントラクト側はデコードするロジックを自分で実装する必要がある(ライブラリ等が整備されていない)ことを考えると、現時点でこれを使いこなすのは非常にハードルが高いと言える。
DaicovoStandardToken
DaicovoStandardTokenは、ERC20のメンバーのうちtransfer()のみをERC223に差し替えた形になっている。ERC223と違って、approve関連の機能を削除しておらず、その他のインターフェイスもERC20に沿っている(ERC223はERC20と重複するfunctionに関してもところどころ異なるインターフェイスを持つ)。さらに、以下のfunctionや変数を追加している。
1 2 3 4 5 6 7 8 9 |
function forceTransfer(address _to, uint _value) external returns(bool); function increaseApproval(address _spender, uint _addedValue) external returns (bool) function decreaseApproval(address _spender, uint _subtractedValue) external returns (bool) string public name; string public symbol; uint8 public decimals; |
forceTransfer
forceTransferは、ERC20のtransferの機能をそのまま残すために追加したfunctionだ。つまり、送り先がコントラクトであるかどうかに関係なく、強制的にトークンをtransferする。
1 2 3 4 5 6 7 8 9 10 11 |
function forceTransfer(address _to, uint _value) external returns(bool) { require(_to != address(0x0)); require(_value <= balances[msg.sender]); //送り先をチェックせずに残高を移行 balances[msg.sender] = balances[msg.sender].sub(_value); balances[_to] = balances[_to].add(_value); emit Transfer(msg.sender, _to, _value); return true; } |
・increaseApproval, decreaseApproval
この2つのfunctionは、approveに関する既知の問題を回避するために用意された関数だ。DaicovoStandardTokenのベースとなるOpen Zeppelinのコードもこのfunctionを採用している。
approveの問題について詳細はここでは述べないので、こちらの議論を参照してほしい。一言で言えば、approve値の変更の際にDouble Spendにより想定より多くのトークンを引き出されてしまう問題が、ERC20に残っているということだ。
approve()はallowance値(他の人に一定量のトークンの引出し権限を与える)をダイレクトに変更するfunction(たとえば50→100に変更)だが、これを加減算で変更する(たとえば50+50→100)ことによりDouble Spendを回避することができる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function increaseApproval(address _spender, uint _addedValue) external returns (bool) { allowed[msg.sender][_spender] = allowed[msg.sender][_spender].add(_addedValue); emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]); return true; } function decreaseApproval(address _spender, uint _subtractedValue) external returns (bool) { uint oldValue = allowed[msg.sender][_spender]; if (_subtractedValue > oldValue) { allowed[msg.sender][_spender] = 0; } else { allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue); } emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]); return true; } |
name, symbol, decimals
これら3つの変数はERC20ではお馴染みのもので、それぞれ「トークン名」「トークンシンボル」「最小単位の小数以下の桁数(たとえば0.001がトークンの最小単位ならdecimals=4)」を示す。実はこれらの変数はERC20に正式には定義されていない。しかし、ほぼ全てのERC20トークンはこれらの変数を備えており、すでにERC20のデファクトスタンダードと言えるため、DaicovoStandardTokenは標準でこれらを持つ仕様にした。
以上がDaicovoStandardTokenの仕様だ。繰り返しになるが、これはあくまでICOVOの提案するトークンのテンプレートであって 、DAICOVOで使用するトークンは先述した要件を満たしていれば何でも良い。
次回はトークンの発行を管理するTokenControllerの動作とコードについて解説する。
Profile
解説:小幡 拓弥
ICOVO AGのCOOであり、ブロックチェーンエンジニア。2016年Blockchain HackathonにてMVP。自然言語処理や機械学習、チェスライクゲームAI開発の分野での経験を持つ。