はじめに

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

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

今回は、ERC2612の仕組みをERC721形式のNFTに適用して、approve + transferFromのUXを向上させる仕組みを提案しているERC4494についてまとめていきます!

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

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

概要

この規格は、ERC2612で提案されているPermit承認フローをERC721に適用する仕組みを提案しています。

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

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

ERC2612では、Permitという仕組みを提案していて、ERC20トークンをガスレス(ガスコストなし)で送ることなどができました。 通常、ERC20トークンを別アドレス(運営アドレスなど)にtransferしてもらう時、approveとtransferFromを実行する必要があります。 approveをユーザーが実行して、運営のアドレスなどにtransferの権限を与え、その権限をもとに運営のアドレスからtransferFromを実行してERC20トークンを送るという流れです。 この場合、ユーザーはガス代を支払うために少額でもETHを持っていないといけないです。 これは特にWeb3に不慣れなユーザーにとってはUXが悪く改善の必要がありました。 そこで提案されたのが、Permitです。 Permitでは、ユーザーが実行する必要があるapproveを署名だけ実行すればよく、approveとtransferFromの実行を運営のアドレスなどに託すことができるような仕組みを提案しています。 署名だけであればガス代は不要なので、ユーザーはETHを持っていなくても運営のアドレスなどにガス代を負担してもらってトークンのMintなどができるようになりました。

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

ERC20トークンとERC721トークンでは構造が異なるため、別のEIPが必要になります。 ERC20では、approveされるトークン量と所有者のアドレスのnonceによってPermitが作成されますが、ERC721ではNFTのtokenIdとnonceをもとにPermitが作成されます。

動機

ERC2612で提案されているPermitの構造は、署名されたメッセージ(ERC712)を使用してapproveを作成します。

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

通常のapproveしてトークンをtransferするフローでは、先ほど述べたようにapproveとtransferFromの2つの関数を実行するための、2つのトランザクションが必要になります。 ただ、これはUXが悪くユーザーを混乱させる可能性があります。 一方、Permitスタイルのフローでは、approve部分が署名だけで良くなり、トランザクションも1回で済むためUXが良くなります。

また、ERC2612ではERC20トークンのPermitアーキテクチャのみ説明されていますが、この規格では署名されたメッセージベースのapproveフローのメリットを享受できるapproveアーキテクチャを含むERC721形式のNFTのアーキテクチャを提案しています。

仕様

ERC721に以下の3つの関数を追加する必要があります。

js
pragma solidity 0.8.10;

import "./IERC165.sol";

///
/// @dev Interface for token permits for ERC-721
///
interface IERC4494 is IERC165 {
  /// ERC165 bytes to add to interface array - set in parent contract
  ///
  /// _INTERFACE_ID_ERC4494 = 0x5604e225

  /// @notice Function to approve by way of owner signature
  /// @param spender the address to approve
  /// @param tokenId the index of the NFT to approve the spender on
  /// @param deadline a timestamp expiry for the permit
  /// @param sig a traditional or EIP-2098 signature
  function permit(address spender, uint256 tokenId, uint256 deadline, bytes memory sig) external;
  /// @notice Returns the nonce of an NFT - useful for creating permits
  /// @param tokenId the index of the NFT to get the nonce of
  /// @return the uint256 representation of the nonce
  function nonces(uint256 tokenId) external view returns(uint256);
  /// @notice Returns the domain separator used in the encoding of the signature for permits, as defined by EIP-712
  /// @return the bytes32 domain separator
  function DOMAIN_SEPARATOR() external view returns(bytes32);
}

permit

トークン所有者が署名を使用して指定したspenderにtokenIdのトークンを使用許可する関数。 deadlineは署名の有効期限を示すタイムスタンプです。 sigは所有者の署名(secp256k1またはEIP2098形式)です。

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

nonces

指定されたtokenIdのトークンのnonce(使い捨てカウンター)を返す関数。 これにより、署名の一意性が保証されて使い回しを防止できます。

DOMAIN_SEPARATOR

EIP712に従って署名のエンコーディングに使用されるドメインセパレータを返す関数。

署名の検証

署名の検証は以下の条件を満たす場合にのみ成功します。

  • 現在のブロックタイムがdeadline以下である。
  • トークンの所有者がゼロアドレスでない。
  • 指定されたtokenIdのnonceが現在のノンスと一致する。
  • 署名sigが正しい形式で、所有者のtokenIdに対応する。

署名の計算

署名は以下の方法で計算されます。

js
keccak256(abi.encodePacked(
  hex"1901",
  DOMAIN_SEPARATOR,
  keccak256(abi.encode(
    keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"),
    spender,
    tokenId,
    nonce,
    deadline))
));

DOMAIN_SEPARATORの定義

DOMAIN_SEPARATORはEIP712に従って以下のように定義されます。

js
DOMAIN_SEPARATOR = keccak256(
  abi.encode(
    keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
    keccak256(bytes(name)),
    keccak256(bytes(version)),
    chainid,
    address(this)
));
  • name
    • コントラクトの名前。
  • version
    • コントラクトのバージョン。
  • chainId
    • チェーンID。
  • address(this)
    • コントラクトアドレス。

これにより、異なるドメインからのリプレイ攻撃を防ぐためにユニークなドメインセパレータが生成されます。

署名メッセージ

署名メッセージは以下のERC712形式の構造になります。

json
{
"types": {
  "EIP712Domain": [
    {
      "name": "name",
      "type": "string"
    },
    {
      "name": "version",
      "type": "string"
    },
    {
      "name": "chainId",
      "type": "uint256"
    },
    {
      "name": "verifyingContract",
      "type": "address"
    }
  ],
  "Permit": [
    {
      "name": "spender",
      "type": "address"
    },
    {
      "name": "tokenId",
      "type": "uint256"
    },
    {
      "name": "nonce",
      "type": "uint256"
    },
    {
      "name": "deadline",
      "type": "uint256"
    }
  ],
  "primaryType": "Permit",
  "domain": {
    "name": erc721name,
    "version": version,
    "chainId": chainid,
    "verifyingContract": tokenAddress
},
"message": {
  "spender": spender,
  "value": value,
  "nonce": nonce,
  "deadline": deadline
}
}}

上記に加えて以下の条件があります。 特定のtokenIdのnonce(nonces[tokenId])は、tokenIdに紐付くNFTのtransfer時にインクリメントされる必要があります。

同じ署名を作成されることを防止するためです。

また、permit関数は署名者がゼロアドレスでないことをチェックする必要があります。

この仕組みでは、msg.sender(関数実行アドレス)を参照する仕組みがなく、permit関数の呼び出しもとは任意のアドレスにすることができます。

EIP165

この規格にはEIP165が必要になります。 EIP165を使用することで、他のNFTコントラクトがこのEIPを実装しているか簡単に検証できるようになります。 この規格で提案されているコントラクトがEIP165で返されるインターフェースIDは0x5604e225です。

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

補足

permit関数は追加のトランザクションを必要とせずに、safeTransferFromを実行できるようになります。 このフォーマットは予期しないコードへの呼び出しを避けるように設計されています。 また、リプレイ保護のためにnoncesマッピングが提供されています。

一般的なpermitの使用ケースとして、リレーヤーが所有者に代わってPermitを提出するシナリオがあります。 この場合、リレーヤーはPermitを提出するかどうかの選択権を持つことになります。 この点が懸念される場合、所有者はdeadlineを近い将来に設定することで、Permitの有効期間を制限できます。 また、deadline引数をuint(-1)に設定することで有効期限のないPermitを作成することもできます。

ERC712形式のメッセージはERC2612での使用を考慮しており、多くのウォレットプロバイダで広く採用されているためこのEIPでも採用されています。

ERC2612がapproveされるvalueに焦点を当てているのに対し、このEIPはpermitによってapproveされるNFTのtokenIdに焦点を当てています。 これにより、ERC20やERC1155トークンでは実現できない柔軟性が実現され、特定のNFTに複数のPermitを与えることができます。 ERC721トークンは非代替性という特徴を持つため、permitを与えても各トークンの所有者であるというownerの権限は変わりません。

ERC2612では署名がv, r, sのコンポーネントに分割されていますが、このEIPでは可変長のバイト配列を使用してEIP2098署名(64バイト)をサポートします。 EIP2098署名はr, s, vコンポーネント(65バイト)から簡単に分離・再構成することができません。

互換性

permitを使用したERC721コントラクトはすでにいくつか存在しており、有名なのがUniswapV3です。 ただ、既存の実装とは以下の点で異なります。

  • permitアーキテクチャがownerベースである。
  • nonceがpermitが作成された時点でインクリメントされる。
  • permit関数はNFT所有者によって呼び出される必要があり、ownerとして設定される。
  • 署名がバイト配列ではなくr, s, vに分割されている

テスト

以下にテストコードが格納されています。

参考実装

以下に参考実装コードが格納されています。

セキュリティ

セキュリティ考慮事項

permitとtransfer関数を1つの関数内で使用する場合には、無効なpermitが使用されないように注意する必要があります。 特に自動化されたNFTプラットフォームにおいて、脆弱性のある実装がユーザー資産の損失を引き起こす可能性があります。

以下はERC2612からの引用であり、軽微な適応を加えていますが、同様に重要です:

Permitの署名者が特定の第三者にトランザクションの提出を依頼する場合でも、他の第三者が先回りしてこのトランザクションを実行して想定していた第三者の前にpermitを呼び出すことができます。 Permitの署名者にとっての最終結果は同じですが、このことを認識しておくことが重要です。

ecrecoverプリコンパイルが不正なメッセージを受け取った場合、サイレントに失敗して署名者としてゼロアドレスを返すため、ownerOf(tokenId) != address(0)を確認し、Permitが設定されていないtokenIdに対してpermitがapproveを作成しないようにする。

署名されたPermitメッセージは検閲可能です。 リレーする第三者はPermitを受け取った後、それを実行しないことでずっとpending状態にすることが可能です。 この問題の緩和策として、deadlineパラメータが使用されます。 署名者がETHを保有している場合、自身でPermitを提出することも可能であり、これにより以前に署名されたPermitを無効にすることができます。

ERC20のapproveに関する競合状態は、permitにも適用されます。

例えば以下のような状況です。 ERC20トークンの所有者(Alice)が、第三者(Bob)にトークンを使用する許可を与えるために、approve関数を呼び出します。 このとき、Bobは特定のvalue(例:100トークン)を使用する許可を得ます。 BobはtransferFrom関数を呼び出して、Aliceのアカウントから自分のアカウントにトークンをtransferします。

AliceがBobに100トークンの使用許可を与えた後、Aliceが許可を取り消して、別のvalue(例:50トークン)を再度許可するためにapprove関数を呼び出す場合、競合状態が発生します。 Bobがまだ最初の100トークンの許可を使用していない間に、Aliceが許可を変更すると以下のような問題が発生します。

  • Bobは100トークンの許可を使い切る前に、50トークンの新しい許可が設定される。
  • Bobが100トークンのtransferFromを呼び出そうとすると、50トークンに変更された新しい許可と競合し、予期せぬ挙動になります。

回避策

この問題を回避するには、approve関数を使う前に既存のapproveをゼロにリセットする方法があります。

  1. まず、既存のapproceをゼロにリセットします。
js
approve(spender, 0);
  1. その後、新しいapproveを設定します。
solidity
approve(spender, newAmount);

DOMAIN_SEPARATORがchainIdを含んでコントラクトのデプロイ時に定義され、各署名のたびに再構築されない場合、将来のチェーン分岐時にチェーン間でリプレイ攻撃のリスクがあります。

引用

Simon Fremaux (@dievardump), William Schwab (@wschwab), "ERC-4494: Permit for ERC-721 NFTs [DRAFT]," Ethereum Improvement Proposals, no. 4494, November 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4494.

最後に

今回は「ERC2612の仕組みをERC721形式のNFTに適用して、approve + transferFromのUXを向上させる仕組みを提案しているERC4494」についてまとめてきました! いかがだったでしょうか?

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

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