💰

ERC20トークンを破棄する時は`address(0)`に送金する(送金できるとは言っていない)

2024/08/07に公開

はじめに

本記事は以下の人を対象としています。

  • ERC20が何なのかを知っている
  • MetaMask等のウォレットを使用したことがある

解説対象のコード

ERC20は規格のため、実装は自由です。しかし、すべての実装を自分で行うのはバグを生む可能性があるため、オープンソースの実装を利用することが一般的です。

ここでは、よく利用されるOpenZeppelinのERC20トークンコントラクトの実装を見ていきます。

参考にするコードは以下のものです。
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e203e025234a102406c266d1e798ce1ba00b5d6d/contracts/token/ERC20/ERC20.sol

transferメソッドを呼び出した時の挙動

ウォレットを用いてERC20トークンを送信する場合、ウォレットはスマートコントラクトのtransferメソッドを呼び出します。

OpenZeppelinの実装では、transferメソッド内で_transferメソッドを呼び出します。
そして_transferメソッドでは、_updateメソッドを呼び出します。

_updateメソッド

まずは、最終的に呼び出される_updateメソッドを見ていきます。

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e203e025234a102406c266d1e798ce1ba00b5d6d/contracts/token/ERC20/ERC20.sol#L183-L211

最初にfromaddress(0)であるかを確認しています。
ウォレットからERC20トークンを送信する場合、fromaddress(0)になることはないため、ここでは説明を省略します。

elseブロック(fromaddress(0))のコードを抜き出してみます。

    } else {
        uint256 fromBalance = _balances[from];
        if (fromBalance < value) {
            revert ERC20InsufficientBalance(from, fromBalance, value);
        }
        unchecked {
            // Overflow not possible: value <= fromBalance <= totalSupply.
            _balances[from] = fromBalance - value;
        }
    }

ここでは、from(送信元アドレス)がvalue(送信するトークンの量)以上のトークンを持っているかを確認し、持っている場合はfromからvalue分、残高を減らしています。

unchecked について

コードの中にあるuncheckedブロックはオーバーフロー/アンダーフローのチェック処理を省略します。

solidity0.8以降、コンパイル時にオーバーフロー/アンダーフローのチェック処理が追加されるようになりました。

対象のコードは、if文で残高が不足していないことを事前に確認しており、残高を減らす処理でアンダーフローは発生しないことがわかっています。
uncheckedを使用することで不要なチェック処理を減らし、ガス代を節約しています。

引き続き、_updateメソッドの処理を見ていきます。

    if (to == address(0)) {
        unchecked {
            // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
            _totalSupply -= value;
        }
    } else {
        unchecked {
            // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
            _balances[to] += value;
        }
    }

ここでは、to(送信先アドレス)がaddress(0)であるかを確認しています。

toaddress(0)である場合は、このコントラクトの総供給量からvalue分を減らしています。
そして、toaddress(0)でない場合は、toの残高にvalue分を加算しています。

_updateメソッドの処理をまとめると、以下のような流れになります。

  1. toaddress(0)の場合
    • fromの残高と総供給量からvalue分を減らす
  2. toaddress(0)でない場合
    • fromの残高からvalue分を減らす
    • toの残高にvalue分を加算する

さて、先ほどメソッドの呼び出し順はtransfer_transfer_updateであると説明しました。

_updateの内容は確認したので、次は_transfer、と言いたいところですが、まずは簡単なtransferメソッドの解説です。

transferメソッド

コードは以下のとおりです。少ないですね。

    function transfer(address to, uint256 value) public virtual returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, value);
        return true;
    }

ここでは、_transferメソッドの第一引数(from)に、このコントラクトを呼び出したアドレスを指定しています。
transferメソッドの引数にfromが存在しないのは、他人の残高を操作させないためです。

ちなみに、_msgSenderは、基本的にmsg.senderを返すメソッドです。
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e203e025234a102406c266d1e798ce1ba00b5d6d/contracts/utils/Context.sol#L17-L19

_msgSenderがmsg.senderを返さない場合

基本的に、と書いたのはmsg.senderを返さない場合があるためです。
例えば、メタトランザクションを使用した処理の場合、_msgSenderはガス代を肩代わりしたアドレスではなく、その操作を要求したアドレスの方が都合がよいです。
そのため、そのような場合は_msgSenderがオーバーライドされています。

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e203e025234a102406c266d1e798ce1ba00b5d6d/contracts/metatx/ERC2771Context.sol#L55-L63

それでは、最後に_transferメソッドを見ていきます。

_transferメソッド

ソースコードは以下のとおりです。
fromまたはtoaddress(0)の場合にエラーを発生させて処理を中断するだけです。

    function _transfer(address from, address to, uint256 value) internal {
        if (from == address(0)) {
            revert ERC20InvalidSender(address(0));
        }
        if (to == address(0)) {
            revert ERC20InvalidReceiver(address(0));
        }
        _update(from, to, value);
    }

先ほど、_updateメソッドのtoaddress(0)を指定することで、fromの残高と総供給量を減らす、という処理を行っていることを確認しました。
しかし、_transferメソッドではtoaddress(0)でないことを確認する処理になっているため、toaddress(0)を指定できません。
これはどういうことでしょうか。

結論としては、_updatetoaddress(0)をしたい場合は別のメソッドを使用することになります。
その別のメソッドというのは、_burnメソッドです。

最後に、_burnメソッドの解説と注意事項を記載して終わります。

_burnメソッド

_burnの実装は以下のとおりです。

    function _burn(address account, uint256 value) internal {
        if (account == address(0)) {
            revert ERC20InvalidSender(address(0));
        }
        _update(account, address(0), value);
    }

_updateメソッドに渡す第一引数(from)がaddress(0)でないことを確認し、第二引数(to)は、address(0)の固定値を指定しているだけの内容です。
これで、fromから残高を減らし、総供給量も調整しています。burn(焼却)の意味通りの処理ですね。

最後に、この_burnメソッドを呼び出す時の注意事項を記載します。

_burnメソッドを呼び出す時の注意事項

さて、この_burnメソッドはどこから呼び出されているのでしょうか。

実は、OpenZeppelinのERC20.solには_burnメソッドを呼び出すメソッドが存在していません。
そのため、このメソッドを使いたい場合はまずERC20.solを継承したコントラクトを作成します。そしてそのコントラクトで新しくメソッドを作成し、そこから_burn呼び出すように実装します。

ここで、_burnメソッドはトークンを焼却する対象となるアドレスを引数で指定していることに注目してください。
単純に_burnメソッドを呼び出すようなメソッドを作成してしまうと、他人のトークンを焼却できてしまいます。
新しく作成したコントラクトにOwnableなどの権限の設定ができるコントラクトを継承させ、適切なアクセス制限を掛けるようにしてください。

Discussion