[ERC5643] NFTの所有権をサブスクのように管理できる仕組みを理解しよう！

# はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、NFTの所有権に有効期限を設けて、サブスクリプションのように定期的に更新しないと権利が失われる仕組みを提案している**ERC5643**についてまとめていきます！

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

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

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

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

# 概要

**ERC5643**は、**ERC721**を拡張してNFTに有効期限を設け、定期的に更新（支払い）しないと権利が失われる仕組みを提案しています。
NFTに「更新」や「キャンセル」などの機能を追加し、有効期限を設けることで継続的な支払いを求める仕組みを導入します。

# 動機

NFTは、DAppsのアカウントや会員証、イベントの入場パスなどさまざまな用途で使われています。
ただ、NFTはブロックチェーン上に「永久に」存在して期限が切れることがありません。
そのため、従来のWeb2のように「有効期限付きのメンバーシップ」や「定期課金」の概念がほとんどありませんでした。
しかし、多くの現実世界のサービスでは一定期間ごとに支払いが発生するサブスクリプションモデルが一般的です。

### ENS

このサブスクリプション型NFTの考え方に近いものとして、Ethereum Name Service（ENS）があります。
ENSでは、ドメイン名を一定期間ごとに更新しないと期限切れになってしまいます。
この仕組みと似たインターフェースをNFTにも適用することで、さまざまなプロジェクトが統一されたルールのもとでサブスクリプション型NFTを開発しやすくなります。

### ユーザーの利便性向上

現在のWeb2では、ユーザーが契約しているサブスクリプションを一元管理するのが難しいという課題があります。
しかし、共通のNFTサブスクリプション規格ができれば、単一のアプリケーション上で自分が持っているサブスクリプションを一覧表示し、更新やキャンセルを簡単に管理できるようになります。

### NFT市場と収益モデルの変化

これまで、NFTの発行者は二次流通（二次流通時のロイヤリティ）によって収益を得ることができました。
しかし、最近ではロイヤリティ収入が減少しつつあります。
そのため、NFTを使った継続的な収益モデルが必要になってきています。
サブスクリプション型NFTを導入すれば、発行者は会員権やアクセス権の提供を継続することで安定した収益を得ることができ、同時にユーザーに対して価値を提供し続けることが求められます。

# 仕様

**ERC5643**では、NFTにサブスクリプション機能を追加するための標準インターフェース（**IERC5643**）を定義しています。

```js:IERC5643.sol
interface IERC5643 {
    /// @notice Emitted when a subscription expiration changes
    /// @dev When a subscription is canceled, the expiration value should also be 0.
    event SubscriptionUpdate(uint256 indexed tokenId, uint64 expiration);

    /// @notice Renews the subscription to an NFT
    /// Throws if `tokenId` is not a valid NFT
    /// @param tokenId The NFT to renew the subscription for
    /// @param duration The number of seconds to extend a subscription for
    function renewSubscription(uint256 tokenId, uint64 duration) external payable;

    /// @notice Cancels the subscription of an NFT
    /// @dev Throws if `tokenId` is not a valid NFT
    /// @param tokenId The NFT to cancel the subscription for
    function cancelSubscription(uint256 tokenId) external payable;

    /// @notice Gets the expiration date of a subscription
    /// @dev Throws if `tokenId` is not a valid NFT
    /// @param tokenId The NFT to get the expiration date of
    /// @return The expiration date of the subscription
    function expiresAt(uint256 tokenId) external view returns(uint64);

    /// @notice Determines whether a subscription can be renewed
    /// @dev Throws if `tokenId` is not a valid NFT
    /// @param tokenId The NFT to get the expiration date of
    /// @return The renewability of a the subscription
    function isRenewable(uint256 tokenId) external view returns(bool);
}
```  

## IERC5643

**IERC5643**では、NFTをサブスクリプション（定期更新型）として扱うために、以下の関数とイベントを定義しています。  

### `SubscriptionUpdate`  

NFTのサブスクリプションの有効期限が変更されたときに発行されるイベント。  
サブスクリプションがキャンセルされた場合、`expiration`（有効期限）の値は `0` にする。  

### `renewSubscription`

NFTのサブスクリプションを延長するための関数。   

- **パラメータ**
    - `tokenId`（対象のNFTのID）  
    - `duration`（延長する秒数）  

`tokenId` が無効な場合はエラーを出します。  

### `cancelSubscription`

NFTのサブスクリプションをキャンセルするための関数。

- **パラメータ**
    - `tokenId`（対象のNFTのID）  

`tokenId` が無効な場合はエラーを出す。  

### `expiresAt`

NFTのサブスクリプションの有効期限を取得する関数。  

- **パラメータ**
    - `tokenId`（対象のNFTのID）  
- **戻り値**
    - `uint64`（サブスクリプションの有効期限）。  

`tokenId` が無効な場合はエラーを出す。  

### `isRenewable`

NFTのサブスクリプションが更新可能かどうかを確認する関数。  
- **パラメータ**  
  - `tokenId`（対象のNFTのID）  
- **戻り値**
    - `bool`（更新可能なら `true`、そうでなければ `false`）。  

`tokenId` が無効な場合はエラーを出す。  

## 実装ルール

- **expiresAt** と **isRenewable** は `pure` または `view` で実装可能。  
- **renewSubscription** と **cancelSubscription** は `external` または `public` で実装可能。  
- **SubscriptionUpdate イベント** は、有効期限が変更されたときに必ず発行する。  
- `supportsInterface(0x8c65f84d)` を実装し、このインターフェースをサポートしていることを示す必要がある。  

# 補足

## なぜこの標準が必要なのか  

**ERC5643**は、NFTを使ったオンチェーンのサブスクリプションを簡単に実装できるようにすることを目的としています。
余計な機能は追加せず、サブスクリプション管理に必要な最低限の関数とイベントだけを定義しています。  

## NFTがサブスクリプションの所有権を持つ  

**ERC5643**では、NFTそのものがサブスクリプションの所有権を表します。
つまり、サブスクリプションを管理するために別のトークン（**ERC20**などの代替可能トークンや、**ERC1155**などの別のNFT）を発行する必要はありません。

## サブスクリプションの管理  

サブスクリプションは、一定期間ごとに支払いを行い、サービスや特典を受け取る仕組みです。
そのため、以下のような機能が必要になります。  

- **更新（`renewSubscription`）**  

サブスクリプションの期間を延長するための関数です。
これにより、ユーザーは契約を継続できます。  

- **キャンセル（`cancelSubscription`）**  

ユーザーがサブスクリプションを途中で終了できるようにする関数です。  

- **有効期限の確認（`expiresAt`）**  

サブスクリプションがいつまで有効なのかを取得する関数です。
これを知ることで、ユーザーは更新のタイミングを把握し、アプリケーション側もそのNFTが有効かどうかを判断できます。  

- **更新の可否（`isRenewable`）**  

サブスクリプションが更新可能かどうかを判定する関数です。
中には、期限が切れたら更新できなくなるNFTもありえます。
この関数があれば、ユーザーやアプリケーションがそれを事前に把握できます。  

## 既存のシステムとの統合が簡単  

**ERC5643**は、**ERC721**に完全に準拠しているため、すでにNFTを扱うシステムと簡単に統合できます。
追加する関数も少ないため、既存のプロトコルやマーケットプレイスでも、サブスクリプションNFTをそのまま扱うことができます。  

# 互換性

**ERC5643**は、拡張機能セットを追加することで**ERC721**と完全に互換性を持たすことができます。

**ERC5643**で導入された新機能は、既存の**ERC721**インターフェースに最小限のオーバーヘッドを追加するだけなので、開発者にとっては導入が簡単かつ迅速に行えます。

# テスト

```js
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/ERC5643.sol";

contract ERC5643Mock is ERC5643 {
    constructor(string memory name_, string memory symbol_) ERC5643(name_, symbol_) {}

    function mint(address to, uint256 tokenId) public {
        _mint(to, tokenId);
    }
}

contract ERC5643Test is Test {
    event SubscriptionUpdate(uint256 indexed tokenId, uint64 expiration);

    address user1;
    uint256 tokenId;
    ERC5643Mock erc5643;

    function setUp() public {
        tokenId = 1;
        user1 = address(0x1);

        erc5643 = new ERC5643Mock("erc5369", "ERC5643");
        erc5643.mint(user1, tokenId);
    }

    function testRenewalValid() public {
        vm.warp(1000);
        vm.prank(user1);
        vm.expectEmit(true, true, false, true);
        emit SubscriptionUpdate(tokenId, 3000);
        erc5643.renewSubscription(tokenId, 2000);
    }

    function testRenewalNotOwner() public {
        vm.expectRevert("Caller is not owner nor approved");
        erc5643.renewSubscription(tokenId, 2000);
    }

    function testCancelValid() public {
        vm.prank(user1);
        vm.expectEmit(true, true, false, true);
        emit SubscriptionUpdate(tokenId, 0);
        erc5643.cancelSubscription(tokenId);
    }

    function testCancelNotOwner() public {
        vm.expectRevert("Caller is not owner nor approved");
        erc5643.cancelSubscription(tokenId);
    }

    function testExpiresAt() public {
        vm.warp(1000);

        assertEq(erc5643.expiresAt(tokenId), 0);
        vm.startPrank(user1);
        erc5643.renewSubscription(tokenId, 2000);
        assertEq(erc5643.expiresAt(tokenId), 3000);

        erc5643.cancelSubscription(tokenId);
        assertEq(erc5643.expiresAt(tokenId), 0);
    }
}
```

# 参考実装

```js:ERC5643.sol
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.13;

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

contract ERC5643 is ERC721, IERC5643 {
    mapping(uint256 => uint64) private _expirations;

    constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {}

    function renewSubscription(uint256 tokenId, uint64 duration) external payable {
        require(_isApprovedOrOwner(msg.sender, tokenId), "Caller is not owner nor approved");

        uint64 currentExpiration = _expirations[tokenId];
        uint64 newExpiration;
        if (currentExpiration == 0) {
            newExpiration = uint64(block.timestamp) + duration;
        } else {
            if (!_isRenewable(tokenId)) {
                revert SubscriptionNotRenewable();
            }
            newExpiration = currentExpiration + duration;
        }

        _expirations[tokenId] = newExpiration;

        emit SubscriptionUpdate(tokenId, newExpiration);
    }

    function cancelSubscription(uint256 tokenId) external payable {
        require(_isApprovedOrOwner(msg.sender, tokenId), "Caller is not owner nor approved");
        delete _expirations[tokenId];
        emit SubscriptionUpdate(tokenId, 0);
    }

    function expiresAt(uint256 tokenId) external view returns(uint64) {
        return _expirations[tokenId];
    }

    function isRenewable(uint256 tokenId) external pure returns(bool) {
        return true;
    }

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

# セキュリティ

**ERC5643**はNFTの所有権に影響を与えないため安全です。

# 引用

cygaar (@cygaar), "ERC-5643: Subscription NFTs [DRAFT]," Ethereum Improvement Proposals, no. 5643, September 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5643.

# 最後に

今回は「NFTの所有権に有効期限を設けて、サブスクリプションのように定期的に更新しないと権利が失われる仕組みを提案している**ERC5643**」についてまとめてきました！
いかがだったでしょうか？

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