はじめに

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

今回は、ERC721やERC1155形式のNFTをプールに預けることで、ERC20を発行する仕組みを提案しているERC3386についてまとめていきます!

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

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

概要

ERC3386は、ユニークなERC721またはERC1155トークンをプールにロックすることで、それに関連するERC20トークンを生成するためのスマートコントラクトのインターフェースを提案しています。 生成されるERC20トークン(以下、デリバティブトークン)は、元となるNFTのユニークIDを参照せず、プールにロックされたNFTの集合全体を代表する形で発行されます。

デリバティブトークンはERC20に準拠しているため、AMM(自動マーケットメイカー)などの他のERC20トークン対応のコントラクトで利用でき、取引・担保・流動性提供など幅広い用途で活用できます。 また、これらのトークンはBurnすることで、ロックされている元のNFTトークンを交換比率に基づいて引き出すことが可能です。

動機

EthereumにおいてERC20トークンは最も普及しており、高い流動性を持つ標準です。 一方で、ERC721やERC1155はIDごとの個別管理が必要であり、分割して取引することができません。 この制約により、NFTを用いた資産運用やDeFi活用には限界があります。

デリバティブトークンを用いることで、NFTをロックして流動性の高いERC20トークンとして扱うことが可能になり、以下のようなメリットが生まれます。

  • NFTの分割所有や取引が可能になる
  • AMMでのスワップやプールが容易になる
  • 担保や報酬設計などERC20向けプロトコルに対応できる

さらに、一定の交換比率があることで、デリバティブトークンの価値はプールされたNFTのフロアプライス(最低価格)と比例します。 これにより、NFT市場とデリバティブ市場の間でアービトラージ(裁定取引)が発生する可能性もあります。

プールするNFTの種類を限定することで、プール内の価値差を抑え、より高価なNFTのプール参加も現実的になります。 また、オークション価格モデルを用いることで、NFTのサブクラスごとの価格発見も可能となり、高価なNFTには多くのデリバティブトークンを割り当てる柔軟な設計が可能です。

仕様

インターフェース

IWrapper.sol
pragma solidity ^0.8.0;

/**
    @title IWrapper Identifiable Token Wrapper Standard
    @dev {Wrapper} refers to any contract implementing this interface.
    @dev {Base} refers to any ERC-721 or ERC-1155 contract. It MAY be the {Wrapper}.
    @dev {Pool} refers to the contract which holds the {Base} tokens. It MAY be the {Wrapper}.
    @dev {Derivative} refers to the ERC-20 contract which is minted/burned by the {Wrapper}. It MAY be the {Wrapper}.
    @dev All uses of "single", "batch" refer to the number of token ids. This includes individual ERC-721 tokens by id, and multiple ERC-1155 by id. An ERC-1155 `TransferSingle` event may emit with a `value` greater than `1`, but it is still considered a single token.
    @dev All parameters named `_amount`, `_amounts` refer to the `value` parameters in ERC-1155. When using this interface with ERC-721, `_amount` MUST be 1, and `_amounts` MUST be either an empty list or a list of 1 with the same length as `_ids`.
*/
interface IWrapper /* is ERC165 */ {
    /**
     * @dev MUST emit when a mint occurs where a single {Base} token is received by the {Pool}.
     * The `_from` argument MUST be the address of the account that sent the {Base} token.
     * The `_to` argument MUST be the address of the account that received the {Derivative} token(s).
     * The `_id` argument MUST be the id of the {Base} token transferred.
     * The `_amount` argument MUST be the number of {Base} tokens transferred.
     * The `_value` argument MUST be the number of {Derivative} tokens minted.
     */
    event MintSingle (address indexed _from, address indexed _to, uint256 _id, uint256 _amount, uint256 _value);

    /**
     * @dev MUST emit when a mint occurs where multiple {Base} tokens are received by the {Wrapper}.
     * The `_from` argument MUST be the address of the account that sent the {Base} tokens.
     * The `_to` argument MUST be the address of the account that received the {Derivative} token(s).
     * The `_ids` argument MUST be the list ids of the {Base} tokens transferred.
     * The `_amounts` argument MUST be the list of the numbers of {Base} tokens transferred.
     * The `_value` argument MUST be the number of {Derivative} tokens minted.
     */
    event MintBatch (address indexed _from, address indexed _to, uint256[] _ids, uint256[] _amounts, uint256 _value);

    /**
     * @dev MUST emit when a burn occurs where a single {Base} token is sent by the {Wrapper}.
     * The `_from` argument MUST be the address of the account that sent the {Derivative} token(s).
     * The `_to` argument MUST be the address of the account that received the {Base} token.
     * The `_id` argument MUST be the id of the {Base} token transferred.
     * The `_amount` argument MUST be the number of {Base} tokens transferred.
     * The `_value` argument MUST be the number of {Derivative} tokens burned.
     */
    event BurnSingle (address indexed _from, address indexed _to, uint256 _id, uint256 _amount, uint256 _value);

    /**
     * @dev MUST emit when a mint occurs where multiple {Base} tokens are sent by the {Wrapper}.
     * The `_from` argument MUST be the address of the account that sent the {Derivative} token(s).
     * The `_to` argument MUST be the address of the account that received the {Base} tokens.
     * The `_ids` argument MUST be the list of ids of the {Base} tokens transferred.
     * The `_amounts` argument MUST be the list of the numbers of {Base} tokens transferred.
     * The `_value` argument MUST be the number of {Derivative} tokens burned.
     */
    event BurnBatch (address indexed _from, address indexed _to, uint256[] _ids, uint256[] _amounts, uint256 _value);

    /**
     * @notice Transfers the {Base} token with `_id` from `msg.sender` to the {Pool} and mints {Derivative} token(s) to `_to`.
     * @param _to       Target address.
     * @param _id       Id of the {Base} token.
     * @param _amount   Amount of the {Base} token.
     *
     * Emits a {MintSingle} event.
     */
    function mint(
        address _to,
        uint256 _id,
        uint256 _amount
    ) external;

    /**
     * @notice Transfers `_amounts[i]` of the {Base} tokens with `_ids[i]` from `msg.sender` to the {Pool} and mints {Derivative} token(s) to `_to`.
     * @param _to       Target address.
     * @param _ids      Ids of the {Base} tokens.
     * @param _amounts  Amounts of the {Base} tokens.
     *
     * Emits a {MintBatch} event.
     */
    function batchMint(
        address _to,
        uint256[] calldata _ids,
        uint256[] calldata _amounts
    ) external;

    /**
     * @notice Burns {Derivative} token(s) from `_from` and transfers `_amounts` of some {Base} token from the {Pool} to `_to`. No guarantees are made as to what token is withdrawn.
     * @param _from     Source address.
     * @param _to       Target address.
     * @param _amount   Amount of the {Base} tokens.
     *
     * Emits either a {BurnSingle} or {BurnBatch} event.
     */
    function burn(
        address _from,
        address _to,
        uint256 _amount
    ) external;

    /**
     * @notice Burns {Derivative} token(s) from `_from` and transfers `_amounts` of some {Base} tokens from the {Pool} to `_to`. No guarantees are made as to what tokens are withdrawn.
     * @param _from     Source address.
     * @param _to       Target address.
     * @param _amounts  Amounts of the {Base} tokens.
     *
     * Emits either a {BurnSingle} or {BurnBatch} event.
     */
    function batchBurn(
        address _from,
        address _to,
        uint256[] calldata _amounts
    ) external;

    /**
     * @notice Burns {Derivative} token(s) from `_from` and transfers `_amounts[i]` of the {Base} tokens with `_ids[i]` from the {Pool} to `_to`.
     * @param _from     Source address.
     * @param _to       Target address.
     * @param _id       Id of the {Base} token.
     * @param _amount   Amount of the {Base} token.
     *
     * Emits either a {BurnSingle} or {BurnBatch} event.
     */
    function idBurn(
        address _from,
        address _to,
        uint256 _id,
        uint256 _amount
    ) external;

    /**
     * @notice Burns {Derivative} tokens from `_from` and transfers `_amounts[i]` of the {Base} tokens with `_ids[i]` from the {Pool} to `_to`.
     * @param _from     Source address.
     * @param _to       Target address.
     * @param _ids      Ids of the {Base} tokens.
     * @param _amounts   Amounts of the {Base} tokens.
     *
     * Emits either a {BurnSingle} or {BurnBatch} event.
     */
    function batchIdBurn(
        address _from,
        address _to,
        uint256[] calldata _ids,
        uint256[] calldata _amounts
    ) external;
}

イベント

MintSingle

js
event MintSingle (address indexed _from, address indexed _to, uint256 _id, uint256 _amount, uint256 _value);

単一のベーストークンがプールに送られ、デリバティブトークンが発行されたときに発行されるイベント。 NFT(ERC721またはERC1155)1つがプールに移動し、それに応じた量のERC20トークンが発行されたときに呼び出されます。

パラメータ

  • _from
    • ベーストークンを送ったアドレス。
  • _to
    • デリバティブトークンを受け取るアドレス。
  • _id
    • 対象のベーストークンID。
  • _amount
    • 移動したベーストークンの数。
  • _value
    • 発行されたデリバティブトークンの数。

MintBatch

js
event MintBatch (address indexed _from, address indexed _to, uint256[] _ids, uint256[] _amounts, uint256 _value);

複数のベーストークンがプールに送られ、デリバティブトークンが発行されたときに発行されるイベント。 複数のNFTをまとめてプールに送信し、それに応じた合計のERC20トークンを一括で発行する場合に使用されます。

パラメータ

  • _from
    • ベーストークンを送ったアドレス。
  • _to
    • デリバティブトークンを受け取るアドレス。
  • _ids
    • 各ベーストークンのIDの配列。
  • _amounts
    • 各IDに対応する数量の配列。
  • _value
    • 発行されたデリバティブトークンの合計量。

BurnSingle

js
event BurnSingle (address indexed _from, address indexed _to, uint256 _id, uint256 _amount, uint256 _value);

単一のベーストークンを受け取るために、対応するデリバティブトークンをBurnしたときに発行されるイベント。 保持していたデリバティブトークンを焼却して、プールから元のNFTを1つ引き出す時に使用されます。

パラメータ

  • _from
    • デリバティブトークンをBurnしたアドレス。
  • _to
    • ベーストークンを受け取るアドレス。
  • _id
    • 引き出されたベーストークンのID。
  • _amount
    • 引き出されたベーストークンの数量。
  • _value
    • Burnされたデリバティブトークンの量。

BurnBatch

js
event BurnBatch (address indexed _from, address indexed _to, uint256[] _ids, uint256[] _amounts, uint256 _value);

複数のベーストークンを受け取るために、デリバティブトークンを焼却したときに発行されるイベント。 一度に複数のNFTを引き出すために、デリバティブトークンをまとめて焼却する操作を示します。

パラメータ

  • _from
    • デリバティブトークンをBurnしたアドレス。
  • _to
    • ベーストークンを受け取るアドレス。
  • _ids
    • 引き出されたベーストークンのIDの配列。
  • _amounts
    • 各IDに対応する数量の配列。
  • _value
    • Burnされたデリバティブトークンの合計量。

関数

mint

js
function mint(address _to, uint256 _id, uint256 _amount) external;

単一のベーストークンをロックして、デリバティブトークンを発行する関数。 送信者が所有するNFTを1つプールに預け、受取人にERC20トークンを発行します。

引数

  • _to
    • デリバティブトークンの受取先アドレス。
  • _id
    • 対象のベーストークンのID。
  • _amount
    • 預けるベーストークンの数量(ERC721なら1固定)。

batchMint

js
function batchMint(address _to, uint256[] calldata _ids, uint256[] calldata _amounts) external;

複数のベーストークンをロックして、デリバティブトークンを一括発行する関数。 複数のNFTを一度に預けて、その合計価値に基づくERC20トークンを一括で発行します。

引数

  • _to
    • デリバティブトークンの受取先アドレス。
  • _ids
    • 預けるベーストークンのIDの配列。
  • _amounts
    • 各ベーストークンの数量の配列。

burn

js
function burn(address _from, address _to, uint256 _amount) external;

デリバティブトークンをBurnして任意のベーストークンを引き出す関数。 どのNFTが引き出されるかは指定できませんが、指定した数量分のNFTが返却されます。

引数

  • _from
    • デリバティブトークンの送り元。
  • _to
    • ベーストークンの受取先。
  • _amount
    • 引き出すベーストークンの数。

batchBurn

js
function batchBurn(address _from, address _to, uint256[] calldata _amounts) external;

デリバティブトークンをBurnして、任意の複数ベーストークンを引き出す関数。 特定のIDは指定せず、指定された数量に基づき、プールからベーストークンが一括返却されます。

引数

  • _from
    • デリバティブトークンの送り元。
  • _to
    • ベーストークンの受取先。
  • _amounts
    • 引き出す各トークンの数量の配列。

idBurn

js
function idBurn(address _from, address _to, uint256 _id, uint256 _amount) external;

指定したIDのベーストークンを受け取るため、デリバティブトークンをBurnする関数。 引き出すNFTのIDを指定できるため、特定のNFTを回収したい場合に使用されます。

引数

  • _from
    • デリバティブトークンの送り元。
  • _to
    • ベーストークンの受取先。
  • _id
    • 引き出したいベーストークンのID。
  • _amount
    • 引き出す数量(ERC721の場合は1固定)。

batchIdBurn

js
function batchIdBurn(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _amounts) external;

指定した複数のIDのベーストークンを受け取るためにデリバティブトークンをBurnする関数。 特定のNFTのIDと数量を指定して、まとめて引き出したいときに使用されます。

引数

  • _from
    • デリバティブトークンの送り元。
  • _to
    • ベーストークンの受取先。
  • _ids
    • 引き出すベーストークンのIDの配列。
  • _amounts
    • 各IDごとの数量の配列。

補足

命名規則

ラップされるNFTはBaseトークンと呼ばれます。 これは、もともとのERC721またはERC1155トークンです。 代替名称として「Underlying」や「NFT」がありますが、ERC1155はsemi-fungibleとも言えるため、Baseという中立的な名称が選ばれています。

発行されるERC20トークンは Derivativeトークン(デリバティブトークン)と呼ばれます。 代替名称として「Wrapped」や「Generic」があります。

関数名は既存プロジェクトの用語を参考にしています。

  • mint / burn
    • ERC20由来の一般的な用語
  • mint / redeem
    • NFTXでの用語
  • deposit / withdraw
    • WrappedKittiesでの用語
  • wrap / unwrap
    • MoonCatsWrappedでの用語

特に idBurn という関数名は、何がBurnされるのかを明確にするために使われています(実際にBurnされるのは Derivative です)。

ミント処理

Mintとは、Baseトークンをプールに移してDerivativeトークンを受け取る処理です。 Mint後のBaseトークンはBurn関数以外で移動してはならず、これによりDerivativeトークンの価値が保証されます。

また、NFTX litepaperの提案のように、Baseトークンを移動不能とし担保としてロックすることで、Derivativeトークンを「ローン」として受け取る形も許容されています。 これはMaker Vaultsと類似した設計です。

Burn処理

Burnは、保持しているDerivativeトークンをBurnし、プールからBaseトークンを引き出す処理です。 Burn関数は以下の2種類に分かれます。

  • IDを指定せずにBaseトークンを取り出す
    • burn, batchBurn
  • 特定のIDを指定して取り出す
    • idBurn, batchIdBurn

ID指定によって高価なNFTをピンポイントで引き出すことが可能になるため、NFTXではそのような場合に追加手数料を設定する仕組みが提案されています。

価格設定

価格設定は固定である必要はなく、各Mint・Burn時の value(Derivativeの数量)をイベントに含めることで価格可変性に対応できます。

既存の価格設定方式には以下があります。

  • 固定価格方式(Equal)

全てのBaseトークンは1 Derivativeと等価(NFTXやWrappedKittiesで採用)。

  • 比例方式(Proportional)

NFT20では100 Base = 1 Derivativeという比率を採用。

  • 変動価格方式(Variable)

NFT20ではダッチオークションにより価格変動を実現。NFTXではID指定引き出し時に追加料金を要求。

この多様性に対応するため、Mint系・Burn系のイベントには必ず value を含める必要があります。

NFT20は、ERC721やERC1155などのNFTをERC20トークンとしてラップし、流動性のある資産として取り扱えるようにするDeFiプロトコルです。 主にUniswapなどのAMM(自動マーケットメイカー)で取引可能なERC20トークンを発行し、NFTをフロア価格ベースで分割・取引できるようにすることを目的としています。

継承に関する設計

ERC20の継承

Wrapper コントラクトはERC20を継承して super.mint や super.burn を直接呼び出す設計が可能です。 継承しない場合は、Wrapperが唯一の発行・焼却権限を持つようにDerivaitveトークンコントラクトを設計する必要があります。

ERC721Receiver / ERC1155Receiver

このコントラクトを継承しない場合、Baseトークンは mint / burn 関数を通じてのみ送信可能とする制限を設けなければなりません。

ERC721トークンはアドレスとIDの組み合わせで一意であり、ERC1155は同一IDに対して複数枚を保持できます。 両者の違いを考慮して、ERC3386では以下のように統一しています。

  • _amount はERC721の場合必ず 1。
  • _amounts はERC721の場合空のリストまたは各IDに対して1つの値が必要。

この統一により、ERC1155のイベント仕様との整合性が保たれます。

代替案としては以下のような実装パターンがあります。

  • ERC721専用・ERC1155専用の個別インターフェース(例:ERC721Wrapper, ERC1155Wrapper)
  • mintFromERC721 などの関数による分岐

ERC721 / ERC1155 の継承

Baseトークンを自ら発行するような設計(例:Initial NFT Offering)では、WrapperがERC721/ERC1155を継承する形が選ばれることもあります。 継承しない場合はIERC721 / IERC1155 を通じてトークン送信処理を実装する必要があります。

承認

Mint・Burnの実行前に、適切なトークン送付の承認がされている必要があります。 これには2つの方式があります。

  • Wrapper内部でERC20/ERC721/ERC1155の approve を処理する。
  • Wrapper外部でユーザーが IERC721 / IERC1155 の承認をあらかじめ済ませる。

この要件を満たさなければ、MintまたはBurnの処理は実行できません。

参考実装

互換性

ERC3386は、既存の多くの実装と互換性を持つように設計されています。 特に、ERC20を継承して mint および burn 関数を使用しているプロジェクトとは高い互換性があります。

すでに市場に存在する実装例として、以下のようなプロジェクトがあります。

  • Wrapped Kitties(WK)

    • Mint時のイベント名:DepositKittyAndMintToken
    • Burn時のイベント名:BurnTokenAndWithdrawKitty
  • NFTX

    • ミント時のイベント名:Mint
    • バーン時のイベント名:Redeem

これらの実装は関数の構造や目的がこの標準と一致しており、導入が比較的容易です。 また、共通のイベント設計を採用することで、統一的な監視やインデックス化が可能になります。

セキュリティ

Wrapperコントラクトは、ERC20 Burnableを継承することが推奨されます。 もし継承しない場合でも、Derivativeトークンの供給量の管理権限はWrapperコントラクトに限定されなければなりません。 これは、デリバティブの価値が常にBaseトークンに裏付けられている必要があるためです。

また、特定のIDを指定してNFTを引き出せる idBurn や batchIdBurn の存在は、ユーザーがより価値の高いNFTを狙って引き出す可能性を生み出します。 この場合、BaseトークンのIDごとに大きな価値の差があると、プールが意図せず価値の高いNFTだけを失ってしまうリスクがあります。

そのため、以下のような対策が考慮されるべきです。

  • NFTX方式
    • 高価なNFTとそうでないNFTを別のプールに分けて管理する(専用プール方式)。
  • NFT20方式
    • ID指定による引き出しに追加手数料を課す(動的価格設定)。

このような価格のばらつきに対応する戦略を取り入れることで、プールの持続的な価値維持と公平性を確保できます。

このように、Wrapperコントラクトを実装する時は、供給量の制御・価格の整合性・ターゲティング機能の悪用防止といった観点から慎重な設計が求められます。

引用

Calvin Koder (@ashrowz), "ERC-3386: ERC-721 and ERC-1155 to ERC-20 Wrapper [DRAFT]," Ethereum Improvement Proposals, no. 3386, March 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3386.

最後に

今回は「ERC721やERC1155形式のNFTをプールに預けることで、ERC20を発行する仕組みを提案しているERC3386」についてまとめてきました! いかがだったでしょうか?

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

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