Solidityの文法に関する勉強がひと段落して、次にスマートコントラクトを実際に書いてみようと思って、最初に作ったのがICOで使われるトークンでした。
これがERCとの出会いでした。
「ふ〜ん、このトークンの規格のことをERC20と呼ぶんだな、なるほど」
「あれ、その上位互換が223?、CryptoKittiesで使われているのが721?、その中に165が使われていて...」
...ERC、多ない?
ということで、復習の意味も込めて、ERC○○についてまとめてみました。
#0. そもそもERCとは
ERCとは「Ethereum Request for Comments」の略称でEthereum Improvement Proposalで提案されて、採択されるEthereum上で発行されるトークンの規格のことです。
Ethereum に関連する技術仕様の提案書を指します。提案が重要と判断されると、議論が進み、最終的にEIP(Ethereum Improvement Proposal)として採択されます。
つまり、ERC20とは20番目に提案された文章のことを指します。
#1. ERC20
まずは、みんな大好き?ERC20です。
ICOで使われる独自トークンを発行するための規格で、仮想通貨取引所やウォレットで、独自トークンを一元管理するための規格として、提案されました。
インターフェースは以下の通りです。
contract ERC20Interface {
function totalSupply() public constant returns (uint);
function balanceOf(address tokenOwner) public constant returns (uint balance);
function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
function transfer(address to, uint tokens) public returns (bool success);
function approve(address spender, uint tokens) public returns (bool success);
function transferFrom(address from, address to, uint tokens) public returns (bool success);
event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}
ERC20に関しては多くの記事や解説があるので、各自で調べてみてください。
#2. ERC223
ICOで利用されていて、もはやデファクトスタンダードになっている、ERC20ですが、実は重大な問題を抱えています。
それは、トークンコントラクト自体にトランザクションを処理する機能がないということです。
通常のEOAではなく、誤って、コントラクトアカウントにトークンを送金してしまうと、そのトランザクションを処理する機能がないため、トークンが凍結されて、使えなくなってしまうため、実質的に消滅してしまうことになります。この問題により数億円分のトークンが消滅していると言われています。
この問題を解決するために提案されて規格がERC223です。
contract ERC223Interface {
function name() constant returns (string name)
function symbol() constant returns (string symbol)
function decimals() constant returns (uint8 decimals)
function totalSupply() constant returns (uint256 totalSupply)
function balanceOf(address _owner) constant returns (uint256 balance)
function transfer(address _to, uint256 _value) returns (bool)
function transfer(address _to, uint _value, bytes _data) returns (bool)
function tokenFallback(address _from, uint _value, bytes _data)
event Transfer(address indexed _from, address indexed _to, uint256 indexed _value, bytes _data)
}
2.1 tokenFallback
一番の特徴はtokenFallback関数です。この関数が、自身が受け取ることができるトークンかを判断し、もし、受け取ることができないトークンの場合、送金元に送り返すようになっています。
これにより、ERC20の問題を解決しています。
##2.2 transfer
また、インターフェースを見ると、transfer関数が2つあることが分かります。1つ目のtransferはERC20と互換性を持たせるためのもので、2つ目のtransferが223で独自に追加されたtransferです。トランザクションにdataを持たせることができるようになっています。
##2.3 transferFrom, allowance, approve
これらの関数は削除されました。
- [Reference]
https://github.com/ethereum/EIPs/issues/223
#3. ERC777
ERC223がtokenFallbackによって、トークンを送り返すことで、トークンの消滅を防いでいるのに対して、ERC777はコントラクトアドレスがトークンの送・受信ができるようなインターフェースが定義されています。
トークンのコントラクトは、Ierc777インタフェースをEIP-820を介して登録しなければなりません。
interface ERC777Token {
function name() public constant returns (string);
function symbol() public constant returns (string);
function totalSupply() public constant returns (uint256);
function granularity() public constant returns (uint256);
function balanceOf(address owner) public constant returns (uint256);
function send(address to, uint256 amount) public;
function send(address to, uint256 amount, bytes userData) public;
function authorizeOperator(address operator) public;
function revokeOperator(address operator) public;
function isOperatorFor(address operator, address tokenHolder) public constant returns (bool);
function operatorSend(address from, address to, uint256 amount, bytes userData, bytes operatorData) public;
event Sent(
address indexed operator,
address indexed from,
address indexed to,
uint256 amount,
bytes userData,
bytes operatorData
);
event Minted(address indexed operator, address indexed to, uint256 amount, bytes operatorData);
event Burned(address indexed operator, address indexed from, uint256 amount, bytes userData, bytes operatorData);
event AuthorizedOperator(address indexed operator, address indexed tokenHolder);
event RevokedOperator(address indexed operator, address indexed tokenHolder);
}
具体的にはEIP820で提案されている疑似イントロスペクションを使い、送信先が ITokenRecipient
に対応しているかをチェックしてトークンを送ります。
interface ITokenRecipient {
function tokensReceived(address from, address to, uint amount, bytes userData, address operator, bytes operatorData) public
}
##3.1 send
20, 223のtransferに対応する関数がこのsend関数です。
##3.2 operatorSend
この関数を使って、ITokenReceipentで実装されたアドレスかどうかを判断して、toのアドレスにトークンを送金するかを決定します。もし、送金が可能であれば、send関数を呼び出します。
- [Reference]
https://github.com/ethereum/EIPs/issues/777
#4. ERC721
ここまでに紹介したERC20, 223, 777は全て、Fungible Token(代替可能なトークン)でした。これらは、お互いに同じ価値を持つトークンのことを表しています。(1ETHは誰が持っていても、別の1ETHと交換が可能)
それに対して、ERC721はNon-Fungible Token(代替不可能なトークン)を定義するために、提案されました。
この規格を一躍有名なものにしたのは、間違いなくCryptoKittiesだと思われます。
interface ERC721 /* is ERC165 */ {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
function setApprovalForAll(address _operator, bool _approved) external;
function getApproved(uint256 _tokenId) external view returns (address);
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}
##4.1 ownerOf
見慣れた関数もたくさんありますが、特徴はownerOfです。この関数で、あるtokenIdのトークンを所有するアドレスを識別しています。
またERC721の列挙拡張で、ERC721EnumerableはNFTの完全なリストを公開し、それらを発見可能にすることができます。
interface ERC721Enumerable /* is ERC721 */ {
function totalSupply() external view returns (uint256);
function tokenByIndex(uint256 _index) external view returns (uint256);
function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}
##4.2 tokenOfOwnerByIndex
オーナーが所持している_index番目のトークンIDを返却する
ERC721の要件にはERC165に準拠することも含まれているため、ERC165 と ERC721 のインターフェース ID も定義しています。(ERC165については次節参照)
#5. ERC165
ERC165はスマートコントラクトがどのインタフェースを実装するかを公開し発見する標準的なメソッドを作成します。
ざっくり言うと、コントラクトがどんなインターフェースを実装しているのかを確認できるようにするためのインターフェースです。
interface ERC165 {
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
ERC20トークンインターフェイスのようないくつかの「標準インターフェイス」では、コントラクトがインターフェイスをサポートしているかどうかを照会することが有用である場合があります。特にERC20では、バージョン識別子が既に提案されています。ERC165の目的は、インタフェースの概念を標準化し、インタフェース・バージョンをインタフェース識別子にマッピングすることになります。
#6. ERC820
ERC820は、任意のアドレス(コントラクトまたは通常のアカウント)がそれがどのインタフェースを実装するかを登録することができ、どのスマートコントラクトがその実装を担当するかという、ユニバーサルなレジストリスマートコントラクトを定義します。
contract EIP820ImplementerInterface {
/// @notice Contracts that implement an interferce in behalf of another contract must return true
/// @param addr Address that the contract woll implement the interface in behalf of
/// @param interfaceHash keccak256 of the name of the interface
/// @return true if the contract can implement the interface represented by
/// `ìnterfaceHash` in behalf of `addr`
function canImplementInterfaceForAddress(address addr, bytes32 interfaceHash) view public returns(bool);
}
contract EIP820Registry {
mapping (address => mapping(bytes32 => address)) interfaces;
mapping (address => address) managers;
modifier canManage(address addr) {
require(getManager(addr) == msg.sender);
_;
}
/// @notice Query the hash of an interface given a name
/// @param interfaceName Name of the interfce
function interfaceHash(string interfaceName) public pure returns(bytes32) {
return keccak256(interfaceName);
}
/// @notice GetManager
function getManager(address addr) public view returns(address) {
// By default the manager of an address is the same address
if (managers[addr] == 0) {
return addr;
} else {
return managers[addr];
}
}
/// @notice Sets an external `manager` that will be able to call `setInterfaceImplementer()`
/// on behalf of the address.
/// @param addr Address that you are defining the manager for.
/// @param newManager The address of the manager for the `addr` that will replace
/// the old one. Set to 0x0 if you want to remove the manager.
function setManager(address addr, address newManager) public canManage(addr) {
managers[addr] = newManager == addr ? 0 : newManager;
ManagerChanged(addr, newManager);
}
/// @notice Query if an address implements an interface and thru which contract
/// @param addr Address that is being queried for the implementation of an interface
/// @param iHash SHA3 of the name of the interface as a string
/// Example `web3.utils.sha3('Ierc777`')`
/// @return The address of the contract that implements a speficic interface
/// or 0x0 if `addr` does not implement this interface
function getInterfaceImplementer(address addr, bytes32 iHash) public constant returns (address) {
return interfaces[addr][iHash];
}
/// @notice Sets the contract that will handle a specific interface; only
/// the address itself or a `manager` defined for that address can set it
/// @param addr Address that you want to define the interface for
/// @param iHash SHA3 of the name of the interface as a string
/// For example `web3.utils.sha3('Ierc777')` for the Ierc777
function setInterfaceImplementer(address addr, bytes32 iHash, address implementer) public canManage(addr) {
if ((implementer != 0) && (implementer!=msg.sender)) {
require(EIP820ImplementerInterface(implementer).canImplementInterfaceForAddress(addr, iHash));
}
interfaces[addr][iHash] = implementer;
InterfaceImplementerSet(addr, iHash, implementer);
}
event InterfaceImplementerSet(address indexed addr, bytes32 indexed interfaceHash, address indexed implementer);
event ManagerChanged(address indexed addr, address indexed newManager);
}
ERC165に似ていますが、ERC165はインターフェースで定義された実装すべきmethodのIDからinterfaceIDを生成しているのに対して、ERC820では実装している標準インターフェース名(例えば、ERC20)のhash値からinterfaceIDを生成しているというところに違いがあります。
- [Reference]
https://github.com/ethereum/EIPs/issues/820
#7. ERC827
ERC20をトークン用に拡張し、transferとapproveメソッド内でcallの実行を可能にする規格がERC827です。
ERC20では、AからBへトークンを送る時に、
- approveでBさんへの転送を許可し、
- transferでBさんへ送金する
という2つのトランザクションを発生させなければなりません。
そうするとgasも多くかかるし、ネットワークへの負荷も大きくなります。これを改善し、1つのトラザクションでERC20を送金しようというのが、ERC827です。
transferに関わる関数だけ、抜粋してみます。
##7.1 transfer
_dataパラメータで_toの関数を実行する。関数が正常終了した場合は_value量のトークンを_toに転送し、Transferイベントを発生させなければいけません。
_toアドレスへの呼び出しが失敗した場合、または_from口座残高に十分なトークンがない場合は、関数は例外を投げます。
ERC20のtransferは、_to.call(_data)の前に呼び出されます。
function transfer(address _to, uint256 _value, bytes _data) public returns (bool) {
require(_to != address(this));
require(super.transfer(_to, _value));
require(_to.call(_data));
return true;
}
また、transferFromとapproveは以下の通りです。transferと同じように第3引数として_dataが追加されていることが分かります。
function transferFrom(address _from, address _to, uint256 _value, bytes _data) public returns (bool) {
require(_to != address(this));
require(super.transferFrom(_from, _to, _value));
require(_to.call(_data));
return true;
}
function approve(address _spender, uint256 _value, bytes _data) public returns (bool) {
require(_spender != address(this));
require(super.approve(_spender, _value));
require(_spender.call(_data));
return true;
}
なお、Referenceの注意事項には、
「このメソッドをパラメーターとして転送された値を受け取るフォールバック関数で使用しないでください。フォールバック関数で転送された値の量を確認する方法はありません。」
とあります。これはERC223のtokenFallback関数みたいなものを指していると思われます。
ERC827はapprove, transfer, transferFromに第3引数としてbytes _dataを追加しただけの非常にシンプルな規格です。しかし、この拡張によってERC20がEthereumのトランザクションと同等の機能を持つことを可能にしています。
- [Reference]
https://github.com/ethereum/EIPs/issues/827
#8. まとめ
今回はトークンの規格についてまとめてみました。
しかし、まじめに調べて見ると、(当たり前ですが、)これ以上にもたくさんの規格が提案されていることが分かります。
Ethereumの学習を始めたばかりのときはこれらを1つずつ調べていると、途方も無いことになるので、まずはERC20をしっかりと理解して、その後はNFTの実装ができるERC721を学習するという流れが良いのかもしれません。