[ERC7507] NFTに複数のユーザーが制限付きのアクセスができる仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、**ERC721**を拡張して、トークンに複数のユーザーが制限付きのアクセスができるような仕組みを提案している規格である**ERC7507**についてまとめていきます！

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

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

:::note warn
**7507**は現在（2023年10月11日）では「**Draft**」段階です。
:::

# 概要

通常の**ERC721**トークンは、単一の所有者（オーナー）によって保持され、その所有者がそのトークンに対する完全なコントロール権を持っています。
しかし、この新しい規格では、トークンは複数のユーザーによって保持されることができ、それぞれのユーザーにはトークンを利用するための期限が設定されます。

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

https://chaldene.net/erc721

これにより、同じNFT（トークン）が異なるユーザーによって同時に利用されることが可能になります。
例えば、あるNFTの所有権を複数の人々が共有し、それぞれが異なる期間にそのNFTを使用できるようになります。
これは、購読モデルに非常に適しており、複数の人が同じ資産を使いたい場合や、一時的に所有権を譲渡したい場合などに便利です。

:::note info
この拡張規格はNFTの所有と利用に新しい柔軟性をもたらし、複数のユーザーが同じNFTを活用できるようにするものです。
これにより、NFTの新たな応用可能性を開く可能性があります。
:::

# 動機

NFTの一部は知的財産（IP）資産を表しています。
IP資産は、所有権を譲渡せずに他の人にアクセス権を提供する必要がある場合があります。
このような場合、購読モデルが一般的に使用されます。
このモデルでは、複数のユーザーがNFTに購読し、アクセス権を取得できます。
各購読は通常、有効期限を持ち、その期限が記録されます。

一方、既存の**ERC4907**は似たような機能を提供していますが、1つ以上のユーザーに対応しておらず、ユーザーがNFTの独占的な使用権を持つレンタルシナリオに適しています。
このレンタルモデルは、物理的なアセットを表すNFT（例えば、ゲーム内のアイテム）に適していますが、共有可能なIP資産には適していません。

:::note info
**ERC4907**については以下を参考にしてください。
:::

https://qiita.com/cardene/items/253672c18241edcc57eb

したがって、IP資産を表すNFTの場合、複数のユーザーがアクセスできるようにする購読モデルが非常に重要です。
新しい規格は、この要求を満たすために設計されており、共有IP資産のアクセスを効果的に管理できます。
これにより、IP資産のライセンス化とアクセス管理が効率的に行えるようになります。

# 仕様

以下は**IERC7507.sol**で利用可能なSolidityインターフェースです。

```javascript
interface IERC7507 {

    /// @notice Emitted when the expires of a user for an NFT is changed
    event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);

    /// @notice Get the user expires of an NFT
    /// @param tokenId The NFT to get the user expires for
    /// @param user The user to get the expires for
    /// @return The user expires for this NFT
    function userExpires(uint256 tokenId, address user) external view returns(uint256);

    /// @notice Set the user expires of an NFT
    /// @param tokenId The NFT to set the user expires for
    /// @param user The user to set the expires for
    /// @param expires The user could use the NFT before expires in UNIX timestamp
    function setUser(uint256 tokenId, address user, uint64 expires) external;

}
```

# 補足

この規格は、**ERC4907**という規格を補完するもので、複数のユーザーをサポートするために設計されました。
そのため、提案されたインターフェースでは、関数やパラメーターの命名に、既存の**ERC4907**と一貫性を保つことを意識しています。

:::note info
**ERC4907**については以下を参考にしてください。
:::

https://qiita.com/cardene/items/253672c18241edcc57eb

しかし、注目すべき点は、提案されたインターフェースに`usersOf(uint256 tokenId)`という関数が含まれていないことです。
この関数は、複数のユーザーを一覧表示する必要がある場合に使用されるもので、全ての実装において必ずしも必要とはされない機能です。
例えば、オープンな購読のような場合には、この関数は必要ないかもしれません。
そのため、この関数をインターフェースに含めず、実装者にその選択権を委ねることに決定しました。

:::note info
この新しい規格は、**ERC4907**との整合性を保ちながら、複数ユーザーのサポートを提供するものですが、全ての実装においてユーザーの列挙が必要でない場合もあるため、この特定の関数をインターフェースに含めない選択を行ったということです。
:::

# 後方互換性

後方互換性の問題は特にありません。

# テスト

```javascript
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";

const NAME = "NAME";
const SYMBOL = "SYMBOL";
const TOKEN_ID = 1234;
const EXPIRATION = 2000000000;
const YEAR = 31536000;

describe("ERC7507", function () {

  async function deployContractFixture() {
    const [deployer, owner, user1, user2] = await ethers.getSigners();

    const contract = await ethers.deployContract("ERC7507", [NAME, SYMBOL], deployer);
    await contract.mint(owner, TOKEN_ID);

    return { contract, owner, user1, user2 };
  }

  describe("Functions", function () {
    it("Should not set user if not owner or approved", async function () {
      const { contract, user1 } = await loadFixture(deployContractFixture);

      await expect(contract.setUser(TOKEN_ID, user1, EXPIRATION))
        .to.be.revertedWith("ERC7507: caller is not owner or approved");
    });

    it("Should return zero expiration for nonexistent user", async function () {
      const { contract, user1 } = await loadFixture(deployContractFixture);

      expect(await contract.userExpires(TOKEN_ID, user1)).to.equal(0);
    });

    it("Should set users and then update", async function () {
      const { contract, owner, user1, user2 } = await loadFixture(deployContractFixture);

      await contract.connect(owner).setUser(TOKEN_ID, user1, EXPIRATION);
      await contract.connect(owner).setUser(TOKEN_ID, user2, EXPIRATION);

      expect(await contract.userExpires(TOKEN_ID, user1)).to.equal(EXPIRATION);
      expect(await contract.userExpires(TOKEN_ID, user2)).to.equal(EXPIRATION);

      await contract.connect(owner).setUser(TOKEN_ID, user1, EXPIRATION + YEAR);
      await contract.connect(owner).setUser(TOKEN_ID, user2, 0);

      expect(await contract.userExpires(TOKEN_ID, user1)).to.equal(EXPIRATION + YEAR);
      expect(await contract.userExpires(TOKEN_ID, user2)).to.equal(0);
    });
  });

  describe("Events", function () {
    it("Should emit event when set user", async function () {
      const { contract, owner, user1 } = await loadFixture(deployContractFixture);

      await expect(contract.connect(owner).setUser(TOKEN_ID, user1, EXPIRATION))
        .to.emit(contract, "UpdateUser").withArgs(TOKEN_ID, user1.address, EXPIRATION);
    });
  });

});
```

# 参考実装

```javascript
// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

import "./IERC7507.sol";

contract ERC7507 is ERC721, IERC7507 {

    mapping(uint256 => mapping(address => uint64)) private _expires;

    constructor(
        string memory name, string memory symbol
    ) ERC721(name, symbol) {}

    function supportsInterface(
        bytes4 interfaceId
    ) public view virtual override returns (bool) {
        return interfaceId == type(IERC7507).interfaceId || super.supportsInterface(interfaceId);
    }

    function userExpires(
        uint256 tokenId, address user
    ) public view virtual override returns(uint256) {
        require(_exists(tokenId), "ERC7507: query for nonexistent token");
        return _expires[tokenId][user];
    }

    function setUser(
        uint256 tokenId, address user, uint64 expires
    ) public virtual override {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC7507: caller is not owner or approved");
        _expires[tokenId][user] = expires;
        emit UpdateUser(tokenId, user, expires);
    }

}
```

---

### `_expires`

```javascript
mapping(uint256 => mapping(address => uint64)) private _expires;
```

**概要**

トークンごとに異なるユーザーの有効期限を格納するデータ構造。

**詳細**

最初のマッピングはトークンID（`uint256`型）をキーとし、2番目のマッピングはユーザーアドレス（`address`型）をキーとし、それぞれのユーザーに対する有効期限（`uint64`型）を関連付けて格納します。

具体的には、トークンIDとユーザーアドレスの組み合わせを指定することで、そのトークンを所有するユーザーの有効期限を取得できるようになります。
この情報は、購読モデルなどで複数のユーザーが同じトークンを利用する際に、各ユーザーのアクセス権を管理するのに役立ちます。

**パラメータ**

- `uint256 tokenId`
    - トークンの一意の識別子。
- `address userAddress`
    - ユーザーのEthereumアドレス。
- `uint64 expirationTime`
    - ユーザーの有効期限を表すUnixタイムスタンプ。

---

### `supportsInterface`

```javascript
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
    return interfaceId == type(IERC7507).interfaceId || super.supportsInterface(interfaceId);
}
```

**概要**

**ERC165**インターフェースをサポートするための標準の関数で、指定されたインターフェースIDがサポートされているかどうかを確認します。
また、この規格に特有のインターフェースID (`IERC7507`のもの) をチェックします。

**詳細**

与えられた`interfaceId`が`IERC7507`のインターフェースIDと一致するか、親クラスの`supportsInterface`関数を呼び出して、指定されたインターフェースIDがサポートされているかどうかを確認します。
これにより、他のコントラクトやユーザーがこのコントラクトが特定の機能をサポートしているかどうかを調べるのに役立ちます。

**引数**

- `interfaceId`
    - 確認するインターフェースID。

**戻り値**

- `bool`
    - 指定されたインターフェースIDがサポートされている場合は`true`、それ以外の場合は`false`。

---

### `userExpires`

```javascript
function userExpires(uint256 tokenId, address user) public view virtual override returns(uint256) {
    require(_exists(tokenId), "ERC7507: query for nonexistent token");
    return _expires[tokenId][user];
}
```

**概要**

指定されたトークンIDとユーザーアドレスに対する有効期限を取得する関数。

**詳細**

指定されたトークンIDが存在するかどうかを確認し、存在しない場合はエラーメッセージを表示します。
その後、指定されたトークンIDとユーザーアドレスに対する有効期限を`_expires`変数から取得して返します。
この情報は、ユーザーが特定のトークンのアクセス期間を知りたい場合に使用されます。

**引数**

- `tokenId`
    - 有効期限を取得したいトークンのID。
- `user`
    - 有効期限を取得したいユーザーのEthereumアドレス。

**戻り値**

- `uint256`
    - 指定されたトークンIDとユーザーアドレスに対する有効期限（Unixタイムスタンプ）。

---

### `setUser`

```javascript
function setUser(uint256 tokenId, address user, uint64 expires) public virtual override {
    require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC7507: caller is not owner or approved");
    _expires[tokenId][user] = expires;
    emit UpdateUser(tokenId, user, expires);
}
```

**概要**

指定されたトークンIDとユーザーアドレスに対する有効期限を設定する関数。

**詳細**

呼び出し元が指定されたトークンの所有者または承認済みユーザーであることを確認します。
確認が成功すると、指定されたトークンIDとユーザーアドレスに対する有効期限を`_expires`変数に設定します。
そして、`UpdateUser`イベントを発行して、この変更を通知します。

**引数**

- `tokenId`
    - 有効期限を設定したいトークンのID。
- `user`
    - 有効期限を設定したいユーザーのEthereumアドレス。
- `expires`
    - 設定する有効期限を表すUnixタイムスタンプ。

**戻り値**

この関数は戻り値を持ちません。

---

# セキュリティ考慮事項

特になし。

# 引用

Ming Jiang (@minkyn), Zheng Han (@hanbsd), Fan Yang (@fayang), "ERC-7507: Multi-User NFT Extension [DRAFT]," Ethereum Improvement Proposals, no. 7507, August 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7507.

# 最後に

今回は「**ERC721**を拡張して、トークンに複数のユーザーが制限付きのアクセスができるような仕組みを提案している規格である**ERC7507**」についてまとめてきました！
いかがだったでしょうか？

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

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

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

https://chaldene.net/

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58