[ERC4675] NFTをトークンIDごとに分割所有することができる仕組みを理解しよう！

# はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、NFTの`tokenId`ごとに分割所有できる仕組みを提案している**ERC4675**についてまとめていきます！

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

https://eips.ethereum.org/EIPS/eip-4675

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

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

# 概要

**ERC4675**は、1つのスマートコントラクトで複数のNFTを分割管理できるようにするインターフェースを提案しています。
従来は、**ERC1633**のような既存の仕組みを使ってNFTを分割する場合、**ERC20**互換の新たなトークンコントラクトを別途デプロイする必要がありました。
しかし**ERC4675**では、各NFTのtoken IDごとに異なる分割トークン（フラクショントークン）を定義でき、同じコントラクト内でそれらを表現・管理することが可能です。

**ERC4675**の仕組みは一見**ERC1155**と似ていますが、決定的な違いがあります。
**ERC1155**では1つのIDが同一アイテムの複数トークンを表すのに対し、**ERC4675**では各IDが完全に独立したNFTを表してそれぞれを個別に分割可能にする点が異なります。

# 動機

現在主流のNFT分割方法では、NFTを分割してFTにするたびに、新たなFT用のコントラクトをデプロイする必要があります。
これにより、Ethereumブロックチェーン上でのバイトコードの無駄遣いや、各FTがそれぞれ独立したコントラクトに属することによる機能制限が問題となっていました。

NFTプロジェクトの増加に伴い、効率的かつ柔軟にNFTを分割・管理できる仕組みが求められています。
**ERC4675**は、1つのコントラクト内で多様なNFTの分割管理を可能にし、既存の非効率的な構造に代わる新たなトークン標準として提案されています。

# 仕様

```js:IMFNFT.sol
/**
    @title Multi-Fractional Non-Fungible Token Standard
    @dev Note : The ERC-165 identifier for this interface is 0x83f5d35f.
*/
interface IMFNFT {
    /**
        @dev This emits when ownership of any token changes by any mechanism.
        The `_from` argument MUST be the address of an account/contract sending the token.
        The `_to` argument MUST be the address of an account/contract receiving the token.
        The `_id` argument MUST be the token type being transferred. (represents NFT)
        The `_value` argument MUST be the number of tokens the holder balance is decrease by and match the recipient balance is increased by.
    */
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _id, uint256 _value);

    /**
        @dev This emits when the approved address for token is changed or reaffirmed.
        The `_owner` argument MUST be the address of account/contract approving to withdraw.
        The `_spender` argument MUST be the address of account/contract approved to withdraw from the `_owner` balance.
        The `_id` argument MUST be the token type being transferred. (represents NFT)
        The `_value` argument MUST be the number of tokens the `_approved` is able to withdraw from `_owner` balance.
    */
    event Approval(address indexed _owner, address indexed _spender, uint256 indexed _id, uint256 _value);

    /**
        @dev This emits when new token type is added which represents the share of the Non-Fungible Token.
        The `_parentToken` argument MUST be the address of the Non-Fungible Token contract.
        The `_parentTokenId` argument MUST be the token ID of the Non-Fungible Token.
        The `_id` argument MUST be the token type being added. (represents NFT)
        The `_totalSupply` argument MUST be the number of total token supply of the token type.
    */
    event TokenAddition(address indexed _parentToken, uint256 indexed _parentTokenId, uint256 _id, uint256 _totalSupply);

    /**
        @notice Transfers `_value` amount of an `_id` from the msg.sender address to the `_to` address specified
        @dev msg.sender must have sufficient balance to handle the tokens being transferred out of the account.
        MUST revert if `_to` is the zero address.
        MUST revert if balance of msg.sender for token `_id` is lower than the `_value` being transferred.
        MUST revert on any other error.
        MUST emit the `Transfer` event to reflect the balance change.
        @param _to      Source address
        @param _id      ID of the token type
        @param _value   Transfer amount
        @return         True if transfer was successful, false if not
    */
    function transfer(address _to, uint256 _id, uint256 _value) external returns (bool);

    /**
        @notice Approves `_value` amount of an `_id` from the msg.sender to the `_spender` address specified.
        @dev msg.sender must have sufficient balance to handle the tokens when the `_spender` wants to transfer the token on behalf.
        MUST revert if `_spender` is the zero address.
        MUST revert on any other error.
        MUST emit the `Approval` event.
        @param _spender Spender address(account/contract which can withdraw token on behalf of msg.sender)
        @param _id      ID of the token type
        @param _value   Approval amount
        @return         True if approval was successful, false if not
    */
    function approve(address _spender, uint256 _id, uint256 _value) external returns (bool);

    /**
        @notice Transfers `_value` amount of an `_id` from the `_from` address to the `_to` address specified.
        @dev Caller must be approved to manage the tokens being transferred out of the `_from` account.
        MUST revert if `_to` is the zero address.
        MUST revert if balance of holder for token `_id` is lower than the `_value` sent.
        MUST revert on any other error.
        MUST emit `Transfer` event to reflect the balance change.
        @param _from    Source address
        @param _to      Target Address
        @param _id      ID of the token type
        @param _value   Transfer amount
        @return         True if transfer was successful, false if not

    */
    function transferFrom(address _from, address _to, uint256 _id, uint256 _value) external returns (bool);

    /**
        @notice Sets the NFT as a new type token
        @dev The contract itself should verify if the ownership of NFT is belongs to this contract itself with the `_parentNFTContractAddress` & `_parentNFTTokenId` before adding the token.
        MUST revert if the same NFT is already registered.
        MUST revert if `_parentNFTContractAddress` is address zero.
        MUST revert if `_parentNFTContractAddress` is not ERC-721 compatible.
        MUST revert if this contract itself is not the owner of the NFT.
        MUST revert on any other error.
        MUST emit `TokenAddition` event to reflect the token type addition.
        @param _parentNFTContractAddress    NFT contract address
        @param _parentNFTTokenId            NFT tokenID
        @param _totalSupply                 Total token supply
    */
    function setParentNFT(address _parentNFTContractAddress, uint256 _parentNFTTokenId, uint256 _totalSupply) external;

    /**
        @notice Get the token ID's total token supply.
        @param _id      ID of the token
        @return         The total token supply of the specified token type
    */
    function totalSupply(uint256 _id) external view returns (uint256);

    /**
        @notice Get the balance of an account's tokens.
        @param _owner  The address of the token holder
        @param _id     ID of the token
        @return        The _owner's balance of the token type requested
    */
    function balanceOf(address _owner, uint256 _id) external view returns (uint256);

    /**
        @notice Get the amount which `_spender` is still allowed to withdraw from `_owner`
        @param _owner   The address of the token holder
        @param _spender The address approved to withdraw token on behalf of `_owner`
        @param _id      ID of the token
        @return         The amount which `_spender` is still allowed to withdraw from `_owner`
    */
    function allowance(address _owner, address _spender, uint256 _id) external view returns (uint256);

    /**
        @notice Get the bool value which represents whether the NFT is already registered and fractionalized by this contract.
        @param _parentNFTContractAddress    NFT contract address
        @param _parentNFTTokenId            NFT tokenID
        @return                             The bool value representing the whether the NFT is already registered.
    */
    function isRegistered(address _parentNFTContractAddress, uint256 _parentNFTTokenId) external view returns (bool);
}

interface ERC165 {
    /**
        @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);
}
```

### `Transfer`

```js
event Transfer(address indexed _from, address indexed _to, uint256 indexed _id, uint256 _value);
```

トークンの所有者が変更された際に発行されるイベント。
NFTの分割トークンが移転されたとき、送信アドレスと受け取りアドレス、対象トークンIDとトークン量に関する情報をイベントに含めます

**パラメータ**  
- `_from`  
    - トークンの送付元アドレス。  
- `_to`  
    - トークンの受け取りアドレス。  
- `_id`  
  - 対象となるトークンタイプ（NFTに紐づくID）。  
- `_value`  
  - 移転されるトークンの数量。  

### `Approval`

```js
event Approval(address indexed _owner, address indexed _spender, uint256 indexed _id, uint256 _value);
```

特定のアドレスの代わりに送付できるよう承認された時に発行されるイベント。
`_spender`が`_owner`の代わりにNFT分割トークンを送付できるようになったことを示します。

**パラメータ**  
- `_owner`  
    - トークンの所有者アドレス。  
- `_spender`  
    - 承認された送付アドレス。  
- `_id`  
    - トークンタイプのID。  
- `_value`  
    - 許可する送付トークン数量。  

### `TokenAddition`

```js
event TokenAddition(address indexed _parentToken, uint256 indexed _parentTokenId, uint256 _id, uint256 _totalSupply);
```

新しいNFTを分割トークンとして登録した時に発行されるイベント。

既存の**ERC721**トークンを分割管理可能なトークンIDとして登録する時に発行され、NFTの情報とその分割トークンの供給量が記録されます。

**パラメータ**  
- `_parentToken`  
    - 元となるNFTコントラクトアドレス。  
- `_parentTokenId`  
    - 元NFTのトークンID。  
- `_id`  
    - 登録される分割トークンタイプのID。  
- `_totalSupply`  
    - 発行される分割トークンの総数。  

### `transfer`

```js
function transfer(address _to, uint256 _id, uint256 _value) external returns (bool);
```

呼び出し元のアカウントから指定先アドレスに分割トークンを送付する関数。
送付アドレスが保有しているNFT分割トークンのうち、指定された量を指定アドレスに送付します。
不正な送付（残高不足やゼロアドレスに送付など）の場合は処理を中断します。

**引数**  
- `_to`  
    - トークンの送付先アドレス。  
- `_id`  
    - トークンタイプのID。  
- `_value`  
    - 送付するトークンの量。  

**戻り値**  
- `bool`  
    - 成功した場合は`true`、失敗時は`false`。  

### `approve`

```js
function approve(address _spender, uint256 _id, uint256 _value) external returns (bool);
```

指定されたアドレスに対して、指定トークンを送付する許可を行う関数。
呼び出し元が所有するNFT分割トークンを、指定されたアドレスが代わりに送付できるようにします。
ゼロアドレスへの承認は拒否されます。

**引数**  
- `_spender`  
    - 承認されたアドレス。  
- `_id`  
    - トークンタイプのID。  
- `_value`  
    - 承認するトークンの量。  

**戻り値**  
- `bool`  
    - 承認が成功した場合は`true`。  

### `transferFrom`

```js
function transferFrom(address _from, address _to, uint256 _id, uint256 _value) external returns (bool);
```

承認されたアドレスが、指定された送付元からトークンを指定アドレスに送付する関数。
`_from`アドレスから`_to`アドレスへ、指定されたトークンIDと数量を送付します。
`approve`されている必要があります。

**引数**  
- `_from`  
    - トークンの送信元アドレス。  
- `_to`  
    - トークンの受信先アドレス。  
- `_id`  
    - トークンタイプのID。  
- `_value`  
    - 送付するトークンの数量。  

**戻り値**  
- `bool`  
    - 送付が成功した場合は`true`。  

### `setParentNFT`

```js
function setParentNFT(address _parentNFTContractAddress, uint256 _parentNFTTokenId, uint256 _totalSupply) external;
```

既存のNFTを新しい分割トークンとして登録する関数。
**ERC721**トークンを分割管理するために、このコントラクト自身が保有するNFTをトークンIDとして登録して分割数を設定します。

**引数**  
- `_parentNFTContractAddress`  
    - 元となるNFTのコントラクトアドレス。  
- `_parentNFTTokenId`  
    - 元NFTのトークンID。  
- `_totalSupply`  
    - 分割トークンの総供給量。  

### `totalSupply`

```js
function totalSupply(uint256 _id) external view returns (uint256);
```

指定されたトークンIDの総供給量を取得する関数。
各NFTに対応する分割トークンの供給総数を返します。

**引数**  
- `_id`  
    - 対象のトークンタイプID。  

**戻り値**  
- `uint256`  
    - 該当トークンの総供給量。  

### `balanceOf`

```js
function balanceOf(address _owner, uint256 _id) external view returns (uint256);
```

特定のアカウントが保有しているトークン量を取得する関数。
NFT分割トークンにおける所有者の残高を返します。

**引数**  
- `_owner`  
    - アカウントアドレス。  
- `_id`  
    - トークンID。  

**戻り値**  
- `uint256`  
    - 指定されたアカウントの残高。  

### `allowance`

```js
function allowance(address _owner, address _spender, uint256 _id) external view returns (uint256);
```

`approve`されているトークン数量を取得する関数。
`_spender`が`_owner`からどれだけのトークンを代わりに送付できるかを返します。

**引数**  
- `_owner`  
    - トークン保有者アドレス。  
- `_spender`  
    - 承認を受けたアドレス。  
- `_id`  
    - トークンID。  

**戻り値**  
- `uint256`  
    - 残りの承認済みトークン量。  

### `isRegistered`

```js
function isRegistered(address _parentNFTContractAddress, uint256 _parentNFTTokenId) external view returns (bool);
```

指定されたNFTが分割トークンとしてすでに登録済みか確認する関数。
このコントラクトが管理しているNFTであるかどうかを確認するために使用します。

**引数**  
- `_parentNFTContractAddress`  
    - NFTコントラクトアドレス。  
- `_parentNFTTokenId`  
    - トークンID。  

**戻り値**  
- `bool`  
    - 登録済みなら`true`、未登録なら`false`。  

### `onERC721Received`

```js
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external pure returns (bytes4);
```

ERC721の`safeTransferFrom`によってNFTを受け取ったときに呼ばれる関数。
このコントラクトが**ERC721**を安全に受け取るために実装が必要な関数です。
返り値として固定の関数識別子（`0x150b7a02`）を返します。

**引数**  
- `_operator`  
    - `safeTransferFrom`を呼び出したアドレス。  
- `_from`  
    - NFTの元所有者アドレス。  
- `_tokenId`  
    - 受け取ったNFTのトークンID。  
- `_data`  
    - 追加データ。  

**戻り値**  
- `bytes4`  
    - `onERC721Received`の関数識別子。  

# 補足

**ERC4675**では、`symbol` や `name` のような関数は含まれていません。
理由としては、これらの情報は元のNFTコントラクトから直接取得できるためです。
毎回これらの値をコピーしてコントラクト内に保持すると、Ethereumブロックチェーン上に不要なバイトコードを蓄積することにつながるため、効率性の観点から省略されています。
ただし、必要に応じて元NFTからメタデータを取得して各トークンタイプに組み込むことは可能です。

**ERC4675**の設計は、プロジェクトごとの多様なトークン構造やアーキテクチャに対応できる柔軟性を重視して決定されています。
そのため、`mint`、`burn`、ガバナンスなどの機能に関しては、最低限の共通仕様のみを定めて実装者が自由に拡張できるようになっています。

# 互換性

**ERC4675**は、既存の標準と互換性を持たせるために、関数名やイベント名を＊＊＊＊のインターフェースに合わせています。
また、`_id`を使ってトークンタイプを識別する構造は**ERC1155**に類似しています。

**ERC4675**は**ERC721**との連携を前提としているため、拡張機能には依存せずに標準仕様のみに基づいた設計となっています。
これにより、特定のプロジェクトが自分たちのトークンの用途やシナリオに合わせて柔軟に設計できるようになっています。

# セキュリティ

すでにミントされたNFTを分割（フラクショナライズ）する場合、そのNFTの所有権を事前にこのコントラクトに移転しておく必要があります。
NFTの所有権がない状態で分割処理を行うと、NFTとは無関係なトークンが発行されるリスクがあります。
そのため、`setParentNFT` 関数では、NFTの所有者がこのコントラクトであることを厳密に検証する必要があります。

また、`setParentNFT` を誰でも実行可能にすると、NFT所有者とは無関係な第三者が先回り（フロントラン）してNFTを登録する可能性があります。
このような問題を避けるために、次のような対策が推奨されています。

- 関数実行アドレスを管理者（`admin`）のみに制限する。
- NFTの送信と登録を1つのアトミックトランザクションとして処理する（例：フラッシュローンやスワップのような形式）。

これにより、NFTの分割における正当性と安全性を確保することができます。

# 引用

David Kim (@powerstream3604), "ERC-4675: Multi-Fractional Non-Fungible Tokens [DRAFT]," Ethereum Improvement Proposals, no. 4675, January 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4675.

# 最後に

今回は「NFTの`tokenId`ごとに分割所有できる仕組みを提案している**ERC4675**」についてまとめてきました！
いかがだったでしょうか？

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

[Twitter @cardene777](https://twitter.com/cardene777)

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

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4