はじめに

初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、自動化された価格設定と流動性向上のためにFunction Oracle Automated Market Maker(FOAMM)という仕組みを使用して、ERC20やETHをERC721トークン(NFT)にラップ/アンラップする提案をしているERC7527についてまとめていきます!

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

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

概要

この規格では、ERC20トークンやETHをERC721トークンに変換するためのインターフェース、そしてその逆変換のためのシステムを提案しています。

ERCトークンとは

まず、ERCトークンとは何かを簡単に説明します。

  • ERC20
    • イーサリアムネットワーク上で最も一般的に使用されるトークン規格です。
    • 主に通貨や投票権など、分割可能な価値や権利を表すのに用いられます。
  • ERC-721
    • 一つ一つがユニークな特性を持つ非代替性トークン(NFT)を作成するための規格です。
    • 主にデジタルアートやコレクターズアイテムなど、個々のアイテムに固有の価値がある場合に用いられます。

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

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

提案内容の概要

この提案では、ERC20トークンやETHをERC721トークンに「変換」(wrap)するためのインターフェースと、ERC721トークンをERC20トークンやETHに「戻す」(unwrap)ためのインターフェースを提供することを目指しています。 つまり、特定の価値を持つトークンを、一つとしてユニークなNFTに変換したり、その逆をしたりすることが可能になります。

Function Oracle Automated Market Maker (FOAMM)

重要な点は、この変換の時に用いられる価格(mint/burn価格)を決定するための機能である「Function Oracle Automated Market Maker(FOAMM)」です。 これは、組み込まれた方程式を基に価格を供給するオラクル機能であり、NFTの発行(mint)や破棄(burn)を実行し、クリアする際に活用されます。

なぜこれが重要か?

この提案の背後にある考え方は、流動性のあるトークンとユニークな価値を持つNFTの間の橋渡しをすることにあります。 例えば、特定のERC20トークンを持っている人が、それをユニークなアート作品などのNFTに変換したい場合、このシステムを通じて簡単に実現できるようになります。また、その逆も同様です。

技術的実装への挑戦

実装にあたっては、安全性や効率性、そして利用者の利便性を確保するために、いくつかの技術的な挑戦が伴います。 例えば、FOAMMを用いた価格決定メカニズムの正確さや、ERC20/ERC721間の変換プロセスのセキュリティ、さらにはユーザーインターフェースの使いやすさなどが考慮されるべきポイントです。

この提案が実現すれば、イーサリアム上の資産の流動性と多様性がさらに拡大し、新しい種類のトークン経済が生まれる可能性があります。

動機

提案されているFunction Oracle Automated Market Maker(FOAMM)システムは、特にユニークで取引量の少ないトークンやNFT(非代替性トークン)など、分散型システム内で流動性が大きな課題となっている問題を解決するために設計されています。 このシステムの目的は、スマートコントラクトのメカニズムを用いて透明性が保たれる中で、NFTに流動性を提供し、自動的な価格設定ソリューションを提供することです。

自動価格発見

FOAMMシステムでは、取引が行われる時に対応するもう相手を必要としません。プールと直接やり取りを行う時、FOAMMは事前に定義された関数を使用してオラクルから自動的に価格を供給します。 これにより、市場でのNFTの価格発見が自動化され、NFTの購入や販売を行いたいユーザーは、市場で直接的な対応者を見つける必要がなくなります。

自動価格発見の概念は、FOAMMシステムにおいて、NFTの取引時に価格を自動で決定するメカニズムです。 通常、市場での取引には買い手と売り手が必要ですが、FOAMMシステムではこのプロセスを自動化し、直接対応者を探す必要をなくします。 ここで重要な役割を果たすのが「オラクル」です。 オラクルは、外部の情報源または事前に定義されたルールに基づいて価格情報を提供するシステムです。

FOAMMの自動価格発見のプロセス

価格決定

ユーザーがNFTをラップ(ERC20やETHをNFTに変換)またはアンラップ(NFTをERC20やETHに戻す)しようとする時、FOAMMはオラクルを利用します。 このオラクルは、事前に設定された数学的な関数を使って、その時点での適正な価格を計算します。

自動化 このシステムでは、ユーザーが取引を望む時に、別の個人を見つけて価格を交渉する必要はありません。 FOAMMが提供する価格は、オラクルによって自動的に決定され、ユーザーはその価格で即座に取引を行うことができます。

透明性と効率性 価格は全ての参加者にとって透明であり、人の介入を必要としないため、取引はより迅速かつ効率的に行われます。

例

例えば、あるアーティストが作成したデジタルアートのNFTを販売したいと考えています。 従来の市場では、このNFTに対する適正な価格を設定し、購入希望者を見つける必要があります。 しかし、FOAMMシステムを利用すれば、アーティストはNFTをプラットフォームにデポジットするだけで良く、システムが自動で価格を設定し、購入希望者は即座にこの価格でNFTを購入することができます。

このように、FOAMMシステムによる自動価格発見は、NFT市場の流動性を高め、取引の障壁を低減することで、より多くの人々がNFTを容易に取引できるようにすることを目的としています。

流動性の向上

従来の分散型取引所(DEX)モデルでは、流動性提供者(LP)がトークンを流動性プールに預け入れることで、取引を促進し、流動性を供給しています。 これらのLPが撤退すると、市場で利用可能な流動性に大きな変動をもたらし、不安定さを引き起こす可能性があります。

FOAMMシステムでは、流動性は外部のLPに依存することなく、wrapまたは解くunwrapを通じて内部的に追加または削除されます。 これにより、LPの急な撤退によって引き起こされる不安定性のリスクを軽減し、参加者の持続的な相互作用を通じて流動性が継続的に補充され、維持されます。

FOAMMの動機は、NFTエコシステムに信頼性をもたらすことにあります。 自動化された価格発見メカニズムと流動性の向上により、NFT市場における主要な課題のいくつかを解決し、より効率的で透明性の高い取引の促進を目指しています。 このようなイノベーションにより、NFTおよびその他の独特な資産の取引がよりアクセスしやすく、安全になることが期待されます。

仕様

提案されているコントラクトインターフェースには、Agency、App、Factoryという3つの主要なコンポーネントが含まれています。 これらのインターフェースは、ERC20やETHをERC721にラップ(wrap)したり、その逆のアンラップ(unwrap)を行うプロセスを管理するためのものです。 ここでの主な目的は、特定のトークンやETHをユニークなNFTに変換する際の自動化された価格発見と流動性の向上を実現することです。

AgencyとAppの役割

AgencyとAppは、同じコントラクトによって実装されることもあれば、別々に実装されることもあります。 別々に実装される場合、初期化後にアップグレード可能ではないように、相互に結びつけられます。 Appコントラクトは、onlyAgency() 修飾子を実装し、これにより、Mint(発行)とBurn(破棄)の関数は、対応するAgencyを通じてのみ呼び出された場合にのみ効果を発揮するように制限されます。 つまり、AppのMintとBurnの機能はAgencyを介して制御されることになります。 AgencyがonlyApp()を実装するのはオプション機能です。 これは、操作がAppによってのみ呼び出されるべき場合に有用です。

Factoryインターフェース

Factoryインターフェースはオプション機能であり、AgencyとAppが繰り返しデプロイされる必要がある場合に最も有用です。 Factoryパターンを使うことで、AgencyとAppのインスタンスを簡単に作成し、管理することが可能になります。

Function Oracleの実装

Function Oracleは、getWrapOracleとgetUnwrapOracleを通じて実装され、これらの関数内で定義されたパラメータと数学的方程式に基づいて価格を供給します。 これにより、ラップまたはアンラップ時の価格が自動的に決定されます。

FOAMMの実装

wrapとunwrapは、getWrapOracleとgetUnwrapOracleを呼び出して価格フィードを取得し、自動的にクリアします。 wrapを実行するために、FOAMMはプレミアムを受け取り、Appでmintを開始します。 unwrapを実行するために、FOAMMはプレミアムを送付し、Appでburnを開始します。

  • Agencyは、すべてのmintとburnのtransferに対する単一のエントリポイントとして機能します。

プレミアム

「プレミアム」とは、特定のアクション(例えば、トークンをラップしたりアンラップしたりする行為)を実行する時に、ユーザーが支払う追加の料金や価格のことを指します。 このプレミアムは、トークンをユニークなNFTに変換する時の手数料、またはその逆のプロセスを行う時の手数料と考えることができます。

プレミアムの目的

価格の調整 プレミアムは市場の需給に基づいて価格を調整する役割を果たします。 特に流動性が低いアイテムや、高い需要が見込まれるアイテムにおいては、プレミアムを通じてその価値を反映させることができます。

インセンティブの提供 プレミアムは、システムの維持や開発に貢献する者へのインセンティブとして機能します。 例えば、トークンをラップする時に発生するプレミアムの一部がシステムの運営者や開発者に支払われることで、システムの持続的な改善や安定稼働が促進されます。

リスクの管理 プレミアムは、変換プロセスのリスク(例えば価格の変動リスク)をカバーするためにも使用されます。 トークンの変換には市場価格の変動が伴うため、プレミアムによってそのリスクを一部吸収することができます。

プレミアムの計算

プレミアムの具体的な金額は、システムの設計や市場の状況によって異なります。 通常、プレミアムはFOAMMシステムの中で定義された数学的な関数に基づいて自動的に計算され、ラップやアンラップの際に適用される価格に加算されます。

例

仮に、あるユーザーがETHを特定のNFTにラップしたいとします。 このプロセスを実行するためには、基本的な変換料金に加えて、プレミアムが必要になります。 このプレミアムは、変換の便利さ、速さ、またはその他の付加価値に対する料金として解釈することができます。

この設計は、NFTのmintとburnプロセスを、外部の介入やマッチングパーティーを必要とせず、スムーズに行えるようにするためのものです。 また、FOAMMを通じて、流動性の問題を解決し、NFT市場における価格の自動発見と安定化を目指しています。

仕様

Agencyインターフェース

ts
pragma solidity ^0.8.20;

/**
 * @dev The settings of the agency.
 * @param currency The address of the currency. If `currency` is 0, the currency is Ether.
 * @param premium The base premium of the currency.
 * @param feeRecipient The address of the fee recipient.
 * @param mintFeePercent The fee of minting.
 * @param burnFeePercent The fee of burning.
 */
struct Asset {
    address currency;
    uint256 premium;
    address feeRecipient;
    uint16 mintFeePercent;
    uint16 burnFeePercent;
}

interface IERC7527Agency {
    /**
     * @dev Allows the account to receive Ether
     *
     * Accounts MUST implement a `receive` function.
     *
     * Accounts MAY perform arbitrary logic to restrict conditions
     * under which Ether can be received.
     */
    receive() external payable;

    /**
     * @dev Emitted when `tokenId` token is wrapped.
     * @param to The address of the recipient of the newly created non-fungible token.
     * @param tokenId The identifier of the newly created non-fungible token.
     * @param price The price of wrapping.
     * @param fee The fee of wrapping.
     */
    event Wrap(address indexed to, uint256 indexed tokenId, uint256 price, uint256 fee);

    /**
     * @dev Emitted when `tokenId` token is unwrapped.
     * @param to The address of the recipient of the currency.
     * @param tokenId The identifier of the non-fungible token to unwrap.
     * @param price The price of unwrapping.
     * @param fee The fee of unwrapping.
     */
    event Unwrap(address indexed to, uint256 indexed tokenId, uint256 price, uint256 fee);

    /**
     * @dev Wrap currency premium into a non-fungible token.
     * @param to The address of the recipient of the newly created non-fungible token.
     * @param data The data to encode into ifself and the newly created non-fungible token.
     * @return The identifier of the newly created non-fungible token.
     */
    function wrap(address to, bytes calldata data) external payable returns (uint256);

    /**
     * @dev Unwrap a non-fungible token into currency premium.
     * @param to The address of the recipient of the currency.
     * @param tokenId The identifier of the non-fungible token to unwrap.
     * @param data The data to encode into ifself and the non-fungible token with identifier `tokenId`.
     */
    function unwrap(address to, uint256 tokenId, bytes calldata data) external payable;

    /**
     * @dev Returns the strategy of the agency.
     * @return app The address of the app.
     * @return asset The asset of the agency.
     * @return attributeData The attributeData of the agency.
     */
    function getStrategy() external view returns (address app, Asset memory asset, bytes memory attributeData);

    /**
     * @dev Returns the price and fee of unwrapping.
     * @param data The data to encode to calculate the price and fee of unwrapping.
     * @return price The price of wrapping.
     * @return fee The fee of wrapping.
     */
    function getUnwrapOracle(bytes memory data) external view returns (uint256 price, uint256 fee);

    /**
     * @dev Returns the price and fee of wrapping.
     * @param data The data to encode to calculate the price and fee of wrapping.
     * @return price The price of wrapping.
     * @return fee The fee of wrapping.
     */
    function getWrapOracle(bytes memory data) external view returns (uint256 price, uint256 fee);
}

Wrap

javascript
event Wrap(address indexed to, uint256 indexed tokenId, uint256 price, uint256 fee);

概要

トークンがラップされた時に発行されるイベント。

詳細

このイベントは、ERC20トークンやETHがERC721トークンに変換された時に、該当の情報をブロックチェーン上に記録するために使用されます。 ラッププロセスの完了を示し、関係者が取引の詳細を追跡できるようにします。

パラメータ

  • to
    • 新しく作成された非代替性トークンの受取人のアドレス。
  • tokenId
    • 新しく作成された非代替性トークンの識別子。
  • price
    • ラッピングの価格。
  • fee
    • ラッピングの手数料。

Unwrap

javascript
event Unwrap(address indexed to, uint256 indexed tokenId, uint256 price, uint256 fee);

概要

トークンがアンラップされた時に発行されるイベント。

詳細

このイベントは、ERC721トークンがERC20トークンやETHに戻された時に、その情報をブロックチェーン上に記録するために使用されます。 アンラッププロセスの完了を示し、関係者が取引の詳細を追跡できるようにします。

パラメータ

  • to
    • 通貨の受取人のアドレス。
  • tokenId
    • アンラップされる非代替性トークンの識別子。
  • price
    • アンラッピングの価格。
  • fee
    • アンラッピングの手数料。

wrap

javascript
function wrap(address to, bytes calldata data) external payable returns (uint256);

概要

通貨を非代替性トークンにラップする関数。

詳細

この関数は、通貨(例:ERC20 トークンやETH)を新しく作成される ERC721 非代替性トークンに変換するプロセスを行います。 変換時に特定のデータをトークンにエンコードすることも可能です。

引数

  • to
    • 新しく作成された非代替性トークンの受取人のアドレス。
  • data
    • 非代替性トークンにエンコードするデータ。

戻り値

  • 新しく作成された非代替性トークンの識別子。

unwrap

javascript
function unwrap(address to, uint256 tokenId, bytes calldata data) external payable;

概要

非代替性トークンを通貨にアンラップする関数。

詳細

この関数は、特定の ERC721 非代替性トークンを元の通貨(ERC20 トークンやETHなど)に戻すプロセスを行います。 アンラップ時には特定のデータを指定し、トークンやプロセスに関連する情報をエンコードすることができます。

引数

  • to
    • 通貨の受取人のアドレス。
  • tokenId
    • アンラップする非代替性トークンの識別子。
  • data
    • アンラッププロセスに関連するデータ。

getStrategy

javascript
function getStrategy() external view returns (address app, Asset memory asset, bytes memory attributeData);

概要

エージェンシーの情報を取得する関数。

詳細

この関数は、エージェンシーに関連する情報を提供します。 具体的には、使用しているアプリケーション(App)のアドレス、アセットの種類、およびその他の属性データを返します。

戻り値

  • app
    • アプリケーションのアドレス。
  • asset
    • アセットの種類。
  • attributeData
    • 属性データ。

getUnwrapOracle

javascript
function getUnwrapOracle(bytes memory data) external view returns (uint256 price, uint256 fee);

概要

アンラッピングの価格と手数料を返す関数。

詳細

この関数は、アンラップ操作の時に適用される価格と手数料を計算し、それらの情報を提供します。 計算は指定されたデータに基づいて行われます。

引数

  • data
    • アンラッピングの価格と手数料の計算に使用されるデータ。

戻り値

  • price
    • アンラッピングの価格。
  • fee
    • アンラッピングの手数料。

getWrapOracle

javascript
function getWrapOracle(bytes memory data) external view returns (uint256 price, uint256 fee);

概要

ラッピングの価格と手数料を返す関数。

詳細

この関数は、ラップ操作の時に適用される価格と手数料を計算し、それらの情報を提供します。 計算は指定されたデータに基づいて行われます。

引数

  • data
    • ラッピングの価格と手数料の計算に使用されるデータ。

戻り値

  • price
    • ラッピングの価格。
  • fee
    • ラッピングの手数料。

Appインターフェース

ERC7527App は、インタフェース ERC721Metadata を継承する必要があります。

ts
pragma solidity ^0.8.20;

interface IERC7527App {
    /**
     * @dev Returns the maximum supply of the non-fungible token.
     */
    function getMaxSupply() external view returns (uint256);

    /**
     * @dev Returns the name of the non-fungible token with identifier `id`.
     * @param id The identifier of the non-fungible token.
     */
    function getName(uint256 id) external view returns (string memory);

    /**
     * @dev Returns the agency of the non-fungible token.
     */
    function getAgency() external view returns (address payable);

    /**
     * @dev Sets the agency of the non-fungible token.
     * @param agency The agency of the non-fungible token.
     */
    function setAgency(address payable agency) external;

    /**
     * @dev Mints a non-fungible token to `to`.
     * @param to The address of the recipient of the newly created non-fungible token.
     * @param data The data to encode into the newly created non-fungible token.
     */
    function mint(address to, bytes calldata data) external returns (uint256);

    /**
     * @dev Burns a non-fungible token with identifier `tokenId`.
     * @param tokenId The identifier of the non-fungible token to burn.
     * @param data The data to encode into the non-fungible token with identifier `tokenId`.
     */
    function burn(uint256 tokenId, bytes calldata data) external;
}

トークンIDはmint関数のdataパラメータで指定できます。


getMaxSupply

javascript
function getMaxSupply() external view returns (uint256);

概要

非代替性トークンの最大供給量を返す関数。

詳細

この関数は、コントラクトで定義された非代替性トークンの最大供給量を取得します。 これにより、発行可能なトークンの上限数を知ることができます。

戻り値

  • 最大供給量。

getName

javascript
function getName(uint256 id) external view returns (string memory);

概要

特定の非代替性トークンの名前を返す関数。

詳細

指定された識別子(id)を持つ非代替性トークンの名前を取得します。 各トークンは一意の名前を持つことができ、この関数を通じてその名前を知ることが可能です。

引数

  • id
    • 非代替性トークンの識別子。

戻り値

  • トークンの名前。

getAgency

javascript
function getAgency() external view returns (address payable);

概要

非代替性トークンのエージェンシーを返す関数。

詳細

非代替性トークンに関連するエージェンシーのアドレスを取得します。 エージェンシーはトークンの管理や操作に関連する機能を持つことがあります。

戻り値

  • エージェンシーのアドレス。

setAgency

javascript
function setAgency(address payable agency) external;

概要

非代替性トークンのエージェンシーを設定する関数。

詳細

指定されたアドレスを新しいエージェンシーとして設定します。 この操作により、トークンの管理権限を新しいエージェンシーに移譲することができます。

引数

  • agency
    • 新しいエージェンシーのアドレス。

mint

javascript
function mint(address to, bytes calldata data) external returns (uint256);

概要

新しい非代替性トークンを発行する関数。

詳細

指定されたアドレスに新しい非代替性トークンを発行(mint)します。 発行時には、トークンに関連するデータをエンコードすることができます。

引数

  • to
    • 非代替性トークンの受取人のアドレス。
  • data
    • トークンにエンコードするデータ。

戻り値

  • 新しく発行されたトークンの識別子。

burn

javascript
function burn(uint256 tokenId, bytes calldata data) external;

概要

非代替性トークンを破棄(burn)する関数。

詳細

指定された識別子(tokenId)を持つ非代替性トークンを破棄(burn)します。 破棄時には、トークンやプロセスに関連するデータをエンコードすることができます。

引数

  • tokenId
    • 破棄する非代替性トークンの識別子。
  • data
    • トークンに関連するデータ。

Factoryインターフェース

バインドされたAppおよびAgencyをデプロイするためにファクトリが必要な場合、ファクトリは、以下のインターフェースを実装する必要があります。

ts
pragma solidity ^0.8.20;

import {Asset} from "./IERC7527Agency.sol";

/**
 * @dev The settings of the agency.
 * @param implementation The address of the agency implementation.
 * @param asset The parameter of asset of the agency.
 * @param immutableData The immutable data are stored in the code region of the created proxy contract of agencyImplementation.
 * @param initData If init data is not empty, calls proxy contract of agencyImplementation with this data.
 */
struct AgencySettings {
    address payable implementation;
    Asset asset;
    bytes immutableData;
    bytes initData;
}

/**
 * @dev The settings of the app.
 * @param implementation The address of the app implementation.
 * @param immutableData The immutable data are stored in the code region of the created proxy contract of appImplementation.
 * @param initData If init data is not empty, calls proxy contract of appImplementation with this data.
 */
struct AppSettings {
    address implementation;
    bytes immutableData;
    bytes initData;
}

interface IERC7527Factory {
    /**
     * @dev Deploys a new agency and app clone and initializes both.
     * @param agencySettings The settings of the agency.
     * @param appSettings The settings of the app.
     * @param data The data is additional data, it has no specified format and it is sent in call to `factory`.
     * @return appInstance The address of the created proxy contract of appImplementation.
     * @return agencyInstance The address of the created proxy contract of agencyImplementation.
     */
    function deployWrap(AgencySettings calldata agencySettings, AppSettings calldata appSettings, bytes calldata data)
        external
        returns (address, address);
}

AgencySettings

javascript
struct AgencySettings {
    address payable implementation;
    Asset asset;
    bytes immutableData;
    bytes initData;
}

概要

エージェンシーの設定を定義する構造体。

詳細

この構造体は、エージェンシーの実装に関連する設定を格納します。 これには、エージェンシーの実装アドレス、資産パラメータ、不変データ、初期化データが含まれます。 不変データはプロキシコントラクトのコード領域に保存され、初期化データが空でない場合、プロキシコントラクトを通じて初期化処理が呼び出されます。

パラメータ

  • implementation
    • エージェンシー実装のアドレス。
  • asset
    • エージェンシーの資産パラメータ。
  • immutableData
    • プロキシコントラクトのコード領域に保存される不変データ。
  • initData
    • 初期化データ。
    • 空でない場合、プロキシコントラクトを介して呼び出されます。

AppSettings

javascript
struct AppSettings {
    address implementation;
    bytes immutableData;
    bytes initData;
}

概要

アプリの設定を定義する構造体。

詳細

この構造体は、アプリの実装に関連する設定を格納します。 これには、アプリの実装アドレス、不変データ、初期化データが含まれます。 不変データはプロキシコントラクトのコード領域に保存され、初期化データが空でない場合、プロキシコントラクトを通じて初期化処理が呼び出されます。

パラメータ

  • implementation
    • アプリ実装のアドレス。
  • immutableData
    • プロキシコントラクトのコード領域に保存される不変データ。
  • initData
    • 初期化データ。
    • 空でない場合、プロキシコントラクトを介して呼び出されます。

deployWrap

javascript
function deployWrap(AgencySettings calldata agencySettings, AppSettings calldata appSettings, bytes calldata data)
        external
        returns (address, address);

概要

新しいエージェンシーとアプリのクローンをデプロイし、両方を初期化する関数。

詳細

この関数は、エージェンシーとアプリの設定を受け取り、それらに基づいて新しいプロキシコントラクトをデプロイして初期化します。 追加のデータは、factory呼び出しにおいて使用され、特定のフォーマットは必要としません。 このプロセスを通じて、アプリとエージェンシーの新しいインスタンスが作成され、そのアドレスが返されます。

引数

  • agencySettings
    • エージェンシーの設定。
    • AgencySettings構造体を使用します。
  • appSettings
    • アプリの設定。
    • AppSettings構造体を使用します。
  • data
    • 追加のデータ。
    • 指定されたフォーマットはなく、factory呼び出し時に送信されます。

戻り値

  • appInstance
    • 作成されたアプリ実装のプロキシコントラクトのアドレス。
  • agencyInstance
    • 作成されたエージェンシー実装のプロキシコントラクトのアドレス。

補足

。この提案には、既存のERC721標準を拡張して、mint(発行)とburn(破棄)を一貫した方法で行うIERC5679Ext721インターフェースが含まれています。

以前のインターフェース

ERC5679は、ミントとバーンを一貫した方法で拡張するためのIERC5679Ext721インターフェースを提案しています。 後方互換性を保証するため、一部のコントラクトがERC721TokenReceiverを実装していないことを考慮して、IERC7527AppではsafeMintの代わりにmint関数を使用します。 burn関数では、安全性と相互結合の一意性を保証するために、_fromパラメータが結びつけられたエージェンシーのコントラクトアドレスでなければならないとされています。

相互結合(Mutual Bound)

IERC7527AppとIERC7527Agencyを実装するコントラクトは、互いに唯一の所有者となるように実装されます。 ラッププロセスでは、受け取った代替可能トークンのプレミアム額をチェックし、その後アプリで非代替トークンをミントします。 非代替トークンの所有者または承認者のみがアンラップを行うことができます。

実装の多様性

ユーザーは、エージェンシーとアプリのインターフェースを実装する時に、機能と手数料の割合をカスタマイズすることができます。 異なるエージェンシーの実装には、異なるラップ、アンラップの関数ロジック、および異なるオラクル関数があります。 異なるアプリの実装は、様々なユースケースに対応しています。

通貨タイプ

IERC7527Agencyの通貨は、代替可能トークンのアドレスです。 システム内で代替可能トークンとして一つの通貨タイプのみを定義できます。 この通貨は、ETHやERC20を含む様々な種類の代替可能トークンをサポートします。

トークンID

各ラッププロセスで、一意のトークンIDが生成されるべきです。 このトークンIDは、アンラッププロセス中の検証に不可欠であり、トークンの排他的な証明書として機能します。 このメカニズムは、コントラクト内の資産の安全性を保証します。

上記の説明は、非代替性トークン(NFT)を扱う新しいイーサリアムのインターフェース、特にIERC7527AppとIERC7527Agencyにおける「ラップとミント」と「アンラップとバーン」のプロセスに焦点を当てています。以下で、重要なポイントを詳しく説明します。

ラップとミント

戦略設定

エージェンシーインターフェースを実装する時、strategyは設定され、デプロイ後はアップグレード不可能であることが保証されます。

ラップ関数の実行

ラップを実行する時、あらかじめ定められたstrategyパラメータがgetWrapOracle関数に渡され、現在のプレミアムと手数料が取得されます。 それに応じたプレミアムはエージェンシーインスタンスにtransferされ、mintFeePercentに従った手数料は手数料受取人(feeRecipient)にtransferされます。 その後、アプリはユーザーのアドレスにNFTをミントします。

プレミアムの取扱い

エージェンシーにtransferされたプレミアム(トークン)は、アンラッププロセスを通じてのみ移動可能です。 ラップの実行はミントプロセスを引き起こす唯一のトリガーです。

アンラップとバーン

アンラップを実行する時、あらかじめ定められたstrategyパラメータがgetUnwrapOracle関数に渡され、現在のプレミアムと手数料が読み取られます。 アプリはNFTをバーンします。 そして、burnFeePercentに応じた手数料を差し引いた対応するプレミアムがユーザーのアドレスにtransferされ、手数料は手数料受取人にtransferされます。 アンラップの実行はバーンプロセスを引き起こす唯一のトリガーです。

二つのインターフェースの組み合わせ使用

IERC7527AppとIERC7527Agencyは安全性を高めるために一緒に実装することができますが、初期化前には独立して実装することで柔軟性を確保することができます。

価格設定

価格取得

getWrapOracleとgetUnwrapOracleは現在のプレミアムと手数料を取得するために使用されます。 これらは、オラクル関数を通じてチェーン上での価格取得を実装します。 ラップとアンラップのプロセス中だけでなく、貸出コントラクトなど他のコントラクトがプレミアムと手数料を取得するためにこれらを呼び出すこともサポートします。

関数オラクル

チェーン上およびチェーン外のパラメータに基づく関数オラクルをサポートしますが、チェーン上の実際の合意にのみチェーン上のパラメータが推奨されます。

この提案は、NFTの流動性を高め、取引プロセスを自動化することに焦点を当てており、ユーザーがカスタマイズ可能な手数料と戦略を設定することにより、さまざまなユースケースに対応できるように設計されています。

実装

ts
pragma solidity ^0.8.20;

import {
    ERC721Enumerable,
    ERC721,
    IERC721Enumerable
} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {ClonesWithImmutableArgs} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol";
import {IERC7527App} from "./interfaces/IERC7527App.sol";
import {IERC7527Agency, Asset} from "./interfaces/IERC7527Agency.sol";
import {IERC7527Factory, AgencySettings, AppSettings} from "./interfaces/IERC7527Factory.sol";

contract ERC7527Agency is IERC7527Agency {
    using Address for address payable;

    receive() external payable {}

    function unwrap(address to, uint256 tokenId, bytes calldata data) external payable override {
        (address _app, Asset memory _asset,) = getStrategy();
        require(_isApprovedOrOwner(_app, msg.sender, tokenId), "LnModule: not owner");
        IERC7527App(_app).burn(tokenId, data);
        uint256 _sold = IERC721Enumerable(_app).totalSupply();
        (uint256 premium, uint256 burnFee) = getUnwrapOracle(abi.encode(_sold));
        _transfer(address(0), payable(to), premium - burnFee);
        _transfer(address(0), _asset.feeRecipient, burnFee);
        emit Unwrap(to, tokenId, premium, burnFee);
    }

    function wrap(address to, bytes calldata data) external payable override returns (uint256) {
        (address _app, Asset memory _asset,) = getStrategy();
        uint256 _sold = IERC721Enumerable(_app).totalSupply();
        (uint256 premium, uint256 mintFee) = getWrapOracle(abi.encode(_sold));
        require(msg.value >= premium + mintFee, "ERC7527Agency: insufficient funds");
        _transfer(address(0), _asset.feeRecipient, mintFee);
        if (msg.value > premium + mintFee) {
            _transfer(address(0), payable(msg.sender), msg.value - premium - mintFee);
        }
        uint256 id_ = IERC7527App(_app).mint(to, data);
        require(_sold + 1 == IERC721Enumerable(_app).totalSupply(), "ERC7527Agency: Reentrancy");
        emit Wrap(to, id_, premium, mintFee);
        return id_;
    }

    function getStrategy() public pure override returns (address app, Asset memory asset, bytes memory attributeData) {
        uint256 offset = _getImmutableArgsOffset();
        address currency;
        uint256 premium;
        address payable awardFeeRecipient;
        uint16 mintFeePercent;
        uint16 burnFeePercent;
        assembly {
            app := shr(0x60, calldataload(add(offset, 0)))
            currency := shr(0x60, calldataload(add(offset, 20)))
            premium := calldataload(add(offset, 40))
            awardFeeRecipient := shr(0x60, calldataload(add(offset, 72)))
            mintFeePercent := shr(0xf0, calldataload(add(offset, 92)))
            burnFeePercent := shr(0xf0, calldataload(add(offset, 94)))
        }
        asset = Asset(currency, premium, awardFeeRecipient, mintFeePercent, burnFeePercent);
        attributeData = "";
    }

    function getUnwrapOracle(bytes memory data) public pure override returns (uint256 premium, uint256 fee) {
        uint256 input = abi.decode(data, (uint256));
        (, Asset memory _asset,) = getStrategy();
        premium = _asset.premium + input * _asset.premium / 100;
        fee = premium * _asset.burnFeePercent / 10000;
    }

    function getWrapOracle(bytes memory data) public pure override returns (uint256 premium, uint256 fee) {
        uint256 input = abi.decode(data, (uint256));
        (, Asset memory _asset,) = getStrategy();
        premium = _asset.premium + input * _asset.premium / 100;
        fee = premium * _asset.mintFeePercent / 10000;
    }

    function _transfer(address currency, address recipient, uint256 premium) internal {
        if (currency == address(0)) {
            payable(recipient).sendValue(premium);
        } else {
            IERC20(currency).transfer(recipient, premium);
        }
    }

    function _isApprovedOrOwner(address app, address spender, uint256 tokenId) internal view virtual returns (bool) {
        IERC721Enumerable _app = IERC721Enumerable(app);
        address _owner = _app.ownerOf(tokenId);
        return (spender == _owner || _app.isApprovedForAll(_owner, spender) || _app.getApproved(tokenId) == spender);
    }
    /// @return offset The offset of the packed immutable args in calldata

    function _getImmutableArgsOffset() internal pure returns (uint256 offset) {
        // solhint-disable-next-line no-inline-assembly
        assembly {
            offset := sub(calldatasize(), add(shr(240, calldataload(sub(calldatasize(), 2))), 2))
        }
    }
}

contract ERC7527App is ERC721Enumerable, IERC7527App {
    constructor() ERC721("ERC7527App", "EA") {}

    address payable private _oracle;

    modifier onlyAgency() {
        require(msg.sender == _getAgency(), "only agency");
        _;
    }

    function getName(uint256) external pure returns (string memory) {
        return "App";
    }

    function getMaxSupply() public pure override returns (uint256) {
        return 100;
    }

    function getAgency() external view override returns (address payable) {
        return _getAgency();
    }

    function setAgency(address payable oracle) external override {
        require(_getAgency() == address(0), "already set");
        _oracle = oracle;
    }

    function mint(address to, bytes calldata data) external override onlyAgency returns (uint256 tokenId) {
        require(totalSupply() < getMaxSupply(), "max supply reached");
        tokenId = abi.decode(data, (uint256));
        _mint(to, tokenId);
    }

    function burn(uint256 tokenId, bytes calldata) external override onlyAgency {
        _burn(tokenId);
    }

    function _getAgency() internal view returns (address payable) {
        return _oracle;
    }
}

contract ERC7527Factory is IERC7527Factory {
    using ClonesWithImmutableArgs for address;

    function deployWrap(AgencySettings calldata agencySettings, AppSettings calldata appSettings, bytes calldata)
        external
        override
        returns (address appInstance, address agencyInstance)
    {
        appInstance = appSettings.implementation.clone(appSettings.immutableData);
        {
            agencyInstance = address(agencySettings.implementation).clone(
                abi.encodePacked(
                    appInstance,
                    agencySettings.asset.currency,
                    agencySettings.asset.premium,
                    agencySettings.asset.feeRecipient,
                    agencySettings.asset.mintFeePercent,
                    agencySettings.asset.burnFeePercent,
                    agencySettings.immutableData
                )
            );
        }

        IERC7527App(appInstance).setAgency(payable(agencyInstance));
        if (agencySettings.initData.length != 0) {
            (bool success, bytes memory result) = agencyInstance.call(agencySettings.initData);

            if (!success) {
                assembly {
                    revert(add(result, 32), mload(result))
                }
            }
        }

        if (appSettings.initData.length != 0) {
            (bool success, bytes memory result) = appInstance.call(appSettings.initData);

            if (!success) {
                assembly {
                    revert(add(result, 32), mload(result))
                }
            }
        }
    }
}

セキュリティ

詐欺防止

コントラクトの安全性を保証するために、以下の点を検討する必要があります。

修飾子の実装と適用のチェック

onlyAgency()とonlyApp()修飾子が適切に実装され、適用されているかどうかを確認します。 これらの修飾子は、関数が特定のエージェンシーやアプリからのみ呼び出されることを保証し、不正なアクセスや操作を防ぎます。

関数戦略のチェック

コントラクトの関数が適切なstrategyに従っているかどうかを確認します。 これは、コントラクトのロジックが想定されたビジネスロジックに従っているかどうかを保証するために重要です。

リエントランシー攻撃に対するチェック

コントラクトがリエントランシー(再入)攻撃に対して脆弱でないかどうかを確認します。 リエントランシー攻撃は、外部コントラクトが実行中の関数を中断して、再度その関数を呼び出すことにより発生する可能性があります。 これを防ぐために、状態変更を行う前に外部コールを行う、あるいは再入防止パターン(例えば、Ethereumのロック)を実装するなどの対策が必要です。

全ての非代替トークンがFOAMMから計算されたプレミアムでアンラップ可能かどうかのチェック

FOAMM(Function Oracle Automated Market Maker)から計算されたプレミアムを使用して、全ての非代替トークンがアンラップ可能であることを確認します。 これは、プレミアム計算の正確性と、アンラッププロセスの公平性を保証するために重要です。

引用

Elaine Zhang (@lanyinzly) lz8aj@virginia.edu, Jerry jerrymindflow@gmail.com, Amandafanny amandafanny200@gmail.com, Shouhao Wong (@wangshouh) wongshouhao@outlook.com, 0xPoet 0xpoets@gmail.com, "ERC-7527: Token Bound Function Oracle AMM [DRAFT]," Ethereum Improvement Proposals, no. 7527, September 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7527.

最後に

今回は「自動化された価格設定と流動性向上のためにFunction Oracle Automated Market Maker(FOAMM)という仕組みを使用して、ERC20やETHをERC721トークン(NFT)にラップ/アンラップする提案をしているERC7527」についてまとめてきました! いかがだったでしょうか?

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

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