はじめに

初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、ネイティブトークンであるETHと同様の振る舞いをするように設計された、ERC20ベースのトークン取引処理モデルを提案している規格であるERC223についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なERCについてまとめています。

概要

この規格では、ERC20トークンにtokenReceivedという関数を追加して、トークンを受け取ったときにトークンを受け取ったというこを通知するcallback機能をサポートするインターフェースとロジックについて提案しています。 これにより、トークンはネイティブトークンであるETHと同じように振る舞います。

ERC20については以下を参考にしてください。

動機

このトークンは、トークンとコントラクトの相互作用を整理するための通信モデルを提供しています。 具体的には、以下の提案について説明します。

  1. トークンを受け取ったコントラクトから、transferされたという通知を受け取る。
  2. トークンをコントラクトに預け入れる時のガス効率が向上します。
  3. トークンのtransfer時に関連データを記録できるようになります。

このトークンの提案により、コントラクトとトークンの相互作用がスムーズに行え、トークンのtransferに関する情報やデータをより効果的に管理できるようになります。

仕様

ERC20トークンを受け取るコントラクトは、tokenReceived関数を実装する必要があります。 tokenReceived関数を実装していないコントラクトへのトークンtransferの実行はエラーを返します。

この仕組みにより、トークンを受け取るコントラクトが適切に設計され、トークンの取り扱いが確実に行われることが保証されます。 不正確な操作やセキュリティリスクを排除するために、tokenReceived関数の実装が必要とされます。

トークンコントラクト

totalSupply

javascript
function totalSupply() view returns (uint256)

概要

トークンの総供給量を返す関数。 このメソッドの機能はERC20と同じです。

戻り値

  • totalSupply
    • トークンの総供給量。

name

javascript
function name() view returns (string memory)

概要

トークンの名前を返す関数。 このメソッドの機能はERC20と同じです。

戻り値

  • name
    • トークンの名前。

symbol

javascript
function symbol() view returns (string memory)

概要

トークンのシンボルを返す関数。 このメソッドの機能はERC20と同じです。

戻り値

  • symbol
    • トークンのシンボル。

decimals

javascript
function decimals() view returns (uint8)

概要

トークンの小数点以下の桁数を返す関数。 このメソッドの機能はERC20と同じです。

戻り値

  • decimals
    • トークンの小数点以下の桁数。

standard

javascript
function standard() view returns (string memory)

概要

トークンの規格を識別するための識別子を返す関数。 トークンがERC223である場合、「223」を返します。

戻り値

  • standard
    • トークンの規格識別子。
    • ERC223の場合は「223」。

balanceOf

javascript
function balanceOf(address _owner) view returns (uint256)

概要

指定したアドレス(所有者)のアカウント残高を返す関数。 このメソッドの機能はERC20と同じです。

引数

  • _owner
    • 残高を取得したいアカウントのアドレス。

戻り値

  • balance
    • 指定されたアカウントのトークン残高。

transfer

javascript
function transfer(address _to, uint _value) returns (bool);
function transfer(address _to, uint _value, bytes calldata _data) returns (bool);

概要

トークンを指定したアドレス(_to)に送付する関数。

詳細

この関数は、指定したアドレス(_to)に指定した数量(_value)のトークンを送付します。 もし_toがコントラクトである場合、そのコントラクトのtokenReceived関数を呼び出す必要があります。 ただし、_toがコントラクトであり、tokenReceived関数が実装されていない場合、トランザクションは失敗します。 _toがEOAアドレスである場合、トランザクションはtokenReceivedを実行せずに送付されます。 また、このトークントランザクションには_dataを添付できますが、その分ガスを消費します。 _dataは空でも問題ないです。

引数

  • _to
    • トークンを転送するアドレス。
  • _value
    • 転送するトークンの数量。

戻り値

  • bool
    • トークンの転送が成功した場合はtrue、失敗した場合はfalse。

イベント

Transfer

javascript
event Transfer(address indexed _from, address indexed _to, uint256 _value, bytes _data)

概要

トークンが転送された時に発行されるイベント。 ERC20のTransferイベントと互換性があり、似たような機能を提供します。

パラメータ

  • _from
    • トークンの送信元アドレス。
  • _to
    • トークンの送信先アドレス。
  • _value
    • 転送されたトークンの数量。
  • _data
    • 任意の追加データ。
    • 通常、トークンの移動に関する詳細情報が含まれます。

ERC-223 Token Receiver

tokenReceived

javascript
function tokenReceived(address _from, uint _value, bytes calldata _data) returns (bytes4)

概要

トークンをtransferした時に、トークンコントラクトから呼び出され、transfer実行するための関数。

詳細

この関数内ではmsg.senderはトークンコントラクト自体であり、どのトークンが送信されたかをフィルタリングします(トークンコントラクトのアドレスでフィルタリング可能)。 tokenReceived関数はトークンをtransferした後、特定の値0x8943ec02を返す必要があります。 また、この関数呼び出しはトークンを受け取ったコントラクトのfallback関数によって処理できます(この場合、0x8943ec02を返す必要はありません)。

引数

  • _from
    • トークンの送信元アドレス。
  • _value
    • 送付トークンの数量。
  • _data
    • トークン転送に関連付けられたデータ。

戻り値

  • bytes4
    • 0x8943ec02または他の適切な値。

補足

この標準は、トークンのtransfer時に送付先アドレスでハンドラ関数の実行を要求し、新しい通信モデルを導入しています。 トークンを受け取るコントラクトがこの関数を実装していない場合、トークンのtransferは中止されます。

この標準は、トークンのtransferが送信者によって開始され、受信者によって処理される「プッシュトランザクションモデル」に従っています。 その結果、ERC223トークンのtransferは、コントラクトへの預け入れを扱う時よりガス効率が良くなります。 ERC223トークンは、1つのトランザクションでトークンを預け入れることができますが、ERC20トークンは少なくとも2つの呼び出し(approveとtransferFromの2つ)が必要です。

  • ERC20の預け入れ
    • approve
      • 約46ガス。
    • transferFrom
      • 約75Kガス。
  • ERC223の預け入れ
    • トランザクションと受信者側での処理約
      • 54Kガス。

この標準は、ユーザーエラーを訂正するための機能を導入し、トークン受け取り側で任意のトランザクションを処理し、不正確または適切でないtransferを拒否できるようにします。 これにより、コントラクトとEOAアドレスとの相互作用に対して1つのtransferメソッドを使用でき、ユーザーエクスペリエンスを簡素化し、ユーザーエラーを回避できます。

一般的なERC20標準の欠点の1つは、ERC20が2つのトークンtransferメソッド(1つはtransfer関数、もう1つはapprove + transferFromパターン)を実装していることです。 ERC20のtransfer関数はトークン受け取り時にに通知しないため、transfer関数を使用してコントラクトにトークンが送信される場合、トークンの受け取り側はこのtransferを認識せず、トークンが受信者のアドレスに固定されてしまう可能性があります。 ERC20標準はトークンのtransfer方法の判断をユーザーに委ねており、誤った方法が選択された場合、ユーザーはトークンを失う可能性があります。 ERC223はtransfer方法を自動的に決定し、誤った方法を選択してトークンを失うリスクを回避します。

ERC223はトークンをやりとりコントラクトを簡素化することを目指しており、ETHの預け入れと同様の「預け入れ」パターンを利用します。 ERC223のコントラクトへの預け入れは、transfer関数を呼び出すだけのトランザクションです。 これはapprove + transferFromの2ステッププロセスとは異なります。

この標準は、bytes calldata _dataパラメータを使用してトランザクションにデータを添付する能力を導入しており、このデータを使用して宛先アドレスで2番目の関数呼び出しをエンコードしたり、金融トランザクションに必要な場合にはチェーン上での公開ログを許可します。

互換性

このトークンのインターフェースは、ERC20をベースにしており、ほとんどの関数はERC20と同じ目的で使用されます。 ただし、transfer(address, uint256, bytes calldata)関数はERC20のインターフェースと後方互換性がないことに注意が必要です。

ERC20トークンは、transfer関数を使用してEOAアドレスにトークンを送付することができます。 また、ERC20トークンはコントラクトアドレスにトークンを預け入れるためにapprove + transferFromパターンを使用できます。 しかし、ERC20トークンをtransfer関数を使用してコントラクトアドレスに預け入れる場合、受信コントラクトでそのトークンの預け入れが認識されない可能性があることに注意が必要です。

以下は、ERC20トークンの預け入れを処理するコントラクトコードの例です。 このコントラクトはtokenAの預け入れを受け付けますが、tokenA以外の預け入れを防ぐことはできません。 tokenAがtransfer関数を使用して預け入れられると送信アドレスの残高は減りますが、ERC20Receiverのdeposits変数の値は増加せず預け入れが記録されません。 2023年5月9日現在、Ethereumメインネットで50のERC20トークンのうち、約2億1,000万ドル相当のトークンがこの方法で失われています。

javascript
contract ERC20Receiver
{
    address tokenA;
    mapping (address => uint256) deposits;
    function deposit(uint _value, address _token) public
    {
        require(_token == tokenA);
        IERC20(_token).transferFrom(msg.sender, address(this), _value);
        deposits[msg.sender] += _value;
    }
}

一方、ERC223トークンは、transfer関数と同様にEOAアドレスまたはコントラクトアドレスにトークンを送付する必要があります。 以下はERC223トークンの預け入れを処理するコントラクトコードの例です。 このコントラクトはトークンをフィルタリングし、tokenAのみを受け入れて他のERC223トークンは拒否されます。

javascript
contract ERC223Receiver
{
    address tokenA;
    mapping (address => uint256) deposits;
    function tokenReceived(address _from, uint _value, bytes memory _data) public returns (bytes4)
    {
        require(msg.sender == tokenA);
        deposits[_from] += _value;
        return 0x8943ec02;
    }
}

ERC223トークンは、送信者と受信者が一貫して同じ方法でトークンを取り扱う必要があるため、ERC20とは異なるトークン転送モデルを提供します。

セキュリティ考慮事項

このトークンはネイティブトークンであるETHの送金に似たモデルを利用しています。 そのため、リプレイ攻撃の問題を考慮する必要があります。

リプレイ攻撃については以下を参考にしてください。

参考実装

javascript
pragma solidity ^0.8.19;

library Address {
    /**
     * @dev Returns true if `account` is a contract.
     *
     * This test is non-exhaustive, and there may be false-negatives: during the
     * execution of a contract's constructor, its address will be reported as
     * not containing a contract.
     *
     * > It is unsafe to assume that an address for which this function returns
     * false is an externally-owned account (EOA) and not a contract.
     */
    function isContract(address account) internal view returns (bool) {
        // This method relies in extcodesize, which returns 0 for contracts in
        // construction, since the code is only stored at the end of the
        // constructor execution.

        uint256 size;
        // solhint-disable-next-line no-inline-assembly
        assembly { size := extcodesize(account) }
        return size > 0;
    }
}

abstract contract IERC223Recipient {
/**
 * @dev Standard ERC-223 receiving function that will handle incoming token transfers.
 *
 * @param _from  Token sender address.
 * @param _value Amount of tokens.
 * @param _data  Transaction metadata.
 */
    function tokenReceived(address _from, uint _value, bytes memory _data) public virtual returns (bytes4);
}

/**
 * @title Reference implementation of the ERC223 standard token.
 */
contract ERC223Token {

     /**
     * @dev Event that is fired on successful transfer.
     */
    event Transfer(address indexed from, address indexed to, uint value, bytes data);

    string  private _name;
    string  private _symbol;
    uint8   private _decimals;
    uint256 private _totalSupply;
    
    mapping(address => uint256) private balances; // List of user balances.

    /**
     * @dev Sets the values for {name} and {symbol}, initializes {decimals} with
     * a default value of 18.
     *
     * To select a different value for {decimals}, use {_setupDecimals}.
     *
     * All three of these values are immutable: they can only be set once during
     * construction.
     */
     
    constructor(string memory new_name, string memory new_symbol, uint8 new_decimals)
    {
        _name     = new_name;
        _symbol   = new_symbol;
        _decimals = new_decimals;
    }

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

    /**
     * @dev Returns the symbol of the token, usually a shorter version of the
     * name.
     */
    function symbol() public view 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 value {ERC223} uses, unless {_setupDecimals} is
     * called.
     *
     * NOTE: This information is only used for _display_ purposes: it in
     * no way affects any of the arithmetic of the contract, including
     * {IERC223-balanceOf} and {IERC223-transfer}.
     */
    function decimals() public view returns (uint8)
    {
        return _decimals;
    }

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

    /**
     * @dev See {IERC223-standard}.
     */
    function standard() public view returns (string memory)
    {
        return "223";
    }

    
    /**
     * @dev Returns balance of the `_owner`.
     *
     * @param _owner   The address whose balance will be returned.
     * @return balance Balance of the `_owner`.
     */
    function balanceOf(address _owner) public view returns (uint256)
    {
        return balances[_owner];
    }
    
    /**
     * @dev Transfer the specified amount of tokens to the specified address.
     *      Invokes the `tokenFallback` function if the recipient is a contract.
     *      The token transfer fails if the recipient is a contract
     *      but does not implement the `tokenFallback` function
     *      or the fallback function to receive funds.
     *
     * @param _to    Receiver address.
     * @param _value Amount of tokens that will be transferred.
     * @param _data  Transaction metadata.
     */
    function transfer(address _to, uint _value, bytes calldata _data) public returns (bool success)
    {
        // Standard function transfer similar to ERC20 transfer with no _data .
        // Added due to backwards compatibility reasons .
        balances[msg.sender] = balances[msg.sender] - _value;
        balances[_to] = balances[_to] + _value;
        if(Address.isContract(_to)) {
            IERC223Recipient(_to).tokenReceived(msg.sender, _value, _data);
        }
        emit Transfer(msg.sender, _to, _value, _data);
        return true;
    }
    
    /**
     * @dev Transfer the specified amount of tokens to the specified address.
     *      This function works the same with the previous one
     *      but doesn't contain `_data` param.
     *      Added due to backwards compatibility reasons.
     *
     * @param _to    Receiver address.
     * @param _value Amount of tokens that will be transferred.
     */
    function transfer(address _to, uint _value) public returns (bool success)
    {
        bytes memory _empty = hex"00000000";
        balances[msg.sender] = balances[msg.sender] - _value;
        balances[_to] = balances[_to] + _value;
        if(Address.isContract(_to)) {
            IERC223Recipient(_to).tokenReceived(msg.sender, _value, _empty);
        }
        emit Transfer(msg.sender, _to, _value, _empty);
        return true;
    }
}

引用

Dexaran (@Dexaran) dexaran@ethereumclassic.org, "ERC-223: Token with transaction handling model," Ethereum Improvement Proposals, no. 223, May 2017. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-223.

最後に

今回は「ネイティブトークンであるETHと同様の振る舞いをするように設計された、ERC20ベースのトークン取引処理モデルを提案している規格であるERC223」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!