はじめに
イーサリアムといえばトークン。ということで
ERC20トークンの概要と特徴、作成方法についてまとめました。
ERC20トークンとは
2015年11月にファビアン・フォゲルシュテラーによって最初の標準トークンERC20が提案された。ERC20はファンジブルトークン(fungible tokens)の標準である。ファンジブルトークンは様々な単位の互換性を持つが、固有の特性はない。大部分のトークンは現在、ERC20に基づいている。
ERC20に必要な関数とイベント
一つ一つ説明してるのでサーっと読みたい人はここはパスしてもおk
ERC20に必要な関数とイベント
balanceOf
指定されたアドレスのトークン量を返す。
transfer
アドレスと金額を指定されると、transfer関数を実行したアドレスの残高から、指定されたアドレスに、指定されたトークン金額を送る。
transferFrom
送信者、受信者、および金額が指定されると、あるアカウントから別のアカウントにトークンを送る。approve関数と組み合わせて使用する。
approve
受信者のアドレスとトークン金額が指定されると、承認を発行したアカウントから、指定されたトークン金額を上限とする複数の送金を実行できるように、指定されたアドレスに権限を与える。
allowance
所有者アドレスと使用者アドレスが指定されると、使用者が所有者から引き出せる金額を返す。
Transfer
送金が成功された時にトリガーされるイベント。transfer, transferFrom関数のどちらもトリガーされる。
Apploval
approve関数の呼び出しが成功した時にログを出力するイベント。
以下のオプション関数もERC20標準で定義されている。
name
人間が読めるトークンの名前(例えば「米ドル」)を返す。
symbol
人間が読めるトークンの記号(例えば「USD」)を返す。
decimals
トークンが利用する小数点以下の桁数を返す。例えば、decimalsが8を返す時、1トークンは100000000を示す。ユーザビリティ向上のために使われる。
ERC20のtransferとapprove & transferFrom
ERC20には、2つの異なるワークフローが存在する。
transferワークフローは、transfer関数を使用した、単一トランザクションの単純なワークフローで、ウォレットが他のウォレットにトークンを送金する際に使われる。このワークフローの実行は簡単である。例えば、送信者が受信者にトークンを送りたい場合、送信者のウォレットがトークンコントラクトにトランザクションを送り、受信者のウォレットアドレスと送金量を引数にtransfer関数を呼び出すだけである。
approve & transferFromワークフローはapprove関数、transferFrom関数を使用する2つのトランザクションのワークフローである。このワークフローにより、トークン所有者は制御を別のアドレスに委任することができ、トークン配布用のコントラクトに制御を委任するために最も使用される。approve関数はトークンの販売者が販売するトークン量の上限を決める。transferFrom関数は第三者から実行され、approve関数によって定められたトークン上限以下のトークン量、販売者アドレス、購入者アドレスを指定することで、販売者の所有するトークンを購入者アドレスに送信する。ICOというクラウドファウンディング方法に使うこともできる。
ERC20の実装
実行環境は以下である。
###[実行環境]
-
MacBook Pro (16-inch, 2019) プロセッサ 2.3 GHz 8 コア Intel Core i9 メモリ 16 GB 2667 MHz DDR4
-
node v12.15.0 ・npm 6.13.7
パッケージ管理ツール。truffle をインストールする際に使用する。 -
solidity v0.6.2 イーサリアムのコントラクトを作成するために使用するプログラミング言語。最新のOpenZeppelinのライブラリのコンパイラバージョンがv0.6.2だったのでそれに合わせた。 2020年4月16日現在、最新バージョンはv0.6.6。
-
truffle v5.1.23
Solidity のフレームワーク。作成したコントラクトのコントラクトフォルダ、マイグレー
ションフォルダ、テストフォルダを自動で作成する。またコンパイラのバージョンを切り替えたり、外部ネットワークに接続することも可能。 -
Ganache
起動すると Eth を持つアカウントが作成され、Ethereum のプライベートテストネットワ
ークを構築するアプリケーション。開発をテストする際に使用される。
ERC20を実装する際は一から書くよりOpenZeppelinのライブラリを使うことがセキュリティ面を考えると良いだろうと思われる。OpenZeppelinを使うことで、潜在的なセキュリティリスクを減らすためであり、また実装が煩雑になりにくいからである。その他にもよりシンプルで読みやすいライブラリのConsensys EIP20というものがある。OpenZeppelinにはSafeMathライブラリなど他にもセキュアな関数を提供してくれるライブラリがあるため、今回はOpenZeppelinを使用する。使用したライブラリは以下である。
-
@openzeppelin/contracts/utils/ReentrancyGuard.sol
-
@openzeppelin/contracts/ERC20/IERC20.sol
-
@openzeppelin/contracts/ERC20/ERC20.sol
-
@openzeppelin/contracts/math/SafeMath.sol
-
@openzeppelin/contracts/GSN/context.sol
IERC20.solはインターフェースでERC20.solがオーバーライドする。ReentarncyGuard.solは関数修飾子nonReentrantをインポートし、再入不可能な関数を定義することができる。SafeMath.solは数値計算上のオーバーフロー、アンダーフロー脆弱性の対策を行った四則演算関数を提供している。
作成したコントラクト
マイトークンを作成しICOをするコントラクトを作成した。
MyToken.sol
pragma solidity >=0.5.0 <0.7.0;
import "../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../node_modules/@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
contract MyToken is ERC20 {
string public constant CoinName = "masaki obayashi coin";
string public constant Symbol = "MOC";
//100億トークンが初期値
uint256 constant _initial_supply = 10000000000;
address private _MakerAddress;
constructor() public ERC20(CoinName, Symbol) {
_MakerAddress = msg.sender;
_mint(msg.sender, _initial_supply);
}
function getMakerAddress() public view returns (address) {
return _MakerAddress;
}
}
MyToken.solでは、ERC20トークンを定義する。コンストラクタではデプロイしたアカウントをトークンの作成者とし、作成者の権限で初期量のトークンを_mint関数で発行している。
MyTokenICO.sol
pragma solidity >=0.5.0 <0.7.0;
import "../node_modules/@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "../node_modules/@openzeppelin/contracts/math/SafeMath.sol";
import "../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol";
//このコントラクトはOpenZeppelin(release-v2.3.0) CrowdSaleを参考に作成した。
contract MyTokenICO is ReentrancyGuard {
using SafeMath for uint256;
uint256 private _rate;
address payable private _MOCOwner;
IERC20 private _token;
uint256 private _weiRaised;
constructor(uint256 rate, address payable MOCOwner, IERC20 token) public {
require(rate > 0, "Crowdsale: rate is 0");
require(
MOCOwner != address(0),
"Crowdsale: wallet is the zero address"
);
require(
address(token) != address(0),
"Crowdsale: token is the zero address"
);
_rate = rate;
_MOCOwner = MOCOwner;
_token = token;
}
event TokensPurchased(address beneficiary, uint256 value, uint256 amount);
receive() external payable {
buyMOC(msg.sender);
}
function token() public view returns (IERC20) {
return _token;
}
function MOCOwner() public view returns (address payable) {
return _MOCOwner;
}
function rate() public view returns (uint256) {
return _rate;
}
function buyMOC(address beneficiary) public payable nonReentrant {
uint256 weiAmount = msg.value;
require(
beneficiary != address(0),
"Crowdsale: beneficiary is the zero address"
);
require(weiAmount != 0, "Crowdsale: weiAmount is 0");
//購入するトークン量を計算
uint256 tokens = _getTokenAmount(weiAmount);
_weiRaised = _weiRaised.add(weiAmount);
_token.transferFrom(_MOCOwner, msg.sender, tokens);
_forwardFunds();
emit TokensPurchased(beneficiary, weiAmount, tokens);
}
function _getTokenAmount(uint256 weiAmount)
internal
view
returns (uint256)
{
return weiAmount.mul(_rate);
}
function _deliverTokens(address beneficiary, uint256 tokenAmount) internal {
_token.transfer(beneficiary, tokenAmount);
}
function _processPurchase(address beneficiary, uint256 tokenAmount)
internal
{
_deliverTokens(beneficiary, tokenAmount);
}
function _forwardFunds() internal {
MOCOwner().transfer(msg.value);
}
}
MyTokenICO.solでは、作成したERC20トークンを販売者からいつでも買えるようにするためのコントラクトである。このコントラクトがイーサを含んで呼び出されるreceive関数が呼び出され、イーサ分のトークンを購入できる。buyMOC関数で購入処理をするが、ここは再入不可能でなければならないため、nonReentrantを使用する。62行目でtransferFrom関数を呼び出し、トークンの作成者からトークンを受け取る。63行目で購入に使用したイーサを販売者の利益として送金する。
図1 approve & transactionFromワークフロー
テスト
テストは簡単にだけ。(本当はちゃんとやらないといけないけど、めんどいので)
実行してパスしたらデプロイにうつる。
/test/my_token.js
const MyToken = artifacts.require("MyToken");
const assert = require('assert');
contract("MyToken", function () {
//コインベースのアカウントに100億トークン入ったか確認
it("accounts[0] own 10000000000 MOC", async function () {
var instance = await MyToken.deployed();
return instance.balanceOf("0xcf55542d580f40cA9218101846EF1427f50dB58b").then((balance) => {
assert.equal(balance, 10000000000, "accounts[0] not have 10000000000 MOC");
});
});
//他のアドレスはトークンを所持していないか
it("accounts[1] own 0 MOC", async function () {
var instance = await MyToken.deployed();
return instance.balanceOf("0x8a738D6e0Bea8310CeD3222203615A5203b97087").then((balance) => {
assert.equal(balance, 0, "0x8a738D6e0Bea8310CeD3222203615A5203b97087 not have 0 MOC");
});
});
//デプロイに使用したアカウントがオーナーのアドレスとして格納されているか
it("accounts[0] is MakerAddress", async function () {
var instance = await MyToken.deployed();
var MakerAddress = await instance.getMakerAddress();
return assert.equal(MakerAddress, "0xcf55542d580f40cA9218101846EF1427f50dB58b", "incorrect");
})
});
/test/my_token_i_c_o.js
const MyTokenICO = artifacts.require("MyTokenICO");
const MyToken = artifacts.require("MyToken");
const assert = require('assert');
contract("MyTokenICO", function () {
// トークンを送ることが可能か
it("send success", async function () {
let MICO = await MyTokenICO.deployed()
MyToken.deployed().then(inst => inst.approve(MICO.address, 100))
MyTokenICO.deployed().then(inst => inst.sendTransaction({ from: "0xdf81f6233147a64d6146Fb03B9050FC706753c99", value: 10 }))
return true
})
})
デプロイ
/migration/1588168229_my_token.js
const MyToken = artifacts.require("MyToken");
const MyTokenICO = artifacts.require("MyTokenICO");
module.exports = function (_deployer) {
// Use deployer to state migration tasks.
_deployer.deploy(MyToken).then(async () => {
var instance = await MyToken.deployed();
const MOCOwner = await instance.getMakerAddress();
return _deployer.deploy(MyTokenICO, 1, MOCOwner, MyToken.address);
})
};
MyTokenICOコントラクトはMyTokenコントラクトのコントラクトアドレスを引数で使用するため、非同期として処理する。(7~11行目)
プライベートブロックチェーンネットワークで実行
[実行前]
Ganacheを起動しtruffle consoleに接続する。
コントラクトをデプロイする時に使用したアカウント(Coinbase)はaccounts[0]であるため、初期量のトークンが入っている。accounts[1]はトークンを持っていないため、0である。
また、購入前のイーサの所持数は以下である。(accounts[0]のイーサが減っているのはデプロイにガスを消費したため)
[approve関数実行]
購入可能なトークンを100 MOCとし、approve関数を呼び出す。
[transferFrom関数実行]
accounts[1]を購入者としてMOCOwnerから購入するためにMyTokenICOを呼び出すトランザクションを作成。
[実行後]
実行後、トークンはaccounts[0]は100MOC送信され、accounts[1]に100MOC入ったことが確認できる。accounts[0]が実行後、100増えているためICOとして成り立っているのがわかる。ERC20の問題点
ERC20トークンには2つの問題点がある。
トークン送金時に受信者側に通知する方法が定義されていない。
イーサを送金する時とは違い、トークンはコントラクト内のmappingデータを書き換えるだけであるため、トークンが実際に送られるわけではないため、受信者は受け取り通知を定義されていない。追加で定義することも可能だが、無駄なgasを使用してしまうため、非効率な規格とされている。
誤送信した際にトークンが喪失してしまう
トークンの受信通知の方法がないため、送金を拒否することができない。つまり、誤送信した場合でもトランザクションが解決されてしまう。実際に誤送信してしまったために数百万ドル分のトークンが引き出し不可能になってしまった。
考察
ERC20コントラクトはmappingデータ構造によってaddressをkeyとしamountをvalueとしてデータを管理している。また、トークンを受取者側がコントラクトを呼び出すことでトークンを送信者の通知なしに引き出せる。しかしそれは、一見トークンをイーサと同じように送受金しているように振る舞うが、実際はコントラクト内のステータスを変更されているだけに過ぎない。そのため、送信者は常にウォレットを監視していなければ、トランザクションが発行されたことに気がつかない。ERC223はそのような問題点がどのように改善されているのだろうか。より深く調査を進めていきたい。
まとめ
ERC20トークンの特徴
- トークンの規格
- ファンジブルトークン(一つのトークンがユニークではない)
- mappimgデータ構造にトークン量を記録
- transfer関数、approve & transferFrom関数を使用して送受金
- 受信側に通知はない => 問題点
ERC20トークンは以上のような特徴がある。主な問題点としてはトークンの受信をサポートしてない送信先へのトークンの送信が失敗し、トークンを喪失してしまうことである。
トークンの規格に関しては、多くの議論が交わされており用途によって使用するトークンを変える必要がありそうである。よりトークンに関して調査を進めていきたい。
参考文献
[1] OpenZeppelin (https://blog.openzeppelin.com/)
[2] solidity Doc (https://solidity.readthedocs.io/en/latest/index.html)
[3] truffle Doc (https://www.trufflesuite.com/docs)
[4] Github issues ERC:Token Standard (https://github.com/ethereum/eips/issues/20)
[5] mastering Ethereum 著 Andreas M. Antonopoulos, Gavin Wood
[6] Github ConsenSys/token (https://github.com/ConsenSys/Tokens)