[ERC7628] ERC721のNFTの所有権を分割する仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、**ERC721**形式のNFTの各トークンに所有権シェアを導入し、部分所有の仕組みを可能にする提案している**ERC7628**についてまとめていきます！

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

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

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

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

# 概要

この規格では、NFTの各トークンに「**シェア**」と呼ばれる所有権示す値を付与して、NFTの部分所有の仕組みを提案しています。
この**シェア**の取得、譲渡（`transfer`）、他のアドレスへの操作権限の付与（`approve`）の機能を実装し、さまざまなユースケースへの適用を可能にします。

# 動機

NFTの各トークンに対して配当金を分配したり権利を付与したい場合、NFTの所有権割合のデータを管理する必要があります。
**ERC1155**では、各アドレスが保有する特定の`tokenId`の量で所有権割合を表すことができますが、一意性は失われます。

:::note info
**ERC1155**については以下の記事を参考にしてください。
:::

https://chaldene.net/erc1155

逆に、**ERC721**ではトークンの一意性は保たれますが、所有権の割合のデータは管理しておらず、メタデータに所有権のシェアを渡す仕組みがありません。

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

https://chaldene.net/erc721

:::note info
ここで述べられている「**一意性**」とは、`tokenId`のことを指しています。
**ERC721**形式のNFTでは、`tokenId`に紐づくNFTは必ず1つです。
一方、**ERC1155**形式のNFTでは、`tokenId`に紐づくNFTは複数の可能性があります。
これにより、**ERC1155**形式のNFTでは「**一意性**」が失われていると述べています。
:::

この規格では、**ERC1155**と**ERC721**の機能を組み合わせて、NFTの各トークンに所有権を示す値である「**シェア**」というデータを追加して譲渡などができるようにします。
各トークンは`tokenId`という値でユニーク性を担保しているため、このトークンごとに**シェア**を発行することで所有権のみを分割することを実現しています。

# 仕様

この規格に準拠するコントラクトは以下の機能を実装する必要があります。

```js
pragma solidity ^0.8.0;

interface IERC7628 /* is IERC721 */ {
    /// @notice Returns the number of decimal places used for ownership shares.
    /// @return The number of decimal places for ownership shares.
    function shareDecimals() external view returns (uint8);

    /// @notice Returns the total sum of ownership shares in existence for all tokens.
    /// @return The total sum of ownership shares.
    function totalShares() external view returns (uint256);

    /// @notice Returns the ownership share of the specified token.
    /// @param tokenId The identifier of the token.
    /// @return The ownership share of the token.
    function shareOf(uint256 tokenId) external view returns (uint256);

    /// @notice Returns the share allowance granted to the specified spender by the owner for the specified token.
    /// @param tokenId The identifier of the token.
    /// @param spender The address of the spender.
    /// @return The share allowance granted to the spender.
    function shareAllowance(uint256 tokenId, address spender) external view returns (uint256);

    /// @notice Approves the specified address to spend a specified amount of shares on behalf of the caller.
    /// @param tokenId The identifier of the token.
    /// @param spender The address of the spender.
    /// @param shares The amount of shares to approve.
    function approveShare(uint256 tokenId, address spender, uint256 shares) external;

    /// @notice Transfers ownership shares from one token to another.
    /// @param fromTokenId The identifier of the sender token.
    /// @param toTokenId The identifier of the recipient token.
    /// @param shares The amount of shares to transfer.
    function transferShares(uint256 fromTokenId, uint256 toTokenId, uint256 shares) external;

    /// @notice Transfers ownership shares from one token to another address (resulting in a new token or increased shares at the recipient address).
    /// @param fromTokenId The identifier of the sender token.
    /// @param to The address of the recipient.
    /// @param shares The amount of shares to transfer.
    function transferSharesToAddress(uint256 fromTokenId, address to, uint256 shares) external; 

    /// @notice Adds a specified amount of shares to a token, only callable by the contract owner.
    /// @param tokenId The identifier of the token.
    /// @param shares The amount of shares to add.
    function addSharesToToken(uint256 tokenId, uint256 shares) external;

    /// @notice Emitted when ownership shares are transferred from one token to another.
    /// @param fromTokenId The identifier of the sender token.
    /// @param toTokenId The identifier of the recipient token.
    /// @param amount The amount of shares transferred.
    event SharesTransfered(uint256 indexed fromTokenId, uint256 indexed toTokenId, uint256 amount);

    /// @notice Emitted when an approval is granted for a spender to spend shares on behalf of an owner.
    /// @param tokenId The token identifier.
    /// @param spender The address of the spender.
    /// @param amount The amount of shares approved.
    event SharesApproved(uint256 indexed tokenId, address indexed spender, uint256 amount);
}
```

### `shareDecimals`

```javascript
function shareDecimals() external view returns (uint8);
```

**概要**

所有権シェアの小数点以下の桁数を返す関数。

---

### `totalShares`

```javascript
function totalShares() external view returns (uint256);
```

**概要**

全てのトークンに対する所有権シェアの合計を返す関数。

**詳細**

この関数は、全てのトークンに対する所有権シェアの合計を返します。
これにより、コントラクト内の総所有権シェアを把握できます。

---

### `shareOf`

```javascript
function shareOf(uint256 tokenId) external view returns (uint256);
```

**概要**

指定されたトークンの所有権シェアを返す関数。

**引数**

- `tokenId`
    - トークンの識別子。

---

### `shareAllowance`

```javascript
function shareAllowance(uint256 tokenId, address spender) external view returns (uint256);
```

**概要**

指定されたトークンに対して所有者が特定のアドレスに許可したシェア数を返す関数。

**詳細**

特定のトークン所有者が特定のアドレスに対してどれだけのシェアを操作する権限を与えたかを返します。
権限を付与されたアドレスはこのシェア数まで操作が可能です。

**引数**

- `tokenId`
    - トークンの識別子。
- `spender`
    - 支出者のアドレス。

**戻り値**

- `uint256`
    - 許可されたシェア数。

---

### `approveShare`

```javascript
function approveShare(uint256 tokenId, address spender, uint256 shares) external;
```

**概要**

指定されたアドレスに対して、NFT保有者が指定したシェア数を使うことを許可する関数。

**詳細**

トークンの所有者が特定のアドレスに対して特定のシェア数を操作する権限を与えるために使用します。
これにより、第三者が指定されたシェアを操作できます。

**引数**

- `tokenId`
    - トークンの識別子。
- `spender`
    - 支出者のアドレス。
- `shares`
    - 許可するシェア数。

---

### `transferShares`

```javascript
function transferShares(uint256 fromTokenId, uint256 toTokenId, uint256 shares) external;
```

**概要**

所有権シェアをあるトークンから別のトークンに`transfer`する関数。

**詳細**

指定された数のシェアをあるトークンから別のトークンに`transfer`します。
これにより、トークン間でシェアを再配分することができます。

**引数**

- `fromTokenId`
    - 送信元のトークン識別子。
- `toTokenId`
    - 受け取りアドレスのトークン識別子。
- `shares`
    - `transfer`するシェア数。

---

### `transferSharesToAddress`

```javascript
function transferSharesToAddress(uint256 fromTokenId, address to, uint256 shares) external;
```

**概要**

所有権シェアをあるトークンから他のアドレスに`transfer`する関数。

**詳細**

指定された数のシェアをあるトークンから他のアドレスに`transfer`します。
これにより、新しいトークンの発行や受け取りアドレスのシェアの増加が実現されます。

**引数**

- `fromTokenId`
    - 送信元のトークン識別子。
- `to`
    - 受信者のアドレス。
- `shares`
    - `transfer`するシェア数。

---

### `addSharesToToken`

```javascript
function addSharesToToken(uint256 tokenId, uint256 shares) external;
```

**概要**

指定されたトークンにシェアを追加する関数。

**詳細**

この操作はコントラクトの`owner`によってのみ実行可能です。

**引数**

- `tokenId`
    - トークンの識別子。
- `shares`
    - 追加するシェア数。

---

### `SharesTransfered`

```javascript
event SharesTransfered(uint256 indexed fromTokenId, uint256 indexed toTokenId, uint256 amount);
```

**概要**

所有権シェアがあるトークンから別のトークンに`transfer`されたときに発行されるイベント。

**引数**

- `fromTokenId`
    - 送信元のトークン識別子。
- `toTokenId`
    - 受け取りアドレスのトークン識別子。
- `amount`
    - `transfer`されたシェア数。

---

### `SharesApproved`

```javascript
event SharesApproved(uint256 indexed tokenId, address indexed spender, uint256 amount);
```

**概要**

NFT保有者が特定のアドレスにシェアを使う権限を与えたときに発行されるイベント。

**引数**

- `tokenId`
    - トークンの識別子。
- `spender`
    - 操作権限を付与されるアドレス。
- `amount`
    - 許可されたシェア数。

---

# 補足

## 追加シェアの発行

追加のシェアをNFTに対して発行することで、所有権管理に柔軟性を持たせることができます。
これにより、利益分配や投資などのシナリオに対応しやすくなります。

## アドレスへのシェア送付

シェアを他の`tokenId`に送るのではなく、アドレスに送ることでNFTの部分所有が可能になります。
これにより、部分的なNFTの売却や担保かが可能になります。

# 互換性

この規格は**ERC721**と完全に互換性があります。

# 参考実装

```js
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract ERC7628 is IERC7628, ERC721, Ownable, ReentrancyGuard {
    mapping(uint256 => uint256) private _shareBalances;
    mapping(uint256 => mapping(address => uint256)) private _shareAllowances;
    uint256 private _totalShares;
    uint256 private _nextTokenId;

    constructor(address initialOwner)
        ERC721("MyToken", "MTK")
        Ownable(initialOwner)
    {}

    function addSharesToToken(uint256 tokenId, uint256 shares) public override onlyOwner {
        require(tokenId > 0, "ERC7628: tokenId cannot be zero");
        _shareBalances[tokenId] += shares;
        _totalShares += shares;
        emit SharesTransfered(0, tokenId, shares);
    }

    function shareDecimals() external pure override returns (uint8) {
        return 18;
    }

    function totalShares() external view override returns (uint256) {
        return _totalShares;
    }

    function shareOf(uint256 tokenId) external view override returns (uint256) {
        return _shareBalances[tokenId];
    }

    function shareAllowance(uint256 tokenId, address spender) external view override returns (uint256) {
        return _shareAllowances[tokenId][spender];
    }

    function approveShare(uint256 tokenId, address spender, uint256 shares) external override {
        require(spender != ownerOf(tokenId), "ERC7628: approval to current owner");
        require(msg.sender == ownerOf(tokenId), "ERC7628: approve caller is not owner");

        _shareAllowances[tokenId][spender] = shares;
        emit SharesApproved(tokenId, spender, shares);
    }

    function transferShares(uint256 fromTokenId, uint256 toTokenId, uint256 shares) external override nonReentrant {
        require(_shareBalances[fromTokenId] >= shares, "ERC7628: insufficient shares for transfer");
        require(_isApprovedOrOwner(msg.sender, fromTokenId), "ERC7628: transfer caller is not owner nor approved");

        _shareBalances[fromTokenId] -= shares;
        _shareBalances[toTokenId] += shares;
        emit SharesTransfered(fromTokenId, toTokenId, shares);
    }

    function transferSharesToAddress(uint256 fromTokenId, address to, uint256 shares) external override nonReentrant {
        require(_shareBalances[fromTokenId] >= shares, "ERC7628: insufficient shares for transfer");
        require(_isApprovedOrOwner(msg.sender, fromTokenId), "ERC7628: transfer caller is not owner nor approved");

        _nextTokenId++;
        _safeMint(to, _nextTokenId);
        _shareBalances[_nextTokenId] = shares;
        emit SharesTransfered(fromTokenId, _nextTokenId, shares);
    }

    // Helper function to check if an address is the owner or approved
    function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
        return (spender == ownerOf(tokenId) || getApproved(tokenId) == spender || isApprovedForAll(ownerOf(tokenId), spender));
    }
}
```

# セキュリティ

## `transfer`時の`approve`リセット

NFTの所有権がかわるとき（`transfer`時）、現在設定されている`approve`をクリアする必要があります。

:::note info
そうしないと、`transfer`されたアドレスが許可した覚えのないアドレスによってシェアがコントロールされてしまうためです。
:::

## リエントランシー攻撃の防止

リエントランシー攻撃によって、所有権をやシェア数の想定しない`transfer`や許可をふせぐ必要があります。

:::note info
リエントランシー攻撃については以下の記事を参考にしてください。
:::

https://chaldene.net/solidity-reentrancy-attack

## `tokenId`とアドレスの検証

各操作において、`tokenId`とアドレスの正当性の検証が必要です。
これにより、不正な`tokenId`やアドレスが利用されることを防ぎます。

## 所有権変更時のシェア管理

所有権が変わったとき、シェアの量を適切に管理する必要があります。
シェアを正確に計算して管理することで、所有権の一貫性を保つことができます。

# 引用

Chen Liaoyuan (@chenly) <cly@kip.pro>, "ERC-7628: ERC-721 Ownership Shares Extension [DRAFT]," Ethereum Improvement Proposals, no. 7628, February 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7628.

# 最後に

今回は「**ERC721**形式のNFTに所有権シェアを導入し、部分所有権の仕組みを可能にする提案している**ERC7628**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下の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