[ERC5501] 期限までNFTのレンタル権を取り消されない仕組みを理解しよう！

# はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、有効期限が切れるまで、レンタルの状態を維持できる仕組みを提案している**ERC5501**についてまとめていきます！

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

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

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

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

# 概要

**ERC5501**では、**ERC721**に「`user`」という新しいロールを追加する標準を提案しています。
この利用者はNFTを実際に使うことはできますが、`transfer`したり他の`user`に渡すことはできません。

`user`ロールには有効期限（`expires`）と、現在そのNFTが「貸し出されているかどうか（`isBorrowed`）」を示すフラグが付きます。
`owner`は、自分のNFTを「**利用する権利**」だけを他のウォレットに渡すことができます。
これにより、安全なホットウォレットにNFTの利用権を委任したり、誰かにレンタルすることが可能になります。

もしNFTが「貸し出し中（`isBorrowed = true`）」になっている場合は、有効期限が切れるか、`owner`と借り手の双方が合意しない限り、`owner`であっても利用者を変更できません。
このようにして、「`owner」と「`user`」の両方の状態を同時に維持できるように設計されています。

# 動機

NFTのユースケースは単なるアート作品にとどまらず、ゲームアイテム、メタバースの土地、イベントチケット、音楽や映像、ドメイン、実物商品の証明など多岐にわたります。
しかし、現在の**ERC721**の仕様では、NFTを所有している人（`owner`）しかその価値を活用できません。

この提案では、NFTの所有者と利用者を分けることで以下のような現実的な使い方が可能になります。

## 利用者を設定する目的

### 委任（delegation）

`owner`が自分のNFTを、別のホットウォレットで使えるように設定します。
例えば安全性のために、普段使い用のウォレットに利用権だけを与えるようなケースです。
この場合、`owner`はいつでも利用者を変更できます。

### レンタル（renting）

誰かにNFTを貸す場合、期限が過ぎるまで`owner`が勝手に利用者を変えてはいけません。
そのために「**有効期限**」と「**`isBorrowed`フラグ**」が必要になります。
これによって借り手の権利が守られます。

## 想定される利用例

- セキュリティ目的の委任

`owner`権限は動かさず、安全なホットウォレットに利用権を渡す。

- ゲームアイテムのレンタル

ゲームで使うアイテムを一時的に試してみたいプレイヤーに貸し出す。

- ギルド管理

NFT自体はマルチシグで管理し、利用権だけをメンバーが共有するホットウォレットに与える。

- イベント

`owner`は主催者としてのアクセス、`user`は入場者としてのアクセスというように権限を分ける。

- ソーシャル機能

`owner`が読み書きできる部屋に、`user`は読み取りのみのアクセスなど用途を分ける。

## 従来との違いと進化した点

この提案は**ERC4400**や**ERC4907**をベースにしながら、以下のようなアップグレードが含まれています。

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

https://qiita.com/cardene/items/9807131ded7d1da7f9d9

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

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

- レンタル中でもNFTは`owner`のウォレットに残る

NFT自体は`transfer`されず、`owner`のアドレスに保持されたまま。

- レンタル中でもNFTの売却が可能

借り手の利用権は維持したまま、`owner`はNFTをマーケットプレイスに出品できる。

- エアドロップや特典の受け取りが可能

`owner`はNFT保有者としての特典（エアドロップ、フリーミントなど）を利用可能。

- ステーキングが不要になる

`isBorrowed`による保護があるため、NFTをステーキングせずに貸し出すことが可能になります。

これにより、NFTレンタルマーケットプレイスの実装も簡素化され、借り手・貸し手ともに安心して使える仕組みが整います。

# 仕様

## インターフェース

**ERC5501**では、NFTに「`user`」というロールを追加するための標準インターフェース **IERC5501** を定義しています。  
**ERC721**を拡張する形で実装されますが、**ERC721**の実装には必須ではありません。

**ERC5501**に準拠するコントラクトは必ずこのインターフェースを実装する必要があります。

```js:IERC5501.sol
/**
 * @title IERC5501: Rental & Delegation NFT - EIP-721 Extension
 * @notice the EIP-165 identifier for this interface is 0xf808ec37.
 */
interface IERC5501 /* is IERC721 */ {
    /**
     * @dev Emitted when the user of an NFT is modified.
     */
    event UpdateUser(uint256 indexed _tokenId, address indexed _user, uint64 _expires, bool _isBorrowed);

    /**
     * @notice Set the user info of an NFT.
     * @dev User address cannot be zero address.
     * Only approved operator or NFT owner can set the user.
     * If NFT is borrowed, the user info cannot be changed until user status expires.
     * @param _tokenId uint256 ID of the token to set user info for
     * @param _user address of the new user
     * @param _expires Unix timestamp when user info expires
     * @param _isBorrowed flag whether or not the NFT is borrowed
     */
    function setUser(uint256 _tokenId, address _user, uint64 _expires, bool _isBorrowed) external;

    /**
     * @notice Get the user address of an NFT.
     * @dev Reverts if user is not set.
     * @param _tokenId uint256 ID of the token to get the user address for
     * @return address user address for this NFT
     */
    function userOf(uint256 _tokenId) external view returns (address);

    /**
     * @notice Get the user expires of an NFT.
     * @param _tokenId uint256 ID of the token to get the user expires for
     * @return uint64 user expires for this NFT
     */
    function userExpires(uint256 _tokenId) external view returns (uint64);

    /**
     * @notice Get the user isBorrowed of an NFT.
     * @param _tokenId uint256 ID of the token to get the user isBorrowed for
     * @return bool user isBorrowed for this NFT
     */
    function userIsBorrowed(uint256 _tokenId) external view returns (bool);
}
```

## 関数とイベント

- `setUser(tokenId, user, expires, isBorrowed)`  

NFTの利用者情報を設定する関数。
`user`はゼロアドレスではいけません。  
`owner`または`operator`のみが実行できます。  
NFTがレンタル中（`isBorrowed = true`）かつ、まだ有効期限が切れていない場合は`user`の変更はできません。

- `userOf(tokenId)`  

現在の利用者アドレスを返す関数。
利用者が設定されていない、もしくは期限切れの場合は`revert`します。

- `userExpires(tokenId)`  

利用権がいつまで有効か（UNIXタイムスタンプ）を返す関数。

- `userIsBorrowed(tokenId)`  

現在そのNFTが貸出中かどうかを返す関数（`bool`）。

- `UpdateUser`  

利用者が更新された時に発行されるイベント。
`tokenId`、`user`、`expires`、`isBorrowed`情報を含みます。

## 必要な実装

- `user`は`owner`とは別の存在として扱う必要があります。
- `user`はNFTの`transfer`や`approve`を実行してはいけません。
- NFTが貸出中かつ、利用期限がまだ残っている場合は、`setUser`関数の実行が失敗される必要があります。
- トークンが`transfer`されたとき、貸出中でなければ利用者情報はリセットされる必要があります。
    - 貸出中なら変更してはいけません。

## IERC5501Balance

この拡張機能では、特定アドレスが「`user`」としていくつNFTを持っているかを取得できる関数を追加しています。

```js:IERC5501Balance.sol
/**
 * @title IERC5501Balance
 * Extension for ERC5501 which adds userBalanceOf to query how many tokens address is userOf.
 * @notice the EIP-165 identifier for this interface is 0x0cb22289.
 */
interface IERC5501Balance /* is IERC5501 */{
    /**
     * @notice Count of all NFTs assigned to a user.
     * @dev Reverts if user is zero address.
     * @param _user an address for which to query the balance
     * @return uint256 the number of NFTs the user has
     */
    function userBalanceOf(address _user) external view returns (uint256);
}
```

- `userBalanceOf(user)`  

指定したアドレスが「`user`」として設定されているNFTの数を返す関数。  
0アドレスを渡すと`revert`します。

## IERC5501Enumerable

この拡張機能では、`user`に割り当てられているNFTの一覧を取得できます。
主にUI表示や検索で役立ちます。

```js:IERC5501Enumerable.sol
/**
 * @title IERC5501Enumerable
 * This extension for ERC5501 adds the option to iterate over user tokens.
 * @notice the EIP-165 identifier for this interface is 0x1d350ef8.
 */
interface IERC5501Enumerable /* is IERC5501Balance, IERC5501 */ {
    /**
     * @notice Enumerate NFTs assigned to a user.
     * @dev Reverts if user is zero address or _index >= userBalanceOf(_owner).
     * @param _user an address to iterate over its tokens
     * @return uint256 the token ID for given index assigned to _user
     */
    function tokenOfUserByIndex(address _user, uint256 _index) external view returns (uint256);
}
```

- `tokenOfUserByIndex(user, index)`  

指定したアドレスが利用者になっているNFTのうち、`index`番目のトークンIDを返す関数。  
`アドレスや存在しないインデックスを指定すると`revert`します。

## IERC5501Terminable

この拡張機能では、NFTの貸出契約を`owner`と借り手の両者が同意すれば、有効期限前に終了できる仕組みを提供します。

```js:IERC5501Terminable.sol
/**
 * @title IERC5501Terminable
 * This extension for ERC5501 adds the option to terminate borrowing if both parties agree.
 * @notice the EIP-165 identifier for this interface is 0x6a26417e.
 */
interface IERC5501Terminable /* is IERC5501 */ {
    /**
     * @dev Emitted when one party from borrowing contract approves termination of agreement.
     * @param _isLender true for lender, false for borrower
     */
    event AgreeToTerminateBorrow(uint256 indexed _tokenId, address indexed _party, bool _isLender);

    /**
     * @dev Emitted when agreements to terminate borrow are reset.
     */
    event ResetTerminationAgreements(uint256 indexed _tokenId);

    /**
     * @dev Emitted when borrow of token ID is terminated.
     */
    event TerminateBorrow(uint256 indexed _tokenId, address indexed _lender, address indexed _borrower, address _caller);

    /**
     * @notice Agree to terminate a borrowing.
     * @dev Lender must be ownerOf token ID. Borrower must be userOf token ID.
     * If lender and borrower are the same, set termination agreement for both at once.
     * @param _tokenId uint256 ID of the token to set termination info for
     */
    function setBorrowTermination(uint256 _tokenId) external;

    /**
     * @notice Get if it is possible to terminate a borrow agreement.
     * @param _tokenId uint256 ID of the token to get termination info for
     * @return bool, bool first indicates lender agrees, second indicates borrower agrees
     */
    function getBorrowTermination(uint256 _tokenId) external view returns (bool, bool);

    /**
     * @notice Terminate a borrow if both parties agreed.
     * @dev Both parties must have agreed, otherwise revert.
     * @param _tokenId uint256 ID of the token to terminate borrow of
     */
    function terminateBorrow(uint256 _tokenId) external;
}
```

### イベント

- `AgreeToTerminateBorrow(tokenId, party, isLender)`  

オーナー（`lender`）または借り手（`borrower`）のどちらかが、レンタル終了に同意した時に発行されるイベント。

- `ResetTerminationAgreements(tokenId)`  

トークンが`transfer`されたり、`setUser`や`terminateBorrow`が呼ばれたときに、終了合意がリセットされた時に発行されるイベント。

- `TerminateBorrow(tokenId, lender, borrower, caller)`  

両者の合意が揃ってレンタルが終了した時に発行されるイベント。

### 関数

- `setBorrowTermination(tokenId)`  

呼び出した人が`lender`なら「`lender`の同意」を、`borrower`なら「`borrower`の同意」を記録する関数。  
もし`lender`と`borrower`が同一アドレスであれば、両方の同意を一度に記録します。

- `getBorrowTermination(tokenId)`  

`lender`と`borrower`、それぞれが同意済みかどうかを返す関数。

- `terminateBorrow(tokenId)`  

`isBorrowed`を`false`にして、NFTのレンタルを終了させる関数。
誰でも呼び出せるが、`lender`と`borrower`の両者が同意していない場合は`revert`する。  

## 実装時の注意点

各関数やイベントには、呼び出し条件や制限が明確に定められています。
特に`setUser`や`terminateBorrow`には慎重な制御が必要です。

複数の拡張（`Balance`, `Enumerable`, `Terminable`）は全てオプションです。
必要な機能だけを取り入れても問題ありません。

`supportsInterface`関数では、**ERC165**に基づいた識別子（例：`0xf808ec37`）を正しく返す必要があります。

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

https://qiita.com/cardene/items/541184977a5cba51b759

# 補足

## 既存の標準（ERC4400, ERC4907）を発展させる

**ERC5501**は、以前提案された**ERC4400**や**ERC4907**の考え方を元に設計されています。  
これらと同じく、NFTの所有権はそのままにして利用権だけを別の人に貸し出すというモデルをベースにしています。

NFTを貸すのに、ステーキングや過剰な担保（**overcollateralization**）を必要としません。
所有者は自分のウォレットでNFTを保有したまま、他のウォレットに利用権を渡すことができます。
利用権の委任（**delegation**）やレンタルを柔軟に行えるように設計されています。

## 最小限のインターフェース

定義する関数の数はできるだけ絞るように工夫されています。  
これにより、NFTプロジェクト側の実装負担を抑えつつ、目的に合った拡張だけを選んで使えるようにしています。

## モジュール構造

機能ごとに「**オプション拡張**」として分けることで、プロジェクトに必要なものだけを追加できます。

例えば、ユーザーが保有しているNFTの数を知りたい場合は `Balance` 拡張を実装できます。
ユーザーが持つNFTを列挙したいなら `Enumerable` 拡張を追加できます。
貸出契約を途中で終わらせたい場合は `Terminable` 拡張が有効できます。

このように自由度の高いモジュール構成になっています。

## `user`

新しく追加されたロールには「`user`」という名前が使われています。  
これは**ERC4907**と互換性を保つためであり、既存の開発者にとっても直感的に理解しやすくなっています。

## 所有者の特典を守るために

最近では、NFTコレクションがエアドロップやフリーミントなどの特典を、所有者（`owner`）に対して提供することが多くなっています。

しかし、NFTをコントラクトに預けて貸し出す従来の方式では、所有者がそのNFTを直接持っていない状態になり、特典が受け取れなくなるという問題がありました。

この仕様では、NFTがオーナーのウォレットにそのまま残るので、以下のようなメリットがあります。

- エアドロップやミントの恩恵を引き続き受けられる。
- 所有者と利用者でアプリ内のアクセス権や体験を分けることができる。

## Balance / Enumerable 拡張がオプションである理由

`userBalanceOf` や `tokenOfUserByIndex` など、利用者が関わる保有情報を扱う拡張はあえてオプションにされています。  
なぜなら、ユーザーの利用権は期限切れによって無効化されるため、オンチェーン上で「**リアルタイムに正確な情報**」を取得するのが難しいためです。

期限切れを考慮せずにカウントやリストを返してしまうと、意図しない挙動になる可能性があるためこれらの拡張は実装に注意が必要です。

## Terminable 拡張

もし所有者が誤って「**長期間有効な利用権付きでNFTを貸し出してしまった場合**」、その期間が終わるまで変更できないという問題が起こり得ます。  
そのままでは`user`をリセットすることもできず、NFTが事実上ロックされた状態になります。

このような事態を防ぐため、`lender`と`borrower`の双方が合意すれば早期に利用状態を解除できる「**Terminable**」拡張が用意されています。  
これにより、安全性と柔軟性の両方を担保する設計となっています。

# 互換性

**ERC5501**は、既存のNFT標準である**ERC721**と互換性があります。  
これは、**ERC721**の仕様に追加の関数をオプションとして加えるだけの形になっているため、既存のコントラクトやアプリケーションの構造を壊すことなく導入できます。

また、`user`という概念と有効期限の扱いは、すでに提案されている**ERC4907**と似ているため、**ERC4907**に対応しているアプリは、**ERC5501**にもスムーズに対応できます。

新しい設計ながらも既存の標準に沿った形式になっているため、開発者にとって導入のハードルが低く移行や対応がしやすくなっています。

# セキュリティ

**ERC5501**では、NFTに`owner`と`user`という2つのロールが同時に存在するようになります。  
これにより、新しい便利な使い方が可能になる一方でいくつかの注意点もあります。

## 二重使用の防止

両者が同時にアクティブであることで、同じNFTが二重に使われるリスクが生まれます。  
例えば、所有者がマーケットに出品している間に利用者がアプリ上でNFTを使い続けてしまうようなケースです。

開発者やアプリ側は、どちらに何ができるかを明確に設計し、衝突を防ぐ必要があります。

## ガスコストの考慮

`userBalanceOf` などのB**alance**関連の機能を実装する場合、利用期限が切れたNFTをどう扱うかが問題になります。  
リアルタイムで期限をチェックするにはコストがかかるため、ガス代の最適化を考慮した設計が求められます。

## マーケットプレイスでの表示

NFTが「貸出中（`isBorrowed = true`）」なのかどうかを、マーケットプレイス上でもわかりやすく表示する必要があります。  
ユーザーが「**購入したらすぐ自由に使える**」と思っていたのに、実は期限が残っていて使えないというトラブルを防ぐためです。

# 引用

Jan Smrža (@smrza), David Rábel (@rabeles11), Tomáš Janča <tomas.janca@jtbstorage.eu>, Jan Bureš (@JohnyX89), DOBBYLABS (@DOBBYLABS), "ERC-5501: Rental & Delegation NFT - EIP-721 Extension [DRAFT]," Ethereum Improvement Proposals, no. 5501, August 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5501.

# 最後に

今回は「有効期限が切れるまで、レンタルの状態を維持できる仕組みを提案している**ERC5501**」についてまとめてきました！
いかがだったでしょうか？

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