ERC-223 トークン標準
はじめに
ERC-223とは?
ERC-223は、ERC-20標準と同様の代替性トークンのための標準です。主な違いは、ERC-223がトークンのAPIだけでなく、送信者から受信者へトークンを送金するロジックも定義している点です。これにより、トークンの送金を受信者側で処理できる通信モデルが導入されています。
ERC-20との違い
ERC-223はERC-20のいくつかの制限に対処し、トークンコントラクトとトークンを受信する可能性のあるコントラクトとの間の新しい相互作用の方法を導入しています。ERC-20では不可能ですが、ERC-223では可能ないくつかの点があります。
- 受信者側でのトークン送金処理: 受信者は、ERC-223トークンが入金されていることを検知できます。
- 不適切に送信されたトークンの拒否: ユーザーがトークンを受信することを想定していないコントラクトにERC-223トークンを送信した場合、コントラクトはトランザクションを拒否し、トークンの喪失を防ぐことができます。
- 送金時のメタデータ: ERC-223トークンにはメタデータを含めることができ、トークンのトランザクションに任意の情報を添付できます。
前提知識
本文
ERC-223は、スマート・コントラクト内のトークン用APIを実装するトークン標準です。また、ERC-223トークンを受信することを想定したコントラクト用のAPIも宣言しています。ERC-223 Receiver APIをサポートしていないコントラクトはERC-223トークンを受信できないため、ユーザーのエラーを防ぐことができます。
スマート・コントラクトが以下のメソッドとイベントを実装している場合、それはERC-223互換のトークンコントラクトと呼ぶことができます。デプロイされると、イーサリアム上で作成されたトークンを追跡する役割を担います。
コントラクトはこれらの関数のみを持つ義務はなく、開発者は他のトークン標準から任意の機能を追加することができます。例えば、approveやtransferFrom関数はERC-223標準の一部ではありませんが、必要であればこれらの関数を実装することができます。
EIP-223 (opens in a new tab)より:
メソッド
ERC-223トークンは以下のメソッドを実装する必要があります。
function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transfer(address _to, uint256 _value, bytes calldata _data) public returns (bool success)
ERC-223トークンを受信することを想定しているコントラクトは、以下のメソッドを実装する必要があります。
function tokenReceived(address _from, uint _value, bytes calldata _data)
ERC-223トークンがtokenReceived(..)関数を実装していないコントラクトに送信された場合、送金は失敗し、送信者の残高からトークンが移動してはなりません。
イベント
event Transfer(address indexed _from, address indexed _to, uint256 _value, bytes calldata _data)
例
ERC-223トークンのAPIはERC-20のAPIと似ているため、UI開発の観点からは違いはありません。唯一の例外は、ERC-223トークンにはapproveおよびtransferFrom関数がない場合があることです。これらはこの標準ではオプションであるためです。
Solidityの例
以下の例は、基本的なERC-223トークンコントラクトがどのように動作するかを示しています。
pragma solidity ^0.8.19;
abstract contract IERC223Recipient {
function tokenReceived(address _from, uint _value, bytes memory _data) public virtual;
}
contract VeryBasicERC223Token {
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;
function name() public view returns (string memory) { return _name; }
function symbol() public view returns (string memory) {return _symbol; }
function decimals() public view returns (uint8) { return _decimals; }
function totalSupply() public view returns (uint256) { return _totalSupply; }
function balanceOf(address _owner) public view returns (uint256) { return balances[_owner]; }
function isContract(address account) internal view returns (bool) {
uint256 size;
assembly { size := extcodesize(account) }
return size > 0;
}
function transfer(address _to, uint _value, bytes calldata _data) public returns (bool success){
balances[msg.sender] = balances[msg.sender] - _value;
balances[_to] = balances[_to] + _value;
if(isContract(_to)) {
IERC223Recipient(_to).tokenReceived(msg.sender, _value, _data);
}
emit Transfer(msg.sender, _to, _value, _data);
return true;
}
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(isContract(_to)) {
IERC223Recipient(_to).tokenReceived(msg.sender, _value, _empty);
}
emit Transfer(msg.sender, _to, _value, _empty);
return true;
}
}
次に、tokenAがERC-223トークンであると仮定して、別のコントラクトがtokenAの入金を受け付けるようにします。このコントラクトはtokenAのみを受け付け、他のトークンは拒否しなければなりません。コントラクトがtokenAを受け取ったとき、Deposit()イベントを発行し、内部のdeposits変数の値を増加させる必要があります。
コードは以下の通りです。
contract RecipientContract is IERC223Recipient {
event Deposit(address whoSentTheTokens);
uint256 deposits = 0;
address tokenA; // 受け入れる唯一のトークン。
function tokenReceived(address _from, uint _value, bytes memory _data) public override
{
// この関数内では以下のことを理解しておくことが重要です。
// msg.sender は受信しているトークンのアドレスであり、
// ほとんどの場合、トークンコントラクトはイーサを所有または送信しないため、msg.value は常に 0 であり、
// _from はトークン送金の送信者であり、
// _value は預け入れられたトークンの量です。
require(msg.sender == tokenA);
deposits += _value;
emit Deposit(_from);
}
}
よくある質問
コントラクトにtokenBを送信するとどうなりますか?
トランザクションは失敗し、トークンの送金は行われません。トークンは送信者のアドレスに返還されます。
このコントラクトにどうやって入金できますか?
RecipientContractのアドレスを指定して、ERC-223トークンのtransfer(address,uint256)またはtransfer(address,uint256,bytes)関数を呼び出します。
このコントラクトにERC-20トークンを送金するとどうなりますか?
ERC-20トークンがRecipientContractに送信された場合、トークンは送金されますが、その送金は認識されません(Deposit()イベントは発行されず、入金値も変更されません)。望まないERC-20の入金をフィルタリングしたり防いだりすることはできません。
トークンの入金完了後に何らかの関数を実行したい場合はどうすればよいですか?
これを行うには複数の方法があります。この例では、ERC-223の送金をイーサの送金と同じにする方法に従います。
contract RecipientContract is IERC223Recipient {
event Foo();
event Bar(uint256 someNumber);
address tokenA; // 受け入れる唯一のトークン。
function tokenReceived(address _from, uint _value, bytes memory _data) public override
{
require(msg.sender == tokenA);
address(this).call(_data); // 受信したトランザクションを処理し、後続の関数呼び出しを実行します。
}
function foo() public
{
emit Foo();
}
function bar(uint256 _someNumber) public
{
emit Bar(_someNumber);
}
}
RecipientContractがERC-223トークンを受け取ると、コントラクトはトークントランザクションの_dataパラメータとしてエンコードされた関数を実行します。これは、イーサのトランザクションが関数呼び出しをトランザクションのdataとしてエンコードするのと同じです。詳細については、データフィールドをお読みください。
上記の例では、ERC-223トークンはtransfer(address,uin256,bytes calldata _data)関数を使用してRecipientContractのアドレスに送金される必要があります。データパラメータが0xc2985578(foo()関数の署名)である場合、トークンの入金を受け取った後にfoo()関数が呼び出され、Foo()イベントが発行されます。
パラメータはトークン送金のdataにエンコードすることもできます。例えば、_someNumberに12345の値を指定してbar()関数を呼び出すことができます。この場合、dataは0x0423a13200000000000000000000000000000000000000000000000000000000000004d2でなければなりません。ここで、0x0423a132はbar(uint256)関数の署名であり、00000000000000000000000000000000000000000000000000000000000004d2はuint256としての12345です。
制限事項
ERC-223はERC-20標準に見られるいくつかの問題に対処していますが、独自の制限がないわけではありません。
- 普及と互換性: ERC-223はまだ広く普及していないため、既存のツールやプラットフォームとの互換性が制限される可能性があります。
- 下位互換性: ERC-223はERC-20との下位互換性がありません。つまり、既存のERC-20コントラクトやツールは、変更を加えない限りERC-223トークンでは機能しません。
- ガス代: ERC-223の送金における追加のチェックや機能により、ERC-20のトランザクションと比較してガス代が高くなる可能性があります。