# [ERC7858] NFTとSBTに有効期限を付与する仕組みを理解しよう！

> ERC7858は、ERC721トークン（NFT）および**SBT（Soulbound Token）**に「有効期限（Expiration）」というを追加する拡張提案です。

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

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、NFTとSBTに有効期限（ブロック高または時刻）を付与しする仕組みを提案している**ERC7858**についてまとめていきます！

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

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

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

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

## 概要

**ERC7858**は、**ERC721**トークン（NFT）および**SBT（Soulbound Token）**に「**有効期限（Expiration）**」というを追加する拡張提案です。
この仕組みによって、NFTやSBTがあらかじめ設定された期間を過ぎると自動的に無効になることが可能になります。

従来の**ERC721**規格ではトークンは永続的に存在し、削除や期限切れの概念はありませんでした。
しかし、この拡張を導入することで、例えば「**チケット**」、「**会員証**」、「**契約書**」、「**貸出期間つきアイテム**」など、期限がある利用シナリオに対応できるようになります。

さらに、**ERC7858**は以下の点を重視しています。

* **既存のNFT / SBT 干渉しない設計**
    既存のトークン送付やマーケットプレイスとの互換性を保ちます。
    有効期限の有無がトークンそのものの移転や表示に悪影響を与えないよう設計されています。

* **柔軟な有効期限設定**
    有効期限を「**ブロック高（block height）**」または「**タイムスタンプ（timestamp）**」のどちらでも指定できます。
    これにより、ブロックチェーンの種類やユースケースに合わせた柔軟な設定が可能です。

このように、トークンの寿命を明確に定義できることで、従来の永続的なトークンモデルにはない新しいユースケースを実現します。

## 動機

**ERC7858**が生まれた背景には、既存のNFT / SBT実装が抱えるいくつかの課題があります。

従来、開発者がNFTやSBTに有効期限を設けたい場合、スマートコントラクトの中でカスタムの `mapping`（マッピング）を使って期限を管理していました。
しかし、この方法には以下のような問題がありました。

- **他のスマートコントラクトとの非互換性**
    各プロジェクトが独自の仕組みで有効期限を実装しているため、
    他のコントラクトやサービスと連携する際に一貫性がなく、動作が不安定になることがありました。

- **NFTマーケットプレイスとの不整合**
    独自仕様の**ERC721**実装では、マーケットプレイスなど既存のDAppで正しく動作しないケースがありました。
    例えば、期限切れトークンの扱いが定義されていないため、取引時にエラーを起こす可能性があります。

**ERC7858**は、こうした問題を解決するために、標準化された有効期限機構と対応インターフェースを提供しています。

**ERC7858**では、トークンごと、または全体（エポック単位）での有効期限をサポートします。
これにより、開発者はユースケースに合わせて以下の2種類の方式を選択できます。

| 方式 | 説明 |
| ---- | ----- |
| **トークン単位（Per-token expiry）**   | 各トークンごとに異なる有効期限を設定できます。例：イベントチケット、限定ライセンスなど。 |
| **エポック単位（Epoch-based expiry）** | 全トークンに共通の有効期限を設定します。例：特定シーズン中のアクセス権など。       |

#### 対応するユースケース例

この提案の有効期限機能は、以下のような多様な分野で活用できます。

| 分野 | 具体的な利用例 |
| --- | ----------- |
| **アクセス・認証**     | IAM（Identity and Access Management）システムでの一時的なアクセス権付与。  |
| **メンバーシップ**     | 会員制サービス（MMS：Membership Management System）での期限付き会員証。    |
| **チケット・パス**     | MICE（会議・展示会）向けのチケット管理。**ERC2135**や**ERC7578**と組み合わせて利用可能。 |
| **サブスクリプション**   | デジタルプラットフォームの期間限定アクセスや有料購読。                            |
| **デジタル証明書・契約書** | 有効期限付きの電子契約書、著作権ライセンス、ポリシー文書など。                        |
| **ロイヤリティ・クーポン** | ポイントプログラムや割引券の期限管理。 |
| **ガバナンス** | 投票権や提案権に期限を設定し、一定期間後に無効化。 |
| **金融商品**  | 債券・ローン・オプション契約など、有効期間を持つデジタル金融商品。                      |
| **レンタル**  | 不動産・デジタル資産・DePIN などの一時的貸出契約。 |

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

[\[ERC2135\] NFTの使用を管理するインターフェースの仕組みを理解しよう！](/articles/7174e91de844899eca72)

## 仕様

### インターフェース

```javascript
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0 <0.9.0;

/**
 * @title ERC-7858: Expirable NFTs and SBTs
 * @notice unique/granular expiry
 */

// import "./IERC721.sol";

// The EIP-165 identifier of this interface is `0x3ebdfa31`.
interface IERC7858 /**is IERC721 */ {
    enum EXPIRY_TYPE {
        BLOCKS_BASED, // block.number
        TIME_BASED // block.timestamp
    }
    
    event TokenExpiryUpdated(
        uint256 indexed tokenId,
        uint256 indexed startTime,
        uint256 indexed endTime
    );

    function expiryType() external view returns (EXPIRY_TYPE);
    function isTokenExpired(uint256 tokenId) external view returns (bool);
    function startTime(uint256 tokenId) external view returns (uint256);
    function endTime(uint256 tokenId) external view returns (uint256);
}
```

#### `EXPIRY_TYPE`

```javascript
enum EXPIRY_TYPE {
    BLOCKS_BASED,
    TIME_BASED
}
```

有効期限の基準を定義する列挙型。

この列挙型は、トークンの有効期限をどの単位で測定するかを指定します。
`BLOCKS_BASED` はブロック番号（`block.number`）を使用し、
`TIME_BASED` はブロックのタイムスタンプ（`block.timestamp`）を使用します。

**パラメータ**

* `BLOCKS_BASED`
  * 有効期限をブロックの高さで管理します。
* `TIME_BASED`
  * 有効期限を時間（UNIXタイム）で管理します。

#### `TokenExpiryUpdated`

```javascript
event TokenExpiryUpdated(
    uint256 indexed tokenId,
    uint256 indexed startTime,
    uint256 indexed endTime
);
```

トークンの有効期限が設定または更新されたときに発行されるイベント。

トークンの発行（mint）や、有効期限情報（`startTime`・`endTime`）が変更された際に呼び出されます。
これにより、外部のDAppやウォレットがトークンの状態変更を検知できます。

**パラメータ**

* `tokenId`
  * 更新対象のトークンID（**ERC721** 準拠）。
* `startTime`
  * 有効期間の開始（`block.number` または `block.timestamp`）。
* `endTime`
  * 有効期間の終了（`block.number` または `block.timestamp`）。

#### `expiryType`

```javascript
function expiryType() external view returns (EXPIRY_TYPE);
```

有効期限の種類を取得する関数。

コントラクトがブロックベースで有効期限を管理しているのか、それとも時間ベースで管理しているのかを返します。

**戻り値**

* `EXPIRY_TYPE`
  有効期限の測定単位（`BLOCKS_BASED` または `TIME_BASED`）。

#### `isTokenExpired`

```javascript
function isTokenExpired(uint256 tokenId) external view returns (bool);
```

トークンが有効期限切れかどうかを判定する関数。

指定された `tokenId` のトークンが期限切れであれば `true` を返します。
トークンが存在しない場合、この関数はエラーを返します（`revert`）。

**引数**

* `tokenId`
  * 判定対象のトークンID。

**戻り値**

* `bool`
  * 期限切れなら `true`、有効なら `false`。

#### `startTime`

```javascript
function startTime(uint256 tokenId) external view returns (uint256);
```

トークンの有効期間の開始時刻を取得する関数。

トークンごとの有効開始時間を返します。
測定単位は `expiryType()` に依存します（ブロック番号またはタイムスタンプ）。

**引数**

* `tokenId`
  * 対象のトークンID。

**戻り値**

* `uint256`
  * 有効開始ブロックまたは時刻。

#### `endTime`

```javascript
function endTime(uint256 tokenId) external view returns (uint256);
```

トークンの有効期間の終了時刻を取得する関数。

トークンがいつまで有効であるかを返します。
`0` が設定されている場合、そのトークンは期限がないものとして扱われます。

**引数**

* `tokenId`
  * 対象のトークンID。

**戻り値**

* `uint256`
  * 有効期限の終了ブロックまたは時刻。


#### 関数の処理

| 関数 | 説明 |
| --- | ---- |
| `balanceOf`  | **ERC721**から継承された関数。期限切れトークンも含めてカウントします。トークンは存在するが「使用不可」として扱われます。 |
| `transferFrom` / `safeTransferFrom` | 有効期限切れでも転送可能。これにより既存マーケットプレイスとの互換性を維持します。 |
| `expiryType` | コントラクトの有効期限の測定単位を返します。 |
| `isTokenExpired`  | 期限切れであれば `true`、存在しない場合はエラー。 |
| `startTime` / `endTime` | どちらも `expiryType` に応じて `block.number` または `block.timestamp` を使用。`startTime <= endTime` である必要があります。 |
| `TokenExpiryUpdated` | トークン発行または有効期限更新時に発行されます。 |

### 拡張インターフェース

以下のインターフェースは**IERC7858Epoch**です。
エポック（期間単位）による一括有効期限管理をサポートします。

```javascript
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0 <0.9.0;

/**
 * @title ERC-7858: Expirable NFTs and SBTs
 * @notice epoch expiry extension
 */

// The EIP-165 identifier of this interface is `0xec7ffd66`.
interface IERC7858Epoch /** is IERC7858 */ {
    function currentEpoch() external view returns (uint256);
    function epochLength() external view returns (uint256);
    function isEpochExpired(uint256 epoch) external view returns (bool);
    function unexpiredBalanceOf(address account) external view returns (uint256);
    function unexpiredBalanceOfAtEpoch(uint256 epoch, address account) external view returns (uint256);
    function validityDuration() external view returns (uint256);
}
```

#### `currentEpoch`

```javascript
function currentEpoch() external view returns (uint256);
```

現在のエポック番号を取得する関数。

コントラクトが現在どのエポック（期間）にあるかを返します。
これにより、トークンが有効かどうかを判断する基準になります。

**戻り値**

* `uint256`
  * 現在のエポック番号。

#### `epochLength`

```javascript
function epochLength() external view returns (uint256);
```

1つのエポックがどのくらいの期間で構成されているかを返す関数。

この値はブロック数または時間（秒）で表され、`expiryType` によって単位が異なります。

**戻り値**

* `uint256`
  * エポックの長さ。

#### `isEpochExpired`

```javascript
function isEpochExpired(uint256 epoch) external view returns (bool);
```

指定したエポックが有効期限切れかどうかを確認する関数。
最新のエポックと比較して、指定エポックが期限切れであれば `true` を返します。

**引数**

* `epoch`
  * チェック対象のエポック番号。

**戻り値**

* `bool`
  * 期限切れなら `true`。

#### `unexpiredBalanceOf`

```javascript
function unexpiredBalanceOf(address account) external view returns (uint256);
```

指定アドレスが保有する有効なトークン数を返す関数。
期限切れでないトークンのみをカウントして返します。

**引数**

* `account`
  * 対象のウォレットアドレス。

**戻り値**

* `uint256`
  * 有効トークンの数。

#### `unexpiredBalanceOfAtEpoch`

```javascript
function unexpiredBalanceOfAtEpoch(uint256 epoch, address account) external view returns (uint256);
```

指定したエポック時点での有効トークン数を返す関数。

そのエポックがすでに期限切れの場合は、常に `0` を返します。

**引数**

* `epoch`
  * 対象のエポック番号。
* `account`
  * アカウントアドレス。

**戻り値**

* `uint256`
  * エポック内で有効なトークン数。

#### `validityDuration`

```javascript
function validityDuration() external view returns (uint256);
```

各トークンの有効期間をエポック単位で返す関数。
この値により、トークンがいくつのエポック期間だけ有効かがわかります。

**戻り値**

* `uint256`
  * 有効期間（エポック単位）。

#### 拡張関数の処理

| 関数 | 説明  |
| --- | ------- |
| `unexpiredBalanceOfAtEpoch` | 指定エポックが有効なら、その時点の使用可能トークン数を返す。期限切れなら 0。 |
| `unexpiredBalanceOf` | 現在有効なトークン数を返す。 |
| `currentEpoch`  | 現在のエポックを返す。 |
| `epochLength` | 1エポックの期間を返す。 |
| `validityDuration` | トークンの有効期間を返す（エポック数単位）。 |
| `isEpochExpired` | 指定したエポックが期限切れなら `true`。 |

## 追加の関数

以下の関数は任意に実装する関数です。

#### `getRemainingDurationBeforeTokenExpired`

```javascript
function getRemainingDurationBeforeTokenExpired(uint256 tokenId) public view returns (uint256);
```

指定トークンの期限までの残り時間または残りブロック数を返す関数。
`expiryType` に応じて、残りブロックまたは残り秒数を返します。

**引数**

* `tokenId`
  * 対象トークンID。

**戻り値**

* `uint256`
  * 残り期間。

#### `getEpochBalance`

```javascript
function getEpochBalance(uint256 epoch) public view returns (uint256);
```

指定エポックにおけるトークン数を返す関数。
期限切れのエポックでも、その時点での保持量を確認できます。

**引数**

* `epoch`
  * 対象エポック。

**戻り値**

* `uint256`
  * トークン数。

#### `getEpochInfo`

```javascript
function getEpochInfo(uint256 epoch) public view returns (uint256,uint256);
```

指定エポックの開始と終了情報を返す関数。
1つのエポックの始まりと終わりを確認できます。

**引数**

* `epoch`
  * 対象エポック。

**戻り値**

* `(uint256, uint256)`
  * 開始時刻と終了時刻。

#### `getNearestExpiryOf`

```javascript
function getNearestExpiryOf(address account) public view returns (uint256);
```

指定アカウントの中で、最も期限が近いトークンを返す関数。
トークンIDと推定される有効期限（ブロックまたは時刻）を返します。

**引数**

* `account`
  * 対象ウォレットアドレス。

**戻り値**

* `uint256`
  * 最も近い期限のトークンIDまたは期限時刻。

#### `getRemainingDurationBeforeEpochChange`

```javascript
function getRemainingDurationBeforeEpochChange() public view returns (uint256);
```

次のエポックへ切り替わるまでの残り時間を返す関数。
現在のブロックまたは時間を基に、次のエポックまでの残り期間を算出します。

**戻り値**

* `uint256`
  * 残りブロック数または残り秒数。

## 補足

#### 既存の仕組みに影響を与えない

**ERC7858**の設計思想の中心にあるのは、「**既存の仕組みを壊さない**」という考え方です。

有効期限（`expiration`）という機能は、既存のNFTやSBTの基本動作を変更せず、拡張的に付加されるレイヤーとして導入されています。
つまり、この標準を導入しても、既存のマーケットプレイスやウォレット、DAppが正常に動作しなくなることはありません。

具体的には以下のような特徴があります。

| 対象 | 影響 |
| --- | --- |
| **NFT（非SBT）** | transfer機能はそのまま保持されます。期限切れになっても送付可能です。 |
| **SBT（Soulbound Token）** | SBTの「****譲渡不可****」という特性を保ちながら、有効期限を設定できます。 |
| **既存DAppとの互換性** | 有効期限切れのトークンは「**存在するが無効**」として扱われるため、アプリケーションの互換性が維持されます。 |

このように、期限切れ（expired）＝削除ではなく、使用不可の状態として扱うことで、
安全かつ後方互換性の高い拡張仕様を実現しています。

#### 有効期限の種類

有効期限の指定方法として、**ERC7858**では以下の2種類が定義されています。

| 種類 | 定義 | 適した用途 |
| --- | --- | ------- |
| **Block-based（ブロックベース）** | `block.number` を基準に有効期限を計算。 | ブロック生成の間隔が安定しており、ネットワークアクティビティに依存するアプリケーション。例：マイニング系、オンチェーンゲーム。 |
| **Time-based（時間ベース）**    | `block.timestamp` を基準に有効期限を計算。 | 現実の時間軸に連動させたいアプリケーション。例：イベントチケット、サブスクリプション、会員証など。 |

この2種類の方式をサポートすることで、ブロックチェーンの種類やアプリケーションの特性に合わせた柔軟な運用が可能です。

ブロックベースはネットワークの活動量に比例する正確なブロック数制御を重視するケースに適し、時間ベースはユーザー体験や実時間に基づいたサービス運用に向いています。

## 互換性

**ERC7858**は、以下の既存規格との完全互換性を持つように設計されています。

| 規格 | 概要 | 互換性内容 |
| --- | --- | --------- |
| **ERC721**   | 一般的なNFTの標準規格。  | トークンの転送・所有・メタデータ機能を保持。 |
| **ERC5484**  | SBT（譲渡不可トークン）の規格。 | SBTとしての譲渡制限を維持しつつ、有効期限設定が可能。 |
| **その他SBT規格** | Soulbound系トークン全般。 | 有効期限機能を追加しても動作を妨げない。         |

つまり、既存のNFTやSBTのスマートコントラクトを再設計する必要はなく、
**ERC7858**を継承することでシームレスに有効期限機能を統合できます。

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

[\[ERC5484\] SBTのMint時にBurnできる権限を付与する仕組みを理解しよう！](/articles/12fbba291e17f96b4610)

## 参考実装

https://eips.ethereum.org/assets/eip-7858/

## セキュリティ

有効期限を導入することは、NFTやSBTの管理ロジックに新たな要素を加えることになります。
そのため、不正な再利用や権限なき更新への対策が必要です。

#### Burnと再発行

もしトークンを一度「`burn`」し、同じ `tokenId` で再発行（re-mint）できてしまうと、不正な期限延長が行える可能性があります。

対策としては以下が挙げられます。

* 一度Burnされた `tokenId` は、再利用不可にする設計が推奨されます。
* 再発行を許可する場合は、必ず期限・所有履歴・署名などを検証する仕組みを組み込み、不正なリセットを防ぐ必要があります。

#### 不正な更新

有効期限（`startTime` や `endTime`）の更新権限を誤って設定すると、第三者がトークンの有効期限を勝手に変更する危険性があります。

対策としては以下が挙げられます。

* `startTime` と `endTime` の更新は、**発行者（admin）または認可済みのロールのみ** が実行できるようにする。
* `onlyOwner` や `AccessControl`（例：OpenZeppelinの`DEFAULT_ADMIN_ROLE`）を利用して、更新操作を厳格に制限することが推奨されます。
* イベントログ `TokenExpiryUpdated` を監視することで、不正更新の早期検知も可能です。

## 引用

sirawt (@MASDXI), ADISAKBOONMARK (@ADISAKBOONMARK), parametprame (@parametprame), Nacharoen (@najaroen), "ERC-7858: Expirable NFTs and SBTs," Ethereum Improvement Proposals, no. 7858, January 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7858.

## 最後に

今回は「NFTとSBTに有効期限（ブロック高または時刻）を付与しする仕組みを提案している**ERC7858**」についてまとめてきました！
いかがだったでしょうか？

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