ERC20トークンを破棄する時は`address(0)`に送金する(送金できるとは言っていない)
はじめに
本記事は以下の人を対象としています。
- ERC20が何なのかを知っている
- MetaMask等のウォレットを使用したことがある
解説対象のコード
ERC20は規格のため、実装は自由です。しかし、すべての実装を自分で行うのはバグを生む可能性があるため、オープンソースの実装を利用することが一般的です。
ここでは、よく利用されるOpenZeppelinのERC20トークンコントラクトの実装を見ていきます。
参考にするコードは以下のものです。
transfer
メソッドを呼び出した時の挙動
ウォレットを用いてERC20トークンを送信する場合、ウォレットはスマートコントラクトのtransfer
メソッドを呼び出します。
OpenZeppelinの実装では、transfer
メソッド内で_transfer
メソッドを呼び出します。
そして_transfer
メソッドでは、_update
メソッドを呼び出します。
_update
メソッド
まずは、最終的に呼び出される_update
メソッドを見ていきます。
最初にfrom
がaddress(0)
であるかを確認しています。
ウォレットからERC20トークンを送信する場合、from
がaddress(0)
になることはないため、ここでは説明を省略します。
else
ブロック(from
≠address(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)
であるかを確認しています。
to
がaddress(0)
である場合は、このコントラクトの総供給量からvalue
分を減らしています。
そして、to
がaddress(0)
でない場合は、to
の残高にvalue
分を加算しています。
_updateメソッドの処理をまとめると、以下のような流れになります。
-
to
がaddress(0)
の場合-
from
の残高と総供給量からvalue
分を減らす
-
-
to
がaddress(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
を返すメソッドです。
_msgSenderがmsg.senderを返さない場合
基本的に、と書いたのはmsg.sender
を返さない場合があるためです。
例えば、メタトランザクションを使用した処理の場合、_msgSender
はガス代を肩代わりしたアドレスではなく、その操作を要求したアドレスの方が都合がよいです。
そのため、そのような場合は_msgSender
がオーバーライドされています。
それでは、最後に_transfer
メソッドを見ていきます。
_transfer
メソッド
ソースコードは以下のとおりです。
from
またはto
がaddress(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
メソッドのto
にaddress(0)
を指定することで、from
の残高と総供給量を減らす、という処理を行っていることを確認しました。
しかし、_transfer
メソッドではto
がaddress(0)
でないことを確認する処理になっているため、to
にaddress(0)
を指定できません。
これはどういうことでしょうか。
結論としては、_update
のto
にaddress(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