[ERC4907] NFTを使用できる期限付きのロールを付与する仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、**ERC721**規格のNFTに期限付きののロールを付与する**ERC4907**についてまとめていきます！

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

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

# 概要
この標準は、**EIP721**の拡張機能です。
新たに「`user`」ロールが提案され、このロールがアドレスに付与される期間が自動的に取り消される（`expires`）仕組みが提案されています。

![user.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/9362e5b4-35cf-2055-3e2b-b7cef88718d1.png)

`user`ロールはNFTの「**使用**」権限を表し、NFTの譲渡や`user`ロールを別のアドレスに付与したり、何かしらの権限を他のアドレスに付与できません。

![userNg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/788fa670-41b3-f69a-d61f-46515053dd2e.png)


# 動機
一部のNFTには特定のユーティリティがあります。
例えば、メタバースはNFTを「**使用**」してシーンを構築したり、ゲームアセットを表すNFTはゲーム内で「**使用**」されます。
場合によっては、NFTの所有者と実際に使用する「**ユーザー**」が一致しないこともあります。
NFTの所有者が自身が保有するNFTを「**ユーザー**」に貸し出す場合が考えられます。
こうした場合、アドレスが「**所有者**」を表すのか「**ユーザー**」を表すのかを区別する別々のロールがあり、それに応じてアクションを管理することが合理的です（例えば、NFTを使用する「**ユーザー**」は、NFTの所有権を売却する権限を持つべきではありません）。
このような状況においては、「`operator`」や「`controller`」として異なる名前でこのような設計がすでにいくつかのプロジェクトで使用されていますが、より広く一般的に使用されるために、すべてのアプリケーション間で使用できるような統一された標準が必要です。

![role.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/026667de-238d-2384-1fc5-5cf046f385ea.png)

さらに、このモデルのアプリケーション（例：**レンタル**）では、ユーザーアドレスがNFTの使用に対して一時的なアクセスしか持たないことがよくあります。
通常、これは所有者が2つのオンチェーントランザクションを送信する必要があります。

![transaction.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/de774470-e593-c06a-0bf2-bd623f0f0869.png)

1つは利用期間の開始時に新しいアドレスを`user`ロールとして登録し、もう1つは利用期間の終了時に`user`ロールを回収するためのものです。
しかしこの方法は作業とガス代の面で非効率的です。
そのため、「`expires`」関数を導入することで、第二のトランザクションなしに利用期間が自動的に終了する仕組みが提供されます。

![auto-expier.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/c1ddf24a-ea4e-3a74-d257-ba818dabad1e.png)

# 仕様

## インターフェース

```javascript
interface IERC4907 {
    event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);

    function setUser(uint256 tokenId, address user, uint64 expires) external;

    function userOf(uint256 tokenId) external view returns(address);

    function userExpires(uint256 tokenId) external view returns(uint256);
}
```

### UpdateUser

NFTの「`user`」の使用期限が変更された時に発行されるイベント。

**引数**

- `tokenId`
    - NFTの識別子（トークンID）。
- `user`
    - NFTを「**使用する**」アドレス。
    - `0`アドレスの場合、「**使用する**」アドレスが存在しないことを示す。
- `expires`
    - UNIXタイムスタンプ形式で表される「`user`」の使用期限。
    - この期限が過ぎると、ユーザーの権限が自動的に終了します。

### setUser

NFTの「`user`」の使用期限を設定する関数。

**引数**

- `tokenId`
    - `user`情報を更新するNFTのトークンID。
- `user`
    - NFTを「**使用する**」アドレス。
    - `0`アドレスの場合、「**使用する**」アドレスが存在しないことを示す。
- `expires`
    - UNIXタイムスタンプ形式で表される「`user`」の使用期限。
    - この期限が過ぎると、ユーザーの権限が自動的に終了します。

### userOf

特定のNFTの「`user`」のアドレスを取得する関数。

**引数**

- tokenId`
    - `user`情報を取得するNFTのトークンID。

**戻り値**
- NFTの「`user`」のアドレス。
- `0`アドレスの場合、「**使用する**」アドレスが存在しないことを示す。

### userExpires

特定のNFTの「`user`」の使用期限を取得する関数。

- `tokenId`
    - 使用期限を取得するNFTのトークンID。

**戻り値**
- UNIXタイムスタンプ形式で表されるNFTの「`user`」の使用期限。
- `0`の値は使用期限が設定されていないことを示す。


### supportsInterface

**ERC165**のインターフェースをサポートしているかを確認するための関数。
引数として `0xad092b5c`（**IERC4907**インターフェースのコンパクトID）を渡した場合にのみ`true`を返します。
これにより、他のコントラクトが対応しているインターフェースを簡単に特定できます。

# 補足

## ロールの割り当て
「`owner`」と「`user`」という2つのロールを持つことで、貸し手と借り手がNFTに対して何ができるか、できないかの権限を管理することが容易になります。
さらに、所有者はどのアドレスが`user`であるかを制御でき、他のプロジェクトも独自の権限を`owner`または`user`に割り当てることが容易になります。

## シンプルなオンチェーンの時間管理
レンタル期間が終了した場合、「`user`」ロールをリセットし、「`user`」がNFTの使用権限を失う必要があります。
通常、これは2回目のオンチェーントランザクションで行われますが、これはガスの効率が悪く手間がかかります。
しかし、`expires`関数を使用すると、2回目のトランザクションなしにレンタル期間が終了すると自動的に「`user`」の権限が無効になります。

## 簡単なサードパーティの統合
パーミッションなしの相互運用性の思想に則り、この標準はサードパーティのプロトコルがNFTの使用権限を管理することを容易にします。
プロジェクトが追加の`user`ロールと期限を採用した後、他のプロジェクトは直接これらの機能と交互作用し、独自のトランザクションタイプを実装することができます。
例えば、この標準を使用している**PFP**（Profile Picture）NFTは、ユーザーがNFTを30日間レンタルできるレンタルプラットフォームと、同時にユーザーがNFTを使用し、最終的に分割払いでNFTの所有権を購入できる抵当プラットフォームの両方に統合されることができます。
これはすべて、元のPFPプロジェクトの許可を得る必要がなく行われます。

# 互換性
この標準は、拡張関数を追加することで完全に**EIP721**と互換性があります。
さらに、この標準で導入された新しい関数は、既存の**EIP721**の関数と多くの類似点があります。
これにより、開発者は標準を簡単に迅速に実装できます。

# コード

以下にコントラクトの実装を格納しています。

https://github.com/cardene777/base-code/tree/develop/contract/contracts/ERC4907

```javascript
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;

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

contract ERC4907 is ERC721, IERC4907 {
    struct UserInfo 
    {
        address user;   // address of user role
        uint64 expires; // unix timestamp, user expires
    }

    mapping (uint256  => UserInfo) internal _users;

    constructor(string memory name_, string memory symbol_)
     ERC721(name_, symbol_)
     {
     }
    
    function setUser(uint256 tokenId, address user, uint64 expires) public virtual{
        require(_isApprovedOrOwner(msg.sender, tokenId), "ERC4907: transfer caller is not owner nor approved");
        UserInfo storage info =  _users[tokenId];
        info.user = user;
        info.expires = expires;
        emit UpdateUser(tokenId, user, expires);
    }

    function userOf(uint256 tokenId) public view virtual returns(address){
        if( uint256(_users[tokenId].expires) >=  block.timestamp){
            return  _users[tokenId].user;
        }
        else{
            return address(0);
        }
    }

    function userExpires(uint256 tokenId) public view virtual returns(uint256){
        return _users[tokenId].expires;
    }

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

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override{
        super._beforeTokenTransfer(from, to, tokenId);

        if (from != to && _users[tokenId].user != address(0)) {
            delete _users[tokenId];
            emit UpdateUser(tokenId, address(0), 0);
        }
    }
} 
```

# テスト

以下にテストコードを格納しています。

https://github.com/cardene777/base-code/blob/develop/contract/test/ERC4907/ERC4907.test.ts

# セキュリティ
このEIP規格は所有者の権利を完全に保護することができ、所有者はいつでもNFTの`user`を変更し、有効期限を延長することができます。

# 考察
NFTの使用期限を設定できる規格は個人的に面白いと思います。
また、有効期限が切れるとトランザクションを起こさずに、自動で有効期限が切れるのもポイント高いです。
パッと思いつく使用例としては、「**NFTのレンタル**」ですね。
特定の条件を満たしたアドレスに、`user`権限を付与することで一定期間NFTを使用することを許可します。
また、**ERC4907**規格のNFTを使用した複数のDappで`user`権限を参照して処理ができるのも面白いですね。
例えば、2つのゲームで1つの**ERC4907**規格のNFTを参照して、どちらのゲームでも使用権限があるとなるとできることの幅が広がりそうです。
**ERC4907**だけでなく、**ERC4907**を使用したDappでも創意工夫の余地があるのが面白いなと思いました。


# 引用

Anders (@0xanders), Lance (@LanceSnow), Shrug <shrug@emojidao.org>, "ERC-4907: Rental NFT, an Extension of EIP-721," Ethereum Improvement Proposals, no. 4907, March 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4907.

# 最後に

今回は「**ERC721**規格のNFTに期限付きののロールを付与する**ERC4907**」についてまとめてきました！
いかがだったでしょうか？

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

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

# 採用強化中！

CryptoGamesでは一緒に働く仲間を大募集中です。

https://www.wantedly.com/companies/cryptogames

この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。
「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう！