巷ではICO(Initial Coin Offering)が話題です。2017年7月時点において日本国内でEthereum上のスマートコントラクトを使ったICOの事例がないというのも寂しいということで、今回飲食店では世界初で日本で初となるETH建のICOが開催される運びとなりましたのでサンタルヌーさん向けに提供したICOのSolidityのソースとコントラクトアーキテクチャの仕組みを解説して行きたいと思います。
ご意見、苦情はこちらまで -> @syrohei
ERC-20 Token Standard とは
ERC-20はトークンシステムを実現するためのプロトコルで主にトークンインターフェイスの機能を実装するfunctionが規定されています。
contract ERC20 {
function totalSupply() constant returns (uint totalSupply);
function balanceOf(address _owner) constant returns (uint balance);
function transfer(address _to, uint _value) returns (bool success);
function transferFrom(address _from, address _to, uint _value) returns (bool success);
function approve(address _spender, uint _value) returns (bool success);
function allowance(address _owner, address _spender) constant returns (uint remaining);
event Transfer(address indexed _from, address indexed _to, uint _value);
event Approval(address indexed _owner, address indexed _spender, uint _value);
}
ICOを規定する
pragma solidity ^0.4.11;
/// @title SaintArnould (Tokyo) Token (SAT) -
contract SaintArnouldToken {
string public constant name = "Saint Arnould Token";
string public constant symbol = "SAT";
uint8 public constant decimals = 18; // 18 decimal places, the same as ETH.
uint256 public constant tokenCreationRate = 5000; //creation rate 1 ETH = 5000 SAT
uint256 public constant firstTokenCap = 10 ether * tokenCreationRate;
uint256 public constant secondTokenCap = 920 ether * tokenCreationRate; //27,900,000 YEN
uint256 public fundingStartBlock;
uint256 public fundingEndBlock;
uint256 public locked_allocation;
uint256 public unlockingBlock;
// Receives ETH for founders.
address public founders;
// The flag indicates if the SAT contract is in Funding state.
bool public funding_ended = false;
// The current total token supply.
uint256 totalTokens;
pragma solidity ^0.4.11;
truffleフレームワーク上ではpragma宣言文があり、異なるバージョンのSoliditycompiler では実行コードの厳密なオーディットができたい為、極めて重要な設定項目です。
現在 v0.4.13までオーディットされており、今回は0.4.11+commit.68ef5810
を使いました。
solidityのコードのデバッグはremixがおすすめです
2行目以降
以下の3つの変数がERC20を使ったICO必須項目となっており、トークンの名称とシンポル、単位を決定します
string public constant name = "Saint Arnould Token";
string public constant symbol = "SAT";
uint8 public constant decimals = 18; // 18 decimal places, the same as ETH.
名称 | 型 | スペック |
---|---|---|
name | bytes32 | 上場時のトークン表記を規定 |
symbol | bytes32 | トークンのシンポル |
decimals | uint8 | 単位桁数 |
トークンの発行量
uint256 totalTokens;
トークンのバランスを提供するハッシュ
mapping (address => uint256) balances;
イベントトリガー
event Transfer(address indexed _from, address indexed _to, uint256 _value);
function SaintArnouldToken(address _founders,
uint256 _fundingStartBlock,
uint256 _fundingEndBlock) {
if (_founders == 0) throw;
if (_fundingStartBlock <= block.number) throw;
if (_fundingEndBlock <= _fundingStartBlock) throw;
founders = _founders;
fundingStartBlock = _fundingStartBlock;
fundingEndBlock = _fundingEndBlock;
}
コンストラクタではファウンダーのアドレスとスタートブロック、エンドブロックを指定します。
/// @notice Transfer `_value` SAT tokens from sender's account
/// `msg.sender` to provided account address `_to`.
/// @param _to The address of the tokens recipient
/// @param _value The amount of token to be transferred
/// @return Whether the transfer was successful or not
function transfer(address _to, uint256 _value) public returns (bool) {
// Abort if not in Operational state.
if (!funding_ended) throw;
if (msg.sender == founders) throw;
var senderBalance = balances[msg.sender];
if (senderBalance >= _value && _value > 0) {
senderBalance -= _value;
balances[msg.sender] = senderBalance;
balances[_to] += _value;
Transfer(msg.sender, _to, _value);
return true;
}
return false;
}
function totalSupply() external constant returns (uint256) {
return totalTokens;
}
function balanceOf(address _owner) external constant returns (uint256) {
return balances[_owner];
}
トークンを購入するfunctionを定義する
ここで重要なのはinternal modifierをつけることでコントラクト内部でしか実行できないように制限する。
// Crowdfunding:
/// @notice Create tokens when funding is active.
/// @dev Required state: Funding Active
/// @dev State transition: -> Funding Success (only if cap reached)
function buy(address _sender) internal {
// Abort if not in Funding Active state.
if (funding_ended) throw;
// The checking for blocktimes.
if (block.number < fundingStartBlock) throw;
if (block.number > fundingEndBlock) throw;
// Do not allow creating 0 or more than the cap tokens.
if (msg.value == 0) throw;
var numTokens = msg.value * tokenCreationRate;
totalTokens += numTokens;
// Assign new tokens to the sender
balances[_sender] += numTokens;
// sending funds to founders
founders.transfer(msg.value);
// Log token creation event
Transfer(0, _sender, numTokens);
}
ファイナライズを規定する。external modifierを使ってcontractの外部コールを有効にする。
送金ロックを解除し、ファウンダー側のトークンを約6ヶ月間ロックする。ファウンダー側のトークンはロックされているため最初に送金する場合はtransferFounders
を利用することとする。
function finalize() external {
if (block.number <= fundingEndBlock) throw;
//locked allocation for founders
locked_allocation = totalTokens * 10 / 100;
balances[founders] = locked_allocation;
totalTokens += locked_allocation;
unlockingBlock = block.number + 864000; //about 6 months locked time.
funding_ended = true;
}
function transferFounders(address _to, uint256 _value) public returns (bool) {
if (!funding_ended) throw;
if (block.number <= unlockingBlock) throw;
if (msg.sender != founders) throw;
var senderBalance = balances[msg.sender];
if (senderBalance >= _value && _value > 0) {
senderBalance -= _value;
balances[msg.sender] = senderBalance;
balances[_to] += _value;
Transfer(msg.sender, _to, _value);
return true;
}
return false;
}
最後に着金トリガーをファンクションコールに流す
/// @notice If anybody sends Ether directly to this contract, consider he is
function() public payable {
buy(msg.sender);
}
これにより、コントラクトインターフェイスに着金したETHが自動的にfunctionをトリガーするため、エンドユーザはコントラクトに送金するのみでコードが実行可能である。
ICOのテストケース
ICOではコードのオーディットが非常に重要です。コントラクトの挙動をブロックタイムを変化させながらEVM上の状態遷移を確認する。
今回はtestrpcとテストネットワーク(ropsten.testnetwork)を利用しました。
it("should deploy ico contract", function (done) {
SaintArnouldToken.deployed().then(function (instance) {
meta = instance;
done()
})
});
it(`should be blocktime start= ${startblock} end= ${endblock}`, function (done) {
meta.fundingStartBlock.call().then(function (startblock) {
// console.log(startblock.toNumber())
assert.equal(startblock.toNumber(), sblock, "startblock is not match");
return meta.fundingEndBlock.call().then((function (endblock) {
// console.log(endblock.toNumber())
assert.equal(endblock.toNumber(), eblock, "endblock is not match");
done()
}))
})
})
it("should be unable to investment for call a crowdsale ended", function (done) {
source = rx.Observable.create((observer) => {
const getblock = () => {
//console.log(`blocktime = ${web3.eth.blockNumber}`)
meta.finalize({
from: owner
}).then((result) => {
observer.onNext(result)
}).catch((err) => {
setTimeout(() => {
getblock()
}, 500)
})
}
getblock()
})
source.subscribe(x => {
web3.eth.sendTransaction({
from: sender,
to: meta.address,
value: web3.toWei(20, 'ether'),
gas: 200000,
gasPrice: 50000000000
}, (err, result) => {
if (err) {
meta.funding_ended.call().then((result) => {
assert.equal(result, true, "funding is not ended.");
return meta.balanceOf.call(owner)
}).then((result) => {
const owner_token_value = result.toNumber()
assert.equal(owner_token_value, sendETH * 5000 * 1e18 * 10 / 100, "funding is not ended.");
done()
})
} else {
//observer.onNext(result)
}
})
})
コードの監査を行う論点は以下の2つ
- function Callにアクセス権限をつけているか。想定されるセキュリティ要件を満たしているかどうか。
- コンパイルされたバイトコードが正常な挙動をどの実行環境でも動くかどうか (testrpc, private, ropsten net, main net)
コードのオーディットが完了すると、テストネットワークにデプロイされテストを経たあと、そののちメインネットにデプロイされました。
mainnetにデプロイされたものはこちらです
gasLimit > 200000
を設定してこのアドレスに送金すると上記のfunction buy()がコールされ、ユーザはトークンを購入することができます。
以上が今回のコードの開発の紹介でした。
最近のICOに一言
私は基本的にICOという概念には共感をしています。しかしながらプロジェクトの調査が極めて重要と考えています。具体的には
- コミュニティやシステムのネットワーク効果を伴わないICOプロジェクトのトークン
はエコシステム上のトークンインターフェイスとして機能しないため、将来性のあるトークンとは言えないと考えています。トークンの価値は自分自身で判断し、プロジェクトの支援や投資判断をすることをオススメします。