SafeERC20について

2021/12/10に公開

1. Overview

ガイドのターゲット

  • SafeERC20とは何か、使うことのメリット
  • SafeERC20のコード解説
  • コードの参考

必要な知識

  • Solidityの簡単な文法

2. SafeERC20とは何か、使うことのメリット

SafeERC20はコントラクト内から安全にERC20トークンのtransfer関数を呼び出せるライブラリです。使うことのメリットは2つあります。1つ目は、transfer関数を呼び出すトークンアドレスがコントラクトアドレスか確かめられることです。extcodesize関数でアドレスのコードの長さを調べて確かめます。2つ目は、transfer関数の戻り値を受け取ることができることです。今のERC20トークンには戻り値がありますが、昔のERC20トークンには戻り値がありませんでした。そこで、call関数を使うことでbool値を受け取り、transfer関数が実行できたか確認できます。

3. SafeERC20のコード解説

OpenZeppelinのgithubより
SafeERC20.solのコード
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol
Address.solのコード
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol

今回はsafeTransferがどうやって処理されていくかコードを見ていきます。

library SafeERC20 {
    // addressのライブラリを使用します。
    using Address for address;

    // 使い方の例
    // address.safeTransfer(address to, uint256 value)
    function safeTransfer(
        IERC20 token, // トークンアドレス(コントラクトアドレス)
        address to, // 送り先
        uint256 value // ERC20トークンの送金額
    ) internal {
        // エンコードしてbytesの型にする
        _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
    }

それではSafeERC20ライブラリの下にある_callOptionalReturn関数を見てみましょう。

    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
        // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that
        // the target address contains contract code and also asserts for success in the low-level call.

        // transferの情報をdataにしてaddressライブラリのfunctionCall関数を呼ぶ
        bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
        if (returndata.length > 0) {
            // Return data is optional
            // 戻り値のデータをデコードし、bool値を読み取る
            require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
        }
    }

次にaddressライブラリを見ていきます。引数の数が異なるfaunctionCall関数が2つあり順番に呼ばれています。

    function functionCall(address target, bytes memory data) internal returns (bytes memory) {
        // エラー文を追加
        return functionCall(target, data, "Address: low-level call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
     * `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        // etherの送金額を設定
        // ERC20トークンを送るためここは0
        return functionCallWithValue(target, data, 0, errorMessage);
    }

その次はfunctionCallWithValue関数です。この中で外部コントラクトを呼び出すcall関数を使っています。

    function functionCallWithValue(
        address target, // トークンアドレス(コントラクトアドレス)
        bytes memory data, // transferの情報
        uint256 value, // etherの金額、ERC20トークンの場合は0
        string memory errorMessage // エラー文
    ) internal returns (bytes memory) {
        // このコントラクトのetherの保有額を確認、ERC20トークンの場合は関係ない
        require(address(this).balance >= value, "Address: insufficient balance for call");
        // targetのトークンアドレスはコントラクトアドレスでなければならない
        // extcodesize関数を使用して調べている
        require(isContract(target), "Address: call to non-contract");

        // call関数で外部のコントラクトの呼び出し
        // call関数はデータの参照はできないが戻り値でbool値を受け取れる
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        // successならreturndataを返し、それ以外ならrevertしてエラー文を返す
        return verifyCallResult(success, returndata, errorMessage);
    }

3. コードの参考

以下、説明を簡略化しますがAddressライブラリのfunctionCallWithValue関数で呼ばれた関数です。

    function isContract(address account) internal view returns (bool) {
        // This method relies on extcodesize, which returns 0 for contracts in
        // construction, since the code is only stored at the end of the
        // constructor execution.

        uint256 size;
        // extcodesize関数で該当のアドレスのコードの長さを調べられる
        // EOAまたはまだ割り当てられていない、もしくはなんらかの理由でコードが存在しない場合0になる
        assembly {
            size := extcodesize(account)
        }
        return size > 0;
    }
    function verifyCallResult(
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal pure returns (bytes memory) {
        if (success) {
            return returndata; // そのまま返す
        } else {
            // Look for revert reason and bubble it up if present
            if (returndata.length > 0) {
                // The easiest way to bubble the revert reason is using memory via assembly

                // returndataのメモリーの内容をエラー文にして返す
                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert(errorMessage);
            }
        }
    }

Discussion