# [ERC7818] ERC20トークンに有効期限を持たせる仕組みを理解しよう！

> ERC7818では、ERC20標準と互換性を保ちつつ、トークンの有効期限をコントラクトレベルで管理・強制できる機能を追加する提案をしています。

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

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、**ERC20**トークンに有効期限を持たせて管理する仕組みを提案している**ERC7818**についてまとめていきます！

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

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

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

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

## 概要

**ERC7818**では、**ERC20**標準と互換性を保ちつつ、トークンの有効期限をコントラクトレベルで管理・強制できる機能を追加する提案をしています。

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

https://chaldene.net/erc20

有効期限を設定することで、以下のようなシナリオで特に有効です。

- 時間制限のある債券や金融商品。
    - 例：償還期限があるトークン化された国債や社債。
- ゲーム内で一定期間のみ利用可能なアイテムや通貨。
- 有効期限付きのロイヤルティプログラム。
    - 例：ポイントやクーポンの失効。
- 事前購入型のクレジットやサービス
    - 例：通信データ、キャッシュバック、ガソリン、計算資源。
- 後払いの通信データパッケージ。
    - 例：毎月リセットされる携帯データプラン。
- 特定のエコシステム内でのみ利用可能な電子マネー
    - 例：交通機関、フードコート、小売店など。

## 動機

この提案の背景として、時間制限があるトークンの必要性が挙げられます。
現在の**ERC20**標準では、トークンは基本的に無期限で有効ですが、多くのユースケースでは有効期限を設けることでより適切に管理できるようになります。

### 適用例

#### 金融商品・債券

例えば、一定期間後に満期を迎えるトークン化された債券などに適用できます。
これにより、満期後にトークンが無効化されて償還が実行されます。

#### ゲーム内資産

イベント限定アイテムや期間限定のゲーム内通貨に適用し、ゲーム内経済のバランスを維持できます。

#### ロイヤルティプログラム

有効期限付きのポイントを管理し、一定期間内の利用を促進できます。

例）クレジットカードのキャッシュバックやマイレージ など。

#### プリペイド・ポストペイドサービス

事前に購入したデータパッケージ、ガソリン、クラウドコンピューティングのクレジット などを、一定期間内に使用しないと失効するように設定できる。
例）通信会社のデータプラン（毎月リセットされる）。

#### 電子マネー（e-Money）

交通機関やフードコート、小売店内でのみ利用可能な電子マネーに適用可能。
期限付きの支払い手段を導入し、短期間での利用を促すことができる。

## 仕様

### Epoch Mechanism

エポック（Epoch）とは、トークンの有効性を管理するために設定された特定の期間またはブロック範囲のことを指します。
このメカニズムにより、トークンが一定の時間またはブロック数の範囲内でのみ有効となり、その後は無効（期限切れ）となるように制御できます。

エポックは以下の2種類の方法で定義されます。

- **ブロックベース（Block-based）**
   - 一定のブロック数（例：1000ブロック）を1エポックとする。
   - エポックが進むたびに、トークンの有効性が判定される。
   - PoW（Proof of Work）やPoS（Proof of Stake）のブロック生成間隔が一定ではない場合、時間よりもブロック単位の方が適切なケースがある。

- **時間ベース（Time-based）**
   - 一定の秒数（例：1000秒）を1エポックとする。
   - コントラクト内部で、ブロックのタイムスタンプを参照して有効期間を判定する。

トークンが特定のエポックにリンクされている場合、そのエポックがアクティブな間はトークンは有効だが、エポックが終了すると無効（期限切れ）となり送信や使用ができなくなります。

### Balance Look Back Over Epochs

以下のエポックメカニズムでは、トークンの有効な`balance`（利用可能残高）を取得するために、現在のエポックと過去のエポックを参照することができます。

1. 現在のエポックから、過去のエポック（nエポック前）までの有効なトークンをチェックする。
2. 期限切れでないトークンの合計を利用可能`balance`として計算する。
3. 設定された`n`により、どの範囲まで遡ってトークンを有効とするかを調整できる。

これにより、一定の範囲（nエポック前まで）の有効なトークンの合計を計算することが可能です。

#### 例

| **エポック** | **バランス** |
|-------------|------------|
| 1           | 100        |
| 2           | 150        |
| 3           | 200        |

- 現在のエポック。
    - `3`
- 1エポック前まで（`n=1`）有効なトークンを取得。
- 利用可能残高
    - `150 (Epoch 2) + 200 (Epoch 3) = 350`

この仕組みを活用すると、「**nエポック前までの有効なトークン**」を取得できるため、時間制約付きのトークンに対して柔軟な`balance`管理が可能になります。

### コントラクトの要件

この拡張を**ERC20**と互換性を持たせるために、以下の条件を満たす必要があります。

- **ERC20の標準インターフェースを継承**
   - 全ての基本的な**ERC20**機能（`balanceOf`、`transfer`、`approve`など）をサポートする。
   
- **エポック管理機能を実装**
   - `epoch`の管理。
   - `epoch`に紐づく`balance`の管理。
   - エポックの進行に伴うトークンの無効化。

- **有効な残高の計算機能**
   - 過去`n`エポック前までの有効なトークンを取得。
   - `usableBalanceOf(address, n)`関数を実装。

このように、エポックメカニズムを用いることで一定期間のみ有効なトークンを設計でき、トークンの自動失効や有効期限付きの資産管理が容易になるというメリットがあります。

### インターフェース

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

/**
 * @title ERC-7818 interface
 * @dev Interface for adding expirable functionality to ERC20 tokens.
 */

import "./IERC20.sol";

interface IERC7818 is IERC20 {
    /**
     * @dev Enum represents the types of `epoch` that can be used.
     * @notice The implementing contract may use one of these types to define how the `epoch` is measured.
     */
    enum EPOCH_TYPE {
        BLOCKS_BASED, // measured in the number of blocks (e.g., 1000 blocks)
        TIME_BASED // measured in seconds (UNIX time) (e.g., 1000 seconds)
    }

    /**
     * @dev Retrieves the balance of a specific `epoch` owned by an account.
     * @param epoch The `epoch for which the balance is checked.
     * @param account The address of the account.
     * @return uint256 The balance of the specified `epoch`.
     * @notice "MUST" return 0 if the specified `epoch` is expired.
     */
    function balanceOfAtEpoch(
        uint256 epoch,
        address account
    ) external view returns (uint256);

    /**
     * @dev Retrieves the latest epoch currently tracked by the contract.
     * @return uint256 The latest epoch of the contract.
     */
    function currentEpoch() external view returns (uint256);

    /**
     * @dev Retrieves the duration of a single epoch.
     * @return uint256 The duration of a single epoch.
     * @notice The unit of the epoch length is determined by the `validityPeriodType` function.
     */
    function epochLength() external view returns (uint256);

    /**
     * @dev Returns the type of the epoch.
     * @return EPOCH_TYPE  Enum value indicating the unit of an epoch.
     */
    function epochType() external view returns (EPOCH_TYPE);
    
    /**
     * @dev Retrieves the validity duration in `epoch` counts.
     * @return uint256 The validity duration in `epoch` counts.
     */
    function validityDuration() external view returns (uint256);

    /**
     * @dev Checks whether a specific `epoch` is expired.
     * @param epoch The `epoch` to check.
     * @return bool True if the token is expired, false otherwise.
     * @notice Implementing contracts "MUST" define and document the logic for determining expiration,
     * typically by comparing the latest epoch with the given `epoch` value,
     * based on the `EPOCH_TYPE` measurement (e.g., block count or time duration).
     */
    function isEpochExpired(uint256 epoch) external view returns (bool);

    /**
     * @dev Transfers a specific `epoch` and value to a recipient.
     * @param epoch The `epoch` for the transfer.
     * @param to The recipient address.
     * @param value The amount to transfer.
     * @return bool True if the transfer succeeded, otherwise false.
     */
    function transferAtEpoch(
        uint256 epoch,
        address to,
        uint256 value
    ) external returns (bool);

    /**
     * @dev Transfers a specific `epoch` and value from one account to another.
     * @param epoch The `epoch` for the transfer.
     * @param from The sender's address.
     * @param to The recipient's address.
     * @param value The amount to transfer.
     * @return bool True if the transfer succeeded, otherwise false.
     */
    function transferFromAtEpoch(
        uint256 epoch,
        address from,
        address to,
        uint256 value
    ) external returns (bool);
}
```

#### `EPOCH_TYPE`

```js
enum EPOCH_TYPE {
    BLOCKS_BASED, // measured in the number of blocks (e.g., 1000 blocks)
    TIME_BASED // measured in seconds (UNIX time) (e.g., 1000 seconds)
}
```

**概要**
エポックをどの単位で測定するかを定義している列挙型。
コントラクトの実装に応じて、2種類の方法でエポックを計算。

**詳細**

- `BLOCKS_BASED`

ブロック数でエポックを定義する。
例）`1000`ブロックごとに`1`つのエポックをカウントする場合、ブロックが`1000`増えるごとにエポックが進む。
ブロック時間が一定でない場合に適用しやすい。

- `TIME_BASED`

UNIXタイム（秒単位） でエポックを定義する。
例）`1000`秒ごとに`1`つのエポックをカウントする場合、`1000`秒が経過するごとにエポックが進む。
ブロック生成時間が不安定な場合でも、一貫した時間管理が可能。

---

#### `balanceOfAtEpoch`

```js
function balanceOfAtEpoch(uint256 epoch, address account) external view returns (uint256);
```

**概要**  
指定されたエポックでのアカウントのトークン残高を取得する関数。  

**詳細**  
この関数は、指定されたエポック内でのアカウントのトークン残高を返します。  
ただし、指定されたエポックが期限切れの場合は以下のように`0`を返します。  
例）エポック`5`が有効期限を超えている場合、`balanceOfAtEpoch(5, address)`は`0`を返す。  

**引数**  
- `epoch`  
  - 取得するエポックの番号。  
- `account`  
  - 残高を取得するアカウントのアドレス。  

**戻り値**  
- `uint256`  
  - 指定されたエポックでのトークン残高。
  - 期限切れの場合は`0`。  

---

#### `currentEpoch`

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

**概要**  
コントラクトで管理されている現在のエポックを取得する関数。  

**詳細**  
この関数は、現在のエポック番号を返します。  
エポックはブロックベースまたは時間ベースで進行し、`epochType()`の設定によって決まります。  

**戻り値**  
- `uint256`  
  - 現在のエポック番号。  

---

#### `epochLength`

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

**概要**  
1つのエポックの長さを取得する関数。  

**詳細**  
この関数は、エポックの単位（ブロック数または時間）に基づいた持続時間を返します。  
例えば、`epochType()`が`BLOCKS_BASED`の場合はブロック数、`TIME_BASED`の場合は秒数となる。  

**戻り値**  
- `uint256`  
  - 1エポックの長さ（ブロック数または秒数）。  

---

#### `epochType`

```js
function epochType() external view returns (EPOCH_TYPE);
```

**概要**  
エポックの単位（ブロックベースか時間ベース）を取得する関数。  

**詳細**  
この関数は、エポックの種類を`EPOCH_TYPE`列挙型として返します。  

**戻り値**  
- `EPOCH_TYPE`  
  - エポックの種類（`BLOCKS_BASED`または`TIME_BASED`）。  

---

#### `validityDuration`

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

**概要**  
トークンの有効期間をエポック単位で取得する関数。  

**詳細**  
この関数は、トークンが有効である期間（エポック数）を返します。  
例えば、`validityDuration()`が`10`の場合、トークンは`10`エポック後に期限切れとなります。  

**戻り値**  
- `uint256`  
  - トークンの有効期間（エポック単位）。  

---

#### `isEpochExpired`

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

**概要**  
指定されたエポックが期限切れかどうかを判定する関数。  

**詳細**  
この関数は、指定されたエポックが現在のエポックから見て期限切れかどうかを返します。  
通常、`validityDuration()`を用いて有効期間を決定し、現在のエポックと比較して期限切れを判定します。  

**引数**  
- `epoch`  
  - 期限切れかどうかを判定するエポック番号。  

**戻り値**  
- `bool`  
  - `true`
      - 期限切れ。  
  - `false`
      - 有効。  

---

#### `transferAtEpoch`

```js
function transferAtEpoch(uint256 epoch, address to, uint256 value) external returns (bool);
```

**概要**  
指定されたエポックのトークンを別のアドレスに送付する関数。  

**詳細**  
この関数は、指定されたエポックに属するトークンを送付します。  
期限切れのトークンを送信しようとした場合、トランザクションは`revert`するか`false`を返します。  

**引数**  
- `epoch`  
  - 転送するトークンのエポック。  
- `to`  
  - 受取アドレス。  
- `value`  
  - 送信するトークンの数量。  

**戻り値**  
- `bool`  
  - `true`
      - 送信成功。  
  - `false`
      - 送信失敗（期限切れトークンの送信など）。  

---

#### `transferFromAtEpoch`

```js
function transferFromAtEpoch(uint256 epoch, address from, address to, uint256 value) external returns (bool);
```

**概要**  
指定されたエポックのトークンを、保有アドレス以外のアドレスから送信する関数。  

**詳細**  
この関数は、`transferAtEpoch()`の`from`指定バージョンであり、`msg.sender`以外のアドレスのトークンを送付できます。  
期限切れのトークンを送信しようとした場合、トランザクションは`revert`するか`false`を返します。  
通常、`approve`を利用して事前に`msg.sender`に送信権限を付与する必要があります。  

**引数**  
- `epoch`  
  - 送付するトークンのエポック。  
- `from`  
  - 送信元のアドレス。  
- `to`  
  - 受取アドレス。  
- `value`  
  - 送信するトークンの数量。  

**戻り値**  
- `bool`  
  - `true`
      - 送信成功。  
  - `false`
      - 送信失敗（期限切れトークンの送信など）。  

---

### オプション

#### `getEpochBalance`

指定したエポックのトークン残高を取得する関数。  
この関数は期限切れのエポックも含めて残高を取得できます。

#### `getEpochInfo`

指定したエポックの開始時刻（またはブロック）と終了時刻（またはブロック）を取得する関数。  
これにより、特定のエポックがいつ開始・終了するかを確認できます。

#### `getNearestExpiryOf`

期限が最も近いトークンの情報を取得する関数。  
ユーザーはどのトークンを優先して使用するべきかを判断できるようになります。

#### `getRemainingDurationBeforeEpochChange`

現在のエポックが終了するまでの時間またはブロック数を取得する関数。  
- `BLOCKS_BASED`の場合、現在のブロックとエポックのブロック長から計算します。  
- `TIME_BASED`の場合、現在のタイムスタンプとエポックの秒数から計算します。  

この関数を使えば、次のエポックがいつ切り替わるかをユーザーが把握できるようになります。

## 補足

### エポックの抽象性と柔軟性
エポックという概念は抽象的なものであり、さまざまな方法で実装できる余地があります。  
エポックを導入することで、トークンの有効期限管理においてより細かい制御や*バルク管理が可能になります。  
具体的には、以下のような異なるアプローチを選択できるようになります。

#### より細かいトラッキング

エポックを活用すると、各エポック内でより細かくトークンを管理することができます。  

例えば以下のようなことが可能です。
- トークンごとに異なるエポックを設定し、有効期間を個別に管理。
- 動的なエポック期間を設定し、トークンの用途に応じて有効期間を調整可能。

この仕組みを導入すれば、用途に応じて特定の条件下でのみ有効なトークンを作成することができます。  

- ステーキング報酬のように、獲得したタイミングに応じて異なる期限を持つトークンを発行できる。  
- サブスクリプション型のトークン（例：ゲームの月額課金）で、発行日によって有効期限を変えることができる。  

### Bulk Expiration

一方で、エポックを活用すれば、まとめてトークンを期限切れにするバルク処理（一括管理）も可能です。  
このアプローチでは、全てのトークンが特定のエポックで同時に期限切れになるという仕組みを採用します。  

この仕組みを採用することで以下のことが実現可能です。

- **ゲームのシーズンごとの通貨リセット**
  - 例）シーズン2が開始したら、シーズン1の通貨をすべて無効化する。
- **キャンペーンのポイント管理**
  - 例）特定のキャンペーンで付与したポイントを、キャンペーン終了後に一括で失効させる。
- **政府・企業のクーポン**
  - 例）ある一定の期間（1年間）で発行されたクーポンを、年度末で無効化する。

この方法は、個別のトークンの期限を管理するよりもガスコストが抑えられる というメリットがあります。

### Lazy Expiry導入

エポックベースのトークン管理を採用することで、Lazyな方法でトークンの期限切れを管理できるようになります。  
つまり、トークンの有効期限を明示的に更新するのではなく、コントラクトの「現在のエポック」を参照するだけで期限を管理できるようになります。

通常の有効期限管理では、ユーザーや管理者が手動で期限切れを更新したり、定期的にトークンのステータスを変更するために、書き込み操作が必要です、

一方で、エポックベースの仕組みでは、期限切れかどうかを「現在のエポック」との比較で判定できたり、明示的な書き込みなしでトークンの期限を自動的に管理できるようになります。
これにより、ガス代を削減してより効率的な管理が可能になります。

#### Lazy Expiryのメリット

- **ガスコストの削減**
   - 期限切れの状態を毎回書き換えずに、`currentEpoch()`の値を計算するだけで済む。
- **柔軟な有効期限の管理**
   - トークンの有効期限をユーザーや管理者が手動で変更する必要がなく、コントラクトがエポックの進行によって自動で管理できる。
- **外部サービスの不要化**
   - 通常、定期的に期限切れを処理するためにオフチェーンサービスを使うことがあるが、エポックを使うことでその必要がなくなる。

#### Lazy Expiry の具体的な動作

1. トークンの有効期限は、エポックの進行によってのみ決まる。
2. あるエポック（例：10）が現在のエポック（例：15）を超えた場合、そのトークンは期限切れと判定される。
3. 書き込み操作なしに、関数`isEpochExpired(10)`を呼び出せば自動的に`true`となる。

## 互換性

**ERC7818**は、**ERC20**と完全に互換性があります。

## 参考実装

以下に参考実装がまとめられています。

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

## セキュリティ

### Denial of Service（DoS攻撃）

エポックを活用したトークン管理において、複数のエポックに分散している小規模なトークンを送付するようなケースでは、トランザクションのガス消費量が急増する可能性があります。

#### 具体例

例えば、ユーザーが100エポックに分かれているトークンを1つの`transfer`で送ろうとした場合、全てのエポックをチェックする必要があるためガス消費が大きくなります。
また、ループ処理で`balanceOfAtEpoch(epoch, user)`を複数回呼び出す場合、特にエポックが長期間積み重なるとガスが膨大になります。

#### 対策

- **バッチ処理の制限**  
   - 1回のトランザクションで処理できるエポック数を制限する。  
   - 例）一度に処理可能な最大エポック数を`10`に制限。  
- **ガス最適化の工夫**  
   - 期限切れのエポックはスキップして計算量を減らす。  
   - 可能な限りループを回避し、計算量を抑える設計をする。  
- **Lazy Expiryの活用**  
   - 明示的な書き込み操作を減らし、エポック単位で処理することでガス消費を抑える。  


###  ガスリミットの脆弱性

ブロックチェーンには、1つのブロックで実行できるガスの上限が存在します。 
例えば、EthereumのL1では30Mガスが上限となっています。  
もし、1つのトランザクションがこの上限を超えてしまうと、トランザクションが永久にブロックに含まれなくなります。

#### 具体例
- 大量のエポックを管理するトークン（例：10,000エポック）で、  
  `balanceOf(account)`を呼び出した時、各エポックを参照する処理が大量に発生してガスリミットを超える可能性がある。
-*流動性プール（Liquidity Pool）に多くのトークンを入れておき、エポックごとに管理するような場合、`transfer`処理時に全てのエポック情報を確認しようとするとガスリミットを超えてしまう。

#### 対策

- **効率的なデータ管理**
   - `mapping`を使用し、不要なエポックデータを参照しない設計にする。  
   - `mapping(uint256 => uint256)`のような形で、エポックごとにバランスを管理する。  
- **ブロックガス制限を考慮した処理**
   - 1つのトランザクション内で処理するデータ量を制限する。  
   - `gasleft()`を活用して、トランザクション中にガスが不足しそうな場合は早期終了する。  
- **オフチェーン処理を活用**
   - オフチェーンで期限切れを管理し、必要最小限のデータをコントラクトに送信する。  


### ブロック値を時間の代替として使うリスク

通常、`block.timestamp`を使って時間ベースのエポックを管理するが、  
ネットワークが停止すると`block.timestamp`も更新されなくなります。

#### 具体例

- **EthereumやL2のネットワークが数時間停止した場合**  
    - `block.timestamp`が進まなくなり、トークンの期限管理が完全にフリーズする。  
- **PoSチェーンにおいて、バリデータがスラッシュ（罰則）を恐れてブロックを生成しなくなった場合**
    - その間に`block.timestamp`が更新されず、エポック管理が機能しなくなる。  

これにより、期限切れになるはずのトークンがいつまでも有効になってしまったり、流動性プールやゲームのバランスが崩れる恐れがあります。

#### 対策
- **時間とブロックの両方を考慮**
   - `block.timestamp`だけでなく、ブロック番号も考慮したロジックを設計する。  
   - 例）
     ```js
     uint256 estimatedTime = block.number * AVERAGE_BLOCK_TIME;
     ```
- **オフチェーンオラクルを活用**
   - ChainlinkやPythのようなオラクルを使い、外部の時間情報を取得する。  
- **手動での期限管理オプション**
   - 管理者がエポックを強制的に進める機能を用意することで、ネットワーク停止時のリスクを軽減する。  


### 公平性の懸念

全てのトークンが同時に期限切れになる設計の場合、特定のユーザーが不公平な扱いを受ける可能性があります。  

#### 具体例
- **ゲームのイベント報酬で、期限が同じトークンを発行した場合**  
    - 一部のプレイヤーは直前に使えて、一部のプレイヤーは使えないまま期限切れになる。  
- **バルクエクスパイアを悪用した経済攻撃**
    - 意図的に期限切れ直前のトークンを売却し、他のユーザーに価値のないトークンを掴ませる。  

#### **対策**
- **トークンごとの有効期限をランダム化する**
   - 例）トークン発行時に `validityDuration` を個別設定。  
- **期限前の通知メカニズムを導入**
   - `onExpirationWarning()`のようなイベントを追加し、ユーザーに通知。  


### 流動性プールでのリスク

DEXの流動性プール（Uniswapなど）に期限付きトークンを預けた場合、プール内で期限が切れてしまい取引不能なトークンが発生する可能性があります。

#### 対策
- **期限切れのトークンを自動で取り除く仕組みを導入**。
- **流動性プール内で期限が切れたトークンの取引を制限**。
- **期限切れのトークンが LP トークンに影響を与えないように設計**。

## 最後に

今回は「**ERC20**トークンに有効期限を持たせて管理する仕組みを提案している**ERC7818**」についてまとめてきました！
いかがだったでしょうか？

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