はじめに

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

今回は、NFT(ERC721)が代替性トークン(ERC20)を安全に保持・管理し、まとめて取引できるようにする仕組みを提案しているERC7590についてまとめていきます!

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

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

概要

ERC7590は、ERC721(非代替性トークンの標準。各トークンが一意で、同じものに置き換えられない資産を表す規格)を拡張し、ERC20(代替性トークンの標準。同一価値・同一性質のトークンを多数発行・移転できる規格)を簡単にやり取りできるようにする提案です。 拡張により、1つのNFT(ERC721トークン)が複数のERC20トークンを束ねて保持・管理し、その入出庫(NFTの中へ引き込み/外へ出す)を制御できます。 具体的には、NFTコントラクトに対して特定のトークンIDにERC20残高をプル(コントラクト側から引き取る操作)するメソッドを用意し、NFTの所有者がその残高をTransfer アウト(外部へ払い出し)できるようにします。 さらに、払い出し時にはノンス(使い捨ての連番)を含め、フロントランニング(誰かの送信前取引を観測し、先回りで有利な取引を差し込む行為)を防ぎます。

動機

ブロックチェーンや分散型エコシステムでは、異なるトークン規格の相互運用性(規格をまたいで資産を一貫して扱える性質)が重要になっています。 ERC721を拡張してERC20を扱えるようにすることで、NFTが複合的な取引に参加できるようになり、代替性資産(ERC20)、非代替性資産(ERC721)、さらには複数クラスの資産を単一のプロトコルの中でまとめて扱えるようになります。

この拡張により、以下の観点で新しい価値が生まれます。

  • ユースケースの拡張 NFTが多様なトークンを扱えるようになることで、ゲームやデジタルコレクティブル、DeFi(分散型金融)、サプライチェーンなどで、1つのNFTが複数の資産を束ねる設計が現実的になります。 例えば、ゲームではあるキャラクターNFTにゲーム内通貨(ERC20)や強化素材(ERC20/ERC721)を一緒に格納し、売買や貸し出しをシンプルにできます。

  • 複合トランザクションの促進 代替性資産と非代替性資産を同時に動かす複合取引を、単一のNFTを介して表現できます。 例えば、マーケットプレイスでNFTを移転すると同時に、そのNFTが保持するERC20残高も一緒に移転される設計にすれば、受け渡しの一体化が進み、ユーザー操作とガスコストの整理につながります。

  • 市場流動性と価値創出 NFTが保管庫のように機能して多種類のトークンを扱えると、資産のまとめ売り・まとめ移転が簡単になります。 結果として、ERC20とERC721の双方で取引機会が増え、流動性(売買のしやすさ)が向上します。 資産の組み合わせ設計も多様になり、新しい価格付けや金融商品が生まれる余地が広がります。

ユーティリティ

ERC7590の核は、「NFTがERC20残高を所有権にひもづけて保持・管理する」という点です。 これにより、NFTの所有者が明確に定義されている限り、その所有者がコントラクトの安全なメソッドを通じて入出庫を行えます。 Transfer アウト時のノンスは、同じ要求の二重実行や取引の先回り(フロントランニング)を抑止する役割を持ちます。

下の表は、従来と拡張後の役割の違いを要点だけ整理したものです。

観点 従来のERC721 本拡張(ERC721がERC20を保持)
扱える資産 非代替性のみ 非代替性 + 代替性(束ねて保持)
所有権の単位 トークンIDごと トークンIDごと(ERC-20残高も付随)
典型操作 NFT転送 NFT転送 + ERC-20入出庫(プル/払い出し)
セキュリティ 所有者検証 所有者検証 + ノンスでフロントラン防止

ユースケースの拡張

ゲームでは、キャラクターやアイテムのNFTにゲーム内通貨(ERC20)を持たせることで、「キャラ+通貨+素材」をまとめて売買できます。 コレクティブル分野では、NFTに特典用トークンや投票権トークンをセットすることで、保有だけで権利や機能が付く形にできます。 DeFiでは、担保NFTに利回り原資となるERC20をひもづけるなど、ポジションの可搬性が高まります。 サプライチェーンでは、NFTがモノの識別子として機能しつつ、手数料や保証金をERC20で内包することで、移転時の清算を簡素化できます。

複合トランザクションの促進

1つのNFTに関連する資産がまとまると、署名・承認・移転の一連の操作を少ない工数で済ませやすくなります。 例えば、NFTの所有権移転をトリガーに、そのNFTにひもづくERC20残高も新オーナーに帰属させられます。 さらに、コントラクト側のプル(引き取り)を採用することで、ユーザーは通常のERC20承認(approve)を一度出すだけで、コントラクトが必要量を安全に回収できます。

市場流動性と価値創出

バンドル化されたNFTは、商品セットとして理解しやすく、売買の意思決定が速くなります。 複数トークンの一括評価やパッケージ割引といった価格戦略も取りやすくなり、買い手・売り手双方にとって発見しやすい価値が生まれます。 これにより、マーケットプレイスは在庫の回転と手数料収益の両面でメリットを得られます。

仕様

インターフェース

js

interface IERC7590 /*is IERC165, IERC721*/  {
    /**
     * @notice Used to notify listeners that the token received ERC-20 tokens.
     * @param erc20Contract The address of the ERC-20 smart contract
     * @param toTokenId The ID of the token receiving the ERC-20 tokens
     * @param from The address of the account from which the tokens are being transferred
     * @param amount The number of ERC-20 tokens received
     */
    event ReceivedERC20(
        address indexed erc20Contract,
        uint256 indexed toTokenId,
        address indexed from,
        uint256 amount
    );

    /**
     * @notice Used to notify the listeners that the ERC-20 tokens have been transferred.
     * @param erc20Contract The address of the ERC-20 smart contract
     * @param fromTokenId The ID of the token from which the ERC-20 tokens have been transferred
     * @param to The address receiving the ERC-20 tokens
     * @param amount The number of ERC-20 tokens transferred
     */
    event TransferredERC20(
        address indexed erc20Contract,
        uint256 indexed fromTokenId,
        address indexed to,
        uint256 amount
    );

    /**
     * @notice Used to retrieve the given token's specific ERC-20 balance
     * @param erc20Contract The address of the ERC-20 smart contract
     * @param tokenId The ID of the token being checked for ERC-20 balance
     * @return The amount of the specified ERC-20 tokens owned by a given token
     */
    function balanceOfERC20(
        address erc20Contract,
        uint256 tokenId
    ) external view returns (uint256);

    /**
     * @notice Transfer ERC-20 tokens from a specific token.
     * @dev The balance MUST be transferred from this smart contract.
     * @dev MUST increase the transfer-out-nonce for the tokenId
     * @dev MUST revert if the `msg.sender` is not the owner of the NFT or approved to manage it.
     * @param erc20Contract The address of the ERC-20 smart contract
     * @param tokenId The ID of the token to transfer the ERC-20 tokens from
     * @param amount The number of ERC-20 tokens to transfer
     * @param data Additional data with no specified format, to allow for custom logic
     */
    function transferHeldERC20FromToken(
        address erc20Contract,
        uint256 tokenId,
        address to,
        uint256 amount,
        bytes memory data
    ) external;

    /**
     * @notice Transfer ERC-20 tokens to a specific token.
     * @dev The ERC-20 smart contract must have approval for this contract to transfer the ERC-20 tokens.
     * @dev The balance MUST be transferred from the `msg.sender`.
     * @param erc20Contract The address of the ERC-20 smart contract
     * @param tokenId The ID of the token to transfer ERC-20 tokens to
     * @param amount The number of ERC-20 tokens to transfer
     * @param data Additional data with no specified format, to allow for custom logic
     */
    function transferERC20ToToken(
        address erc20Contract,
        uint256 tokenId,
        uint256 amount,
        bytes memory data
    ) external;

    /**
     * @notice Nonce increased every time an ERC20 token is transferred out of a token
     * @param tokenId The ID of the token to check the nonce for
     * @return The nonce of the token
     */
    function erc20TransferOutNonce(
        uint256 tokenId
    ) external view returns (uint256);
}

ReceivedERC20

javascript
event ReceivedERC20(
    address indexed erc20Contract,
    uint256 indexed toTokenId,
    address indexed from,
    uint256 amount
);

ERC20トークンが特定のNFTに入金されたときに発行されるイベント。 NFT(ERC721トークン)がERC20トークンを受け取ったことを通知するためのイベントです。 外部アドレスから特定のNFTトークンIDにトークンが送付された時に発行されます。 これにより、ウォレットやアプリケーションが、どのNFTがどのERC20を受け取ったかを正確に把握できます。

パラメータ

  • erc20Contract
    • 対応するERC20コントラクトのアドレス。
  • toTokenId
    • ERC20トークンを受け取ったNFTのトークンID。
  • from
    • トークンを送ったアドレス。
  • amount
    • 受け取ったERC20トークンの数量。

TransferredERC20

javascript
event TransferredERC20(
    address indexed erc20Contract,
    uint256 indexed fromTokenId,
    address indexed to,
    uint256 amount
);

NFTが保有していたERC20トークンを外部アドレスへ送った時に発行されるイベント。 NFT内部で保持されているERC20トークンを外部へ払い出したことを通知するためのイベントです。 NFTの所有者または承認されたアカウントによるトークン送信時に発行され、どのNFTがどのアドレスへトークンを送ったかを明示します。

パラメータ

  • erc20Contract
    • 対象のERC20コントラクトのアドレス。
  • fromTokenId
    • トークンを送り出したNFTのトークンID。
  • to
    • ERC20トークンを受け取るアドレス。
  • amount
    • 送付されたERC20トークンの数量。

balanceOfERC20

javascript
function balanceOfERC20(
    address erc20Contract,
    uint256 tokenId
) external view returns (uint256);

指定したNFTトークンが保有する特定のERC20トークン残高を取得する関数。 この関数は、指定されたERC20コントラクトにおいて、特定のNFTトークンIDがどれだけのERC20残高を保持しているかを確認します。 NFTがどのくらいのERC20資産を持っているかを可視化するために利用します。

引数

  • erc20Contract
    • 対象となるERC20コントラクトのアドレス。
  • tokenId
    • 残高を確認したいNFTのトークンID。

戻り値

  • uint256
    • 指定したNFTが保有しているERC20トークンの残高。

transferHeldERC20FromToken

javascript
function transferHeldERC20FromToken(
    address erc20Contract,
    uint256 tokenId,
    address to,
    uint256 amount,
    bytes memory data
) external;

NFTが内部に保持しているERC20トークンを外部アドレスに送る関数。 この関数は、NFTが保持しているERC20トークンを外部へ送付する時に使用します。 NFTの所有者、またはその管理権限を持つアドレスのみが実行できます。 実行時にはトークンIDごとの送金ノンス(送信回数カウンタ)が1つ増加し、二重送金防止や再実行攻撃対策として機能します。 また、送金時に付随情報(data)を渡せるため、カスタムロジック(たとえば特定アプリ向けの処理)を組み込むことができます。

引数

  • erc20Contract
    • 対象となるERC20コントラクトのアドレス。
  • tokenId
    • トークンを送信するNFTのトークンID。
  • to
    • 送金先のアドレス。
  • amount
    • 送信するERC20トークンの数量。
  • data
    • 追加データ。
    • 任意のカスタム情報を含めることができます。

transferERC20ToToken

javascript
function transferERC20ToToken(
    address erc20Contract,
    uint256 tokenId,
    uint256 amount,
    bytes memory data
) external;

外部アカウントからNFTへERC20トークンを送る関数。 この関数は、指定したERC20トークンをNFTの内部に送付する処理を行います。 送信元のアドレス(msg.sender)は、事前にこのコントラクトに対して送付許可(approve)を与えておく必要があります。 NFTのトークンIDごとにERC20残高が記録され、NFTが資産を保持できるようになります。 付随データ(data)を利用することで、特定アプリやプロトコル用の独自拡張も可能です。

引数

  • erc20Contract
    • 対象となるERC20コントラクトのアドレス。
  • tokenId
    • トークンを受け取るNFTのトークンID。
  • amount
    • 送付するERC20トークンの数量。
  • data
    • 追加データ。
    • 任意の情報を格納できます。

erc20TransferOutNonce

javascript
function erc20TransferOutNonce(
    uint256 tokenId
) external view returns (uint256);

NFTごとに、ERC20トークンを払い出した回数を示すノンスを取得する関数。 この関数は、NFTがERC20トークンを外部へ送金するたびに増加するノンス値を返します。 ノンスは、同一リクエストの再実行防止(リプレイ攻撃対策)やフロントランニング防止のために利用されます。 各NFT(トークンID)ごとに独立して管理されます。

引数

  • tokenId
    • ノンス値を確認するNFTのトークンID。

戻り値

  • uint256
    • 現在のノンス値(送金回数を表すカウンタ)。

補足

プル方式(Pull Mechanism)

ERC7590では、NFTに対してトークンを受け取るのではなく、コントラクトが自らトークンを引き取る(pullする)方式を採用しています。 つまり、NFTコントラクトが自らのアドレスへトークンを送付するという動作を行う形です。

この方法を採用した理由は、以下の2つです。

  • フック(Hooks)による柔軟なカスタマイズが可能になること
  • ERC20には「コールバック付き転送」が存在しないこと
フックによるカスタマイズ性

フック(Hook)とは、特定の操作(この場合はトークン転送)前後に任意の処理を差し込む仕組みです。 プル方式では、コントラクトがトークン転送の開始を自ら行うため、送付前後の任意のタイミングで特定の処理を実行できるようになります。

例えば、NFTに対してERC20トークンを引き取る前に、以下を自動実行することができます。

  • 所有者の認証、
  • 手数料の計算、
  • 特定条件の検証

また、トークン送付後に別のイベントをトリガーするなど、ビジネスロジックを拡張しやすい設計になります。

ERC20にコールバック付き送付が存在しないこと

ERC20規格では、トークン送付を行う時に受信者へ通知する仕組み(コールバック)がありません。 一方、ERC721(NFT)やERC1155(複合トークン)では、safeTransfer メソッドを利用することで、送付時に受信側コントラクトへコールバック通知を送ることができます。 これにより「トークンを受け取ったことを知る」ことができます。

しかし、ERC20にはこれが存在しません。 そのため、transfer や transferFrom でトークンを送っても、受信側コントラクトは送付が成功したことを知る手段がありません。 また、「どのNFT(トークンID)が受け取るべきか」という情報も同時に渡すことができません。

そのため、受け取り型(push型)でのトークン受領では安全かつ確実な動作を保証できないという問題があります。 この制約を回避するために、NFTコントラクト自身が引き取りを行うプル方式を採用しています。

プル方式の欠点

この方式の唯一の欠点は、トークン転送前に明示的な承認(approve)が必要になることです。 つまり、NFTコントラクトがユーザーのトークンを引き取るためには、ユーザーが事前に以下のように許可を与える必要があります。

javascript
ERC20(トークンコントラクト).approve(NFTコントラクトのアドレス, 金額);

この手順を踏まないと、NFTコントラクトはユーザーのトークンを操作できません。 しかし、これは安全性の観点から望ましいとも言えます。 ユーザーが意図しないトークン移転を防ぐことができ、明示的な合意に基づく操作が保証されます。

粒度の高い設計(Granular)と汎用的設計(Generic)の比較

この提案を実装する時、2つの異なるアプローチが検討されました。

アプローチ名 内容 特徴
粒度の高い設計(Granular) トークンの種類ごとに独立したインターフェースを定義する方法 各規格に最適化でき、ガス消費が少ない
汎用的設計(Generic) ERC20・ERC721・ERC1155すべてを1つの汎用インターフェースで扱う方法 コードは一体化されるが、処理が複雑になる
粒度の高い設計

この方式では、ERC20・ERC721・ERC1155それぞれに個別のインターフェースを用意します。 各規格の特性に合わせてメソッドを最適化できるため、余分なデータ(Idやamountなど)を指定する必要がありません。

  • メリット

    • 実装がシンプルで理解しやすい。
    • ガスコストが安い(不要な変数処理が少ない)。
    • コントラクトサイズが小さくなる(必要な規格だけ実装可能)。
  • デメリット

    • 他のトークン規格を追加する場合、個別のインターフェースが必要になる。
汎用的設計

この方式では、1つの統一的なインターフェースであらゆるトークン(ERC20 / ERC721 / ERC1155)を扱うことを想定しています。 送付メソッドも共通化され、同じ関数で送受信を行うことができます。

  • メリット

    • 設計が一元化され、扱うトークン規格が増えても拡張しやすい。
    • 同じメソッドで異なる資産タイプを操作できる。
  • デメリット

    • 常に id と amount の両方を扱う必要があり、不要な引数が発生する。
    • コードが複雑化し、処理コストが高くなる。

特に、ERC20ではidが不要で、ERC721ではamountが不要なため、汎用的設計では無駄なパラメータ指定が避けられません。

なぜ粒度の高い設計を採用したのか

以下の理由から、最終的に粒度の高い設計(Granular approach)が採用されました。

  • ERC20には safeTransfer が存在しないため、特別な対応が必要

ERC721とERC1155には safeTransfer があり、トークン送付時に受信側へ通知できる仕組みがあります。 しかしERC20にはこの機能がないため、専用の管理インターフェースが必要です。

  • ERC721ReceiverやERC1155Receiverといった既存の受信インターフェースが既に存在しているため、新しいものを定義する必要がない

ERC721とERC1155は受信時の挙動が明確であり、標準的な方法で受信確認が可能です。 そのため、新たな汎用的インターフェースを定義する意義が薄いと判断されました。

  • ガス効率とシンプルさの両立

粒度の高い設計は、不要な処理が少なく効率的で、現実的な運用に適しています。

参考実装

セキュリティ

ERC721と共通するリスク

ERC721と同様に、内部関数に隠れたロジック(Hidden Logic)が存在する可能性があります。 例えば、以下のような関数に悪意のある処理を埋め込むことが可能です。

関数名 潜在的なリスク
burn トークンをBurnする時に、他のデータや資産も誤って削除する可能性。
addResource トークンを追加する時に不正な操作(他者トークンの取り込みなど)を行う可能性。
acceptResource 受け入れ処理の中に意図しない再入可能性攻撃(reentrancy attack)を仕込む危険性。

これらの処理が監査されていないコントラクトに含まれる場合、資産喪失や乗っ取りのリスクが発生する可能性があります。 したがって、非監査コントラクトを使用する時には特に慎重な確認が必要です。

トークン送付時の「送信元アドレス」管理

NFTコントラクトがERC20トークンを受け取る場合、実装では必ずmsg.senderをfromパラメータ(送信元)として使用しなければなりません。

理由は、NFTコントラクト自身がユーザーのトークンをpull(引き取る)ために、あらかじめ承認(approve)を受ける設計になっているためです。 もし実装で誤って別の送信元を指定してしまうと、以下のような問題が起こりえます。

問題の内容 結果
コントラクトが誤った送信元アドレスを使用 承認済みトークンを他のNFTへ誤って転送する可能性。
攻撃者が意図的に送信元情報をすり替える 他人のトークンを不正に取り込む行為が可能になるリスク。

したがって、トークン引き取り(pull)操作では常にmsg.senderを使用することが必須です。

送付量の不一致と手数料付きトークンの扱い

ERC20トークンの中には、「送金時に手数料(fee)」を差し引く仕様のものがあります。 例えば、ユーザーが100トークン送ったつもりでも、実際に受け取る側は95トークンしか受け取らない場合があります。 このようなfee-on-transferトークンを正しく扱わないと、NFTが保持する残高計算に誤りが発生します。

そのため、以下の2つの対策が推奨されます。

方法 内容 特徴
① 厳格チェック方式 トークン送付前後のコントラクト残高を比較し、増加量が期待値と一致しなければrevertする。 手数料付きトークンを非対応とする設計。
② 実測反映方式 送付前後の残高差を算出し、その差分を実際に転送された数量として扱う。 手数料付きトークンも柔軟に対応可能。

例:トークン送付の安全な確認コード

javascript
function _safeTransferERC20(address token, address to, uint256 expectedAmount) internal {
    uint256 beforeBal = IERC20(token).balanceOf(address(this));
    IERC20(token).transfer(to, expectedAmount);
    uint256 afterBal = IERC20(token).balanceOf(address(this));

    uint256 actualSent = beforeBal - afterBal;

    require(actualSent == expectedAmount, "Transfer amount mismatch");
}

上記コードのように、残高の変化を直接測定する方法を取ることで、fee付きトークンに対応できます。 ただし、①の方式を採用する方が処理が単純で安全性が高いといえます。

フロントランニング(Front-running)対策

NFTがERC20トークンを内部に保有している場合、マーケットプレイス上での売買に関して「**出品者が、NFTを売却する直前に内部トークンを引き出してしまう」などのようなリスクが発生します。

これにより、購入者は「中身のないNFT」を受け取ってしまう可能性があります。 このような行為を防ぐため、マーケットプレイス実装側でのチェックが重要です。

具体的には、NFTが保有する erc20TransferOutNonce(ERC20の払い出し回数カウンタ)を利用します。

対策内容 説明
出品時にerc20TransferOutNonceの値を記録する 出品時点でNFTのトークン状態を固定化する。
取引実行直前に再度ノンス値を確認する 値が変化していた場合、内部資産が変動しているため取引を中止(revert)する。

このチェックにより、出品後にトークンを引き出す「フロントラン行為」を防止できます。

直接送信されたERC20トークンの喪失リスク

最後に重要な注意点として、ERC20トークンをNFTコントラクトに直接送信してはいけません。 例えば、以下のようにユーザーが誤ってNFTコントラクトのアドレス宛にtransferした場合です。

javascript
ERC20(<トークンコントラクト>).transfer(<NFTコントラクトのアドレス>, 100);

この場合、NFTコントラクトは受信処理(onReceive)を持たないため、どのNFTトークンIDに紐づけるべきかがわからず、結果的にそのトークンは回収不能(ロスト)になります。

この問題は、ERC20規格に「safeTransfer」が存在しないことに起因します。 したがって、必ずコントラクトが提供する transferERC20ToToken 関数を利用し、 トークンの送信先NFT(tokenId)を明示的に指定する必要があります。

引用

Steven Pineda (@steven2308), Jan Turk (@ThunderDeliverer), "ERC-7590: ERC-20 Holder Extension for NFTs [DRAFT]," Ethereum Improvement Proposals, no. 7590, January 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7590.

最後に

今回は「NFT(ERC721)が代替性トークン(ERC20)を安全に保持・管理し、まとめて取引できるようにする仕組みを提案しているERC7590」についてまとめてきました! いかがだったでしょうか?

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

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