# [ERC4885] NFTをサブスクで提供する仕組みを理解しよう！

> ERC4885は、NFT（ERC721）やマルチトークン（ERC1155）の利用権をサブスクリプション形式で提供する標準APIを提案しています。

- URL: https://cardene.dev/articles/12c2ef6d8c2de1586a1c
- 公開日: 2025-04-22
- 更新日: 2025-04-22
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、NFTをFT支払いによるサブスクリプション形式で提供する仕組みを提案している**ERC4885**についてまとめていきます！

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

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

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

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

## 概要

**ERC4885**は、NFT（**ERC721**）やマルチトークン（**ERC1155**）の利用権をサブスクリプション形式で提供する標準APIを提案しています。
利用者は、**ERC20**トークンを支払うことで、期間限定または無期限でこれらのNFTを利用できるサブスクリプショントークンを受け取ります。

このサブスクリプショントークンは、NFTを一定期間利用する権利を表すもので、NFTそのものを譲渡することなくその使用権を柔軟に提供できます。

## 動機

**ERC4885**は、**ERC721**および**ERC1155**形式で発行されるNFTの利用を、定期的なサブスクリプションを通じて可能にする柔軟な仕組みを提案しています。
以下のような背景と目的があります。

#### 中央集権型の配信モデルからの脱却

音楽や映画などのデジタルコンテンツは、現在では大手テック企業による一律のサブスクリプションモデルで流通しています。
クリエイター自身が独自の収益モデルを構築することは困難です。

#### Web3的な分散型配信の実現

**ERC4885**により、開発者はクリエイターが独自にサブスクリプションモデルを設定し、それに基づいてNFTを貸し出す仕組みをDappとして提供できます。
これにより、より自由で創造的な収益モデルや配信形態が実現可能となります。

### ユースケース

**ERC4885**は、以下のような定期利用やアクセス権の提供に適しています。

- 音楽・映像・書籍・ニュース・eラーニングなどの定額配信サービス
- デジタル資産の共有
- フィットネスクラブなどの会員権
- スポーツ・eスポーツのシーズンチケット
- DeFiにおける定額支払いと変動収入の交換契約
- ゲーム内アイテムのレンタル

### サブスクリプショントークンの設計

**ERC4885**では、サブスクリプショントークンに一部**ERC20**の機能を取り入れることで、以下のような拡張的な利用も可能にします。

- セカンダリーマーケットでの転売
- 他人への贈与
- サブスクリプションの払い戻し処理

単なる利用権のトークンというだけでなく、金融的な価値を持たせた柔軟な設計が可能です。

## 仕様

### 基本的な仕組み

- **サブスク開始**
    - 利用者（subscriber）は**ERC20**トークン（baseToken）をデポジットして、NFT利用のサブスクリプションを開始します。
- **残高の減少**
    - サブスクリプショントークンの残高は、使用期間に応じて線形に減少していきます。
- **延長**
    - 利用者は再度デポジットすることでサブスクリプション期間を延長できます。

### 必須インターフェース識別

**ERC4885**を実装するコントラクトは、**ERC165**の`supportsInterface(bytes4)` を実装し、引数に `0xC1A48422` を渡したときに `true` を返す必要があります。

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

[\[ERC165\] コントラクトが特定のインターフェースをサポートしているか確認する仕組みを理解しよう！](/articles/541184977a5cba51b759)

### インターフェース

```js:ISubscriptionToken.sol
interface ISubscriptionToken {
    /**
        @dev This emits when the subscription token constructor or initialize method is
        executed.
        @param name The name of the subscription token
        @param symbol The symbol of the subscription token
        @param provider The provider of the subscription whom receives the deposits
        @param subscriptionToken The subscription token contract address
        @param baseToken The ERC-20 compatible token to use for the deposits.
        @param nft Address of the `nft` contract that the provider mints/transfers from.
        All tokenIds referred to in this interface MUST be token instances of this `nft` contract.
    */
    event InitializeSubscriptionToken(
        string name,
        string symbol,
        address provider,
        address indexed subscriptionToken,
        address indexed baseToken,
        address indexed nft,
        string uri
    );

    /**
        @dev This emits for every new subscriber to `nft` contract of token `tokenId`.
        `subscriber` MUST have received `nft` of token `tokenId` in their account.
        @param subscriber The subscriber account
        @param tokenId MUST be token id of `nft` sent to `subscriber`
        @param uri MUST be uri of the `nft` that was sent to `subscriber` or empty string
    */
    event SubscribeToNFT(
        address indexed subscriber,
        uint256 indexed tokenId,
        string uri
    );

    /**
        @dev Emits when `subscriber` deposits ERC-20 of token type `baseToken` via the `deposit method.
        This tops up `subscriber` balance of subscription tokens
        @param depositAmount The amount of ERC-20 of type `baseToken` deposited
        @param subscriptionTokenAmount The amount of subscription tokens sent in exchange to `subscriber`
        @param subscriptionPeriod Amount of additional time in seconds subscription is extended
    */
    event Deposit(
        address indexed subscriber,
        uint256 indexed tokenId,
        uint256 depositAmount,
        uint256 subscriptionTokenAmount,
        uint256 subscriptionPeriod
    );

    /**
        @return The name of the subscription token
    */
    function name() external view returns (string memory);

    /**
        @return The symbol of the subscription token
    */
    function symbol() external view returns (string memory);

    /**
        @notice Subscribes `subscriber` to `nft` of 'tokenId'. `subscriber` MUST receive `nft`
        of token `tokenId` in their account.
        @dev MUST revert if `subscriber` is already subscribed to `nft` of 'tokenId'
        MUST revert if 'nft' has not approved the `subscriptionToken` contract address as operator.
        @param subscriber The subscriber account. MUST revert if zero address.
        @param tokenId MUST be token id of `nft` contract sent to `subscriber`
        `tokenId` emitted from event `SubscribeToNFT` MUST be the same as tokenId except when
        tokenId is zero; allows OPTIONAL tokenid that is then set internally and minted by
        `nft` contract
        @param uri The OPTIONAL uri of the `nft`.
        `uri` emitted from event `SubscribeToNFT` MUST be the same as uri except when uri is empty.
    */
    function subscribeToNFT(
        address subscriber,
        uint256 tokenId,
        string memory uri
    ) external;

    /**
        @notice Top up balance of subscription tokens held by `subscriber`
        @dev MUST revert if `subscriber` is not subscribed to `nft` of 'tokenId'
        MUST revert if 'nft' has not approved the `subscriptionToken` contract address as operator.
        @param subscriber The subscriber account. MUST revert if zero address.
        @param tokenId The token id of `nft` contract to subscribe to
        @param depositAmount The amount of ERC-20 token of contract address `baseToken` to deposit
        in exchange for subscription tokens of contract address `subscriptionToken`
    */
    function deposit(
        address subscriber,
        uint256 tokenId,
        uint256 depositAmount
    ) external payable;

    /**
        @return The balance of subscription tokens held by `subscriber`.
        RECOMMENDED that the balance decreases linearly to zero for time limited subscriptions
        RECOMMENDED that the balance remains the same for life long subscriptions
        MUST return zero balance if the `subscriber` does not hold `nft` of 'tokenId'
        MUST revert if subscription has not yet started via the `deposit` function
        When the balance is zero, the use of `nft` of `tokenId` MUST NOT be allowed for `subscriber`
    */
    function balanceOf(address subscriber) external view returns (uint256);
}
```

### イベント

#### `InitializeSubscriptionToken`

```js
event SubscribeToNFT(
    address indexed subscriber,
    uint256 indexed tokenId,
    string uri
);
```

サブスクリプショントークンの初期化時に発行されるイベント。

- `name`
    - トークン名。
- `symbol`
    - トークンシンボル。
- `provider`
    - サブスクの提供者アドレス（NFT発行者）。
- `subscriptionToken`
    - サブスクリプショントークンのコントラクトアドレス。
- `baseToken`
    - デポジットに使われる**ERC20**トークンのコントラクトアドレス。
- `nft`
    - 対象NFTコントラクトのアドレス。
- `uri`
    - サブスクリプショントークンのメタデータURI。

#### `SubscribeToNFT`

```js
event SubscribeToNFT(
    address indexed subscriber,
    uint256 indexed tokenId,
    string uri
);
```

NFTへの新規サブスクリプション時に発行されるイベント。

- `subscriber`
    - サブスク利用者のアドレス。
- `tokenId`
    - サブスク対象のNFTのトークンID。
- `uri`
    - NFTのURI。

#### `Deposit`

```js
event Deposit(
    address indexed subscriber,
    uint256 indexed tokenId,
    uint256 depositAmount,
    uint256 subscriptionTokenAmount,
    uint256 subscriptionPeriod
);
```

利用者が**ERC20**トークンをデポジットし、サブスクリプショントークンを受け取る時に発行されるイベント。

- `subscriber`
    - 利用者アドレス。
- `tokenId`
    - サブスク対象NFTのトークンID。
- `depositAmount`
    - デポジットした**ERC20**トークンの量。
- `subscriptionTokenAmount`
    - 発行されたサブスクリプショントークンの量。
- `subscriptionPeriod`
    - 延長されたサブスクリプション期間（秒単位）。

### 関数

#### `name`

```js
function name() external view returns (string memory);
```

サブスクリプショントークンの名前を返す関数。

#### `symbol`

```js
function symbol() external view returns (string memory);
```

サブスクリプショントークンのシンボルを返す関数。

#### `subscribeToNFT`

```js
function subscribeToNFT(
    address subscriber,
    uint256 tokenId,
    string memory uri
) external;
```

`subscriber` が `tokenId` のNFTにサブスクする関数。
`uri` は任意で、NFTのメタデータURIとして記録可能です。

以下の場合にrevertします。

- `subscriber` がすでにサブスクしている場合。
- `nft`がこのコントラクトをoperatorとして許可していない場合。
- `subscriber`がゼロアドレス。

#### `deposit`

```js
function deposit(
    address subscriber,
    uint256 tokenId,
    uint256 depositAmount
) external payable;
```

サブスクリプショントークンをチャージするために**ERC20**トークンを預ける関数。

以下の場合にrevertします。
- `subscriber`がサブスクしていない場合。
- `nft`がこのコントラクトをoperatorとして許可していない場合。
- `subscriber`がゼロアドレス。

#### `balanceOf`

```js
function balanceOf(address subscriber) external view returns (uint256);
```

`subscriber` の現在のサブスクリプショントークンの残高を返す関数。

- 時間制限付きサブスクリプションの場合、時間とともに残高が減少します。
- 永続的サブスクリプションでは残高は変化しません。
- サブスク開始前やNFT保有していない場合はゼロを返します。
- 残高がゼロの場合、NFTの利用は不可です。

### サブスクリプショントークンの動作モデル

- 例：7日間のサブスク → 7トークンが発行 → 1日ごとに1トークンずつ減少

減少処理は `balanceOf` 関数で計算します。

### トークン価格の計算

```text
価格 = depositAmount ÷ (subscriptionTokenAmount × subscriptionPeriod)
```

1秒あたりの単価が明示的に計算可能です。

### NFTのメタデータ活用

メタデータにはサービスの契約条件やNFTのアセット情報を含めることが可能です。
あえて汎用的に設計されており、用途に応じた柔軟な表現が可能です。

### サブスクリプションの有効期限切れ

残高がゼロになった時点で、NFTの利用は無効になります。
具体的な制御（例：配信停止、メタデータのURIを空にするなど）は、実装者に委ねられます。

### 注意点（Caveats）

- **自動更新不可**
    - 従来のクレジットカード等による自動課金は不可能。
    - トークンの都度承認が必要。
- **一時停止未対応**
    - 一時停止（pause）機能は提供されていないため独自実装が必要。
    - OpenZeppelinの `Pausable` を拡張することで実現可能。
- **一時停止期間の考慮**
    - pauseに対応する場合は、`balanceOf` に追加ロジックが必要。
    - pause中の経過時間を除外して残高を計算するなどの工夫が求められます。

## 補足

### サブスクリプションのトークン化

**ERC4885**により、サブスクリプション自体が「**資産**」としてトークン化され、二次流通が可能になります。
例えばスポーツ観戦のシーズンチケットをファン同士で売買するようなユースケースでは、有効期限付きのNFTでは対応が難しいですが、サブスクリプショントークンであれば実現できます。

- サブスクリプショントークンが**ERC20**に準拠していれば送付が可能
- NFTを利用したサブスクでは、購入者は残り期間分の利用権をそのまま引き継ぎ
- FTの場合は、既存のサブスクリプションに追加として統合可能

### NFTの多様なユースケースへの対応

`subscribeToNFT` 関数では `tokenId` や `uri` をオプションにしており、以下のような多様なユースケースに柔軟に対応できます。

- 単に異なる画像を使うNFTコレクションでは `uri` が不要なことがある
- 一方で法的契約が必要なケースなどでは、`uri` に双方向の情報を保存することが重要

### ユーザーに主導権を取り戻す

従来のストリーミングサブスクリプションは中央集権的で、契約条件や価格設定はすべてサービス提供者が管理しています。
**ERC4885**は、分散型アプリケーション上でのサブスクリプションモデルの構築を可能にし、利用者側が自らのエコシステムを形成・運営できるようになります。

## 互換性

**ERC4885**のサブスクリプショントークンは、**ERC20**と互換性を持たせることが可能です。

`name`、`symbol`、`balanceOf` などの関数はすでに仕様に含まれており、`transfer` など他の**ERC20**機能を実装すれば、サブスクの譲渡や売買が可能になります。

また、この仕組みは NFT自体の有効期間を間接的に制御する手段としても機能します。
このため、NFTとサブスクリプショントークンの両コントラクトが同じアプリやプラットフォームによって管理されていることが前提です。

すでに発行された他のNFTに影響を及ぼさないように設計されており、nftは**subscriptionToken** コントラクトをoperatorとして承認していることが必須です。

## セキュリティ

### 前払いとサービス提供の信頼性

**ERC4885**では、`deposit` 関数を通じて利用者が事前に支払う前払いモデルを採用しています。
ただし、サービス提供者がその後、サービスを提供しなかったり不十分な提供に終わった場合の救済措置や担保は**ERC4885**には規定されていません。

これは従来の中央集権的サービスでも起こり得る問題であり、別途信頼や評価システムとの連携が必要になる可能性があります。

### メタデータの改ざんリスク

`subscribeToNFT` 関数では `uri` を指定できますが、これが中央集権型のストレージに保存されている場合、サービス提供者による後からの改ざんや外部からのハッキングのリスクがあります。

そのため、変更不可のストレージ（例：IPFSやArweave）への保存を推奨しています。
特に契約内容が重要なユースケースではこの点がセキュリティ確保の鍵となります。

## 引用

Jules Lai (@julesl23), "ERC-4885: Subscription NFTs and Multi Tokens [DRAFT]," Ethereum Improvement Proposals, no. 4885, March 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4885.

## 最後に

今回は「NFTをFT支払いによるサブスクリプション形式で提供する仕組みを提案している**ERC4885**」についてまとめてきました！
いかがだったでしょうか？

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