はじめに

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

今回は、トークンを大きな分類と細かな属性の二層で整理することで、多様なデジタル資産や現実資産をより柔軟かつわかりやすく扱えるDual Layer Tokenの仕組みを提案しているERC6960についてまとめていきます!

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

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

概要

Dual Layer Token(DLT)は、既存のトークン規格であるERC20(同じ単位がすべて同じ価値を持つトークンの規格)、ERC721(1つ1つが固有なトークン=NFTの規格)、ERC1155(複数種類のトークンを1つのコントラクトでまとめて扱える規格)の役割をひとつにまとめたトークンです。 それに加えて、「分類レイヤー」と呼べる仕組みを持っています。

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

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

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

この分類レイヤーでは、mainId を「資産の大きな種類を表すID」、subId を「その資産の中の個別の属性やバリエーションを表すID」として使います。 例えば、mainId が「ある不動産物件」を表し、subId が「その物件の中の特定フロア」や「持分の区分」を表す、といったイメージです。

Dual Layer Tokenは、このような二重の分類を持つことで、コントラクトの中でトークンを整理して管理しやすくすることを狙っています。 どのトークンがどの種類に属しているか、そしてその中のどのバリエーションなのかを分かりやすく追跡できるようにすることで、「トークンの棚卸し」や「資産ごとの管理」がしやすくなります。

特に、現実世界に存在する資産(Real World Assets / RWAs)をトークン化し、その「持分(fractional ownership)」を表現する用途を強く意識しています。 ここでいう「fractional ownership」は、ひとつの資産を小さな持分単位に分割して複数人で所有できるようにすることを指します。 例えば、1つの不動産を1000口に分けてトークンとして扱う、といったイメージです。

DLT規格で「持分」を表現できる例として、以下のようなものが挙げられています。

資産の例
請求書(Invoices)
企業の株式(Company stocks)
デジタルコレクティブル(デジタル収集品・コレクション)
不動産(Real estate)

これらの資産は、1つのまとまった資産としても扱えますし、細かい持分に分けて複数の保有者に割り当てることもあります。 Dual Layer Tokenは、こういった資産のトークン化と持分管理を、柔軟かつ効率的に行えるようにすることを目指しています。

動機

ERC1155は、Ethereumのエコシステムの中でかなり広く使われているトークン規格です。ひとつのコントラクトで複数種類のトークン(代替可能なものと非代替のもの)をまとめて扱える点が大きな特徴です。

一方で、ERC1155だけでは「複数の分類をまたいでトークンを整理したい」といったケースに対応しにくいという制約があります。 特に、以下のような場面でその制約が問題になります。

  • 現実世界の資産(RWAs)を扱う場合
  • 資産を細かい持分に分割して管理する(fractionalization / 持分化)場合

例えば、1つの不動産を別々の物件タイプ、部屋、フロア、持分率といった観点で分類したいとき、単純に「トークンIDが違うだけ」という表現では、どう分類されているかが分かりづらくなります。 また、請求書や株式など、種類が多い現実世界の資産を同じコントラクトで扱う場合にも、「資産の種類」と「その中の個別の属性」をきれいに切り分けて管理したくなります。

Dual Layer Tokenは、こうした課題を解消するために、「二重の分類(mainId / subId)」を前提としたトークン規格として設計されています。 ERC1155が持つ「複数トークンを1コントラクトで扱える」という強みを活かしつつ、さらに一歩進めて、トークンの種類を細かく整理できるようにすることが目的です。

特に、トークンの種類をさらにサブカテゴリに分けて管理したいシーンで効果を発揮します。 例えば、「不動産」という大きなカテゴリ(mainId)の中で、個別の物件、フロア、部屋番号、持分などをサブカテゴリ(subId)として扱う、といったケースです。

ERC20、ERC721、ERC1155のそれぞれの役割を取り込みながら、Dual Layer Tokenはこうした複雑な分類と持分管理をひとつの仕組みで扱えるようにすることを目指しています。

仕様

IDLT Interface

js
// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.17;

/**
 * @title DLT token standard interface
 * @dev Interface for any contract that wants to implement the DLT standard
 */
interface IDLT {

    /**
     * @dev MUST emit when `subId` token is transferred from `sender` to `recipient`
     * @param sender is the address of the previous holder whose balance is decreased
     * @param recipient is the address of the new holder whose balance is increased
     * @param mainId is the main token type ID to be transferred
     * @param subId is the token subtype ID to be transferred
     * @param amount is the amount to be transferred of the token subtype
     */
    event Transfer(
        address indexed sender,
        address indexed recipient,
        uint256 indexed mainId,
        uint256 subId,
        uint256 amount
    );

    /**
     * @dev MUST emit when `subIds` token array is transferred from `sender` to `recipient`
     * @param sender is the address of the previous holder whose balance is decreased
     * @param recipient is the address of the new holder whose balance is increased
     * @param mainIds is the main token type ID array to be transferred
     * @param subIds is the token subtype ID array to be transferred
     * @param amounts is the amount array to be transferred of the token subtype                
    */
    event TransferBatch(
        address indexed sender,
        address indexed recipient,
        uint256[] mainIds,
        uint256[] subIds,
        uint256[] amounts
    );

    /**
     * @dev MUST emit when `owner` enables `operator` to manage the `subId` token
     * @param owner is the address of the token owner
     * @param operator is the authorized address to manage the allocated amount for an owner address 
     * @param mainId is the main token type ID to be approved
     * @param subId is the token subtype ID to be approved
     * @param amount is the amount to be approved of the token subtype
     */
    event Approval(
        address indexed owner,
        address indexed operator,
        uint256 mainId,
        uint256 subId,
        uint256 amount
    );

    /**
     * @dev MUST emit when `owner` enables or disables (`approved`) `operator` to manage all of its assets
     * @param owner is the address of the token owner
     * @param operator is the authorized address to manage all tokens for an owner address
     * @param approved true if the operator is approved, false to revoke approval
     */
    event ApprovalForAll(
        address indexed owner,
        address indexed operator,
        bool approved
    );
    
    /**
     * @dev MUST emit when the URI is updated for a main token type ID.
     * URIs are defined in RFC 3986.
     * The URI MUST point to a JSON file that conforms to the "DLT Metadata URI JSON Schema".
     * @param oldValue is the old URI value
     * @param newValue is the new URI value
     * @param mainId is the main token type ID
     */
    event URI(string oldValue, string newValue, uint256 indexed mainId);

    /**
     * @dev Approve or remove `operator` as an operator for the caller.
     * Operators can call {transferFrom} or {safeTransferFrom} for any subId owned by the caller.
     * The `operator` MUST NOT be the caller.
     * MUST emit an {ApprovalForAll} event.     
     * @param operator is the authorized address to manage all tokens for an owner address
     * @param approved true if the operator is approved, false to revoke approval
     */
    function setApprovalForAll(address operator, bool approved) external;

    /**
     * @dev Moves `amount` tokens from `sender` to `recipient` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     * MUST revert if `sender` or `recipient` is the zero address.
     * MUST revert if balance of holder for token `subId` is lower than the `amount` sent.
     * MUST emit a {Transfer} event.
     * @param sender is the address of the previous holder whose balance is decreased
     * @param recipient is the address of the new holder whose balance is increased
     * @param mainId is the main token type ID to be transferred
     * @param subId is the token subtype ID to be transferred
     * @param amount is the amount to be transferred of the token subtype
     * @param data is additional data with no specified format
     * @return True if the operation succeeded, false if operation failed
     */
    function safeTransferFrom(
        address sender,
        address recipient,
        uint256 mainId,
        uint256 subId,
        uint256 amount,
        bytes calldata data
    ) external returns (bool);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     * The `operator` MUST NOT be the caller.
     * MUST revert if `operator` is the zero address.
     * MUST emit an {Approval} event.
     * @param operator is the authorized address to manage tokens for an owner address
     * @param mainId is the main token type ID to be approved
     * @param subId is the token subtype ID to be approved
     * @param amount is the amount to be approved of the token subtype
     * @return True if the operation succeeded, false if operation failed
     */
    function approve(
        address operator,
        uint256 mainId,
        uint256 subId,
        uint256 amount
    ) external returns (bool);

    /**
     * @notice Get the token with a particular subId balance of an `account`
     * @param account is the address of the token holder
     * @param mainId is the main token type ID
     * @param subId is the token subtype ID
     * @return The amount of tokens owned by `account` in subId
     */
    function subBalanceOf(
        address account,
        uint256 mainId,
        uint256 subId
    ) external view returns (uint256);

    /**
     * @notice Get the tokens with a particular subIds balance of an `accounts` array
     * @param accounts is the address array of the token holder
     * @param mainIds is the main token type ID array
     * @param subIds is the token subtype ID array
     * @return The amount of tokens owned by `accounts` in subIds
     */
    function balanceOfBatch(
        address[] calldata accounts,
        uint256[] calldata mainIds,
        uint256[] calldata subIds
    ) external view returns (uint256[] calldata);

    /** 
     * @notice Get the allowance allocated to an `operator`
     * @dev This value changes when {approve} or {transferFrom} are called
     * @param owner is the address of the token owner
     * @param operator is the authorized address to manage assets for an owner address
     * @param mainId is the main token type ID
     * @param subId is the token subtype ID
     * @return The remaining number of tokens that `operator` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     */
    function allowance(
        address owner,
        address operator,
        uint256 mainId,
        uint256 subId
    ) external view returns (uint256);

    /**
     * @notice Get the approval status of an `operator` to manage assets
     * @param owner is the address of the token owner
     * @param operator is the authorized address to manage assets for an owner address
     * @return True if the `operator` is allowed to manage all of the assets of `owner`, false if approval is revoked
     * See {setApprovalForAll}
     */
    function isApprovedForAll(
        address owner,
        address operator
    ) external view returns (bool);
}

イベント

Transfer
javascript
event Transfer(
    address indexed sender,
    address indexed recipient,
    uint256 indexed mainId,
    uint256 subId,
    uint256 amount
);

subId のトークンが送信者から受信者へ移動した時に発行されるイベント。 subId が示す特定のサブトークンが、sender から recipient に移動した際に発行されます。 mainId はトークンの大分類を示し、subId はその中にある個別の属性を示します。 残高の変化に対応して発行され、トークン移動の履歴を外部から追跡できるようにします。

パラメータ

  • sender
    • トークンの保有量が減るアドレス。
  • recipient
    • トークンを新たに受け取るアドレス。
  • mainId
    • トークンの大分類を示す識別子。
  • subId
    • トークンのサブ分類を示す識別子。
  • amount
    • 移動するサブトークンの数量。
TransferBatch
javascript
event TransferBatch(
    address indexed sender,
    address indexed recipient,
    uint256[] mainIds,
    uint256[] subIds,
    uint256[] amounts
);

複数の subId トークンが一括で送信者から受信者へ移動した時に発行されるイベント。 一度に複数のトークンをまとめて移動する場合に発行されます。 mainIds・subIds・amounts は同じ順序で対応する配列です。 複数種類のトークン移動を1回で記録できるため、ガスコストの削減と状態管理の整理に役立ちます。

パラメータ

  • sender
    • トークンの保有量が減るアドレス。
  • recipient
    • トークンを新たに受け取るアドレス。
  • mainIds
    • トークンの大分類を並べた配列。
  • subIds
    • トークンのサブ分類を並べた配列。
  • amounts
    • 各トークンの移動数量を並べた配列。
Approval
javascript
event Approval(
    address indexed owner,
    address indexed operator,
    uint256 mainId,
    uint256 subId,
    uint256 amount
);

owner が operator に対して、指定した subId トークンの操作を許可した時に発行されるイベント。 owner が保有する特定のサブトークンについて、operator が使用できる数量を設定したときに発行されます。 この許可は個別のサブトークン単位で行われ、amount の範囲内で transferFrom などを実行できます。

パラメータ

  • owner
    • トークンを保有するアドレス。
  • operator
    • 操作権限が付与されるアドレス。
  • mainId
    • 大分類トークンの識別子。
  • subId
    • サブ分類トークンの識別子。
  • amount
    • 操作を許可する数量。
ApprovalForAll
javascript
event ApprovalForAll(
    address indexed owner,
    address indexed operator,
    bool approved
);

owner が operator に対して、全資産の管理権限を付与または取り消した時に発行されるイベント。 個別の subId に限定せず、owner のすべてのトークンに対する管理権限をまとめて付与する設定です。 approved が true の場合は全権限を与え、false の場合は権限を取り消します。

パラメータ

  • owner
    • 権限を与える側のアドレス。
  • operator
    • 資産管理を行うアドレス。
  • approved
    • 権限が付与されているかどうかの真偽値。
URI
javascript
event URI(string oldValue, string newValue, uint256 indexed mainId);

特定の mainId に紐づく URIが更新された時に発行されるイベント。 URIはトークンのメタデータを指すリンクであり、JSON 形式に従います。 oldValue から newValue へ変更されたことが記録され、外部サービスがメタデータ更新を検知できるようになります。

パラメータ

  • oldValue
    • 更新前のURI。
  • newValue
    • 更新後のURI。
  • mainId
    • URIが更新されたトークンの大分類。

関数

setApprovalForAll
javascript
function setApprovalForAll(address operator, bool approved) external;

指定したアドレスに対して、全ての資産の管理権限を付与または取り消す関数。 operator に対して、msg.sender が所有するすべてのトークンの管理権限を一括で設定します。 自身を operator に設定することはできません。 実行後には必ず ApprovalForAll イベントが発行されます。

引数

  • operator
    • 管理権限を与えるアドレス。
  • approved
    • 権限付与(true)または取消(false)を示す値。
safeTransferFrom
javascript
function safeTransferFrom(
    address sender,
    address recipient,
    uint256 mainId,
    uint256 subId,
    uint256 amount,
    bytes calldata data
) external returns (bool);

許可された範囲内で特定の subId トークンを移動させる関数。 sender から recipient に対して、指定した数量のサブトークンを移動します。 残高不足や address(0) への送信など、不正条件の場合は処理を revert します。 移動後には Transfer イベントが発行されます。

引数

  • sender
    • トークンを送るアドレス。
  • recipient
    • トークンを受け取るアドレス。
  • mainId
    • トークンの大分類。
  • subId
    • トークンのサブ分類。
  • amount
    • 送る数量。
  • data
    • 任意の追加データ。

戻り値

  • True
    • 正常に移動が完了した場合。
  • False
    • 失敗した場合。
approve
javascript
function approve(
    address operator,
    uint256 mainId,
    uint256 subId,
    uint256 amount
) external returns (bool);

特定の subId トークンに対して、操作権限と数量を個別に設定する関数。 operator に対して、msg.sender が所有する特定のサブトークンについて、指定した amount を扱えるようにします。 operator がゼロアドレスの場合は必ず revert します。 実行後に Approval イベントが発行されます。

引数

  • operator
    • 操作権限を与えるアドレス。
  • mainId
    • 大分類トークンの識別子。
  • subId
    • サブ分類トークンの識別子。
  • amount
    • 操作を許可する数量。

戻り値

  • True
    • 設定が成功した場合。
  • False
    • 設定に失敗した場合。
subBalanceOf
javascript
function subBalanceOf(
    address account,
    uint256 mainId,
    uint256 subId
) external view returns (uint256);

特定の subId トークンについて、指定したアドレスの保有量を取得する関数。 account が保有するサブトークンの数量を返します。 大分類とサブ分類を指定するため、非常に細かい粒度で残高確認ができます。

引数

  • account
    • 残高を確認するアドレス。
  • mainId
    • 大分類の識別子。
  • subId
    • サブ分類の識別子。

戻り値

  • uint256
    • 指定されたサブトークンの保有数量。
balanceOfBatch
javascript
function balanceOfBatch(
    address[] calldata accounts,
    uint256[] calldata mainIds,
    uint256[] calldata subIds
) external view returns (uint256[] calldata);

複数アドレス・複数トークンに対して一括で残高を取得する関数。 トークン管理を効率化するため、一度に複数の残高を取得できる仕組みです。 各配列は同じ順序で対応しており、結果も同じ順序で返却されます。

引数

  • accounts
    • 残高確認したいアドレスの配列。
  • mainIds
    • 大分類IDの配列。
  • subIds
    • サブ分類IDの配列。

戻り値

  • uint256[]
    • 取得した残高の一覧。
allowance
javascript
function allowance(
    address owner,
    address operator,
    uint256 mainId,
    uint256 subId
) external view returns (uint256);

個別のサブトークンに対して operator に割り当てられた許可数量を取得する関数。 approve によって設定された数量がどれだけ残っているかを確認できます。 transferFrom によって減少するため、動的に変化します。

引数

  • owner
    • トークンを保有するアドレス。
  • operator
    • 操作権限を持つアドレス。
  • mainId
    • 大分類。
  • subId
    • サブ分類。

戻り値

  • uint256
    • operator が使用できる残り数量。
isApprovedForAll
javascript
function isApprovedForAll(
    address owner,
    address operator
) external view returns (bool);

特定の operator が、指定された owner の全資産を管理できるかどうかを確認する関数。 setApprovalForAll によって設定された権限が有効かどうかをチェックします。 個別のトークンではなく、全体権限の状態を確認します。

引数

  • owner
    • トークン所有者のアドレス。
  • operator
    • 権限を確認する対象アドレス。

戻り値

  • bool
    • 権限が存在する場合は true。

IDLTReceiver Interface

関数

onDLTReceived
javascript
function onDLTReceived(
    address operator,
    address from,
    uint256 mainId,
    uint256 subId,
    uint256 amount,
    bytes calldata data
) external returns (bytes4);

単一のDLTトークン受信時に呼び出されるコールバック関数。 safeTransferFrom によってDLTトークンがこのコントラクトへ送られた時、受信確認として実行されます。 正しい selector を返すことでトークン受信が承認され、誤った値を返すと処理が revert されます。

引数

  • operator
    • トークン移動を実行したアドレス。
  • from
    • 以前トークンを保有していたアドレス。
  • mainId
    • 大分類 ID。
  • subId
    • サブ分類 ID。
  • amount
    • 受け取った数量。
  • data
    • 任意の追加データ。

戻り値

  • bytes4
    • onDLTReceived.selector を返す必要がある。
onDLTBatchReceived
javascript
function onDLTBatchReceived(
    address operator,
    address from,
    uint256[] calldata mainIds,
    uint256[] calldata subIds,
    uint256[] calldata amounts,
    bytes calldata data
) external returns (bytes4);

複数のDLTトークン受信時に呼び出されるコールバック関数。 バッチ形式で複数のトークンが送られた場合に実行されます。 onDLTReceived と同様、正しい selector を返すことで受信が完了します。

引数

  • operator
    • トークン移動を実行したアドレス。
  • from
    • 以前トークンを保有していたアドレス。
  • mainIds
    • 大分類 ID の配列。
  • subIds
    • サブ分類 ID の配列。
  • amounts
    • 受け取った数量の配列。
  • data
    • 任意の追加データ。

戻り値

  • bytes4
    • onDLTBatchReceived.selector を返す必要がある。

補足

二層構造の分類が必要となる理由

DLTが採用している二層構造(mainId と subId)の分類は、トークン管理をより細かく整理するための仕組みです。 ひとつの資産に対して、さらに細かい属性やバリエーションを持たせたい場面は多くあります。

例えば、不動産であれば「物件」自体が mainId に該当し、その物件の中の「持分」、「階層」、「部屋」などが subId に該当します。 このように、サブカテゴリを自然に扱える点が大きな利点です。

ERC1155でも複数種類のトークンを扱えますが、分類の構造を持たず、単なるIDの区別にとどまるため、複雑な資産管理には限界があります。 DLT では二層構造を採用することで、より整理されたトークン管理が可能になります。

mainBalance と subBalance の管理も同時に行えるため、大きな資産カテゴリの総量と、その中のサブ資産の内訳を分けて追跡できます。

mainId と subId をどのように扱うか

サブ分類である subId の使い方として、以下の2つの設計方法があります。

subId の扱い方 説明
Shared SubIds すべての mainId が共通の subId を持つ方式。例えば「持分 1〜100 がどの資産にも適用される」というモデル。
Mixed SubIds 各 mainId が独自の subId を持つ方式。例えば「不動産Aの subId は 1〜20」、「不動産Bの subId は 1〜50」のように、資産ごとに subId を独立させるモデル。

どちらの方式もサポートできるため、多様な資産構造に合わせた実装ができます。

複雑な資産を扱える点で、ERC20・ERC721・ERC1155より柔軟であるため、DLTは同じコントラクト内で代替可能資産と非代替資産の両方を管理できることにつながっています。

名称をDual Layer Tokenとした理由

DLTの名称は、トークンの分類が二段階で構成されていることを示すために選ばれています。

mainId が第1層(Layer 1)、subId が第2層(Layer 2)であり、この構造がトークンの階層的な分類方法を明確に表しています。

分類を二層に限定した理由

サブ資産にさらに子階層を持たせる「多層構造」も検討されていますが、階層を増やすと以下の問題が生まれます。

  • subId ごとの総供給量管理が複雑になる
  • 階層が深いほど、残高追跡のために必要なマッピングや関数が急増する
  • コントラクトの設計と利用が不必要に複雑化する

DLTの目的は柔軟性を保ちながらも、実用的で管理しやすい規格を提供することであるため、二層構造が最適という判断に至っています。

ERC1155を拡張しなかった理由

ERC1155を直接拡張することも検討されていますが、実現するには多くの問題がありました。

ERC1155は「複数のトークンを1コントラクトで扱う」ことに優れていますが、「分類構造を持つこと」を前提としていません。 そのため、階層的な分類を付与するには大幅な仕様変更が必要となり、既存実装と互換性がなくなる可能性が高いという課題があります。

DLTは階層構造の分類を必要とするプロジェクトを想定しているため、ERC1155の機能だけでは不十分であると判断され、独立した規格として設計されています。

引用

Adam Boudjemaa (@aboudjem), Mohamad Hammoud (@mohamadhammoud), Nawar Hisso (@nawar-hisso), Khawla Hassan (@khawlahssn), Mohammad Zakeri Rad (@zakrad), Ashish Sood soodgen@gmail.com, "ERC-6960: Dual Layer Token [DRAFT]," Ethereum Improvement Proposals, no. 6960, April 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6960.

最後に

今回は「トークンを大きな分類と細かな属性の二層で整理することで、多様なデジタル資産や現実資産をより柔軟かつわかりやすく扱えるDual Layer Tokenの仕組みを提案しているERC6960」についてまとめてきました! いかがだったでしょうか?

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

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