Gunosy Blockchain Blog

Gunosyの開発メンバーが知見を共有するブログです。

ERC20トークンのコードをViperとSolidityで比較してみた

はじめに

新規事業開発室の生田です。

今回はERC20と呼ばれる規格のトークンをSolidityとViperという2種類の言語で書いてみたので、その内容について紹介します。 Ethereumではスマートコントラクトを用いてトークンを簡単に発行することができます。

「そもそもEthereum、スマートコントラクトってなんぞや。」という方は先にこちらの記事をご覧ください。 これから紹介するViperについても少し書かれています。

blockchain.gunosy.io

ERC20トークンについて

ERC20とは

ERCは「Ethereum requests for comments」の略です。その20番目でToken Standardに関する議論*1がされ、EIPの20番目*2として提案され採用されたのでERC20トークンという名前がつけられました。 EIPとは「Ethereum Improvement Proposal」の略で、イーサリアムの改善提案を行って、採択されたものは実際にイーサリアムに反映していくという仕組みです。ビットコインにも同様にBIP「Bitcoin Improvement Proposal」があります。

ERC20という規格ができた理由

トークンはウォレットで管理する必要があるため、トークンごとの仕様が異なるとそれぞれウォレットを用意する必要があります。BitcoinとEthereumを別のウォレットで管理しないといけないように(まとめて管理出来るアプリもあったりしますが)、仕様が異なるトークンは別々に管理しなければいけません。

でも、新たにトークンを配布するために、わざわざ専用のウォレットも実装するというのは面倒です。そのため「みんなが自前で作らなくても規格に則ったウォレットがあれば、それ1つで全部のトークンを管理できるよね!」ということでERC20が生まれました。具体的にはMyEtherWallet, MistなどがERC20トークンを受け取ることが出来るウォレットです。

ERC20トークンの例

ICOの際にトークンを配布する際にERC20トークンはよく利用されます。有名なところだと、OmiseGoAugurはERC20に則って作られました。他にも同じ規格で作られているトークンがあると思うのでもしよければ調べてみてください。

ERC20トークンは最初に発行上限を決めてしまいます。そのためビットコインやイーサリアムのマイニングをすることによって新規発行される通貨とは性質が異なります。

ビットコインやイーサリアムは基本的にそれぞれ固有のブロックチェーンがあり、その中に取引記録が記録されていきますが、ERC20トークンはイーサリアムをベースにしており、取引記録はイーサリアムのブロックチェーン上に保存されていきます。自分たちで新たなブロックチェーンを構築せずとも、簡単に発行できる通貨のようなものがトークンです。

ViperとSolidityの概要

Viper

github.com

Pythonベースで記述されている言語です。まだ現在は実験中の言語のため、実際のコントラクトに利用することは推奨されていません。以下で詳しく紹介しますがセキュアでシンプルにコードを記述するために、Solidityではできることがいくつか出来なくなっていたりします。

Solidity

github.com

C++をベースにして作られた言語です。スマートコントラクトの記述に一番利用されている言語です。truffleやopen-zeppelinなど周辺ライブラリもかなり充実しています。

ソースコード

Viper

Transfer: __log__({_from: indexed(address), _to: indexed(address), _value: num256})
Approval: __log__({_owner: indexed(address), _spender: indexed(address), _value: num256})

name: public(bytes32)
symbol: public(bytes32)
decimals: public(num)
totalSupply: public(num)
balances: public(num[address])
allowed: public(num[address][address])

@public
def __init__(_name: bytes32, _symbol:bytes32, _initialSupply: num):
    self.name = _name
    self.symbol = _symbol
    self.decimals = 18
    self.totalSupply = _initialSupply * 10 ** self.decimals
    self.balances[msg.sender] = self.totalSupply

@public
@constant
def totalSupply() -> num256:
    return as_num256(self.totalSupply)

@public
@constant
def balanceOf(_owner: address) -> num256:
    return as_num256(self.balances[_owner])

@private
def _transfer(_from: address, _to: address, _value: num(num256)) -> bool:
    if self.balances[msg.sender] >= _value and self.balances[_to] + _value >= self.balances[_to]:
        self.balances[msg.sender] -= _value
        self.balances[_to] += _value
        log.Transfer(msg.sender, _to, as_num256(_value))
        return true
    else:
        return false

@public
def transfer(_to: address, _value: num(num256)) -> bool:
    self._transfer(msg.sender, _to, _value)
    return true

@public
def transferFrom(_from: address, _to: address, _value: num(num256)) -> bool:
    if _value <= self.allowed[_from][msg.sender] and _value <= self.balances[_from]:
        self.allowed[_from][msg.sender] -= _value
        self._transfer(_from, _to, _value)
    else:
        return false

@public
def approve(_spender: address, _amount: num(num256)) -> bool:
    self.allowed[msg.sender][_spender] = _amount
    log.Approval(msg.sender, _spender, as_num256(_amount))
    return true

@public
@constant
def allowance(_owner: address, _spender:address) -> num256:
    return as_num256(self.allowed[_owner][_spender])

Solidity

pragma solidity ^0.4.19;

contract TokenERC20 {
    string public name;
    string public symbol;
    uint8 public decimals = 18;
    uint256 public totalSupply;
    mapping (address => uint256) public balances;
    mapping (address => mapping (address => uint256)) public allowed;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    function TokenERC20(
        uint256 initialSupply,
        string tokenName,
        string tokenSymbol
    ) public {
        totalSupply = initialSupply * 10 ** uint256(decimals);
        balances[msg.sender] = totalSupply;
        name = tokenName;
        symbol = tokenSymbol;
    }

    function totalSupply() public view returns (uint256) {
        return (totalSupply);
    }

    function baranceOf(address _owner) public view returns (uint256) {
        return (balances[_owner]);
    }

    function _transfer(address _from, address _to, uint _value) internal {
        require(_to != 0x0);
        require(balances[_from] >= _value);
        require(balances[_to] + _value > balances[_to]);
        uint previousBalances = balances[_from] + balances[_to];
        balances[_from] -= _value;
        balances[_to] += _value;
        Transfer(_from, _to, _value);
        assert(balances[_from] + balances[_to] == previousBalances);
    }

    function transfer(address _to, uint256 _value) public {
        _transfer(msg.sender, _to, _value);
    }

    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
        require(_value <= allowed[_from][msg.sender]);
        allowed[_from][msg.sender] -= _value;
        _transfer(_from, _to, _value);
        return true;
    }

    function approve(address _spender, uint256 _value) public
        returns (bool success) {
        allowed[msg.sender][_spender] = _value;
        Approval(msg.sender, _spender, _value);
        return true;
    }

    function allowance(address _owner, address _spender) public view returns (uint256) {
        return (allowed[_owner][_spender]);
    }
}

ソースコードの実行

コントラクトの実行にはどちらもコンパイルする必要があります。コンパイル方法については以下のリンクをご覧ください

また、コンパイルしたコードをデプロイするには専用のクライアントが必要です。CUIだとgeth、GUIだとMistが一般的です。以下のリンク先で分かりやすく紹介されているので実際に手元で動かしてみたい方はこちらもご覧ください。

ソースコードの比較

1. コントラクト(クラス)の継承について

Solidityはコントラクトを contract hogeと名前を定義して、{}の中にコントラクトの内容を記述していきます。Viperはそのような定義はしません。これは、コントラクト(クラス)の継承の有無の違いだと思います。Solidityはコントラクトを継承することが可能です。名前をつけていないと別のところで親コントラクトとして定義することができないので、名前をつけている必要があります。

反対にViperはコントラクトを継承することができません。そのため、そもそもコントラクト自体に名前をつける必要がないので、名前をわざわざつけないと考えられます。

2. メソッドのスコープについて

1との関係で、SolidityとViperではメソッドのスコープの種類が異なります。

Solidity

  • public (外部呼び出し○, 子クラス呼び出し○)
  • private (外部呼び出し×, 子クラス呼び出し×)
  • internal (外部呼び出し×, 子クラス呼び出し○)
  • external (外部呼び出し○, 子クラス呼び出し×)

Viper

  • public (外部呼び出し○)
  • private (外部呼び出し×)

Solidityは継承が可能なので、子クラスから親クラスのメソッドを呼び出せるor呼び出せないを指定できる必要があります。これに外部呼び出しの可否と組み合わせると2*2で4通りのスコープになります。

Viperはそもそも継承がないので、外部から呼び出しできるか否かのみ指定出来れば問題ないため2種類となっています。

EthereumのウォレットParityでは、internalメソッドにすべきところがpublicメソッドになっているというバグが報告*3され、修正されています。このケースだと、外部から呼び出せないはずのメソッドが外部から呼び出せてしまうことになります。特にメソッドのスコープを間違えるとコントラクトの脆弱性に直結するため慎重に取り扱う必要があります。

3. 可読性について

Viperはコードの上から イベントの定義 -> 変数の型定義 -> メソッドの記述という順番を守らないとコンパイル時にエラーになります。Solidityはこの順番を守らなくてもエラーになりません。Viperのgithubのreadmeに it should be maximally difficult to write misleading codeと書かれているように、コードを読みやすくするための制限だと考えられます。

4. Viperの機能制限について

上記のソースコードには出てきませんが、Solidityは出来ることがViperでは出来ないというものがいくつかあります。

  • modifier

Solidityではメソッドを実行する前にmodifierと呼ばれるもの処理を先に実行することができます。Pythonのデコレータみたいなものと考えると分かりやすいかもしれません。これによって複数メソッドで共通で行われる処理を共通化したり、メソッドの実行権限を確認するバリデーションなどに利用することができます。

Viperではこれがありません。もし共通化出来るような処理があったとしてもメソッドにして切り出すか、各メソッドに記述する必要があります。

  • 無限ループ

Solidityでは無限ループを書くことができます。実際にはgaslimitの関係で、無限ループを書くメリットが一切ないのですが、それでも一応書くことができます。Viperはこれも書くことができません。

  • 再帰呼び出し

こちらもSolidityは書くことができますが、Viperは書けません。

まとめ

これまで紹介したように、Solidityでは出来てもViperでは出来ないことがいくつかあります。その代わりに、シンプルに堅牢なコントラクトを書くことがViperでは実現します。また、Pythonに慣れ親しんだ人は、Viperのほうが導入としてはとっつきやすいかもしれません。

現状ではスマートコントラクトを記述するための言語においてSolidityが盤石の地位を築いてますが「Solidity使わなくてもViperで実現出来るならViperで書こう」みたいなコントラクトの種類によって言語の棲み分けがこれから出てくるかもしれません。

最後に

Gunosyではブロックチェーン領域で新しいことにチャレンジしたいエンジニアの方を募集しています。興味のある方は下のリンクをぜひチェックしてください!

www.wantedly.com