はじめに
初めまして。
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つの関数を追加する必要があります。
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
に対応する。
署名の計算
署名は以下の方法で計算されます。
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に従って以下のように定義されます。
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形式の構造になります。
{
"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
をゼロにリセットする方法があります。
- まず、既存の
approce
をゼロにリセットします。
approve(spender, 0);
- その後、新しい
approve
を設定します。
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などからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!