はじめに

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

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

今回は、ERC6551を拡張して、特定のNFTに紐づいた汎用コントラクトのレジストリの仕組みを提案しているERC7656についてまとめていきます!

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

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

概要

この規格では、ERC6551にバリエーションをもたらすことを提案しています。 ERC6551に沿って作成されるコントラクトをアカウントのみに限らず、様々な種類のコントラクトにできるような仕組みが説明されています。

ERC6551とは、各NFTに1対1で紐づく形で生成されるコントラクトアカウントです。 各NFTにはtokneIdという値が紐づいています。 このtokenIdごとに「Token Bound Account(TBA)」と呼ばれるコントラクトを作成します。 このTBAの操作権限はNFTの保有者が持ちます。 TBAはコントラクトであるため、ここに他のNFTやFT、ETHなどを受け取ることができます。 これにより、各NFTが他のNFTやFT、ETHを持つことができるようになります。

7b05f09c4c3c-20230518.png 引用: https://zenn.dev/thirdweb_jp/articles/ce2c7bdc39ff85

さらに、各NFTが売買された時、紐づくTBAの操作権限は各NFTの保有者にあるため、自動で操作権限が移動します。 わかりやすい使用用途としては、「Airdrop」があげられます。 特定のタイミングでNFT保有者にトークンなどを送りたい時、このTBAに向けて配布することでその時点の保有者に配ると同じことが実現できます。

OpenseaはERC6551の表示に対応しているため、実際に確認するとイメージがつきやすいです。

Screenshot 2024-06-15 at 8.43.36.png https://opensea.io/assets/ethereum/0x582048c4077a34e7c3799962f1f8c5342a3f4b12/4

上記のようにERC6551のNFTは左上にバッグボタンが表示されています。

Screenshot 2024-06-15 at 8.45.03.png

このバッグボタンを押すと、そのNFTに紐づいたTBAが保有しているアセットが表示されます。 より詳しくは以下の記事を参考にしてください。

動機

ERC6551は、NFTのtokenIdが所有するアカウントをデプロイするレジストリ(ERC6551Registry)を提供します。 ただ、ERC6551では、このレジストリを介して作成されるコントラクトがIERC6551AccountとIERC6551Executeを実装する必要があります。 これにより、デプロイされるコントラクトがアカウントでなければならず柔軟性が失われます。 この規格では、NFTに関連するあらゆる種類のコントラクトをデプロイ汎用的なレジストリを提案しています。 コントラクトの中の関数名やイベント名も汎用的なものにして、特定の用途にのみ絞られないようにします。 ERC6551Registryでは、NFTに紐づく単一のコントラクトをデプロイしていますが、プロジェクトなどのニーズに合わせてレジストリを調整できるようにします。

提案されているERC7656Registryでは、ERC6551の制約を受けないように設計されています。 新しい規格では以下のような利点があります、。

  • 柔軟性
    • NFTが様々な種類のコントラクトと連携できる。
    • 貸付システム、資産の分割所有、アイデンティティなどの新しいユースケースと機能を実装可能にします。
  • 互換性
    • アカウントのようなコントラクトを識別できるようにして、ERC6551との互換性を維持します。
  • イノベーション
    • NFTに関連するコントラクト二制限を設けないことで、NFTの分野でのイノベーションを促進します。

仕様

IERC7656Registryインターフェースは以下のように定義されています。

js
// interfaceId 0xc6bdc908
interface IERC7656Registry {
  /**
   * @notice The registry MUST emit the Created event upon successful contract creation.
   * @param contractAddress The address of the created contract
   * @param implementation The address of the implementation contract
   * @param salt The salt to use for the create2 operation
   * @param chainId The chain id of the chain where the contract is being created
   * @param tokenContract The address of the token contract
   * @param tokenId The id of the token
   */
  event Created(
    address contractAddress,
    address indexed implementation,
    bytes32 salt,
    uint256 chainId,
    address indexed tokenContract,
    uint256 indexed tokenId
  );

  /**
   * The registry MUST revert with CreationFailed error if the create2 operation fails.
   */
  error CreationFailed();

  /**
   * @notice Creates a token linked account for a non-fungible token.
   * If account has already been created, returns the account address without calling create2.
   * @param implementation The address of the implementation contract
   * @param salt The salt to use for the create2 operation
   * @param chainId The chain id of the chain where the account is being created
   * @param tokenContract The address of the token contract
   * @param tokenId The id of the token
   * Emits Created event.
   * @return account The address of the token linked account
   */
  function create(
    address implementation,
    bytes32 salt,
    uint256 chainId,
    address tokenContract,
    uint256 tokenId
  ) external returns (address account);

  /**
   * @notice Returns the computed token linked account address for a non-fungible token.
   * @param implementation The address of the implementation contract
   * @param salt The salt to use for the create2 operation
   * @param chainId The chain id of the chain where the account is being created
   * @param tokenContract The address of the token contract
   * @param tokenId The id of the token
   * @return account The address of the token linked account
   */
  function compute(
    address implementation,
    bytes32 salt,
    uint256 chainId,
    address tokenContract,
    uint256 tokenId
  ) external view returns (address account);
}

Createdイベント

js
event Created(
address contractAddress,
address indexed implementation,
bytes32 salt,
uint256 chainId,
address indexed tokenContract,
uint256 indexed tokenId
);

このイベントは、コントラクトが正常に作成されたときに発行されます。

  • contractAddress
    • 作成されたコントラクトのアドレス。
  • implementation
    • 実装コントラクトのアドレス。
  • salt
    • create2操作で使用するソルト。
  • chainId
    • コントラクトが作成されるチェーンのID。
  • tokenContract
    • トークンコントラクトのアドレス。
  • tokenId
    • トークンのID。

CreationFailedエラー

js
error CreationFailed();

create2操作が失敗した場合にこのエラーを発生させます。

create関数

js
function create(
    address implementation,
    bytes32 salt,
    uint256 chainId,
    address tokenContract,
    uint256 tokenId
) external returns (address account);

NFTにリンクされたアカウントを作成します。 既にアカウントが作成されている場合は、create2を呼び出さずにアカウントアドレスを返します。 アカウントを作成できた場合、Createdイベントを発行します。 これまでは、implementation部分にIERC6551Accountに準拠したコントラクトを渡す必要がありました。 この規格では、このimplementation部分に任意のコントラクトを渡せることを提案しています。

  • implementation
    • 実装コントラクトのアドレス。
  • salt
    • create2操作で使用するソルト。
  • chainId
    • コントラクトが作成されるチェーンのID。
  • tokenContract
    • トークンコントラクトのアドレス。
  • tokenId
    • トークンのID。
  • account
    • 作成されたコントラクトのアドレスを返します。

compute関数

NFTにリンクされたアカウントアドレスを計算して返します。 create2操作を実際に行うことなく、アドレスを計算するために使用されます。

js
function compute(
address implementation,
bytes32 salt,
uint256 chainId,
address tokenContract,
uint256 tokenId
) external view returns (address account);
  • implementation
    • 実装コントラクトのアドレス。
  • salt
    • create2操作で使用するソルト。
  • chainId
    • コントラクトが作成されるチェーンのID。
  • tokenContract
    • トークンコントラクトのアドレス。
  • tokenId
    • トークンのID。
  • account
    • 計算されたアカウントアドレスを返します。

ERC7656Registryに準拠するコントラクトは、IERC7656RegistryのインターフェースIDである0xc6bdc908をサポートする必要があります。

この提案は、ERC-6551の原則に基づいて、NFTにリンクされたアカウントをより柔軟に管理するための仕組みを提供します。以下に詳しく説明します。 各トークンにリンクされたアカウントは、ERC1167最小プロキシとしてデプロイする必要があります。 これにより、効率的でコストの低いコントラクトデプロイが可能になります。

デプロイされたバイトコードの構造

  • ERC-1167 Header (10バイト)
  • 実装アドレス (20バイト)
  • ERC-1167 Footer (15バイト)
  • ソルト (32バイト)
  • チェーンID (32バイト)
  • トークンコントラクトアドレス (32バイト)
  • トークンID (32バイト)

これらの構造により、各トークンリンクアカウントは一意で、指定されたパラメータに基づいてデプロイされます。

IERC7656Linked インターフェース

ERC7656Registryを使用して作成されるコントラクトは、IERC7656Linkedインターフェースを実装する必要があります。

js
interface IERC7656Linked {
/**
* @notice Returns the token linked to the contract
* @return chainId The chainId of the token
* @return tokenContract The address of the token contract
* @return tokenId The tokenId of the token
*/
function token() external view returns (uint256 chainId, address tokenContract, uint256 tokenId);
}

この関数は、コントラクトにリンクされたトークンの情報(chainId、トークンコントラクトアドレス、tokenId)を返します。 この仕組みはERC6551Accountインターフェースを実装しているコントラクトもサポートされます。

補足

ERC7656gはNFTに紐づくコントラクトの種類を拡張して、汎用的なコントラクトにすることを目的としています。

汎用的な関数名とイベント名

汎用的な名前を採用することで、アカウント以外のさまざまな種類のコントラクトをサポートできるようになります。 特定の役割や機能に限定しないことで、柔軟性が高まりさまざまなユースケースに対応できるようになります。 また、インターフェースがシンプルで様々な実装への適応性が高くなります。

レジストリのシングルトン要件の廃止

ERC6551とは異なり、ERC7656ではレジストリを単一のインスタンスとしてデプロイする必要がありません。 これにより、プロジェクトごとに異なる要件や制約に合わせてレジストリをカスタマイズおよび最適化できます。

IERC7656Registryインターフェースの明確なサポート

全てのレジストリがIERC7656Registryインターフェースを明確にサポートすることで、ERC6551との互換性と認識が確保されます。 これにより、準拠するレジストリとの相互作用が容易になり、標準化された一貫性のあるエコシステムの促進が期待できます。

コントラクト連携の柔軟性

この提案では、アカウントだけでなくNFTに関連するあらゆるコントラクトと紐づけられるように設計されています。 これにより、複雑な金融商品やアイデンティティ検証システムなど、NFTの様々なユースケースに対応できます。

互換性の確保

この提案では、ERC6551のアカウントとの互換性があります。 これにより、ERC6551に基づいて構築されたアプリケーションやサービスが移行しやすくなります。

まとめ

これらの技術的決定は、NFTに関連するコントラクトの適用範囲を広げ、開発者に革新のためのツールを提供し、分散型アプリケーションの成長するエコシステムを支援することを目的としています。ERC-7656は、現在の制限と将来の機会の両方に対処することで、次世代のNFT技術のための柔軟で堅牢な基盤を築くことを目指しています。

参考実装

ERC7656Registry

ERC7656Registryの参考実装は、ERC6551Registryから以下の点が変更されています。

  • 汎用的な関数名。
  • 異なるイベントとエラー。
  • interfaceIdに対してのみtrueを返すsupportsInterfaceの追加。
js
contract ERC7656Registry is IERC7656Registry {
  function create(
    address implementation,
    bytes32 salt,
    uint256 /* chainId */,
    address tokenContract,
    uint256 tokenId
  ) external override returns (address) {
    // solhint-disable-next-line no-inline-assembly
    assembly {
    // Memory Layout:
    // ----
    // 0x00   0xff                           (1 byte)
    // 0x01   registry (address)             (20 bytes)
    // 0x15   salt (bytes32)                 (32 bytes)
    // 0x35   Bytecode Hash (bytes32)        (32 bytes)
    // ----
    // 0x55   ERC-1167 Constructor + Header  (20 bytes)
    // 0x69   implementation (address)       (20 bytes)
    // 0x5D   ERC-1167 Footer                (15 bytes)
    // 0x8C   salt (uint256)                 (32 bytes)
    // 0xAC   chainId (uint256)              (32 bytes)
    // 0xCC   tokenContract (address)        (32 bytes)
    // 0xEC   tokenId (uint256)              (32 bytes)

    // Copy bytecode + constant data to memory
      calldatacopy(0x8c, 0x24, 0x80) // salt, chainId, tokenContract, tokenId
      mstore(0x6c, 0x5af43d82803e903d91602b57fd5bf3) // ERC-1167 footer
      mstore(0x5d, implementation) // implementation
      mstore(0x49, 0x3d60ad80600a3d3981f3363d3d373d3d3d363d73) // ERC-1167 constructor + header

    // Copy create2 computation data to memory
      mstore8(0x00, 0xff) // 0xFF
      mstore(0x35, keccak256(0x55, 0xb7)) // keccak256(bytecode)
      mstore(0x01, shl(96, address())) // registry address
      mstore(0x15, salt) // salt

    // Compute account address
      let computed := keccak256(0x00, 0x55)

    // If the account has not yet been deployed
      if iszero(extcodesize(computed)) {
      // Deploy account contract
        let deployed := create2(0, 0x55, 0xb7, salt)

      // Revert if the deployment fails
        if iszero(deployed) {
          mstore(0x00, 0xd786d393) // `CreationFailed()`
          revert(0x1c, 0x04)
        }

      // Store account address in memory before salt and chainId
        mstore(0x6c, deployed)

      // Emit the Created event
        log4(
          0x6c,
          0x60,
          0xc6989e4f290074742210cbd6491de7ded9cfe2cd247932a53d31005007a6341a,
          implementation,
          tokenContract,
          tokenId
        )

      // Return the account address
        return(0x6c, 0x20)
      }

    // Otherwise, return the computed account address
      mstore(0x00, shr(96, shl(96, computed)))
      return(0x00, 0x20)
    }
  }

  function compute(
    address implementation,
    bytes32 salt,
    uint256 /* chainId */,
    address /* tokenContract */,
    uint256 /* tokenId */
  ) external view override returns (address) {
    // solhint-disable-next-line no-inline-assembly
    assembly {
    // Copy bytecode + constant data to memory
      calldatacopy(0x8c, 0x24, 0x80) // salt, chainId, tokenContract, tokenId
      mstore(0x6c, 0x5af43d82803e903d91602b57fd5bf3) // ERC-1167 footer
      mstore(0x5d, implementation) // implementation
      mstore(0x49, 0x3d60ad80600a3d3981f3363d3d373d3d3d363d73) // ERC-1167 constructor + header

    // Copy create2 computation data to memory
      mstore8(0x00, 0xff) // 0xFF
      mstore(0x35, keccak256(0x55, 0xb7)) // keccak256(bytecode)
      mstore(0x01, shl(96, address())) // registry address
      mstore(0x15, salt) // salt

    // Store computed account address in memory
      mstore(0x00, shr(96, shl(96, keccak256(0x00, 0x55))))

    // Return computed account address
      return(0x00, 0x20)
    }
  }

  /// @dev Returns true if interfaceId is IERC7656Registry's interfaceId
  /// This contract does not explicitly extend IERC165 to keep the bytecode as small as possible
  function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
    return interfaceId == 0xc6bdc908;
  }
}

IERC7656Linked

contract LinkedContract is IERC7656Linked, EIP5313 {

function token() public view virtual returns (uint256, address, uint256) { bytes memory footer = new bytes(0x60); assembly { extcodecopy(address(), add(footer, 0x20), 0x4d, 0x60) } return abi.decode(footer, (uint256, address, uint256)); }

function owner() public view virtual override returns (address) { (uint256 chainId, address tokenContract_, uint256 tokenId_) = token(); if (chainId != block.chainid) return address(0); return IERC721(tokenContract_).ownerOf(tokenId_); } }

セキュリティ

ERC6551で説明されているセキュリティの考慮事項以外特にありません。

引用

Francesco Sullo (@sullof), "ERC-7656: Generalized Token-Linked Contracts [DRAFT]," Ethereum Improvement Proposals, no. 7656, March 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7656.

最後に

今回は「ERC6551を拡張して、特定のNFTに紐づいた汎用コントラクトのレジストリの仕組みを提案しているERC7656」についてまとめてきました! いかがだったでしょうか?

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

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