はじめに

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

今回は、トークン残高をオフチェーンデータとオンチェーンハッシュに分離し、取引の透明性を抑えながら整合性を保つ仕組みを提案しているERC7722についてまとめていきます!

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

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

概要

ERC7722は、トークンの残高情報をオンチェーン上にそのまま記録しないことで、プライバシーを強化するためのトークン仕様です。 従来のトークン規格では、誰がどれだけのトークンを保有しているかがブロックチェーン上で直接確認できますが、ERC7722ではこの前提を変えています。

ERC7722では、残高を「バスケット(basket)」と呼ばれる形で表現します。 バスケットとは、実際の残高データをオフチェーンで管理し、それをハッシュ化した値だけをオンチェーンに保持する仕組みです。

  • オフチェーン ブロックチェーンの外部でデータを管理することです。 全ノードが共有する必要がないため、秘匿性や柔軟性を確保できます。

  • ハッシュ 元のデータから一意に計算される短い値です。 ハッシュ値から元のデータを直接復元することはできません。

  • バスケット 残高情報をまとめたデータ構造で、実体はオフチェーンにあり、オンチェーンにはその内容を示すハッシュのみが存在します。

このバスケットは単なる参照情報ではありません。ERC7722では、以下のような操作をオンチェーンのトークン関数として行えます。

  • バスケットの再構成
  • バスケットの移転
  • バスケットの管理(分割や統合など)

つまり、オンチェーンでは「どのバスケットを誰が管理しているか」だけが分かり、実際の残高の中身は外から見えない状態になります。 これにより、トークンの可用性や完全性を保ちつつ、残高の秘匿性を実現しています。

動機

スマートコントラクトアカウントは、単なるアドレス以上の意味を持つ存在です。 明確なアイデンティティとして振る舞い、さまざまなクレームやアテステーションを紐づけて再利用できます。

  • クレーム(claim) 「このアカウントは○○である」といった主張や属性情報です。
  • アテステーション(attestation) クレームが第三者によって検証・証明された情報です。

これらの仕組みは、分散型IDや認証、信用スコアなど多くの応用が可能で、非常に有用です。 しかし、この強力な特徴がそのままプライバシー上の問題につながるケースがあります。

特に問題になるのが、ERC20互換トークンをスマートコントラクトアカウントで保有する場合です。

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

ERC20では、各アカウントの残高がオンチェーン上に平文で保存されます。 そのため、以下のような状況が発生します。

状況 問題点
明確なアイデンティティを持つアカウント 誰のアカウントか特定しやすい
残高がオンチェーンで公開 保有資産が誰でも確認できる
クレームやアテステーションを再利用 金融情報と個人属性が結びつく

この結果、スマートコントラクトアカウントを使えば使うほど、保有資産という機微な情報が外部に露出しやすくなります。 利便性とプライバシーがトレードオフの関係になってしまう点が大きな課題です。

ERC7722は、この問題を解決するために設計されています。 残高をオンチェーンに直接書かないことで、スマートコントラクトアカウントが持つ以下の利点を維持します。

  • クレームやアテステーションの再利用性
  • スマートコントラクトとしての柔軟な拡張性
  • トークンとしての整合性と安全性

その上で、残高という最もセンシティブな情報だけを秘匿します。 これにより、明確なアイデンティティを持つアカウントがトークンを保有していても、資産状況を不用意に公開せずに済むようになります。

ERC7722は、スマートコントラクトアカウントの実用性を損なうことなく、プライバシーを現実的に改善するための仕様です。

仕様

ERC7722の仕様では、トークン残高をオンチェーン上に数値として保持せず、ハッシュ化された値として扱います。 これにより、ブロックチェーンの透明性を保ったまま、残高という機微な情報を直接公開しない設計になっています。

バスケットによる残高表現(Baskets)

ERC7722では、トークン残高を「バスケット(basket)」と呼ばれるハッシュ値で表現します。 バスケットは、以下の3つの要素を組み合わせてハッシュ化したものです。

javascript
keccak256(abi.encode(salt, tokenId, value))

それぞれの要素の意味は以下です。

要素 説明
salt ランダムな32バイト値。ハッシュの総当たり攻撃を防ぐために使用します。
tokenId トークンコントラクト内で一意なトークンIDです。
value 実際のトークン量を表す数値です。

このハッシュ値だけをオンチェーンに保持するため、ブロックチェーン上から直接トークン量を推測することはできません。 中身が見えない状態で残高を扱う点が、覆いのかかったバスケットに似ているため「バスケット」と呼ばれています。

トークン操作の全体像

ERC7722のトークンインターフェースでは、バスケットを単位として以下の操作が可能です。

  • バスケットの生成
  • バスケットの発行
  • バスケットの移転
  • バスケットの再構成(分割・結合)
  • バスケットの償還

特に再構成では、入力バスケットと出力バスケットの合計値が一致していることを保証する必要があります。 この検証は、外部のオラクルサービスによって行われます。

プライバシー強化の仕組み

ERC7722では、単に残高を隠すだけでなく、トークンの動きを追跡しにくくする工夫も含まれています。

  • バスケットの分割や結合により、取引の関連性を曖昧にします。
  • ノイズを重ねる、空の転送を混ぜるといった差分プライバシーの考え方を取り入れます。

これにより、オンチェーンのイベントログを見ても、実際の取引内容や残高変化を正確に把握することが難しくなります。

トークンインターフェース

以下は、ERC7722に準拠したオペークトークンが必ず実装する必要があるインターフェースです。

OpaqueToken.sol
interface OpaqueToken {
  //
  // TYPES
  //

  struct SIGNATURE {
    uint8 v; bytes32 r; bytes32 s;
  }

  struct ORACLECONFIG {
    uint8 minNumberOfOracles; // min. number of oracle signatures required for reorg
    address[] oracles;        // valid oracles
  }

  //
  // EVENTS
  //

  /**
   * @dev MUST be emitted when new token is created
   * @param initiatedBy address that created and controls the token
   * @param tokenId identifier of the token
   * @param totalSupplyBasket initial supply basket, containing total supply of tokens
   * @param ref custom reference as used by initiator
   */
  event CreateToken(address initiatedBy, bytes32 tokenId, bytes32 totalSupplyBasket, bytes32 ref);

  /**
   * @dev MUST be emitted on issuance
   * @param initiatedBy address that initiated issuance
   * @param baskets baskets that were issued to the receiver
   * @param receiver address that received baskets
   * @param ref custom reference as used by initiator
   */
  event Issue(address initiatedBy, bytes32[] baskets, address receiver, bytes32 ref);

  /**
   * @dev MUST be emitted when holder baskets are restructured
   * @param initiatedBy address that initiated reorg and owner of all baskets
   * @param basketsIn baskets that are restructured and no longer exist
   * @param basketsOut baskets that are newly created
   * @param ref custom reference as used by initiator
   */
  event ReorgHolderBaskets(address initiatedBy, bytes32[] basketsIn, bytes32[] basketsOut, bytes32 ref);

  /**
   * @dev MUST be emitted when supply baskets are restructured
   * @param initiatedBy address that initiated reorg
   * @param basketsIn supply baskets that are restructured and no longer exist
   * @param basketsOut supply baskets that are newly created
   * @param ref custom reference as used by initiator
   */
  event ReorgSupplyBaskets(address initiatedBy, bytes32[] basketsIn, bytes32[] basketsOut, bytes32 ref);

  /**
   * @dev MUST be emitted when baskets are transferred from one address to another
   * @param initiatedBy address that initiated the transfer
   * @param receiver address that is the new owner of baskets
   * @param baskets baskets that were transferred
   * @param ref custom reference as used by initiator
   */
  event Transfer(address initiatedBy, address receiver, bytes32[] baskets, bytes32 ref);

  /**
   * @dev MUST be emitted on redeem
   * @param initiatedBy address that initiated redeem
   * @param baskets baskets that were redeemed
   * @param ref custom reference as used by initiator
   */
  event Redeem(address initiatedBy, bytes32[] baskets, bytes32 ref);

  //
  // FUNCTIONS
  //

  /**
   * @dev returns the configuration for this token
   */
  function oracleConfig() external view returns (ORACLECONFIG memory);

  /**
   * @dev returns the address of the basket owner
   */
  function owner(bytes32 basket) external view returns (address);

  /**
   * @dev returns the total supply for a `tokenId``
   * All token investors are allowed to fetch this value from the token operator's off-chain storage.
   */
  function totalSupply(bytes32 tokenId) external view returns (bytes32);

  /**
   * @dev returns the operator of this token, who is also responsible for providing the main
   * off-chain storage source.
   */
  function operator() external view returns (address);
  
  /**
   * @dev Allows the token operator to create a new token with the specified `tokenId` and an initial 
   * `totalSupplyBasket`. The `totalSupplyBasket` can be partitioned using {reorgSupplyBaskets} as needed 
   * when calling {issue}. The `ref` parameter can be used freely by the caller for any reference purpose.
   */
  function createToken(
      bytes32 tokenId,
      bytes32 totalSupplyBasket,
      bytes32 ref
  ) external;

  /**
   * @dev Allows the token operator to issue tokens by assigning `supplyBaskets` to a `receiver` which 
   * becomes the owner of these baskets. 
   */
  function issue(
      bytes32[] calldata supplyBaskets,
      address receiver,
      bytes32 ref
  ) external;
  
  /**
   * @dev transfers `baskets` to a `receiver` who becomes the new owner of these baskets. 
   */
  function transfer(
      bytes32[] calldata baskets,
      address receiver,
      bytes32 ref
  ) external;

  /**
   * @dev reorganizes a set of holder baskets (`basketsIn`) to a new set (`basketsOut`) having
   * the same value, i.e., the sum of all values from input baskets equals the sum of values
   * in output baskets. In order to ensure the integrity, external oracle service is required that
   * will sign the reorg proposal requested by the basket owner, which is passed as `reorgOracleSignatures`.
   * The minimum number of oracle signatures is defined in the oracle configuration.
   */
  function reorgHolderBaskets(
      SIGNATURE[] calldata reorgOracleSignatures,
      bytes32[] calldata basketsIn,
      bytes32[] calldata basketsOut,
      bytes32 ref
  ) external;

  /**
   * @dev same as {reorgHolderBaskets}, but for the available supply baskets.
   */
  function reorgSupplyBaskets(
      SIGNATURE[] calldata reorgOracleSignatures,
      bytes32[] calldata basketsIn,
      bytes32[] calldata basketsOut,
      bytes32 ref
  ) external;

  /**
   * @dev redeems holder's `baskets` and returns them to available supply
   */
  function redeem(
      bytes32[] calldata baskets,
      bytes32 ref
  ) external;

}

イベント

CreateToken
javascript
event CreateToken(address initiatedBy, bytes32 tokenId, bytes32 totalSupplyBasket, bytes32 ref);

新しいトークンが作成された時に発行されるイベント。 トークンオペレーターが新しいトークンIDを作成し、初期供給量を表すバスケットを登録したことを示します。

パラメータ

  • initiatedBy
    • トークン作成を行ったアドレス。
  • tokenId
    • 作成されたトークンの識別子。
  • totalSupplyBasket
    • トークン全体の供給量を表す初期バスケット。
  • ref
    • 任意の参照情報。
Issue
javascript
event Issue(address initiatedBy, bytes32[] baskets, address receiver, bytes32 ref);

トークンが発行された時に発行されるイベント。 供給用バスケットが分配され、受取人がその所有者になったことを示します。

パラメータ

  • initiatedBy
    • 発行を行ったアドレス。
  • baskets
    • 発行されたバスケット群。
  • receiver
    • バスケットを受け取ったアドレス。
  • ref
    • 任意の参照情報。
ReorgHolderBaskets
javascript
event ReorgHolderBaskets(address initiatedBy, bytes32[] basketsIn, bytes32[] basketsOut, bytes32 ref);

保有者バスケットが再構成された時に発行されるイベント。 既存のバスケットが消費され、新しいバスケットが生成されたことを示します。

パラメータ

  • initiatedBy

    • 再構成を行ったバスケット所有者。
  • basketsIn

    • 再構成前のバスケット。
  • basketsOut

    • 再構成後に生成されたバスケット。
  • ref

    • 任意の参照情報。
ReorgSupplyBaskets
javascript
event ReorgSupplyBaskets(address initiatedBy, bytes32[] basketsIn, bytes32[] basketsOut, bytes32 ref);

供給用バスケットが再構成された時に発行されるイベント。 未発行の供給バスケットを分割または結合したことを示します。

パラメータ

  • initiatedBy
    • 再構成を行ったアドレス。
  • basketsIn
    • 再構成前の供給バスケット。
  • basketsOut
    • 再構成後の供給バスケット。
  • ref
    • 任意の参照情報。
Transfer
javascript
event Transfer(address initiatedBy, address receiver, bytes32[] baskets, bytes32 ref);

バスケットが別のアドレスへ移転した時に発行されるイベント。 数値ではなくバスケット単位でトークンが移動したことを示します。

パラメータ

  • initiatedBy
    • 転送を行ったアドレス。
  • receiver
    • 新しい所有者。
  • baskets
    • 転送されたバスケット。
  • ref
    • 任意の参照情報。
Redeem
javascript
event Redeem(address initiatedBy, bytes32[] baskets, bytes32 ref);

トークンが償還された時に発行されるイベント。 保有者のバスケットが供給側に戻されたことを示します。

パラメータ

  • initiatedBy
    • 償還を行ったアドレス。
  • baskets
    • 償還されたバスケット。
  • ref
    • 任意の参照情報。

構造体

SIGNATURE
javascript
struct SIGNATURE {
  uint8 v; bytes32 r; bytes32 s;
}

オラクル署名を表す構造体。 Ethereumの標準的な署名形式を使用し、再構成操作の正当性を証明するために使われます。

パラメータ

  • v
    • 署名のリカバリID。
  • r
    • 署名データの一部。
  • s
    • 署名データの一部。
ORACLECONFIG
javascript
struct ORACLECONFIG {
  uint8 minNumberOfOracles;
  address[] oracles;
}

オラクル設定を表す構造体。 再構成を有効にするために必要な署名数と、許可されたオラクル一覧を管理します。

パラメータ

  • minNumberOfOracles
    • 必要な最小署名数。
  • oracles
    • 有効なオラクルのアドレス一覧。

関数

oracleConfig
javascript
function oracleConfig() external view returns (ORACLECONFIG memory);

トークンのオラクル設定を取得する関数。 再構成に必要なオラクル条件を参照するために使用されます。

戻り値

  • ORACLECONFIG
    • 現在のオラクル設定。
owner
javascript
function owner(bytes32 basket) external view returns (address);

バスケットの所有者を取得する関数。 特定のバスケットがどのアドレスに紐づいているかを返します。

引数

  • basket
    • 対象となるバスケット。

戻り値

  • address
    • バスケットの所有者。
totalSupply
javascript
function totalSupply(bytes32 tokenId) external view returns (bytes32);

トークンの総供給量バスケットを取得する関数。 実際の数値はオフチェーンで管理され、オンチェーンではハッシュのみが返されます。

引数

  • tokenId
    • トークン識別子。

戻り値

  • bytes32
    • 総供給量を表すバスケット。
operator
javascript
function operator() external view returns (address);

トークンオペレーターを取得する関数。 オフチェーンストレージの管理責任者を示します。

戻り値

  • address
    • トークンオペレーターのアドレス。
createToken
javascript
function createToken(bytes32 tokenId, bytes32 totalSupplyBasket, bytes32 ref) external;

新しいトークンを作成する関数。 初期供給量を表すバスケットを登録し、トークンの発行準備を行います。

引数

  • tokenId
    • トークン識別子。
  • totalSupplyBasket
    • 初期供給量バスケット。
  • ref
    • 任意の参照情報。
issue
javascript
function issue(bytes32[] calldata supplyBaskets, address receiver, bytes32 ref) external;

トークンを発行する関数。 供給用バスケットを指定した受取人に割り当てます。

引数

  • supplyBaskets
    • 発行対象の供給バスケット。
  • receiver
    • 受取人アドレス。
  • ref
    • 任意の参照情報。
transfer
javascript
function transfer(bytes32[] calldata baskets, address receiver, bytes32 ref) external;

バスケットを転送する関数。 トークン量ではなくバスケット単位で所有権を移転します。

引数

  • baskets
    • 転送するバスケット。
  • receiver
    • 新しい所有者。
  • ref
    • 任意の参照情報。
reorgHolderBaskets
javascript
function reorgHolderBaskets(
  SIGNATURE[] calldata reorgOracleSignatures,
  bytes32[] calldata basketsIn,
  bytes32[] calldata basketsOut,
  bytes32 ref
) external;

保有者バスケットを再構成する関数。 入力と出力の合計値が等しいことを、オラクル署名によって検証します。

引数

  • reorgOracleSignatures
    • オラクル署名。
  • basketsIn
    • 再構成前のバスケット。
  • basketsOut
    • 再構成後のバスケット。
  • ref
    • 任意の参照情報。
reorgSupplyBaskets
javascript
function reorgSupplyBaskets(
  SIGNATURE[] calldata reorgOracleSignatures,
  bytes32[] calldata basketsIn,
  bytes32[] calldata basketsOut,
  bytes32 ref
) external;

供給用バスケットを再構成する関数。 未発行トークンの分割や結合を安全に行います。

引数

  • reorgOracleSignatures
    • オラクル署名。
  • basketsIn
    • 再構成前の供給バスケット。
  • basketsOut
    • 再構成後の供給バスケット。
  • ref
    • 任意の参照情報。
redeem
javascript
function redeem(bytes32[] calldata baskets, bytes32 ref) external;

トークンを償還する関数。 保有者のバスケットを供給側へ戻します。

引数

  • baskets
    • 償還対象のバスケット。
  • ref
    • 任意の参照情報。

ユーザーロール

ERC7722のオペークトークンには役割が2つあります。 どちらが何をできるかを分けることで、トークンの発行側と保有者側の責務がはっきりします。

ロール一覧

ロール 役割の概要 主に扱うバスケット 主に使う操作
Token Operator トークンを作成し、発行し、未流通の供給量を管理する人です。 供給バスケット(supply baskets) createToken、reorgSupplyBasket、issue、さらに強制操作として forceTransfer、forceReorg
Token User 流通しているトークンを保有するアドレスです。 所有バスケット(owned baskets) reorgSupplyBaskets、transfer、redeem

ここで「未流通の供給量」は、まだユーザーに配られていないトークンのまとまりで、供給バスケットとして保持されます。 一方でユーザーが保有するトークンは、所有バスケットとして保持されます。

また、Token Operatorには forceTransfer と forceReorg による強制操作の権限があると書かれています。 これは通常の保有者の意思に依らずに移転や再構成を実行できる能力です。 用途は本文では説明されていないため、ここでは「強制操作が可能である」という事実までに留めます。

Off-chain Data Endpoints

ERC7722では、オンチェーンにはバスケットのハッシュしか置かず、バスケットの実データ(salt、tokenId、value)はオフチェーンに置きます。 そのため、トークンのオペレーターは「バスケットを保存・取得できるREST API」を提供する必要があります。

オペレーターに求められる要件

ERC7722では、オペレーターに対して以下の要件が明記されています。

  • オペレーターは、GET basketとPUT basketのRESTエンドポイントを実装したオフチェーンストレージを提供する必要があります。
  • オペレーターは、バスケットデータの可用性を確保する必要があります。
  • オペレーターは、必要最小限の範囲で、資格のある保有者にのみデータを共有する必要があります。 資格があるのは「過去にそのバスケットを持っていたアドレス」または「現在の保有者」です。
  • データが資格者のみに共有され、かつ資格者だけが書き込めるように、両エンドポイントに認証を実装しなければなりません。 認証方式は仕様では決めておらず、オペレーターの環境に依存してよい扱いです。
  • 既存のトークン保有者がPUT basketを実行できるようにしなければなりません。
  • 現在または過去のバスケット保有者がGET basketを実行できるようにしなければなりません。
  • トークン保有者は、オペレーターのサービスが利用できない場合に備えて、自分のバスケットデータを自分のオフチェーンストレージにもコピーしておくことが推奨されています。

ここで重要なのは、ERC7722のプライバシーは「オンチェーンに残高の実数が出ない」だけでなく、「バスケットの中身を誰に見せるかを制御する」ことも前提になっている点です。

REST API(PUT baskets / GET baskets)

仕様で示されているエンドポイントは以下です。

PUT baskets

バスケットを保存するエンドポイントです。保存条件として「basket(ハッシュ)が data(salt/tokenId/value)から計算したハッシュと一致すること」が求められています。 つまり、サーバーは受け取ったデータをそのまま信用せず、整合性を確認してから保存する設計です。

javascript
// Endpoint: PUT baskets
// Description: will store baskets if the `basket` hash is matching `data`.
// PostData:
[
  {
    basket: keccak256(abi.encode(salt, tokenId, value)),
    data: {
      salt: <bytes32>,
      tokenId: <bytes32>,
      value: <uint256>
    }
  },
  ...
]

用語の補足です。

用語 説明
REST HTTPで提供されるAPIの設計スタイルです。ここではPUT/GET/POSTを使います。
PUT 一般に「作成または更新」を意味するHTTPメソッドです。
整合性確認 basket が data から計算される値と一致するかチェックすることです。
GET baskets

バスケットを取得するエンドポイントです。 クエリパラメータの有無で動作が変わります。

  • basket-hash を指定すると、そのハッシュに一致する1件を返します。
  • 何も指定しない場合は、リクエストした人が保有するすべてのバスケットを返します。
javascript
// Endpoint: GET baskets?basket-hash=<bytes32>
// Description: will return the list of baskets depending on the query parameters.
// Query Parameters:
//  - basket-hash (optional): returns one basket matching the requested hash
//  - if no query parameter is set, then the endpoint will return all baskets of the requestor
// Response:
[
  {
    basket: keccak256(abi.encode(salt, tokenId, value)),
    data: {
      salt: <bytes32>,
      tokenId: <bytes32>,
      value: <uint256>
    }
  },
  ...
]

「何も指定しないとリクエスト者の全バスケットを返す」ため、ここでも認証が重要です。 誰でも叩ける状態だとバスケット一覧が漏れてしまうため、仕様が認証を必須にしている意図が分かりやすいです。

reorgエンドポイント(reorg Endpoint)

ERC7722では、バスケットの再構成(reorg)は、誤発行や不正償還を避けるためにオラクルサービスが必要とされています。

ここでの「オラクル」は、ブロックチェーン外で検証し、その結果を署名として返す外部サービスのことです。 価格オラクルのようにデータを提供するというより、「再構成が正しい」ことを署名で保証する役割です。

オラクルに求められる要件

ERC7722では、オラクルに対して以下の要件が明記されています。

  • オラクルは、POST reorgのRESTエンドポイントを提供しなければなりません。
  • オラクルは、次の条件を満たすreorg提案に署名しなければなりません。 1つ目は「tokenId ごとにグルーピングした入力合計値」と「tokenId ごとにグルーピングした出力合計値」が一致することです。 2つ目は「各 item の basket ハッシュが data から計算した keccak256(abi.encode(data.salt, data.tokenId, data.value)) と一致すること」です。
  • reorgエンドポイントはステートレスでなければなりません。
  • オラクルは、後で分析するためにリクエストデータを保存してはいけません。
  • reorgエンドポイントは認証を要求しないことが望ましく、誰でも制限なく利用できるようにしてよいとされています。

用語の補足です。

用語 説明
ステートレス サーバーが過去のリクエスト情報を保持しない設計です。
署名 オラクルの秘密鍵でハッシュに署名し、改ざんされていないことを示します。

POST reorg の入出力

入力は in と out の2つの配列で、それぞれがバスケットとデータの組です。

javascript
// Endpoint: POST reorg
// PostData:
{
  in: [
    {
      basket: keccak256(abi.encode(salt, tokenId, value)),
      data: {
        salt: <bytes32>,
        tokenId: <bytes32>,
        value: <uint256>
      }
    },
    ...
  ],
  out: [
    {
      basket: keccak256(abi.encode(salt, tokenId, value)),
      data: {
        salt: <bytes32>,
        tokenId: <bytes32>,
        value: <uint256>
      }
    },
    ...
  ]
}

レスポンスは、basketsIn と basketsOut(どちらも bytes32[])をABIエンコードしてハッシュ化し、そのハッシュに署名したものです。

javascript
// Response:
{
  // hash is signed with oracles private key
  // basketsIn and basketsOut are bytes32[]
  signature: sign(keccak256(abi.encode(basketsIn, basketsOut)))
}

この署名は、オンチェーンの reorgHolderBaskets や reorgSupplyBaskets の引数として渡され、再構成の整合性を裏付ける材料になります。

正しいreorgリクエストの例

例では salt とハッシュを省略し、(…, tokenX, value) という形で表しています。ポイントは「tokenIdごとに合計が一致している」ことです。

javascript
// Example for valid reorg requests (salt and hashes are omitted for better readability):

in : (..., token1, 10), (..., token1, 30), (..., token2, 5), (..., token2, 95)
out: (..., token1, 40), (..., token2, 100)

in : (..., token1, 40), (..., token2, 100)
out: (..., token1, 10), (..., token1, 30), (..., token2, 5), (..., token2, 95)

ノイズの重ね合わせ(差分プライバシー)

ERC7722では、残高を隠すだけだと「誰から誰に移ったか」や「どのバスケットが次にどこへ行ったか」から推測される可能性が残ります。 そこで、reorgと空の転送(valueが0のバスケット転送)を使い、取引の追跡をさらに難しくする考え方が説明されています。

ここでの「差分プライバシー」は、観測データにノイズを混ぜることで、個別の事実(実際の送金)を推定しにくくする考え方です。

例(reorgとvalue=0バスケットの転送)

以下の例では、BがAから受け取ったバスケットをそのままCへ送らず、いったん自分のバスケット群と一緒にreorgしてから送ります。 さらに value=0 のバスケットも別の相手に送って、観測者の推測を難しくします。

javascript
// Example with reorg and null-value basket transfers:

A owns basket-a1{..., value:10}
B owns basket-b1{..., value:5}, basket-b2{..., value:15}, ...
A: transfer basket-a1 to B
B: reorg [basket-a1, basket-b1, basket-b2]
      to [basket-b3{..., value:10}, basket-b4{..., value:10}, basket-b5:{..., value:10},
            basket-b6:{..., value:0}, basket-b7:{..., value:0}]
      where sum of inputs is the sum of outputs
B: transfer basket-b5{value:10} to C
B: transfer basket-b6{value:0}  to D
B: transfer basket-b7{value:0}  to E

この流れで何が起きているかを、追える範囲と追えない範囲に分けて整理します。

観測できること(オンチェーンから分かる) 分かりにくくなること(狙い)
AがBへバスケットを転送したこと BがAから受け取った value=10 が、その後Cへ送られたのかは分かりにくいです。
Bがreorgを実行したこと reorgで新しいバスケットに変わるため、元のバスケットとの対応関係を追いにくいです。
BがC/D/Eへ転送したこと value=0 の転送が混ざるため、どれが実際の価値移転かを外部から判定しにくいです。

もしBが basket-a1 をそのままCへ送っていたら、Aは「Cが何を受け取ったか」を推測できてしまうが、reorgを挟むことでそれができなくなります。 また、観測者は「誰と誰がやり取りしたか」は見えてしまうものの、ノイズが混ざることで「どれが本当の価値移転か」を判定できなくなる、と説明されています。

図

sequenceDiagram
  participant A as A
  participant B as B
  participant C as C
  participant D as D
  participant E as E
  participant Oracle as Oracle

  A->>B: transfer(basket-a1 value=10)
  B->>Oracle: POST reorg(in:[a1,b1,b2], out:[b3,b4,b5,b6,b7])
  Oracle-->>B: signature(sign(hash(basketsIn,basketsOut)))
  B->>B: reorg(署名付きでオンチェーン実行)
  B->>C: transfer(basket-b5 value=10)
  B->>D: transfer(basket-b6 value=0)
  B->>E: transfer(basket-b7 value=0)

この図では、reorgの前にオラクルへPOSTし、署名を受け取ってからオンチェーンのreorgを成立させる流れを入れています。 これは先に示されたreorgエンドポイントの仕様と整合します。

補足

ERC20互換性をあえて壊す理由

ERC20は、誰でも残高や送金履歴を確認できる高い透明性を持っています。 この性質は、多くのユースケースでは利点ですが、再利用可能なブロックチェーン上のアイデンティティと組み合わさると、大きな問題になります。

スマートコントラクトアカウントやDIDのような仕組みでは、同じアドレスに対してさまざまな属性情報や証明情報が紐づきます。 その状態でERC20トークンを保有すると、次のような状況が発生します。

  • 誰のアドレスかが分かりやすい
  • そのアドレスの残高が常に公開されている
  • 属性情報と資産情報が結びついてしまう

この組み合わせは、金融情報のプライバシーという観点では致命的です。 ERC7722は、この問題を解決するために「ERC20互換性よりもプライバシーを優先する」という選択をしています。

その結果として、ERC7722では以下が前提になります。

項目 ERC20 ERC7722
残高の表現 オンチェーンに数値で保存 オンチェーンにはハッシュのみ
透明性 非常に高い 意図的に抑制
互換性 既存DeFiと高い ERC20とは非互換
プライバシー 低い 高い

ERC7722は、再利用可能なアイデンティティを安全に使うためには、ERC20の透明性がむしろ障害になる、という立場を明確に取っています。

reorgオラクルの設計意図

ERC7722では、バスケットの再構成(reorg)が重要な操作になります。 reorgは、バスケットを分割したり結合したりする操作ですが、ここで一番重要なのは「価値の総和が変わっていないこと」です。

この検証を、コントラクト単体ではなくオラクルに任せています。

分業による相互牽制

reorgにおける役割分担は次のようになっています。

  • バスケットの保有者 入力バスケットと出力バスケットの組み合わせを提案します。
  • オラクル 入力と出力で、tokenId ごとの合計値が一致しているかを検証し、正しければ署名します。

この設計により、以下のような相互牽制が成立します。

  • 保有者は、オラクルの署名なしでは不正なreorgを実行できません。
  • オラクルは、勝手にreorgを起こすことができず、必ず保有者の提案が必要です。

どちらか一方だけでは操作が成立しないため、単独の不正が起きにくい構造になっています。

分散性と完全性のバランス

オラクルは複数設定でき、必要な署名数(minNumberOfOracles)も設定できます。 これにより、運用者は次のバランスを調整できます。

  • 少数オラクル 運用が簡単でコストが低いが、信頼性は相対的に低いです。
  • 多数オラクル 分散性と耐不正性は高まるが、運用コストが上がります。

また、オラクルが不正な署名を行った場合でも、その署名はオンチェーンに残ります。 そのため、不正行為は後から追跡でき、最低限の完全性(weak-integrity)は確保されています。

ZKPを使わなかった理由

理論的には、Zero-Knowledge Proof(ZKP)を使って「合計値が等しい」ことを証明することも可能です。 しかし、ERC7722では以下の理由からオラクル方式を選んでいます。

  • 実装が比較的シンプルです。
  • 計算コストやガスコストを抑えられます。
  • 現実的な運用にすぐ適用できます。

ここでは、暗号学的に最も強い手法ではなく、実用性と効率を優先した選択がされています。

オフチェーンデータ保存の考え方

ERC7722では、バスケットの実データをオフチェーンで管理します。 その主な管理者として、トークンオペレーター(多くの場合は発行者やレジストラ)を選んでいます。

この判断は、金融商品の現実的な運用を強く意識したものです。

なぜオペレーターが持つのか

金融商品では、以下のような前提が一般的です。

  • 誰がどのポジションを持っているかを発行者が把握しています。
  • KYCや法規制に基づいた管理が必要です。
  • トークンのライフサイクル管理(発行、償還、制限)が求められます。

このような環境では、オペレーターがオフチェーンデータの一次管理者になることは自然です。 確かに、Ethereum全体の文脈では「完全分散」ではありませんが、ERC7722はすべてのユースケースを狙っているわけではありません。

ERC7722は、特に以下のような分野に適しています。

  • 規制下の金融商品
  • 証券型トークン
  • 発行体と投資家の関係が明確なトークン

この前提に立てば、オペレーター中心のオフチェーン管理は合理的な選択です。

互換性

ERC7722は、ERC20と互換性がありません。 これは設計上の制約ではなく、意図的な判断です。

理由はすでに説明した通り、ERC20の残高透明性そのものが、ERC7722の目標であるプライバシーと相反するためです。 そのため、ERC20との後方互換性を持たせることは、設計思想と矛盾します。

この点を整理すると、以下のようになります。

観点 結論
ERC20との互換性 なし
理由 残高の透明性がプライバシーを破壊するため
位置づけ ERC20の代替ではなく、別カテゴリのトークン

ERC7722は、既存のDeFiトークンを置き換えるものではありません。 プライバシーを最優先にした、明確に異なる設計思想を持つトークン規格です。

引用

Ivica Aračić (@ivica7), Ante Bešlić (@ethSplit), Mirko Katanić (@mkatanic), SWIAT, "ERC-7722: Opaque Token [DRAFT]," Ethereum Improvement Proposals, no. 7722, June 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7722.

最後に

今回は「トークン残高をオフチェーンデータとオンチェーンハッシュに分離し、取引の透明性を抑えながら整合性を保つ仕組みを提案しているERC7722」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

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