# [ERC1996] 将来の送金を実行できるHold機能の仕組みを理解しよう！

> ERC1996は、ERC20トークンに「Hold（保留）」機能を追加する拡張提案です。この機能により、将来的に確実なトークン送付を約束することが可能になります。

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

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、**ERC20**のトークンをロックして、将来ロックした分のトークンを送付する仕組みを提案している**ERC1996**についてまとめていきます！

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

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

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

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

## 概要

**ERC1996**は、**ERC20**トークンに「**Hold（保留）**」機能を追加する拡張提案です。
この機能により、将来的に確実なトークン送付を約束することが可能になります。Holdにより保留されたトークンは、送付が実行または解除されるまで他の送付や使用ができません。

Holdは「**エスクロー（第三者預かり）**」に似た仕組みで、資産の一時的なブロックと後の確定的な送付を保証します。
この機能は、規制トークンや保証付き支払い、金額未確定の決済など、即時移転が望ましくない場面で有効です。

Holdの仕組みには、以下が関与します。

* **Operator（オペレーター）**
    保留操作を委任されたアカウント
* **Hold Issuer（保留発行者）**
    Holdを作成するアカウント（自身またはオペレーター）
* **Notary（ノータリー）**
    Holdの実行（または解除）を決定するアカウント

Holdは以下の情報によって構成されます。

* 支払元（payer）
* 支払先（payee）
* 最大金額
* ノータリー
* 有効期限

Holdが作成されると、指定された支払元のトークン残高がその分ロックされます。
保留中は他の送付ができません。
Holdを実行できるのはノータリーだけで、ノータリーの判断により支払元から支払先への送付が完了します。
一方で、Holdはノータリーまたは有効期限経過後に誰でも解除することができ、その場合は送付は実行されず、ブロックされていたトークンは再び使用可能となります。

また、Holdは部分的に実行することも可能です。
例えば、最大10トークンまで保留していた場合に、ノータリーが5トークンだけ送付すると、残りの5トークンは再び支払元の利用可能残高として戻ります。
さらに、Holdを永続的に設定することもでき、この場合は期限による解除ができず、必ずノータリーまたは支払先の操作が必要となります。

## 動機

このHold機能は、即時にトークンを送付することが適切でない、あるいは事前に送付を保証する必要がある状況で有用です。

例えば、規制されたトークンの場合、移転の前にコンプライアンスチェックを行う必要があります。
Holdはその間、トークンを一時的にブロックすることで、チェック通過後の確実な送付を保証します。
もし規制に違反していると判断された場合は、そのHoldは解除されてトークンは送付されずに元に戻ります。

また、事前保証が必要な決済においても有効です。
ホテルのチェックイン時に部屋代を保証するために、ゲストの口座に対してHoldを設定するなどです。
この時点で実際に料金は引き落とされませんが、トークンはブロックされており、宿泊後にノータリーが送付を実行することで支払いが完了します。

さらに、金額が確定していない取引の保証にも活用できます。
例えば、ホテルがルームサービスなど追加料金を想定して、一定額をHoldしておくことができます。
チェックアウト時に実際にかかった分だけ実行され、残りは解除されます。

**ERC20**の `approve` 関数にも似た機能がありますが、大きな違いは「**トークンがブロックされるかどうか**」です。
`approve` は承認されているだけで実際にはトークンが確保されておらず、他の用途に使われてしまう可能性があります。
一方、Holdは実際にブロックが行われるため、送付が確実に保証されるのです。

このように、Hold機能はトークン移転における信頼性と柔軟性を高めるための重要な拡張となっています。

## 仕様

```js
interface IHoldable /* is ERC-20 */ {
    enum HoldStatusCode {
        Nonexistent,
        Ordered,
        Executed,
        ReleasedByNotary,
        ReleasedByPayee,
        ReleasedOnExpiration
    }

    function hold(string calldata operationId, address to, address notary, uint256 value, uint256 timeToExpiration) external returns (bool); 
    function holdFrom(string calldata operationId, address from, address to, address notary, uint256 value, uint256 timeToExpiration) external returns (bool);
    function releaseHold(string calldata operationId) external returns (bool);
    function executeHold(string calldata operationId, uint256 value) external returns (bool);
    function renewHold(string calldata operationId, uint256 timeToExpiration) external returns (bool);
    function retrieveHoldData(string calldata operationId) external view returns (address from, address to, address notary, uint256 value, uint256 expiration, HoldStatusCode status);

    function balanceOnHold(address account) external view returns (uint256);
    function netBalanceOf(address account) external view returns (uint256);
    function totalSupplyOnHold() external view returns (uint256);

    function authorizeHoldOperator(address operator) external returns (bool);
    function revokeHoldOperator(address operator) external returns (bool);
    function isHoldOperatorFor(address operator, address from) external view returns (bool);

    event HoldCreated(address indexed holdIssuer, string  operationId, address from, address to, address indexed notary, uint256 value, uint256 expiration);
    event HoldExecuted(address indexed holdIssuer, string operationId, address indexed notary, uint256 heldValue, uint256 transferredValue);
    event HoldReleased(address indexed holdIssuer, string operationId, HoldStatusCode status);
    event HoldRenewed(address indexed holdIssuer, string operationId, uint256 oldExpiration, uint256 newExpiration);
    event AuthorizedHoldOperator(address indexed operator, address indexed account);
    event RevokedHoldOperator(address indexed operator, address indexed account);
}
```

### Enum

#### `HoldStatusCode`

```javascript
enum HoldStatusCode {
    Nonexistent,
    Ordered,
    Executed,
    ReleasedByNotary,
    ReleasedByPayee,
    ReleasedOnExpiration
}
```

Holdの状態を表すための列挙型。
各Holdが現在どのような状態にあるか（未作成、保留中、実行済み、ノータリーによる解除など）を示します。

**パラメータ**

* `Nonexistent`
    * 存在しない（または削除済み）のHold。
* `Ordered`
    * 有効なHoldが設定されている状態。
* `Executed`
    * Holdが実行され、トークンが送付された状態。
* `ReleasedByNotary`
    * ノータリーによって解除された状態。
* `ReleasedByPayee`
    * 支払先によって解除された状態。
* `ReleasedOnExpiration`
    * 有効期限が切れて自動的に解除された状態。

### 関数

#### `hold`

```javascript
function hold(
    string calldata operationId,
    address to,
    address notary,
    uint256 value,
    uint256 timeToExpiration
) external returns (bool);
```

自分自身（`msg.sender`）の残高を使ってHoldを作成する関数。
この関数を使うことで、指定した支払先（`to`）に向けて、指定量のトークンを保留にします。
`notary` はこのHoldを実行または解除する権限を持つアドレスです。

**引数**

* `operationId`
    * 一意の識別子。再利用は不可。
* `to`
    * トークンを受け取るアドレス。
* `notary`
    * Holdを実行・解除する役割のアドレス。
* `value`
    * 保留するトークンの量（保留元の残高以下）。
* `timeToExpiration`
    * 有効期限（ブロックタイムからの秒数）。0なら永続Hold。

**戻り値**

* `bool`
    * 成功すれば `true` を返します。

#### `holdFrom`

```javascript
function holdFrom(
    string calldata operationId,
    address from,
    address to,
    address notary,
    uint256 value,
    uint256 timeToExpiration
) external returns (bool);
```

他人の残高を使ってHoldを作成する関数。
`from` アカウントからトークンを保留するための関数で、事前に `authorizeHoldOperator` による承認が必要です。

**引数**

* `operationId`
    * 一意の識別子。
* `from`
    * 保留元のアドレス（保留されるトークンの持ち主）。
* `to`
    * トークンの受け取り先。
* `notary`
    * Holdを実行・解除する役割のアドレス。
* `value`
    * 保留するトークンの量。
* `timeToExpiration`
    * 有効期限（0の場合は永続Hold）。

**戻り値**

* `bool`
    * 成功時に `true` を返す。

#### `releaseHold`

```javascript
function releaseHold(string calldata operationId) external returns (bool);
```

Holdを解除する関数。
Holdが実行されることなく中止される操作です。
未期限切れの場合は、`notary` または `to` のいずれかが解除できます。
期限切れ後は誰でも解除可能です。

**引数**

* `operationId`
    * 対象となるHoldのID。

**戻り値**

* `bool`
    * 成功時に `true` を返します。

#### `executeHold`

```javascript
function executeHold(string calldata operationId, uint256 value) external returns (bool);
```

Holdを実行し、保留されたトークンを実際に送付する関数。
`notary` のみが実行可能です。
指定金額を送付し、もし保留額より少ない場合は、残りの分は元のアドレスに戻されます。

**引数**

* `operationId`
    * 対象Holdの識別子。
* `value`
    * 実行して送付する金額。

**戻り値**

* `bool`
    * 実行が成功した場合は `true`。

#### `renewHold`

```javascript
function renewHold(string calldata operationId, uint256 timeToExpiration) external returns (bool);
```

Holdの有効期限を更新する関数。
まだ期限が切れていないHoldのみ更新できます。
更新後の期限は現在のブロック時刻からの相対秒数で指定します。
`0` を指定した場合は永続Holdになります。
更新は `payer` または `operator` のみ可能です。

**引数**

* `operationId`
    * 対象のHoldの識別子。
* `timeToExpiration`
    * 新たな有効期限（秒単位）。

**戻り値**

* `bool`
    * 更新が成功した場合は `true`。

#### `retrieveHoldData`

```javascript
function retrieveHoldData(string calldata operationId)
external view returns (
    address from,
    address to,
    address notary,
    uint256 value,
    uint256 expiration,
    HoldStatusCode status
);
```

Holdの情報を取得する関数。
特定のHoldに関連するすべてのデータ（送信者・受取人・ノータリー・金額・期限・状態）を取得できます。

**引数**

* `operationId`
    * 対象のHold識別子。

**戻り値**

* `from`
* `to`
* `notary`
* `value`
* `expiration`
* `status`

#### `balanceOnHold`

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

指定アカウントの保留中トークン残高を取得する関数。
この関数は、アカウントのうち他者への送付や使用ができない「Hold」状態のトークン量を返します。

**引数**

* `account`
    * 保留残高を調べたいアドレス。

**戻り値**

* `uint256`
    * 現在Hold中のトークン量。

#### `netBalanceOf`

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

トークンの実質的な保有残高（通常残高＋Hold中）を取得する関数。
`balanceOf` と `balanceOnHold` を合計した値を返します。
これにより、実際に保有している全トークン量を確認できます。

**引数**

* `account`
    * 対象のアカウントアドレス。

**戻り値**

* `uint256`
    * 合計トークン保有量。

#### `totalSupplyOnHold`

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

コントラクト全体でHold中のトークンの合計量を取得する関数。
全てのアカウントにわたって、現在Hold中で一時的に送付不能なトークンの合計を返します。

**戻り値**

* `uint256`
    * 全Hold中のトークン合計量。

#### `authorizeHoldOperator`

```javascript
function authorizeHoldOperator(address operator) external returns (bool);
```

指定したアドレスに対して、Holdを発行する権限を与える関数。
この関数を使うことで、自分の代わりにHoldを作成できるオペレーターを指定できます。

**引数**

* `operator`
    * Hold操作を許可したいアドレス。

**戻り値**

* `bool`
    * 成功すれば `true` を返す。

#### `revokeHoldOperator`

```javascript
function revokeHoldOperator(address operator) external returns (bool);
```

オペレーターとして承認していたアドレスの権限を取り消す関数。
`authorizeHoldOperator` で承認したオペレーターを無効にすることができます。

**引数**

* `operator`
    * 承認を取り消したいアドレス。

**戻り値**

* `bool`
    * 成功時は `true`。

#### `isHoldOperatorFor`

```javascript
function isHoldOperatorFor(address operator, address from) external view returns (bool);
```

指定されたアドレスが、あるアカウントに対してHoldオペレーターとして承認されているかを確認する関数。
`operator` が `from` の代わりにHoldを発行できるかどうかを確認します。

**引数**

* `operator`
    * オペレーターとして確認したいアドレス。
* `from`
    * 対象となるアカウントアドレス。

**戻り値**

* `bool`
    * オペレーターであれば `true`。

### イベント

#### `HoldCreated`

```javascript
event HoldCreated(
    address indexed holdIssuer,
    string operationId,
    address from,
    address to,
    address indexed notary,
    uint256 value,
    uint256 expiration
);
```

Holdが新たに作成されたときに発行されるイベント。
保留の作成に成功した際に、全ての関係情報を含めて発行されます。

**パラメータ**

* `holdIssuer`
    * Holdの作成者。
* `operationId`
    * 一意の操作ID。
* `from`
    * 保留元のアドレス。
* `to`
    * トークンの受取人。
* `notary`
    * 実行／解除を判断するノータリー。
* `value`
    * 保留したトークン量。
* `expiration`
    * 有効期限のUNIXタイムスタンプ。

#### `HoldExecuted`

```javascript
event HoldExecuted(
    address indexed holdIssuer,
    string operationId,
    address indexed notary,
    uint256 heldValue,
    uint256 transferredValue
);
```

Holdが実行されたときに発行されるイベント。
Holdの実行により、指定金額のトークンが送付された時に発行されます。
部分実行も考慮されています。

**パラメータ**

* `holdIssuer`
    * Holdを作成したアドレス。
* `operationId`
    * 対象のHoldのID。
* `notary`
    * 実行を行ったノータリー。
* `heldValue`
    * 保留されていた金額。
* `transferredValue`
    * 実際に送付された金額。

#### `HoldReleased`

```javascript
event HoldReleased(
    address indexed holdIssuer,
    string operationId,
    HoldStatusCode status
);
```

Holdが解除されたときに発行されるイベント。
ノータリー、受取人、または期限切れにより解除された場合に発行されます。
どの解除方法かは `status` で識別されます。

**パラメータ**

* `holdIssuer`
    * Hold作成者。
* `operationId`
    * Holdの識別ID。
* `status`
    * 解除の種別（`ReleasedByNotary`など）。

#### `HoldRenewed`

```javascript
event HoldRenewed(
    address indexed holdIssuer,
    string operationId,
    uint256 oldExpiration,
    uint256 newExpiration
);
```

Holdの有効期限が更新された時に発行されるイベント。
期限延長または永続化が行われた際に、旧期限と新期限を含んで通知されます。

**パラメータ**

* `holdIssuer`
    * Holdを発行したアドレス。
* `operationId`
    * 対象Holdの識別子。
* `oldExpiration`
    * 更新前の有効期限。
* `newExpiration`
    * 更新後の有効期限。

#### `AuthorizedHoldOperator`

```javascript
event AuthorizedHoldOperator(address indexed operator, address indexed account);
```

オペレーターが新たに承認された時に発行されるイベント。
指定された `operator` が `account` に対してHold操作を行う権限を得たことを通知します。

**パラメータ**

* `operator`
    * 承認されたオペレーター。
* `account`
    * 権限が与えられた対象アカウント。

#### `RevokedHoldOperator`

```javascript
event RevokedHoldOperator(address indexed operator, address indexed account);
```

オペレーターの承認が取り消された時に発行されるイベント。
以前に承認されていた `operator` のHold操作権限が `account` に対して取り消されたことを通知します。

**パラメータ**

* `operator`
    * 承認を取り消されたオペレーター。
* `account`
    * 対象アカウント。

## 補足

### なぜHold機能が必要か

**ERC1996**は、将来のトークン送付を確実に保証するための仕組みを提供します。
ビジネス上、多くの場面で「**今すぐに送金はしないが、将来的に確実に支払う**」ことを証明する必要があります。
例えば、ホテル予約やサブスクリプションサービス、規制されたトークンの決済処理などがその典型です。

既存の**ERC20**における `approve` 関数では、第三者がトークンを使う「許可」を出すことはできますが、実際にそのトークンが確保されているわけではありません。
`approve` によって設定された金額は「**使用可能な上限**」に過ぎず、他の操作で先に使われてしまう可能性もあります。
したがって、支払いの確実性を担保する仕組みとしては不十分です。

一方、**ERC1996**の「Hold」は、実際にトークンをブロックして送付不能状態にすることで、将来の支払いを確実に保証します。

### 関数名の命名方針

**ERC1996**では、オペレーター管理用の関数名に `authorizeHoldOperator`、`revokeHoldOperator`、`isHoldOperatorFor` という形式を採用しています。
これは、**ERC777**トークン規格で使用されている命名規則と整合性を持たせるためです。これにより、開発者やインターフェース利用者が直感的に理解しやすくなっています。

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

[\[ERC777\] トークンコントラクトの標準インターフェースの仕組みを理解しよう！](/articles/c0fcb8f3122f66985226)

### operationIdの形式と意味

Holdの識別子である `operationId` には文字列（`string`）型が使用されています。
ガス効率だけを考えれば `bytes32` や `uint256` の方が適していますが、ここでは人間にとって可読性の高いID（例：`"order-1234"` など）を扱えるようにすることが優先されています。

この設計により、トランザクションやログを外部から追跡しやすくなり、監査やシステム統合の場面で利便性が高まります。
なお、実際にオンチェーンに保存する際に文字列全体を保存するか、ハッシュ化された識別子だけを保存するかは実装者の裁量に委ねられています。
識別子としてユニークであれば十分だからです。

### operationIdの一意性と競合回避

`operationId` は全体で一意である必要がある「競合資源（competitive resource）」です。
そのため、複数のHold発行者が存在する環境では、IDの衝突（同一の `operationId` を複数のHoldが使うこと）を避けるために、発行者ごとにプレフィックスを付けるなどの工夫が推奨されます。
例えば、`"user123:tx001"` のように識別性を高めると、安全に運用することができます。

## 互換性

**ERC1996**は、**ERC20**と互換性があります。

## 引用

Julio Faura <julio@adhara.io>, Fernando Paris <fer@io.builders>, Daniel Lehrner <daniel@io.builders>, "ERC-1996: Holdable Token [DRAFT]," Ethereum Improvement Proposals, no. 1996, April 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1996.

## 最後に

今回は「**ERC20**のトークンをロックして、将来ロックした分のトークンを送付する仕組みを提案している**ERC1996**」についてまとめてきました！
いかがだったでしょうか？

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