【翻訳】ERC-6551: トークンバウンドアカウントについて
0 はじめに
この記事は、EIP-6551を翻訳したものです。
https://eips.ethereum.org/EIPS/eip-6551
1 概要
本提案は、すべてのERC-721トークンにスマートコントラクトのアカウントを与えるシステムを定義するものです。
これらのトークンバウンドアカウントにより、既存のERC-721スマートコントラクトやインフラを変更することなく、ERC-721トークンが資産を所有し、アプリケーションと対話することができます。
2 動機
ERC-721標準は、NFT・アプリケーションの爆発的な普及を可能にしました。注目すべきユースケースには、繁殖可能な猫(CryptoKitties)、生成可能なアートワーク、流動性ポジションなどがあります。
NFTは、チェーン上のアイデンティティの一形態となりつつあります。これはERC-721の仕様からごく自然に導かれたもので、各NFTはグローバルにユニークな識別子を持ち、ひいてはユニークなIDを持つことになります。
他のオンチェーンIDとは異なり、ERC-721トークンはエージェント(主体)として機能したり、他のオンチェーン資産と関連付けたりすることができません。
この制限は、多くの現実の非代替性資産の例と対照的です。
例えば、以下のようなものです。
ロールプレイングゲームのキャラクターが、自分の取った行動に基づいて時間と共に資産や能力を蓄積していく。
多くの代替される部品と代替されない部品で構成された自動車
複数の代替資産で構成される自動投資ポートフォリオ
施設に入場でき、過去の履歴が記録されるパンチパスの会員カード
ERC-721トークンに資産を所有する機能を持たせようとする提案がいくつかなされています。
これらの提案はそれぞれ、ERC-721標準の拡張を定義しています。
このため、スマートコントラクトの作成者は、ERC-721トークンコントラクトに提案のサポートを含める必要があります。
その結果、これらの提案は、以前に導入されたERC-721コントラクトとほとんど互換性がありません。
この提案は、すべてのERC-721トークンにイーサリアムアカウントの全機能を付与する一方で、以前に導入されたERC-721トークンコントラクトとの後方互換性を維持します。
これは、各 ERC-721 トークンに固有の、決定論的にアドレス指定されたスマートコントラクトアカウントを、権限不要のレジストリを介して展開することで実現されます。
各トークンバウンドアカウントは1つのERC-721トークンによって所有され、トークンはブロックチェーンとのやり取り、取引履歴の記録、オンチェーン資産の所有が可能になります。
各トークンバウンドアカウントの制御はERC-721トークンの所有者に委ねられ、所有者は自分のトークンに代わってオンチェーンアクションを開始することができるようになります。
トークンバウンドアカウントは、オンチェーンプロトコルからオフチェーンインデクサーまで、イーサリアムアカウントをサポートするほぼすべての既存インフラとすぐに互換性があります。
トークンバウンドアカウントは、あらゆるタイプのオンチェーン資産を所有することができ、将来的に作成される新しいタイプの資産をサポートするために拡張することができます。
3 仕様
この文書におけるキーワード「MUST」、「MUST NOT」、「REQUIRED」、「SHALL」、「SHALL NOT」、「SHOULD」、「SHOULD NOT」、「RECOMMENDED」、「NOT RECOMMENDED」、「MAY」、「OPTIONAL」は、RFC 2119およびRFC 8174で説明されているように解釈する必要があります。
① 概要
本提案で概説するシステムには、2つの主要なコンポーネントがあります。
トークンバウンドアカウントをデプロイするための許可不要のレジストリ
トークンバウンドアカウントの実装のための標準インターフェース
以下の図は、ERC-721トークン、ERC-721トークン所有者、トークンバウンドアカウント、およびレジストリの関係を示しています。
② レジストリ
レジストリは、トークンバウンドアカウントの利用を希望するプロジェクトのための単一のエントリポイント(開始点)として機能します。レジストリは2つの機能を持っています。
createAccount - ERC-721トークンの実装アドレスに対応したトークンバウンドアカウントをデプロイする。
account - 読み取り専用の関数で、実装アドレスが与えられたERC-721トークンのトークンバウンドアカウントアドレスを計算する。
レジストリは、各トークンバウンドアカウントを、バイトコードに不変の定数データを付加したERC-1167最小プロキシとしてデプロイしなければならない(SHALL)。
各トークンバウンドアカウントのデプロイされたバイトコードは、以下の構造を持つものとする。
例えば、
実装アドレス:「0xbebebebebebebebebebebebe」
ソルト:「0」
チェーンID:「1」
トークンコントラクト:「0xcfcfcfcfcfcfcfcfcfcf」
トークンID:「123」
のトークンバウンドアカウントは、以下のようにデプロイされたバイトコードとなります。
363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcf000000000000000000000000000000000000000000000000000000000000007b
各トークンバウンドアカウントプロキシは、IERC6551Accountインターフェースを実装するコントラクトに実行を委任しなければなりません(SHALL)。
レジストリコントラクトは、許可不要、不変であり、所有者を有しません。
レジストリの完全なソースコードは、以下の「レジストリの実装」セクションに記載されています。
レジストリは、
Nick's Factory :「0x4e59b44847b379578588920cA78FbF26c0B4956C」
salt:「 0x655165516551655165516551 」
を使用され、その具体的なアドレスはまだ未定です
レジストリは、すべてのERC-721トークンのアカウントアドレスが決定的になるように、create2オペコードを使用してすべてのトークンバウンドアカウントコントラクトをデプロイしなければなりません(SHALL)。
各 ERC-721 トークンのアカウントアドレスは、実装アドレス、トークンコントラクトアドレス、トークン ID、EIP-155 チェーン ID、およびオプションのソルトのユニークな組み合わせから導き出されるものとします(SHALL)。
レジストリは、以下のインタフェースを実装しなければならない(SHALL)
interface IERC6551Registry {
/// @dev アカウント作成に成功した場合、レジストリはAccountCreatedイベントを
/// 発行しなければならない(SHALL)。
event AccountCreated(
address account,
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt
);
/// @dev ERC-721 トークンのトークンバウンドアカウントを作成します。
///
/// アカウントが既に作成されている場合は、create2 を呼び出さずにアカウントアドレスを返します。
///
/// initDataが空でなく、アカウントがまだ作成されていない場合、
/// 作成後に与えられたinitDataでアカウントを呼び出します。
///
/// AccountCreated イベントを発行する
///
/// @return アカウントのアドレス
function createAccount(
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt,
bytes calldata initData
) external returns (address);
/// @dev トークンバウンドアカウントの計算されたアドレスを返します。
///
/// @return アカウントの計算されたアドレス
function account(
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt
) external view returns (address);
}
③ アカウントインターフェース
すべてのトークンバウンドアカウントは、レジストリを介して作成されるべきである(SHOULD)。
すべてのトークンバウンドアカウントの実装は、ERC-165のインターフェース検出を実装しなければならない(MUST)。
すべてのトークンバウンドアカウントの実装は、ERC-1271の署名検証を実装しなければならない(MUST)。
すべてのトークンバウンドアカウントの実装は、以下のインタフェースを実装しなければならない(MUST)。
/// @dev このインターフェースの ERC-165 識別子は `0x400a0398` です。
interface IERC6551Account {
/// @dev トークンバウンドアカウントは `receive` 関数を実装しなければならない。
///
/// トークンバウンドアカウントは、イーサを受信できる条件を制限するために、
/// 任意のロジックを実行してもよい(MAY)。
receive() external payable;
/// @dev アドレス `to` に対して、値 `value` とcalldata `data` で` call` を実行する。
///
/// 呼び出しに失敗した場合は、元に戻し、エラーをバブルアップしなければならない。
///
/// デフォルトでは、トークンバウンドアカウントは、そのアカウントを所有するERC-721トークンの所有者が
/// `executeCall` を使用して任意の呼び出しを実行することを許可しなければならない(MUST)。
///
/// トークンバウンドアカウントは、ERC-721トークンの所有者が呼び出しを実行する能力を制限する、
/// 追加の認可メカニズムを実装してもよい(MAY)。
///
/// トークンにバインドされたアカウントは、他の非所有者アカウントに実行権限を付与する
/// 追加の実行機能を実装してもよい(MAY)。
///
/// @return 呼び出しの結果
function executeCall(
address to,
uint256 value,
bytes calldata data
) external payable returns (bytes memory);
/// @dev アカウントを所有する ERC-721 トークンの識別子を返します
///
/// この関数の戻り値は一定でなければならず、時間の経過とともに変化してはならない(MUST NOT)
///
/// @return chainId ERC-721 トークンが存在するチェーンの EIP-155 ID
/// @return tokenContract ERC-721 トークンのコントラクトアドレス
/// @return tokenId ERC-721 トークンID
function token()
external
view
returns (
uint256 chainId,
address tokenContract,
uint256 tokenId
);
/// @dev アカウントを管理するERC-721 トークンが存在する場合、その所有者を返します。
///
/// この値は、ERC-721コントラクトに対して `ownerOf` を呼び出すことで得られます。
///
/// @return アカウントを所有するERC-721トークンの所有者のアドレス
function owner() external view returns (address);
/// @dev トランザクションが成功するたびに更新される nonce 値を返します。
///
/// @return 現在のアカウントの nonce
function nonce() external view returns (uint256);
}
4 根本原理
① カウンターファクトのアカウントアドレス
正規のアカウントレジストリを指定することで、この提案をサポートしたいアプリケーションは、そのアカウントのコントラクトをデプロイする前に、特定の実装を使用して、与えられたトークンバウンドアカウントのアドレスを計算することができます。
これにより、トークンの所有者のアドレスを知らなくても、トークンの所有者に資産を安全に送ることができます。
また、正規のアカウントレジストリによって、クライアント側のアプリケーションは、単一のエントリポイントからトークンが所有するアセットを照会することができます。
② アカウントのあいまいさ
上記で提案した仕様では、ERC-721トークンは実装アドレスごとに1つずつ、複数のトークンバウンドアカウントを持つことができます。
この提案の開発中に、各 ERC-721 トークンに単一のトークンバウンドアカウントを割り当て、各トークンバウンドアカウントアドレスを曖昧でない識別子にする代替アーキテクチャが検討されました。
しかし、これらの代替案は、いくつかのトレードオフをもたらします。
第一に、スマートコントラクトの無許可性により、ERC-721トークンごとに1つのトークンバウンドアカウントという制限を強制することは不可能です。
ERC-721トークンあたり複数のトークンバウンドアカウントを利用したい場合は、レジストリコントラクトを追加で導入することで対応できます。
第二に、各 ERC-721 トークンを 1 つのトークンバウンドアカウントに制限するためには、本提案に含まれる静的で信頼できるアカウント実装が必要である。
この実装は、必然的にトークンバウンドアカウントの能力に特定の制約を課すことになります。
本提案が可能にする未踏のユースケースや、多様なアカウント実装がNFTのエコシステムにもたらす利益を考慮すると、本提案で標準的で制約のある実装を定義することは時期尚早であると著者らは考えています。
最後に、本提案はERC-721トークンにオンチェーン・エージェントとしての機能を付与することを目的としています。
現在の実務では、オンチェーン・エージェントはしばしば複数のアカウントを利用しています。
よくある例としては、日常的に使用する「ホット」アカウントと貴重品を保管する「コールド」アカウントを使用する個人が挙げられます。
オンチェーン・エージェントが複数のアカウントを使用するのが一般的であるならば、ERC-721トークンも同じ能力を受け継ぐべきであることは当然です。
③ プロキシの実装
ERC-1167の最小プロキシは既存のインフラで十分にサポートされており、一般的なスマートコントラクトのパターンとなっています。
この提案では、各トークンバウンドアカウントは、ソルト、実装アドレス、チェーンID、トークンコントラクトアドレス、トークンIDを、コントラクトバイトコードに付加されたABIエンコード定数データとして保存するカスタムERC-1167プロキシ実装を使用してデプロイします。
これにより、トークンバウンドアカウントの実装は、このデータを容易に照会できるようになり、かつ、定数を維持することができます。
このアプローチは、既存のインフラとの互換性を最大限に高めると同時に、スマートコントラクト開発者がカスタムトークンバウンドアカウントの実装を作成する際の柔軟性を確保するために取られたものです。
④ EIP-155のサポート
本提案では、ERC-721トークンの識別にEIP-155チェーンIDを使用し、コントラクトアドレスとトークンIDも併せて提供します。
ERC-721トークン識別子は、単一のイーサリアムチェーンでグローバルに一意ですが、複数のイーサリアムチェーンでは一意でない場合があります。
チェーンIDを使用してERC-721トークンを一意に識別することで、この提案を実装したいスマートコントラクト作成者は、オプションでマルチチェーントークンバインドアカウントをサポートすることができます。
⑤ 後方互換性
本提案は、既存のNFTのコントラクトとの後方互換性を最大限に高めることを目指します。
そのため、ERC-721標準を拡張するものではありません。
さらに、この提案では、アカウント作成前にレジストリがERC-721との互換性を確認するためにERC-165インターフェースを実行する必要がありません。
これは、CryptokittiesのようなERC-721標準より前のNFTのコントラクトとの後方互換性を最大化するための設計によるものです。
この提案を実装するスマートコントラクトの作成者は、オプションでERC-721のインターフェース検出を強制することを選択できます。
クリプトパンクのようなownerOfメソッドを実装しないNFTコントラクトは、本提案と互換性がありません。
本提案で概説するシステムは、わずかな修正でそのようなコレクションをサポートするために使用することができますが、それは本提案の範囲外です。
5 参考実装
① アカウント実装例
pragma solidity ^0.8.13;
import "openzeppelin-contracts/utils/introspection/IERC165.sol";
import "openzeppelin-contracts/token/ERC721/IERC721.sol";
import "openzeppelin-contracts/interfaces/IERC1271.sol";
import "openzeppelin-contracts/utils/cryptography/SignatureChecker.sol";
import "sstore2/utils/Bytecode.sol";
contract ExampleERC6551Account is IERC165, IERC1271, IERC6551Account {
receive() external payable {}
function executeCall(
address to,
uint256 value,
bytes calldata data
) external payable returns (bytes memory result) {
require(msg.sender == owner(), "Not token owner");
bool success;
(success, result) = to.call{value: value}(data);
if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
}
function token()
external
view
returns (
uint256 chainId,
address tokenContract,
uint256 tokenId
)
{
uint256 length = address(this).code.length
return
abi.decode(
Bytecode.codeAt(address(this), length - 0x60, length),
(uint256, address, uint256)
);
}
function owner() public view returns (address) {
(uint256 chainId, address tokenContract, uint256 tokenId) = this
.token();
if (chainId != block.chainid) return address(0);
return IERC721(tokenContract).ownerOf(tokenId);
}
function supportsInterface(bytes4 interfaceId) public pure returns (bool) {
return (interfaceId == type(IERC165).interfaceId ||
interfaceId == type(IERC6551Account).interfaceId);
}
function isValidSignature(bytes32 hash, bytes memory signature)
external
view
returns (bytes4 magicValue)
{
bool isValid = SignatureChecker.isValidSignatureNow(
owner(),
hash,
signature
);
if (isValid) {
return IERC1271.isValidSignature.selector;
}
return "";
}
}
② レジストリ実装
pragma solidity ^0.8.13;
import "openzeppelin-contracts/utils/Create2.sol";
contract ERC6551Registry is IERC6551Registry {
error InitializationFailed();
function createAccount(
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt,
bytes calldata initData
) external returns (address) {
bytes memory code = _creationCode(implementation, chainId, tokenContract, tokenId, salt)
address _account = Create2.computeAddress(
bytes32(salt),
keccak256(code)
);
if (_account.code.length != 0) return _account;
_account = Create2.deploy(0, bytes32(salt), code);
if (initData.length != 0) {
(bool success, ) = _account.call(initData);
if (!success) revert InitializationFailed();
}
emit AccountCreated(
_account,
implementation,
chainId,
tokenContract,
tokenId,
salt
);
return _account;
}
function account(
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt
) external view returns (address) {
bytes32 bytecodeHash = keccak256(
_creationCode(implementation, chainId, tokenContract, tokenId, salt)
);
return Create2.computeAddress(bytes32(salt), bytecodeHash);
}
function _creationCode(
address implementation_,
uint256 chainId_,
address tokenContract_,
uint256 tokenId_,
uint256 salt_
) internal pure returns (bytes memory) {
return
abi.encodePacked(
hex"3d60ad80600a3d3981f3363d3d373d3d3d363d73",
implementation_,
hex"5af43d82803e903d91602b57fd5bf3",
abi.encode(salt_, chainId_, tokenContract_, tokenId_)
);
}
}
6 セキュリティに関する考察
① 不正行為の防止
トークンバウンドアカウントの信頼できる販売を可能にするために、分散型マーケットプレイスは悪意のあるアカウント所有者による詐欺行為に対するセーフガードを実装する必要があります。
次のような詐欺の可能性を考えてみましょう。
① アリスはERC-721トークンXを所有し、トークンバウンドアカウントYを所有しています。
② アリスはアカウントYに10ETHを入金します。
③ BobはトークンXを11ETHで購入することを分散型マーケットプレイスで提案し、アカウントYに保存されている10ETHをトークンと一緒に受け取ると仮定します。
④ アリスはトークンバウンドトークンから10ETHを引き出し、直ちにボブの申し出を受け入れる。
⑤ ボブはトークンXを受け取るが、アカウントYは空っぽになる
悪意のあるアカウント所有者による詐欺行為を軽減するために、分散型マーケットプレイスは、マーケットプレイスレベルでこの種の詐欺行為に対する保護を実装すべきです(SHOULD)。
このEIPを実装するコントラクトは、詐欺行為に対する特定の保護も実装してもよい(MAY)。
以下は、考慮すべきいくつかの緩和策です。
マーケットプレイス注文に、現在のトークンバウンドアカウントのnonceを添付する。
注文後にアカウントの nonce が変更された場合、そのオファーは無効であるとみなす。
この機能は、マーケットプレイスレベルでサポートされる必要がある。
マーケットプレイス注文に、注文が成立したときにトークンバウンドアカウントに残ると予想される資産のコミットメントのリストを添付する。
注文後にコミットメントされた資産のいずれかがアカウントから削除された場合、そのオファーは無効であるとみなす。
これもマーケットプレイスで実装する必要があります。
注文署名を検証する前に上記のロジックを実行する外部のスマートコントラクトを介して、分散型マーケットに注文を提出する。
これにより、マーケットプレイスのサポートがなくても安全な送金を実施することができます。
トークンバウンドアカウントの実装にロック機構を実装し、悪意のある所有者がロック中にアカウントから資産を引き出すことを防止する。
不正行為を防止することは、この提案の範囲外です。
② 所有権のサイクル
トークンバウンドアカウントに保有されているすべての資産は、所有権サイクルが作成されるとアクセスできなくなる可能性があります。
最も単純な例としては、ERC-721トークンが自分のトークンバウンドアカウントに転送されるケースがあります。
この場合、トークンバウンドアカウントはERC-721トークンを転送するトランザクションを実行できないため、ERC-721トークンとトークンバウンドアカウントに保存されているすべての資産の両方に永久的にアクセスできなくなります。
所有権サイクルは、n>0個のトークンバウンドアカウントからなる任意のグラフに導入される可能性があります。
このようなサイクルをオンチェーンで防止することは、無限の検索スペースを必要とするため、実施することが困難であり、そのため、この提案の範囲外です。
この提案を採用したいアプリケーション・クライアントとアカウント実装は、所有権サイクルの可能性を制限する手段を実装することが推奨されます。
ここから第7章の前まで、「所有権サイクル」についての翻訳者の解釈部分です。(2023年6月18日追記)
まず、下のような例を考えます。
「アカウント2」は「NFT1」に紐づいている、トークンバウンドアカウントです。
なお、「アカウント1」はメタマスクなどの普通のアカウント(EOA)を想定しています。
そして、「NFT2」を操作できるのは「アカウント2」ではなく、「NFT1」の所有者である、「アカウント1」です。
これがまずは基本部分です。
さらに、下のように、「NFT2」に紐づいた「トークンバウンドアカウント」である、「アカウント3」ができました。
「トークンバウンドアカウント」作成時に、NFTの所有者は一切関係がないので、「アカウント2」がどんなアカウントかは関係がありません。
ここで、「NFT3」を操作できるのは誰かを考えます。
本来なら、「NFT2」の所有者である「アカウント2」が操作ができるはずです。
(この部分は私の推測です)
しかし、「アカウント2」はすでに「コントラクトアカウント」になっており、通常、「アカウント3」のアイテムを操作する実装は行われません。
ただし、「アカウント2」の「NFT1」の所有者である、「アカウント1」であれば、「NFT2」の所有者であり、ただのアカウント(EOA)であるため、操作が可能です。
最後に、「NFT3」の「トークンバウンドアカウント」がたまたま「アカウント1」になったと考えます。
「アカウント1」はただのアカウント(EOA)だったので、「トークンバウンドアカウント」になることは考えられます。
では、ここで、「NFT1」を誰が操作できるかを考えます。
本来ならば「NFT3」の所有者の「アカウント3」が操作できるはずです。
しかし、すでにコントラクトアカウントになっているので、「アカウント3発」では操作できません。(先ほどと同様のロジック)
これは「アカウント2」も同様です。
そして、「アカウント2」に紐づいている「NFT1」の所有者である「アカウント1」は、「NFT1」を操作できません。
これはそもそもの内容として、「トークンバウンドアカウント」が所有しているアイテムは紐づいている「NFT」の所有者が操作できるものだからです。
この結果、「NFT1」を誰も操作することができなくなりました。
また、この状況を他のNFTである、「NFT2」の視点で見てみましょう。
図は少しごちゃっとしましたが「NFT1」と同じロジックで、「NFT2」も操作ができなくなってしまいます。
つまり、この閉じたサイクル内のアイテムは、全て操作ができなくなってしまいます。
これが所有権サイクルだと考えます。
ここまでが翻訳者の解釈です。
7 著作権について
著作権および関連する権利は、CC0により放棄されています。
8 引用
このドキュメントを以下のように引用してください:
Jayden Windle (@jaydenwindle), Benny Giang bg@futureprimitive.xyz, Steve Jang, Druzy Downs (@druzydowns), Raymond Huynh (@huynhr), Alanah Lam alanah@futureprimitive.xyz, Wilkins Chung (@wwhchung) wilkins@manifold.xyz, Paul Sullivan (@sullivph) paul.sullivan@manifold.xyz, "ERC-6551: Non-fungible Token Bound Accounts [DRAFT]" Ethereum Improvement Proposals, NO. 6551, February 2023. [オンラインシリアル]. 利用可能: https://eips.ethereum.org/EIPS/eip-6551.