はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、ネイティブトークンであるETHと同様の振る舞いをするように設計された、ERC20ベースのトークン取引処理モデルを提案している規格であるERC223についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なERCについてまとめています。
概要
この規格では、ERC20トークンにtokenReceived
という関数を追加して、トークンを受け取ったときにトークンを受け取ったというこを通知するcallback
機能をサポートするインターフェースとロジックについて提案しています。
これにより、トークンはネイティブトークンであるETHと同じように振る舞います。
ERC20については以下を参考にしてください。
動機
このトークンは、トークンとコントラクトの相互作用を整理するための通信モデルを提供しています。
具体的には、以下の提案について説明します。
- トークンを受け取ったコントラクトから、
transfer
されたという通知を受け取る。 - トークンをコントラクトに預け入れる時のガス効率が向上します。
- トークンの
transfer
時に関連データを記録できるようになります。
このトークンの提案により、コントラクトとトークンの相互作用がスムーズに行え、トークンのtransfer
に関する情報やデータをより効果的に管理できるようになります。
仕様
ERC20トークンを受け取るコントラクトは、tokenReceived
関数を実装する必要があります。
tokenReceived
関数を実装していないコントラクトへのトークンtransfer
の実行はエラーを返します。
この仕組みにより、トークンを受け取るコントラクトが適切に設計され、トークンの取り扱いが確実に行われることが保証されます。
不正確な操作やセキュリティリスクを排除するために、tokenReceived
関数の実装が必要とされます。
トークンコントラクト
totalSupply
function totalSupply() view returns (uint256)
概要
トークンの総供給量を返す関数。
このメソッドの機能はERC20と同じです。
戻り値
-
totalSupply
- トークンの総供給量。
name
function name() view returns (string memory)
概要
トークンの名前を返す関数。
このメソッドの機能はERC20と同じです。
戻り値
-
name
- トークンの名前。
symbol
function symbol() view returns (string memory)
概要
トークンのシンボルを返す関数。
このメソッドの機能はERC20と同じです。
戻り値
-
symbol
- トークンのシンボル。
decimals
function decimals() view returns (uint8)
概要
トークンの小数点以下の桁数を返す関数。
このメソッドの機能はERC20と同じです。
戻り値
-
decimals
- トークンの小数点以下の桁数。
standard
function standard() view returns (string memory)
概要
トークンの規格を識別するための識別子を返す関数。
トークンがERC223である場合、「223」を返します。
戻り値
-
standard
- トークンの規格識別子。
- ERC223の場合は「223」。
balanceOf
function balanceOf(address _owner) view returns (uint256)
概要
指定したアドレス(所有者)のアカウント残高を返す関数。
このメソッドの機能はERC20と同じです。
引数
-
_owner
- 残高を取得したいアカウントのアドレス。
戻り値
-
balance
- 指定されたアカウントのトークン残高。
transfer
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
event Transfer(address indexed _from, address indexed _to, uint256 _value, bytes _data)
概要
トークンが転送された時に発行されるイベント。
ERC20のTransfer
イベントと互換性があり、似たような機能を提供します。
パラメータ
-
_from
- トークンの送信元アドレス。
-
_to
- トークンの送信先アドレス。
-
_value
- 転送されたトークンの数量。
-
_data
- 任意の追加データ。
- 通常、トークンの移動に関する詳細情報が含まれます。
ERC-223 Token Receiver
tokenReceived
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万ドル相当のトークンがこの方法で失われています。
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トークンは拒否されます。
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の送金に似たモデルを利用しています。
そのため、リプレイ攻撃の問題を考慮する必要があります。
リプレイ攻撃については以下を参考にしてください。
参考実装
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などからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!