# [ERC2021] トークン支払いリクエスト管理の仕組みを理解しよう！

> ERC2021は、ERC20トークンに対してトークン保有者が自身のウォレットからの支払い（払い出し）をリクエストできる機能を追加する提案です。

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

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、トークン保有者がブロックチェーン上で支払いリクエストを発行し、トークンオペレーターがその処理を進める仕組みを提案している**ERC2021**についてまとめていきます！

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

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

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

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

## 概要

**ERC2021**は、**ERC20**トークンに対してトークン保有者が自身のウォレットからの支払い（払い出し）をリクエストできる機能を追加する提案です。
支払いはスマートコントラクトを通じて指示され、その際に「**支払い指示文字列（payout instruction）**」を添付してリクエストを行います。

この仕組みにより、支払いの申請から実行までのライフサイクルがブロックチェーン上でトレーサブルに管理され、分散型のプロセスとして可視化されるようになります。

トークンウォレット保有者または代理者は、スマートコントラクトの `orderPayout` または `orderPayoutFrom` 関数を呼び出すことで支払いリクエストを開始します。
この時、支払い先を指定するための情報を「**支払い指示文字列**」として入力します。
この文字列は、オペレーター（トークン発行側）が、実際の支払い処理を行うための補助情報として利用されます。

なお、支払い指示をそのままブロックチェーンに公開するのはセキュリティ上のリスクがあるため、暗号化した形で指示を添付するか、別の安全な通信チャネル（プライベートメッセージ、暗号化ストレージなど）を用いることが推奨されています。

## アクター

- **トークンウォレット保有者**
    個人または企業が該当し、自身のウォレットからの支払いリクエストを開始できる当事者です。
- **トークンコントラクトのオーナー／代理者**
    トークンを発行・管理する組織やエンティティで、支払いリクエストを読み取り、それに対応して処理を進める役割を担います。
- **Orderer**
    トークンウォレット保有者の代理として、支払いリクエストを開始することが許可された人物やエンティティです。

## 動機

従来、トークンの払い出しは中央集権的なシステムでの手続きに依存しており、実行には個別のコミュニケーションや処理が必要でした。
しかし**ERC2021**により、支払いリクエストの起点がブロックチェーン上に移ることで以下のようなメリットが得られます。

* 支払いの発生・進行状況がブロックチェーン上で可視化され、トレーサビリティが確保される
* トークンの発行・Burn・移動といった一連のトークンライフサイクルの多くが分散的に処理される
* 支払い指示に関する処理がスマートコントラクトで実行される

### 支払いリクエストのライフサイクルとステータス

支払いリクエストが行われると、その処理は複数のステップを経てステートが変化します。
それぞれの段階でのステータス遷移は以下の通りです。

- **Ordered（注文済み）**
    リクエストが開始されると、対象トークンが一時的に「**ホールド**」状態となり、払い出しの準備が始まります。
    この時点ではまだ実行されていません。
- **InProcess（処理中）**
    オペレーターがリクエストの処理を開始するとこの状態になります。
    この状態では、申請者（オーダラー）はキャンセルできなくなります。
- **FundsInSuspense（一時預かり）**
    支払い実行の準備が整った段階で、対象のトークンが一時的な保管口座（suspense wallet）に移動されます。
- **Executed（実行済み）**
    オペレーターが実際にオフチェーンで支払いを実行し、その結果としてsuspense walletからトークンをBurnします。
    これにより払い出しは完了状態になります。
- **Rejected（拒否）**
    オペレーターが支払いリクエストを拒否した場合、この状態となり、ホールドされたトークンはウォレット保有者に戻されます。
- **Cancelled（キャンセル）**
    オーダラー自身がリクエストを取り下げた場合に適用されます。
    これは処理がInProcessに入る前まで可能です。

### セキュリティと設計上の配慮

支払い指示の内容を平文でチェーン上に書き込むことは、情報漏洩のリスクがあります。
そのため、支払い先や詳細な指示は、以下のいずれかの手段で提供されることが望まれます。

* 暗号化された支払い指示文字列を使用する
* トークンオペレーターと事前に合意されたプライベートチャネルでの指示伝達
* 指示自体をスマートコントラクト上で解釈しない構成とする

## 仕様

```js
interface IPayoutable /* is ERC-20 */ {
    enum PayoutStatusCode {
        Nonexistent,
        Ordered,
        InProcess,
        FundsInSuspense,
        Executed,
        Rejected,
        Cancelled
    }
    function authorizePayoutOperator(address orderer) external returns (bool);
    function revokePayoutOperator(address orderer) external returns (bool);
    function orderPayout(string calldata operationId, uint256 value, string calldata instructions) external returns (bool);
    function orderPayoutFrom(string calldata operationId, address walletToBePaidOut, uint256 value, string calldata instructions) external returns (bool);
    function cancelPayout(string calldata operationId) external returns (bool);
    function processPayout(string calldata operationId) external returns (bool);
    function putFundsInSuspenseInPayout(string calldata operationId) external returns (bool);
    function executePayout(string calldata operationId) external returns (bool);
    function rejectPayout(string calldata operationId, string calldata reason) external returns (bool);

    function isPayoutOperatorFor(address walletToDebit, address orderer) external view returns (bool);
    function retrievePayoutData(string calldata operationId) external view returns (address walletToDebit, uint256 value, string memory instructions, PayoutStatusCode status);

    event PayoutOrdered(address indexed orderer, string indexed operationId, address indexed walletToDebit, uint256 value, string instructions);
    event PayoutInProcess(address indexed orderer, string indexed operationId);
    event PayoutFundsInSuspense(address indexed orderer, string indexed operationId);
    event PayoutExecuted(address indexed orderer, string indexed operationId);
    event PayoutRejected(address indexed orderer, string indexed operationId, string reason);
    event PayoutCancelled(address indexed orderer, string indexed operationId);
    event PayoutOperatorAuthorized(address indexed walletToBePaidOut, address indexed orderer);
    event PayoutOperatorRevoked(address indexed walletToBePaidOut, address indexed orderer);
}
```

### Enum

#### `PayoutStatusCode`

```javascript
enum PayoutStatusCode {
    Nonexistent,
    Ordered,
    InProcess,
    FundsInSuspense,
    Executed,
    Rejected,
    Cancelled
}
```

支払いリクエストの状態を示す列挙型。
支払いリクエストが持つステータスの種類を定義します。
各状態は支払いプロセスの進行状況を示します。

**パラメータ**

* `Nonexistent`
    * 存在しない、または未定義の状態。
* `Ordered`
    * 支払いリクエストが作成された状態。
* `InProcess`
    * 処理が開始され、キャンセル不可の状態。
* `FundsInSuspense`
    * トークンが一時保管口座（suspense wallet）に移された状態。
* `Executed`
    * 支払いが完了し、トークンがBurnされた状態。
* `Rejected`
    * 支払いリクエストが拒否された状態。
* `Cancelled`
    * リクエストがキャンセルされた状態。

### 関数

#### `authorizePayoutOperator`

```javascript
function authorizePayoutOperator(address orderer) external returns (bool);
```

支払いリクエストの代行者（orderer）を承認する関数。
ウォレットの所有者が、指定したアドレスに対して支払いリクエストの実行権限を付与します。

**引数**

* `orderer`
* 支払いリクエストを代行できるようにするアドレス。

**戻り値**

* `bool`
    * 承認に成功したかどうかを示します。

#### `revokePayoutOperator`

```javascript
function revokePayoutOperator(address orderer) external returns (bool);
```

支払いリクエストの代行者（orderer）を取り消す関数。
以前に承認された代行者の権限をウォレットの所有者が取り消します。

**引数**

* `orderer`
    * 取り消し対象の代行者アドレス。

**戻り値**

* `bool`
    * 取り消しに成功したかどうかを示します。

#### `orderPayout`

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

自身のウォレットから支払いリクエストを作成する関数。
リクエストIDを指定して支払いリクエストを作成します。
同じIDのリクエストは再使用できません。

**引数**

* `operationId`
    * リクエストを一意に識別するID。
* `value`
    * 支払い額（トークン単位）。
* `instructions`
    * 支払いに関する指示内容（暗号化された文字列推奨）。

**戻り値**

* `bool`
    * リクエスト作成に成功したかどうかを示します。

#### `orderPayoutFrom`

```javascript
function orderPayoutFrom(string calldata operationId, address walletToBePaidOut, uint256 value, string calldata instructions) external returns (bool);
```

他人のウォレットから支払いリクエストを代理作成する関数。
承認された代行者が、他人のウォレットに対して支払いリクエストを作成します。

**引数**

* `operationId`
    * リクエストを一意に識別するID。
* `walletToBePaidOut`
    * 支払い対象となるウォレットアドレス。
* `value`
    * 支払い額（トークン単位）。
* `instructions`
    * 支払い指示（暗号化された文字列推奨）。

**戻り値**

* `bool`
    * リクエスト作成に成功したかどうかを示します。

#### `cancelPayout`

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

支払いリクエストをキャンセルする関数。
支払いリクエストのステータスが `Ordered` の段階で、ウォレット保有者または代行者がキャンセルできます。
`InProcess` に移行するとキャンセルはできません。

**引数**

* `operationId`
    * キャンセル対象の支払いリクエストのID。

**戻り値**

* `bool`
    * キャンセル処理が成功したかどうかを示します。

#### `processPayout`

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

支払いリクエストを「**処理中（InProcess）**」に変更する関数。
オペレーターがリクエストを受理し、処理を開始する段階で使用されます。
`InProcess` 状態になるとキャンセルはできなくなります。

**引数**

* `operationId`
    * 処理中に変更する支払いリクエストのID。

**戻り値**

* `bool`
    * ステータス変更に成功したかどうかを示します。

#### `putFundsInSuspenseInPayout`

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

支払い対象のトークンを一時保管状態（suspense）に移動させる関数。
リクエストが `InProcess` 状態のときに、オペレーターが対象トークンをsuspense walletに移動させます。
これにより、後続の実行（`executePayout`）が可能になります。

**引数**

* `operationId`
    * 一時保管状態に移す対象のリクエストID。

**戻り値**

* `bool`
    * 移動処理が成功したかどうかを示します。

#### `executePayout`

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

支払いリクエストを実行し、トークンをBurnする関数。
オペレーターが支払いを完了させる際に使用します。
suspense wallet にあるトークンをBurnして、リクエストを `Executed` 状態にします。

**引数**

* `operationId`
    * 実行する支払いリクエストのID。

**戻り値**

* `bool`
    * 実行処理が成功したかどうかを示します。

#### `rejectPayout`

```javascript
function rejectPayout(string calldata operationId, string calldata reason) external returns (bool);
```

支払いリクエストを拒否する関数。
オペレーターが支払いリクエストに対して拒否処理を行う際に使用します。
理由を含めてリクエストを `Rejected` 状態に変更します。

**引数**

* `operationId`
    * 拒否対象の支払いリクエストのID。
* `reason`
    * 拒否の理由。
    * **ERC1066**準拠のコードが推奨されます。

**戻り値**

* `bool`
    * 拒否処理が成功したかどうかを示します。

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

[\[ERC1066\] コントラクトの実行結果を詳細に取得する仕組みを理解しよう！](/articles/f46ae4018f20a2f76c4b)

#### `isPayoutOperatorFor`

```javascript
function isPayoutOperatorFor(address walletToDebit, address orderer) external view returns (bool);
```

特定のアドレスが支払いリクエストを代行できるか確認する関数。
`walletToDebit` の支払いを、`orderer` が実行する権限を持っているかどうかをチェックします。

**引数**

* `walletToDebit`
    * 支払い対象のウォレットアドレス。
* `orderer`
    * 支払いリクエストの代行を試みるアドレス。

**戻り値**

* `bool`
    * 権限があるかどうかを示します。

#### `retrievePayoutData`

```javascript
function retrievePayoutData(string calldata operationId) external view returns (address walletToDebit, uint256 value, string memory instructions, PayoutStatusCode status);
```

支払いリクエストに関するすべての情報を取得する関数。
オペレーター、トークン保有者、または `orderer` のいずれかが呼び出すことで、指定されたリクエストIDの詳細情報を取得できます。

**引数**

* `operationId`
    * 対象の支払いリクエストID。

**戻り値**

* `walletToDebit`
    * 支払い対象のウォレットアドレス。
* `value`
    * 支払い額。
* `instructions`
    * 支払い指示文字列。
* `status`
    * 現在の支払いステータス（`PayoutStatusCode`）。

### イベント

#### `PayoutOrdered`

```javascript
event PayoutOrdered(
    address indexed orderer,
    string indexed operationId,
    address indexed walletToDebit,
    uint256 value,
    string instructions
);
```

支払いリクエストが作成された時に発行されるイベント。
ウォレット保有者または代行者が `orderPayout` または `orderPayoutFrom` を呼び出して支払いリクエストを作成した際に発行されます。

**パラメータ**

* `orderer`
    * 支払いリクエストを発行したアドレス。
* `operationId`
    * リクエストを一意に識別するID。
* `walletToDebit`
    * 支払い対象のウォレットアドレス。
* `value`
    * 支払い要求されたトークン量。
* `instructions`
    * 支払い先や経路などの指示（通常は暗号化文字列）。

#### `PayoutInProcess`

```javascript
event PayoutInProcess(
    address indexed orderer,
    string indexed operationId
);
```

支払いリクエストが「処理中（InProcess）」に変更された時に発行されるイベント。
オペレーターが `processPayout` を呼び出して、支払いの実行準備に入ったことを示します。
この状態になると、リクエストはキャンセルできません。

**パラメータ**

* `orderer`
    * リクエストを発行したアドレス。
* `operationId`
    * 対象の支払いリクエストID。

#### `PayoutFundsInSuspense`

```javascript
event PayoutFundsInSuspense(
    address indexed orderer,
    string indexed operationId
);
```

支払い対象のトークンが一時保管状態（suspense）に移された時に発行されるイベント。
`putFundsInSuspenseInPayout` をオペレーターが実行した時に、トークンがsuspense walletへ一時的に移動したことを通知します。

**パラメータ**

* `orderer`
    * 支払いリクエストを発行したアドレス。
* `operationId`
    * 対象の支払いリクエストID。

#### `PayoutExecuted`

```javascript
event PayoutExecuted(
    address indexed orderer,
    string indexed operationId
);
```

支払いリクエストが実行完了された時に発行されるイベント。
`executePayout` によりオフチェーンへの送金が行われ、対応するトークンがBurnされた後に発行されます。

**パラメータ**

* `orderer`
    * 支払いリクエストを発行したアドレス。
* `operationId`
    * 対象の支払いリクエストID。

#### `PayoutRejected`

```javascript
event PayoutRejected(
    address indexed orderer,
    string indexed operationId,
    string reason
);
```

支払いリクエストが拒否された時に発行されるイベント。
`rejectPayout` 関数によりオペレーターが支払いを拒否し、その理由とともにリクエストを `Rejected` 状態にした際に発行されます。

**パラメータ**

* `orderer`
    * リクエストを発行したアドレス。
* `operationId`
    * 対象の支払いリクエストID。
* `reason`
    * 拒否理由。
    * **ERC1066**形式のコードも使用可能。

#### `PayoutCancelled`

```javascript
event PayoutCancelled(
    address indexed orderer,
    string indexed operationId
);
```

支払いリクエストがキャンセルされた時に発行されるイベント。
ウォレット保有者または代行者が、オペレーターによる処理前に `cancelPayout` を実行し、支払いリクエストを取り消した場合に発行されます。

**パラメータ**

* `orderer`
    * リクエストを発行したアドレス。
* `operationId`
    * 対象の支払いリクエストID。

#### `PayoutOperatorAuthorized`

```javascript
event PayoutOperatorAuthorized(
    address indexed walletToBePaidOut,
    address indexed orderer
);
```

支払いリクエストの代行者が承認された時に発行されるイベント。
ウォレット保有者が `authorizePayoutOperator` を呼び出して、特定のアドレスに対して支払いリクエストの代行権限を付与した際に発行されます。

**パラメータ**

* `walletToBePaidOut`
    * 対象のウォレットアドレス。
* `orderer`
    * 承認された代行者アドレス。

#### `PayoutOperatorRevoked`

```javascript
event PayoutOperatorRevoked(
    address indexed walletToBePaidOut,
    address indexed orderer
);
```

支払いリクエストの代行者の権限が取り消された時に発行されるイベント。
`revokePayoutOperator` が呼び出され、以前に承認されたアドレスから代行権限が取り消されたことを示します。

**パラメータ**

* `walletToBePaidOut`
    * 対象のウォレットアドレス。
* `orderer`
    * 権限を失った代行者アドレス。

## 補足

### トークンオペレーターの役割

**ERC2021**では、支払いリクエストを開始するのはトークン保有者ですが、その後の処理を担うのは「**トークンオペレーター（token operator）**」です。
オペレーターは、支払いの実行状況に応じてリクエストのステータス（Ordered、InProcess、Executedなど）を適切に更新していく必要があります。
つまり、支払いの進行を確実に管理する中心的な存在が、オペレーターというわけです。

### 支払い指示（instruction）のフォーマット

支払い先や処理に関する「**支払い指示（payout instruction）**」については、特定のフォーマットは強制されていません。
つまり、実装者や運用者の裁量で定義できますが、実務的にはISO 20022などの標準的な支払い仕様を参考にするのが望ましいとされています。
将来的な互換性や業務システムとの統合を考慮すれば、標準的な形式の採用は有用です。

### EIP1996との連携によるホールド機能

支払いリクエストが開始された際、指定されたトークン額は即座にユーザーの残高から引き落とされるのではなく、「**ホールド状態**」に置かれます。
このホールド機能は**ERC1996**の仕様に基づいており、実際の支払いが行われるまでトークンが仮押さえ状態になります。

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

[\[ERC1996\] 将来の送金を実行できるHold機能の仕組みを理解しよう！](/articles/828581bf1626b4d8d48c)

この際のホールドの監督を行うのは、「**トークンコントラクトのオーナー**」またはその代理者（agent）であり、**ERC2021**の範囲ではその実装方法は明示されていませんが、通常はこのエンティティが事前に設定されたノータリー（notary）として支払いの実行可否を判断する役割を担います。

### `operationId` の設計と運用方針

**ERC2021**の仕組みの重要な要素の1つが `operationId`（操作ID）です。
各支払いリクエストを一意に識別するために使用されるもので、ユーザーが独自に設定することができます。
このIDには以下の特徴と推奨設計があります。

* `operationId` はstring型であり、よりガス効率の良い `bytes32` 等は採用されていません。
    * これは、人間にとって読みやすく、トレース可能なIDとするための設計上の選択です。
* 実装上は、この文字列をそのままブロックチェーンに保存してもよいですし、代わりにそのハッシュ値のみを保存することも許容されています。
    * どちらの場合でも、一意に識別できれば十分です。
* 複数のユーザーが重複した `operationId` を使用するリスクがあるため、衝突を避けるためにプレフィックスを工夫することが推奨されています。
    * 例えば、企業名やウォレットアドレスの一部を含めるなどの方法が考えられます。

## 互換性

**ERC20**と**ERC1996**と完全に互換性があります。

## 引用

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

## 最後に

今回は「」についてまとめてきました！
いかがだったでしょうか？

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