ERC20

OpenZeppelinのERC20の取扱説明書

openZeppelin-erc20
0xkeesmark

紹介しているOpenZeppelinのバージョンはv5.0.0

ERC20の紹介をしていきたいとおもいます。主にERC20を使って以下のことができます。

  • 簡単にERC20の規格に沿った仮想通貨を作成できる
  • セキュリティの高い実装ができる。

ERC20とは?

ERC20は、イーサリアムブロックチェーンでの標準的なトークン規格です。ERC20のトークン同士なら互換性があるため、ウォレットや仮想通貨交換所やDefiなどに組み込み安くお互いをスワップするのも容易です。

ですが、イーサリアムのetherはこの標準規格ができる前に作られたトークンなので、ERC20の規格に沿っているわけではありません。

ERC20の中身

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/ERC20.sol)

pragma solidity ^0.8.20;

import {IERC20} from "./IERC20.sol";
import {IERC20Metadata} from "./extensions/IERC20Metadata.sol";
import {Context} from "../../utils/Context.sol";
import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol";

/**
 * @dev Implementation of the {IERC20} interface.
 *
 * This implementation is agnostic to the way tokens are created. This means
 * that a supply mechanism has to be added in a derived contract using {_mint}.
 *
 * TIP: For a detailed writeup see our guide
 * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
 * to implement supply mechanisms].
 *
 * The default value of {decimals} is 18. To change this, you should override
 * this function so it returns a different value.
 *
 * We have followed general OpenZeppelin Contracts guidelines: functions revert
 * instead returning `false` on failure. This behavior is nonetheless
 * conventional and does not conflict with the expectations of ERC20
 * applications.
 *
 * Additionally, an {Approval} event is emitted on calls to {transferFrom}.
 * This allows applications to reconstruct the allowance for all accounts just
 * by listening to said events. Other implementations of the EIP may not emit
 * these events, as it isn't required by the specification.
 */
abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
    mapping(address account => uint256) private _balances;

    mapping(address account => mapping(address spender => uint256))
        private _allowances;

    uint256 private _totalSupply;

    string private _name;
    string private _symbol;

    /**
     * @dev Sets the values for {name} and {symbol}.
     *
     * All two of these values are immutable: they can only be set once during
     * construction.
     */
    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    /**
     * @dev Returns the name of the token.
     */
    function name() public view virtual returns (string memory) {
        return _name;
    }

    /**
     * @dev Returns the symbol of the token, usually a shorter version of the
     * name.
     */
    function symbol() public view virtual returns (string memory) {
        return _symbol;
    }

    /**
     * @dev Returns the number of decimals used to get its user representation.
     * For example, if `decimals` equals `2`, a balance of `505` tokens should
     * be displayed to a user as `5.05` (`505 / 10 ** 2`).
     *
     * Tokens usually opt for a value of 18, imitating the relationship between
     * Ether and Wei. This is the default value returned by this function, unless
     * it's overridden.
     *
     * NOTE: This information is only used for _display_ purposes: it in
     * no way affects any of the arithmetic of the contract, including
     * {IERC20-balanceOf} and {IERC20-transfer}.
     */
    function decimals() public view virtual returns (uint8) {
        return 18;
    }

    /**
     * @dev See {IERC20-totalSupply}.
     */
    function totalSupply() public view virtual returns (uint256) {
        return _totalSupply;
    }

    /**
     * @dev See {IERC20-balanceOf}.
     */
    function balanceOf(address account) public view virtual returns (uint256) {
        return _balances[account];
    }

    /**
     * @dev See {IERC20-transfer}.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - the caller must have a balance of at least `value`.
     */
    function transfer(address to, uint256 value) public virtual returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, value);
        return true;
    }

    /**
     * @dev See {IERC20-allowance}.
     */
    function allowance(
        address owner,
        address spender
    ) public view virtual returns (uint256) {
        return _allowances[owner][spender];
    }

    /**
     * @dev See {IERC20-approve}.
     *
     * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on
     * `transferFrom`. This is semantically equivalent to an infinite approval.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function approve(
        address spender,
        uint256 value
    ) public virtual returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, value);
        return true;
    }

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `value`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `value`.
     */
    function transferFrom(
        address from,
        address to,
        uint256 value
    ) public virtual returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, value);
        _transfer(from, to, value);
        return true;
    }

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to`.
     *
     * This internal function is equivalent to {transfer}, and can be used to
     * e.g. implement automatic token fees, slashing mechanisms, etc.
     *
     * Emits a {Transfer} event.
     *
     * NOTE: This function is not virtual, {_update} should be overridden instead.
     */
    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);
    }

    /**
     * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
     * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
     * this function.
     *
     * Emits a {Transfer} event.
     */
    function _update(address from, address to, uint256 value) internal virtual {
        if (from == address(0)) {
            // Overflow check required: The rest of the code assumes that totalSupply never overflows
            _totalSupply += value;
        } else {
            uint256 fromBalance = _balances[from];
            if (fromBalance < value) {
                revert ERC20InsufficientBalance(from, fromBalance, value);
            }
            unchecked {
                // Overflow not possible: value <= fromBalance <= totalSupply.
                _balances[from] = fromBalance - value;
            }
        }

        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;
            }
        }

        emit Transfer(from, to, value);
    }

    /**
     * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).
     * Relies on the `_update` mechanism
     *
     * Emits a {Transfer} event with `from` set to the zero address.
     *
     * NOTE: This function is not virtual, {_update} should be overridden instead.
     */
    function _mint(address account, uint256 value) internal {
        if (account == address(0)) {
            revert ERC20InvalidReceiver(address(0));
        }
        _update(address(0), account, value);
    }

    /**
     * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.
     * Relies on the `_update` mechanism.
     *
     * Emits a {Transfer} event with `to` set to the zero address.
     *
     * NOTE: This function is not virtual, {_update} should be overridden instead
     */
    function _burn(address account, uint256 value) internal {
        if (account == address(0)) {
            revert ERC20InvalidSender(address(0));
        }
        _update(account, address(0), value);
    }

    /**
     * @dev Sets `value` as the allowance of `spender` over the `owner` s tokens.
     *
     * This internal function is equivalent to `approve`, and can be used to
     * e.g. set automatic allowances for certain subsystems, etc.
     *
     * Emits an {Approval} event.
     *
     * Requirements:
     *
     * - `owner` cannot be the zero address.
     * - `spender` cannot be the zero address.
     *
     * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.
     */
    function _approve(address owner, address spender, uint256 value) internal {
        _approve(owner, spender, value, true);
    }

    /**
     * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.
     *
     * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by
     * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any
     * `Approval` event during `transferFrom` operations.
     *
     * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to
     * true using the following override:
     * ```
     * function _approve(address owner, address spender, uint256 value, bool) internal virtual override {
     *     super._approve(owner, spender, value, true);
     * }
     * ```
     *
     * Requirements are the same as {_approve}.
     */
    function _approve(
        address owner,
        address spender,
        uint256 value,
        bool emitEvent
    ) internal virtual {
        if (owner == address(0)) {
            revert ERC20InvalidApprover(address(0));
        }
        if (spender == address(0)) {
            revert ERC20InvalidSpender(address(0));
        }
        _allowances[owner][spender] = value;
        if (emitEvent) {
            emit Approval(owner, spender, value);
        }
    }

    /**
     * @dev Updates `owner` s allowance for `spender` based on spent `value`.
     *
     * Does not update the allowance value in case of infinite allowance.
     * Revert if not enough allowance is available.
     *
     * Does not emit an {Approval} event.
     */
    function _spendAllowance(
        address owner,
        address spender,
        uint256 value
    ) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            if (currentAllowance < value) {
                revert ERC20InsufficientAllowance(
                    spender,
                    currentAllowance,
                    value
                );
            }
            unchecked {
                _approve(owner, spender, currentAllowance - value, false);
            }
        }
    }
}

こんな感じになっています。主要な関数を説明していきます!

_update

function _update(address from, address to, uint256 value) internal virtual {
        // from のアドレスがzero addressの場合はmintになるので、totalSupplyを増やす
        if (from == address(0)) {
            // Overflow check required: The rest of the code assumes that totalSupply never overflows
            _totalSupply += value;
        } else {
            // from 残高を取得
            uint256 fromBalance = _balances[from];
            // from 残高がvalueより小さい場合はrevert
            if (fromBalance < value) {
                revert ERC20InsufficientBalance(from, fromBalance, value);
            }
            // from 残高からvalueを引く
            unchecked {
                // Overflow not possible: value <= fromBalance <= totalSupply.
                _balances[from] = fromBalance - value;
            }
        }

        // to のアドレスがzero addressの場合はburnになるので、totalSupplyを減らす
        if (to == address(0)) {
            unchecked {
                // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
                _totalSupply -= value;
            }
        } else {
            // toへ残高を足す
            unchecked {
                // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
                _balances[to] += value;
            }
        }
        // Transferイベントを発火
        emit Transfer(from, to, value);
    }

updateは主にburnとmintとtransferの際に使われます。設定する引数の内容に応じて各処理が行われるので、共通の関数として存在しています。使い方を間違えると大変なことになるので以下のことだけ覚えておいたらいいと思います!

パターンfromtovalue
mintゼロアドレスミントする相手のアドレストークンの量
burnburnしたいトークン持ち主のアドレスゼロアドレストークンの量
transferトークンの持ち主のアドレス転送したい相手のアドレストークンの量

_transfer

   function _transfer(address from, address to, uint256 value) internal {
        // from がzero addressの場合はrevert
        if (from == address(0)) {
            revert ERC20InvalidSender(address(0));
        }
        // to がzero addressの場合はrevert
        if (to == address(0)) {
            revert ERC20InvalidReceiver(address(0));
        }
        // updateでfrom -> to へvalueを渡す
        _update(from, to, value);
    }

トークンの転送の際に使われる内部関数です。転送なので、fromとtoがゼロアドレスならリバートされます。

transferFrom

   function transferFrom(
        address from,
        address to,
        uint256 value
    ) public virtual returns (bool) {
        // spender は実行者
        address spender = _msgSender();
        // 実行者がfromからのallowanceを確認して、valueを超えていたらrevert
        // okならallowanceを減らす
        _spendAllowance(from, spender, value);
        // fromからtoへvalueを渡す
        _transfer(from, to, value);
        // 成功したらtrueを返す
        return true;
    }

transferFromは第3者がfromからtoに対してトークンを動かす際に使う関数になります。もちろん誰でも動かせたら大変なことになるので、_spendAllowanceにて操作する人に対して同等の金額の許可があるかどうか確認後に実行できるようになってます。

transfer

function transfer(address to, uint256 value) public virtual returns (bool) {
    // 実行者のアドレスを取得
    address owner = _msgSender();
    // 送金を実行
    _transfer(owner, to, value);
    // 成功したらtrueを返す
    return true;
}

transferは自分のトークンを移動させたい時に使う関数です。

_mint

 function _mint(address account, uint256 value) internal {
    // account がzero addressの場合はrevert
    if (account == address(0)) {
        revert ERC20InvalidReceiver(address(0));
    }
    // mintなので_update(address(0), account, value)を実行
    _update(address(0), account, value);
}

mintの内部関数です。ここは単純にmint先のアドレスがゼロアドレスでないことを確認したのちに_updateを実行しています。

_burn

function _burn(address account, uint256 value) internal {
    // account がzero addressの場合はrevert
    if (account == address(0)) {
        revert ERC20InvalidSender(address(0));
    }
    // burnなので_update(account, address(0), value)を実行
    _update(account, address(0), value);
}

_burnのinternal関数です。ここもmint同様に元のアドレスがゼロアドレスでないことを確認したのちに_updateを実行します。

使い方

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyCoin is ERC20, Ownable {
    constructor(
        address initialOwner
    ) ERC20("MyCoin", "MC") Ownable(initialOwner) {}

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

今回はシンプルにownerだけがmintできるMyCoinトークンを作ってみました。これで必要最低限の機能は揃っているので、デプロイ後に自分にmintしてみてください!

こんな簡単に自分のコインが作れるので、OpenZeppelinはかなり便利です。色々な機能を足せるので、もし興味があれば試してみてください!blacklistなんかも用意しているコインもいっぱいあるので、自分好みにカスタマイズしてください。

まとめ

OpenZeppelinのERC20は、コインの作成と管理を簡単かつ標準化された方法実装できます。OpenZeppelinを使用する主な利点は、簡単にコインを実装できることと、セキュリティが検証された方法でERC20の標準規格に準拠した実装ができることです。開発者が個々に同様の実装を行うとセキュリティリスクを抱える可能性があるため、OpenZeppelin使用することが推奨されます。今回は、contractのownerのみがトークンを発行(mint)できるような基本的な実装方法を紹介しましたが、OpenZeppelinではさまざまなカスタマイズが可能です。コインを作りたい際は、このセキュアで柔軟なERC20の実装を活用して、効率的かつ安全にプロジェクトを進めましょう。

ABOUT ME
0xkeesmark
0xkeesmark
Security Researcher
記事URLをコピーしました