はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、NFTをロックせずに分割所有し、ERC721とERC20の性質を1つのコントラクトで両立させる仕組みを提案しているERC7651についてまとめていきます!

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

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

ERC7651は、非代替性トークン(NFT)を分割可能な形で表現・管理できる標準規格です。 ここでいうNFTとは、ERC721に代表される「1つ1つが固有で代替できないトークン」を指します。 一方で分割可能とは、ERC20のように数量として扱える性質を持つことを意味します。

ERC721については以下の記事を参考にしてください。

ERC20については以下の記事を参考にしてください。

ERC7651の大きな特徴は、NFTを物理的・論理的に分割することなく、1つのコントラクトの中で分割所有を実現する点です。 NFTそのものは1つのまま存在し続けますが、その所有権や価値を、内部的には「数量で表現できるトークン(ファンジブルトークン)」としても扱える構造になっています。

これにより、以下のようなことが可能になります。

  • NFT全体を1人が所有する状態
  • NFTを複数人で分割して所有する状態
  • 上記2つの状態を、特別な変換操作なしで共存させること

従来の分割NFTでは、「NFTを分割用のトークンに変換する」、「元に戻す」という明示的な操作が必要でした。 しかしERC7651では、NFTと分割表現が常に同時に存在する設計のため、ユーザーは変換を意識せずに利用できます。

また、ERC7651は以下の機能を標準として定義しています。

  • 分割されたトークン単位での送信
  • NFT全体としての送信
  • 承認(Approve)処理
  • それぞれに対応したイベントの発行

設計思想としてはERC721とERC20の両方を参考にしていますが、どちらとも完全互換ではありません。 これは、NFTと分割表現を同時に扱うという目的に特化した、新しいトークンモデルであるためです。

動機

これまでの分割NFTの問題点

NFTの分割所有は、これまでも需要がありさまざまなプロトコルで実装されてきました。 ただし、その多くはNFTを一度ロックし、代わりに分割用トークンを発行する方式を採用しています。

この方式には、主に2つの問題があります。

まず1つ目は、流動性が分断される問題です。 例えば、NFT AとNFT Bをそれぞれ分割した場合、それぞれから発行される分割トークンは別物になります。 NFT Aの分割トークンとNFT Bの分割トークンは価値も意味も異なるため、同じ市場でまとめて扱うことができません。 その結果、NFTごとに流動性が細かく分かれてしまい、取引が活発になりにくくなります。

2つ目は、NFTの自由な移転が制限される問題です。 分割のためにNFTをロックしてしまうと、分割状態の間はNFTそのものを自由に送信できません。 再びNFTを動かすためには、すべての分割トークンを集めて再構成する必要があります。 これは、利用者にとって大きな制約になります。

ERC7651が目指す解決策

ERC7651は、こうした問題を解消するために設計されています。

この規格では、NFTをロックしたり、分割専用のトークンに変換したりする必要がありません。 NFTは常に1つの存在として保持されつつ、その価値や所有権を数量的にも扱えるようになっています。

その結果、以下のような利点が生まれます。

  • NFT全体としても、分割された形としても、自由に移転できる
  • 分割状態でもNFTが拘束されない
  • 分割所有を前提としつつ、NFTの本来の性質(唯一性)を失わない

ERC7651は、NFTの流動性(売買のしやすさ)とアクセス性(高額NFTへの参加しやすさ)を高めることを目的としています。 同時に、NFTが本来持つ柔軟な移転性や扱いやすさを損なわない点が重要です。

このようにERC7651は、従来の分割NFTが抱えていた構造的な制約を避けつつ、NFTをより実用的な資産として扱えるようにするための標準規格です。

仕様

分割表現付きNFTインターフェース

ERC7651に準拠するコントラクトは、必ずERC7651とERC165の両方のインターフェースを実装する必要があります。 ERC165は、コントラクトがどのインターフェースを実装しているかを外部から判別するための標準です。

ERC165については以下の記事を参考にしてください。

また、ERC7651では「分割表現」と「NFT表現」が同時に存在するため、それぞれの操作に対応したイベント発行が必須になります。

分割表現に対する承認や送付では、分割専用の Approval / Transfer イベントを発行する必要があります。 加えて、NFTとしての承認や送付では、NFT専用の ApprovalForAll / Approval / Transfer イベントを必ず発行します。

このインターフェースはERC721とERC20の設計を参考にしていますが、完全な互換ではありません。 そのため、既存のERC721やERC20の前提で作られたツールでは、挙動の違いを理解した上で対応する必要があります。

IERC7651インターフェース

IERC7651.sol
interface IERC7651 is IERC165 {
  /// @dev This emits when fractional representation approval for a given spender
  ///      is changed or reaffirmed.
  event FractionalApproval(address indexed owner, address indexed spender, uint256 value);

  /// @dev This emits when ownership of fractionally represented tokens changes
  ///      by any mechanism. This event emits when tokens are both created and destroyed,
  ///      ie. when from and to are assigned to the zero address respectively.
  event FractionalTransfer(address indexed from, address indexed to, uint256 amount);

  /// @dev This emits when an operator is enabled or disabled for an owner.
  ///      The operator can manage all NFTs of the owner.
  event ApprovalForAll(
    address indexed owner,
    address indexed operator,
    bool approved
  );

  /// @dev This emits when the approved spender is changed or reaffirmed for a given NFT.
  ///      A zero address emitted as spender implies that no addresses are approved for
  ///      this token.
  event NonFungibleApproval(
    address indexed owner,
    address indexed spender,
    uint256 indexed id
  );

  /// @dev This emits when ownership of any NFT changes by any mechanism.
  ///      This event emits when NFTs are both created and destroyed, ie. when
  ///      from and to are assigned to the zero address respectively.
  event NonFungibleTransfer(address indexed from, address indexed to, uint256 indexed id);

  /// @notice Decimal places in fractional representation
  /// @dev Decimals are used as a means of determining when balances or amounts
  ///      contain whole or purely fractional components
  /// @return Number of decimal places used in fractional representation
  function decimals() external view returns (uint8 decimals);

  /// @notice The total supply of a token in fractional representation
  /// @dev The total supply of NFTs may be recovered by computing
  ///      `totalSupply() / 10 ** decimals()`
  /// @return Total supply of the token in fractional representation
  function totalSupply() external view returns (uint256 totalSupply);

  /// @notice Balance of a given address in fractional representation
  /// @dev The total supply of NFTs may be recovered by computing
  ///      `totalSupply() / 10 ** decimals()`
  /// @param owner_ The address that owns the tokens
  /// @return Balance of a given address in fractional representation
  function balanceOf(address owner_) external view returns (uint256 balance);

  /// @notice Query if an address is an authorized operator for another address
  /// @param owner_ The address that owns the NFTs
  /// @param operator_ The address being checked for approval to act on behalf of the owner
  /// @return True if `operator_` is an approved operator for `owner_`, false otherwise
  function isApprovedForAll(
    address owner_,
    address operator_
  ) external view returns (bool isApproved);

  /// @notice Query the allowed amount an address can spend for another address
  /// @param owner_ The address that owns tokens in fractional representation
  /// @param spender_ The address being checked for allowance to spend on behalf of the owner
  /// @return The amount of tokens `spender_` is approved to spend on behalf of `owner_`
  function allowance(
    address owner_,
    address spender_
  ) external view returns (uint256 allowance);

  /// @notice Query the owner of a specific NFT.
  /// @dev Tokens owned by the zero address are considered invalid and should revert on
  ///      ownership query.
  /// @param id_ The unique identifier for an NFT.
  /// @return The address of the token's owner.
  function ownerOf(uint256 id_) external view returns (address owner);

  /// @notice Set approval for an address to spend a fractional amount,
  ///         or to spend a specific NFT.
  /// @dev There must be no overlap between valid ids and fractional values.
  /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized
  ///      operator of the current owner if an id is provided.
  /// @dev Throws if the id is not a valid NFT
  /// @param spender_ The spender of a given token or value.
  /// @param amountOrId_ A fractional value or id to approve.
  /// @return Whether the approval operation was successful or not.
  function approve(
    address spender_,
    uint256 amountOrId_
  ) external returns (bool success);

  /// @notice Set approval for a third party to manage all of the callers
  ///         non-fungible assets
  /// @param operator_ Address to add to the callers authorized operator set
  /// @param approved_ True if the operator is approved, false if not approved
  function setApprovalForAll(address operator_, bool approved_) external;

  /// @notice Transfer fractional tokens or an NFT from one address to another
  /// @dev There must be no overlap between valid ids and fractional values
  /// @dev The operation should revert if the caller is not `from_` or is not approved
  ///      to spent the tokens or NFT owned by `from_`
  /// @dev The operation should revert if value is less than the balance of `from_` or
  ///      if the NFT is not owned by `from_`
  /// @dev Throws if the id is not a valid NFT
  /// @param from_ The address to transfer fractional tokens or an NFT from
  /// @param to_ The address to transfer fractional tokens or an NFT to
  /// @param amountOrId_ The fractional value or a distinct NFT id to transfer
  /// @return True if the operation was successful
  function transferFrom(
    address from_,
    address to_,
    uint256 amountOrId_
  ) external returns (bool success);

  /// @notice Transfer fractional tokens from one address to another
  /// @dev The operation should revert if amount is less than the balance of `from_`
  /// @param to_ The address to transfer fractional tokens to
  /// @param amount_ The fractional value to transfer
  /// @return True if the operation was successful
  function transfer(address to_, uint256 amount_) external returns (bool success);

  /// @notice Transfers the ownership of an NFT from one address to another address
  /// @dev Throws unless `msg.sender` is the current owner, an authorized
  ///      operator, or the approved address for this NFT
  /// @dev Throws if `from_` is not the current owner
  /// @dev Throws if `to_` is the zero address
  /// @dev Throws if `tokenId_` is not a valid NFT
  /// @dev When transfer is complete, this function checks if `to_` is a
  ///      smart contract (code size > 0). If so, it calls `onERC721Received`
  ///      on `to_` and throws if the return value is not
  ///      `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`.
  /// @param from_ The address to transfer the NFT from
  /// @param to_ The address to transfer the NFT to
  /// @param tokenId_ The NFT to transfer
  /// @param data_ Additional data with no specified format, sent in call to `to_`
  function safeTransferFrom(
    address from_,
    address to_,
    uint256 id_,
    bytes calldata data_
  ) external;

  /// @notice Transfers the ownership of an NFT from one address to another address
  /// @dev This is identical to the above function safeTransferFrom interface
  ///      though must pass empty bytes as data to `to_`
  /// @param from_ The address to transfer the NFT from
  /// @param to_ The address to transfer the NFT to
  /// @param tokenId_ The NFT to transfer
  function safeTransferFrom(address from_, address to_, uint256 id_) external;
}

イベント

FractionalApproval
javascript
event FractionalApproval(address indexed owner, address indexed spender, uint256 value);

分割表現トークンの支出承認が変更または再確認された時に発行されるイベント。 このイベントは、あるアドレスが別のアドレスに対して、分割表現トークンを使用する権限を与えた場合に発行されます。 ERC20の Approval に近い概念ですが、対象はNFTの分割表現です。

パラメータ

  • owner
    • 分割表現トークンの所有者。
  • spender
    • 使用を許可されたアドレス。
  • value
    • 使用を許可する分割表現トークンの量。
FractionalTransfer
javascript
event FractionalTransfer(address indexed from, address indexed to, uint256 amount);

分割表現トークンの所有量が変化した時に発行されるイベント。

詳細

このイベントは、分割表現トークンが移転された場合に発行されます。 また、mintやburnも含まれており、その場合は from または to にゼロアドレスが指定されます。

パラメータ

  • from
    • トークンの送信元アドレス。
  • to
    • トークンの送信先アドレス。
  • amount
    • 移転された分割表現トークンの量。
ApprovalForAll
javascript
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

NFTの全操作権限が設定または解除された時に発行されるイベント。 このイベントは、あるアドレスが別のアドレスに対して、所有するすべてのNFTを操作する権限を与えた場合に発行されます。 ERC721と同じ概念で、マーケットプレイスなどが利用します。

パラメータ

  • owner
    • NFTの所有者。
  • operator
    • 操作権限を付与されるアドレス。
  • approved
    • 権限が有効かどうかを示すフラグ。
NonFungibleApproval
javascript
event NonFungibleApproval(address indexed owner, address indexed spender, uint256 indexed id);

特定のNFTに対する承認が変更または再確認された時に発行されるイベント。 このイベントは、特定のNFTについて操作を許可するアドレスが設定された場合に発行されます。 spender がゼロアドレスの場合、そのNFTには承認が存在しないことを意味します。

パラメータ

  • owner
    • NFTの所有者。
  • spender
    • 操作を許可されたアドレス。
  • id
    • 対象となるNFTのID。
NonFungibleTransfer
javascript
event NonFungibleTransfer(address indexed from, address indexed to, uint256 indexed id);

NFTの所有者が変更された時に発行されるイベント。 このイベントは、NFTが別のアドレスに移転した場合に発行されます。 mintやburnも含まれ、その場合は from または to にゼロアドレスが指定されます。

パラメータ

  • from
    • NFTの送信元アドレス。
  • to
    • NFTの送信先アドレス。
  • id
    • 移転されたNFTのID。

関数

decimals
javascript
function decimals() external view returns (uint8 decimals);

分割表現における小数桁数を取得する関数。 この値は、分割表現トークンがどこまで細かく分割されるかを示します。 ERC7651では、小数桁数を用いて「NFTの丸ごと分」と「純粋な端数」を区別します。

戻り値

  • decimals
    • 分割表現で使用される小数桁数。
totalSupply
javascript
function totalSupply() external view returns (uint256 totalSupply);

分割表現トークンの総供給量を取得する関数。 この値は分割表現としての総量を示します。 NFTの総数は totalSupply / 10 ** decimals で計算できます。

戻り値

  • totalSupply
    • 分割表現トークンの総供給量。
balanceOf
javascript
function balanceOf(address owner_) external view returns (uint256 balance);

指定したアドレスの分割表現トークン残高を取得する関数。 この関数は、NFTの分割表現として、指定アドレスがどれだけの量を保有しているかを返します。

引数

  • owner_
    • 残高を確認するアドレス。

戻り値

  • balance
    • 分割表現トークンの残高。
isApprovedForAll
javascript
function isApprovedForAll(address owner_, address operator_) external view returns (bool isApproved);

NFTの全操作権限が付与されているかを確認する関数。 指定した operator_ が、owner_ のすべてのNFTを操作できるかどうかを返します。

引数

  • owner_
    • NFTの所有者。
  • operator_
    • 操作権限を確認するアドレス。

戻り値

  • isApproved
    • 権限があれば true、なければ false。
allowance
javascript
function allowance(address owner_, address spender_) external view returns (uint256 allowance);

分割表現トークンの使用許容量を取得する関数。 ERC20と同様に、指定した spender_ が owner_ の分割表現トークンをどれだけ使用できるかを返します。

引数

  • owner_
    • トークンの所有者。
  • spender_
    • 使用権限を確認するアドレス。

戻り値

  • allowance
    • 使用を許可された分割表現トークン量。
ownerOf
javascript
function ownerOf(uint256 id_) external view returns (address owner);

指定したNFTの所有者を取得する関数。 NFTがゼロアドレスに紐づいている場合、そのNFTは無効とみなされ、呼び出しは失敗します。

引数

  • id_
    • NFTのID。

戻り値

  • owner
    • NFTの所有者アドレス。
approve
javascript
function approve(address spender_, uint256 amountOrId_) external returns (bool success);

分割表現トークンまたはNFTに対して使用権限を設定する関数。 この関数は、分割量とNFT IDのどちらも受け取れる点が特徴です。 amountOrId_ が分割量であれば分割表現トークンの承認となり、NFT IDであればそのNFTの承認となります。 両者の値が重複しない設計が必須です。

引数

  • spender_
    • 使用を許可するアドレス。
  • amountOrId_
    • 分割量、またはNFTのID。

戻り値

  • success
    • 承認が成功したかどうか。
setApprovalForAll
javascript
function setApprovalForAll(address operator_, bool approved_) external;

NFT全体の操作権限を設定する関数。 呼び出し元が所有するすべてのNFTについて、指定したアドレスに操作権限を付与または解除します。

引数

  • operator_
    • 操作権限を付与するアドレス。
  • approved_
    • 権限の有効・無効。
transferFrom
javascript
function transferFrom(address from_, address to_, uint256 amountOrId_) external returns (bool success);

分割表現トークンまたはNFTを移転する関数。 amountOrId_ に応じて、分割表現トークンまたはNFTのいずれかを移転します。 権限や残高が不足している場合は失敗します。

引数

  • from_
    • 送信元アドレス。
  • to_
    • 送信先アドレス。
  • amountOrId_
    • 分割量、またはNFTのID。

戻り値

  • success
    • 送付が成功したかどうか。
transfer
javascript
function transfer(address to_, uint256 amount_) external returns (bool success);

分割表現トークンを送信する関数。 分割表現トークン専用の送付関数です。 残高が不足している場合は失敗します。

引数

  • to_
    • 送信先アドレス。
  • amount_
    • 送信する分割量。

戻り値

  • success
    • 送付が成功したかどうか。
safeTransferFrom
javascript
function safeTransferFrom(address from_, address to_, uint256 id_, bytes calldata data_) external;
javascript
function safeTransferFrom(address from_, address to_, uint256 id_) external;

NFTを安全に送付する関数。 送信先がコントラクトの場合、onERC721Received を呼び出し、正しい戻り値が返らなければ送付を失敗させます。 NFTが受け取れないコントラクトへ送られる事故を防ぐための仕組みです。

引数

  • from_
    • 送信元アドレス。
  • to_
    • 送信先アドレス。
  • id_
    • NFTのID。
  • data_
    • 追加データ(省略可能)。

ERC165インターフェース

IERC165.sol
interface IERC165 {
    /// @notice Query if a contract implements an interface
    /// @param interfaceID_ The interface identifier, as specified in ERC-165
    /// @dev Interface identification is specified in ERC-165. This function
    ///      uses less than 30,000 gas.
    /// @return `true` if the contract implements `interfaceID` and
    ///         `interfaceID` is not 0xffffffff, `false` otherwise
    function supportsInterface(bytes4 interfaceID_) external view returns (bool);
}

関数

supportsInterface
javascript
function supportsInterface(bytes4 interfaceID_) external view returns (bool);

指定したインターフェースIDを実装しているかを確認する関数。 この関数により、外部コントラクトは対象がERC7651やメタデータ拡張を実装しているかを安全かつ低ガスで判定できます。

引数

  • interfaceID_
    • 判定対象のインターフェースID。

戻り値

  • 戻り値
    • 実装していれば true、そうでなければ false。

分割表現付きNFTメタデータインターフェース

このインターフェースは推奨であり、NFTコレクションの名前やメタデータURIを提供します。 関数定義はERC721 Metadataと同一ですが、ERC721継承と誤解されないよう、別インターフェースとして定義されています。

IERC7651Metadata.sol
/// @title ERC-7651 Fractional Non-Fungible Token Standard, optional metadata extension
interface IERC7651Metadata {
  /// @notice A descriptive, long-form name for a given token collection
  function name() external view returns (string memory name);

  /// @notice An abbreviated, short-form name for a given token collection
  function symbol() external view returns (string memory symbol);

  /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
  /// @dev Throws if `tokenId_` is not a valid NFT. URIs are defined in RFC
  ///      3986. The URI may point to a JSON file that conforms to the "ERC721
  ///      Metadata JSON Schema".
  /// @param id_ The NFT to fetch a token URI for
  /// @return The token's URI as a string
  function tokenURI(uint256 id_) external view returns (string memory uri);
}

関数

name
javascript
function name() external view returns (string memory name);

トークンコレクションの正式名称を取得する関数。 人が識別しやすい長い名称を返します。

戻り値

  • name
    • コレクション名。
symbol
javascript
function symbol() external view returns (string memory symbol);

トークンコレクションの略称を取得する関数。 ウォレットやUIで表示される短い識別子です。

戻り値

  • symbol
    • コレクションの略称。
tokenURI
javascript
function tokenURI(uint256 id_) external view returns (string memory uri);

指定したNFTのメタデータURIを取得する関数。 URIはRFC3986に従った形式で、JSON形式のメタデータを指します。 無効なNFT IDが指定された場合は失敗します。

引数

  • id_
    • NFTのID。

戻り値

  • uri
    • メタデータURI。

分割表現付きNFTバンキングインターフェース

ERC7651 は、分割表現とNFT表現を同一コントラクトで扱います。 その中で「NFT IDを再利用する実装」を行う場合、すでに mint されたが現在は誰にも所有されていないNFT IDを管理する必要が出てきます。 このインターフェースは、そのような未所有NFT(バンクされたNFT)を扱うための、推奨拡張です。

ここでいう「バンク」とは、NFTが存在してはいるものの、分割表現との対応関係や所有状態の都合で、現在はどのアドレスにも割り当てられていないNFT IDの集合を指します。

IERC7651NFTBanking.sol
interface IERC7651NFTBanking {
  /// @notice Get the number of NFTs that have been minted but are not currently owned.
  /// @dev This should be the number of unowned NFTs, limited by the total
  ///      fractional supply.
  /// @return The number of NFTs not currently owned.
  function getBankedNFTsLength() external view returns (uint256 bankedNFTsLength);

  /// @notice Get a paginated list of NFTs that have been minted but are not currently owned.
  /// @param start_ Start index in bank.
  /// @param count_ Number of tokens to return from start index, inclusive.
  /// @return An array of banked NFTs from `start_`, of maximum length `count_`.
  function getBankedNFTs(
    uint256 start_,
    uint256 count_
  ) external view returns (uint256[] memory bankedNFTs);

  /// @notice Query the current supply of NFTs in circulation.
  /// @dev Given supply may remain banked or unminted, this function should always be
  ///      inclusively upper-bounded by `totalSupply() / 10 ** decimals()`.
  /// @return The current supply of minted NFTs
  function totalNonFungibleSupply() external view returns (unit256);
}

関数

getBankedNFTsLength
javascript
function getBankedNFTsLength() external view returns (uint256 bankedNFTsLength);

mint 済みだが現在は所有者が存在しないNFTの数を取得する関数。 この関数は、NFTとしてはすでに生成されているものの、ownerOf で有効な所有者を持たないNFT IDの総数を返します。 返される値は、分割表現の総供給量を上限としなければなりません。 これは、分割表現で表現可能なNFT総数以上のNFTが存在する状態を防ぐためです。

NFT IDを再利用する実装では、この値をもとに「次に割り当て可能なNFTがどれだけ残っているか」を判断できます。

戻り値

  • bankedNFTsLength
    • 現在所有者がいないNFTの総数。
getBankedNFTs
javascript
function getBankedNFTs(
  uint256 start_,
  uint256 count_
) external view returns (uint256[] memory bankedNFTs);

所有者が存在しないNFT IDを、ページング形式で取得する関数。 この関数は、バンクに格納されているNFT IDの一覧を配列として返します。 NFTの数が多い場合でも一度に全件を取得する必要がないよう、start_ と count_ によるページングが採用されています。

返却される配列には、start_ で指定された位置から、最大 count_ 件分のNFT IDが含まれます。

引数

  • start_
    • バンク内での取得開始インデックス。
  • count_
    • 取得するNFT IDの最大件数。

戻り値

  • bankedNFTs
    • 現在所有者が存在しないNFT IDの配列。
totalNonFungibleSupply
javascript
function totalNonFungibleSupply() external view returns (unit256);

現在流通しているNFTの総数を取得する関数。 この関数は、実際に mint され、NFTとして存在している数を返します。 ただし、NFTは「未mint」、「バンク中」、「所有中」といった状態を取り得るため、この値にはバンク中のNFTが含まれる場合もあります。 仕様上、この値は常に totalSupply() / 10 ** decimals() です。 これは、分割表現として表現可能なNFT総数を超えないことを保証するためです。

戻り値

  • 戻り値
    • 現在mintされているNFTの総数。

分割表現付きNFT転送免除インターフェース

ERC7651の実装によっては、分割表現の移動に応じてNFTが自動的に送付・割り当てされる設計が考えられます。 しかし、ユーザーによっては「分割表現だけを扱いたい」、「NFTそのものは受け取りたくない」というケースもあります。

このインターフェースは、NFT転送を受け取らない選択をユーザー自身が行える仕組みを提供する、推奨(RECOMMENDED)拡張です。

IERC7651NFTTransferExemptable.sol
interface IERC7651NFTTransferExemptable {
  /// @notice Returns whether an address is NFT transfer exempt.
  /// @param account_ The address to check.
  /// @return Whether the address is NFT transfer exempt.
  isNFTTransferExempt(address account_) external view returns (bool);

  /// @notice Allows an address to set themselves as NFT transfer exempt.
  /// @param isExempt_ The flag, true being exempt and false being non-exempt.
  setSelfNFTTransferExempt(bool isExempt_) external;
}

関数

isNFTTransferExempt
javascript
isNFTTransferExempt(address account_) external view returns (bool);

指定したアドレスがNFT転送免除状態かどうかを確認する関数。 この関数は、指定されたアドレスが「NFT送付免除」に設定されているかどうかを返します。 免除状態のアドレスに対しては、実装側のロジックによりNFTの自動転送や割り当てを行わない設計が想定されています。 これはERC7651の必須仕様ではなく、実装ごとの柔軟な挙動を補助するための仕組みです。

引数

  • account_
    • ステートを確認するアドレス。

戻り値

  • 戻り値
    • NFT送付免除状態であれば true、そうでなければ false。
setSelfNFTTransferExempt
javascript
setSelfNFTTransferExempt(bool isExempt_) external;

呼び出し元自身をNFT転送免除状態に設定または解除する関数。 この関数により、ユーザーは自分自身がNFTを受け取るかどうかを明示的に選択できます。 true を指定するとNFT送付免除状態になり、false を指定すると通常状態に戻ります。 第三者が他人の設定を変更することは想定されておらず、自己設定のみが可能です。

引数

  • isExempt_
    • true の場合はNFT送付免除、false の場合は免除解除。

補足

ERC7651は、NFTの非代替というモデルを維持しながら、分割所有を「数量として扱える」形でも表現できるように設計されています。 考え方としてはERC721に寄せた構造を取りつつ、送金や承認の体験はERC20のように扱えることを目指しています。

この「両方の性質を持たせる」狙いは、既存のプロトコルやツールへの組み込み負担(integration lift)をできるだけ減らすことです。 つまり、完全互換は無理でも、見た目や使い方が近いAPI・イベントにして、移植や対応のコストを下げようとしています。

そのための中心戦略は2つです。

1つ目は、インターフェースを設計するときに、ERC721側なのかERC20側なのかが分かりやすい形に揃えることです。 2つ目は、同じ関数に見えても役割が重なる部分について、ロジックを明確に分離できる実装方針を示すことです。

IDと数量の分離

ERC7651の設計で最重要なのが、入力値が「NFT ID」と「分割数量」のどちらとして扱われるかが曖昧にならないことです。 同じ uint256 を受け取る関数(例: approve(amountOrId) や transferFrom(amountOrId))が存在するため、同じ数値が両方として解釈できてしまう状態は絶対に避けるという原則が置かれています。

仕様としては実装手順までは踏み込みませんが、実装側で実現する方法として、以下のような考え方が挙げられています。

  • 入力を「ID」として扱う場合は、所有者確認などで「それが本当に有効なNFT IDか」を検証する。
  • NFT IDに使う範囲を予約し、分割数量と被らないようにレンジ分離する。

この分離ができていると、重なりがあるインターフェースでも内部の判断がぶれず、想定外の挙動が起きにくくなります。

イベント

ERC7651では、イベントの互換性が特に難しい問題になります。 理由は単純で、ERC20とERC721は「Approval」、「Transfer」に相当するイベントがどちらにも存在し、シグネチャや indexed の付け方も噛み合わないからです。 既存のインデクサや解析ツールは、イベントの形に強く依存しています。

この問題に対して、いくつかの方針が検討されたと書かれています。

  • どちらか片方の標準に合わせてイベントを寄せる。
  • 衝突を避けるため、パラメータの indexed を変えた独自イベントにする。
  • そもそも完全に新しいイベントにする。

しかし ERC7651は、標準化を目指すなら「イベントが何を意味するか」が明確で、かつ互いに混ざらないことが最優先だと判断しています。 その結果、イベント名や内容は従来の転送・承認の形を維持しつつ、Fractional と NonFungible の接頭辞で明確に区別する方針を取っています。

これはインデックス作成側(インデクサ)が新しいイベント名に対応する必要があり、ソフトウェア側の負担は増えますが、曖昧さによる事故を避ける意図です。

Transfers

ERC7651の送付では、1回の送付で「分割数量」でも「NFT ID」でも指定できるのが特徴です。 ただし、ID送付と数量送付では中身の扱いがかなり違います。

NFT IDによるTransfer

NFT IDを指定した送付は分かりやすいです。 指定されたNFTが送られるだけでなく、そのNFTに対応する分割価値(10 ** decimals() に相当する量)もまるごと一緒に移転します。

つまり、「NFTを1つ送る」ことは「分割表現の中でも1個分(丸ごと分)を送る」ことと一致します。

分割数量によるTransfer

分割数量の送付は複雑になります。分割表現の残高が動くと、NFTの保有数(丸ごと分)が変わる可能性があるからです。 ここでいう「丸ごと分」は、balance / 10 ** decimals() の整数部分を想像すると理解しやすいです。

仕様では大きく3つのシナリオに分けています。

シナリオ 何が起きるか NFT割り当ての扱い
丸ごと分が変わらない 双方の整数部分が変わらない NFT割り当ては変えない
送信者の丸ごと分が減る 送信で整数部分が下がる 送信者からNFTを必要数取り除く
受信者の丸ごと分が増える 受信で整数部分が上がる 受信者へNFTを必要数増やす

ただし ERC7651は、これらをどう実装するかを決め打ちしません。 枠組みだけを示し、手段は実装に任せています。

例として挙げられているのは以下のような手法です。

  • 変化に合わせてNFTを単調に mint/burn する。
  • 分割数量の転送時に、NFTの所有をスタック(後入れ先出し)やキュー(先入れ先出し)のような構造で追跡し、必要に応じて割り当てる。

「スタック」、「キュー」は、どちらもデータ構造で、取り出す順番が決まっている保管方法です。 スタックは最後に入れたものから取り出し、キューは最初に入れたものから取り出します。

NFT送付免除

分割数量を大きく動かすと、整数部分の増減によって多数のNFTが同時に動く可能性があります。 これはガス代が高くなりがちです。 そこでERC7651は、NFT表現が不要な場合に備えて「NFTの送付を受け取らない」仕組みをオプションで推奨しています。 EOA(通常のウォレットアドレス)でもコントラクトでも使える想定です。

さらに重要なのは、免除状態を切り替えるときに、NFTと分割残高の整合性を取るために「方向付きで再調整(directionally rebalanced)」すると書かれている点です。 これは、免除に切り替えた方向に合わせてNFTを動かし、ステートを同期させるという意味です。

免除の挙動は以下のとおりです。

  • NFTの送付を受け取らない側に切り替える(opt-out)と、そのアドレスが持つNFTはバンクに移され、NFT保有数は0になります。
  • NFTの送付を受け取る側に切り替える(opt-in)と、分割残高に見合うNFT数になるまで、バンクからNFTが引き出され、そのアドレスに移転されます。

この仕組みにより、NFTの大量移動を避けたい利用者は、分割表現だけで運用できるようになります。

NFTバンキング(NFT Banking)

分割数量の送付では、受信者が「丸ごと分」を新しく獲得することがあり、その場合はNFTが1つ必要になります。 逆に送信者が丸ごと分を失う場合はNFTを1つ減らす必要があります。 このとき「余ったNFT」や「これから配るNFT」を管理する場所が必要で、それがバンクです。

バンクは、「供給量に対して利用可能だが、現在は誰にも所有されていないNFT」を追跡する領域です。 実装の思想についてERC7651は特定の方式にこだわらず、仕様に合う例をいくつか紹介しています。

mint/burnで整合を取る方式

1つ目は、NFTが流通に入るときに mint し、バンクに戻るときに burn する方式です。 ただしこの方式は、mint のガスコストが重く、burn によるストレージ削除のガス返金では相殺しにくいと説明されています。 加えて、IDが消えていくので、有限で固定されたID空間を維持したいコレクションには不向きです。

IDを保存して再利用する方式

2つ目は、NFT IDを burn せず、バンク内に保存して後で再利用する方式です。 これはガスを大きく節約でき、さらに「外部から追いやすい予測可能なID列」を提供できる利点があります。 バンク内部の管理は、キューでもスタックでも、別の構造でもよく、どのデータ構造を採用するかは実装に任されています。

ERC165インターフェース

ERC7651がERC165を仕様に含める理由は2つあります。

1つは、ERC721の設計思想に合わせることです。 ERC721はERC165によるインターフェース判定を前提にしているため、同じ思想を踏襲しています。 もう1つは、コントラクトが提供するインターフェースを外部へ明示し、統合側が仕様を判別できるようにすることです。

ただし注意点として、ERC7651のコントラクトは supportsInterface を通じてERC721やERC20をサポートすると主張してはいけません。 互換性を強く意識していても、完全には既存仕様に準拠できないためです。 ここを誤ると、統合側が完全なERC721**/ERC20だと誤認して事故になります。

メタデータ

ERC7651はERC721と同様に、メタデータ機能(name / symbol / tokenURI)を分離し、別インターフェースとして切り出す方針を取っています。 これは「ERC721を継承している」と誤解されるのを避けるためです。 機能は同等でも、意味合いが違うため、インターフェース名を分けて整理します。

また、メタデータには従来どおりの名称・URI解決だけでなく、前述のNFTバンキングや送付ロジックに関連する「バンクの可視性」に関するパターンも含める、と書かれています。 つまり、運用上は「今NFTがどこにいるか」、「未所有NFTがどう扱われるか」が外部から追えるようにする考え方が背景にあります。

互換性

ERC7651は、既存のERC721とERC20に「ほぼ後方互換」になることを目指しています。 とはいえ、両方の仕様を完全に満たすとは主張しておらず、その前提で別インターフェースとして提案されています。 これは、見た目や使い勝手はできるだけ既存標準に寄せつつ、完全互換が成立しない部分は無理に合わせず、仕様として明確に線引きする方針です。

イベント

ERC7651が後方互換を意図的に崩している数少ない領域がイベントです。 理由は、ERC721とERC20の Approval / Transfer がシグネチャ上衝突するためです。 ここでいう「シグネチャ」は、イベント名と引数の型・順序で決まる「イベントの形」を指します。衝突すると、ハイブリッドとして両方に完全準拠することができません。

そのため ERC7651は、イベントを新しく定義し、名前に Fractional または NonFungible の接頭辞を付けて区別します。 イベント自体は「Approval」「Transfer」という従来の概念を踏襲しますが、衝突しないことと意味が分かりやすいことを優先しています。

この判断は、インデクサ(イベントを読み取ってデータベース化するソフトウェア)側に対応が必要になり、外部側の作業が増える点はデメリットです。ただ、曖昧な互換を狙って誤解を生むより、イベントを明確に分離したほうが長期的には安全だ、という立場です。

balanceOf

balanceOf はERC20とERC721で意味が違います。

  • ERC20では、balanceOf は「数量(分割可能な単位)の残高」です。
  • ERC721では、balanceOf は「そのアドレスが所有するNFTの個数(整数)」です。

ERC7651は分割所有を中核に置いているため、balanceOf は「分割表現の残高」を返すべきだ、と整理しています。 その結果、balanceOf の戻り値はERC20的な意味になります。

これは、ERC721が想定する「NFTの個数」を返す balanceOf と一致しないため、ERC7651はERC721の balanceOf 仕様には完全には準拠できないことになります。

成功可否の戻り値

ERC7651の transfer と approve は、成功・失敗を表す boolean(true/false)を返す仕様です。

これはERC20では一般的ですが、ERC721では標準的ではありません。 ERC721の多くの関数は、成功時は何も返さず、失敗時はリバート(処理を失敗させる)する作りです。

それでもERC7651が boolean を返すのは、分割表現側を強く意識すると、統合側がERC20的な期待を持つケースが多く、最低限の期待値を満たしたいからです。 つまり、理想的な互換性だけを追うのではなく、現実の統合しやすさを優先した結果として、ERC721からは少し外れる判断をしています。

セキュリティ

インターフェースの誤認

ERC7651がERC20またはERC721と誤認されることによるセキュリティ上の影響が議論されています。 例えば、外部のアプリやコントラクトが対象をERC20/ERC721だと思い込んで統合すると、以下のようなズレが起きる可能性があります。

  • イベント名が異なるため、監視・会計・インデックスが正しく追従できない。
  • balanceOf の意味がERC721想定と違うため、NFT個数の判定を誤る。
  • approve や transferFrom が「数量」と「ID」を同じ引数で扱うため、ERC20/ERC721前提の呼び出しで想定外の解釈になる可能性がある。

引用

Acme (@0xacme), Calder (@caldereth), "ERC-7651: Fractionally Represented Non-Fungible Token [DRAFT]," Ethereum Improvement Proposals, no. 7651, March 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7651.

最後に

今回は「」についてまとめてきました! いかがだったでしょうか?

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

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