はじめに

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

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

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

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

概要

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
    • 拒否処理が成功したかどうかを示します。

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

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の仕様に基づいており、実際の支払いが行われるまでトークンが仮押さえ状態になります。

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

この際のホールドの監督を行うのは、「トークンコントラクトのオーナー」またはその代理者(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などからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!