[ERC223] ETHと同様の振る舞いをするERC20トークンの仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

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

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

https://eips.ethereum.org/EIPS/eip-223

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

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

# 概要

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

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

https://chaldene.net/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**の送金に似たモデルを利用しています。
そのため、リプレイ攻撃の問題を考慮する必要があります。

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

https://chaldene.net/solidity-reentrancy-attack

# 参考実装

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

参考: https://eips.ethereum.org/EIPS/eip-223

# 引用

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などからお気軽に質問してください！

[Twitter @cardene777](https://twitter.com/cardene777)

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

https://chaldene.net/

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58


https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4