はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、ERC712のsecp256k1署名を使用して、ERC20のapproveとtransferFromを1つのトランザクションで実行できる提案している規格であるERC2612についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
今回の内容は以下の記事でよりわかりやすく解説しています。
他にも様々なERCについてまとめています。
概要
このERCは、ERC20トークンを拡張して、承認(approve)とtransferFromという2つの関数に関連する提案をしています。
2つの関数の相互作用により、トークンは外部所有アカウント(EOA)間でだけでなく、トークンアクセス制御として使用されるmsg.senderを抽象化し、アプリケーションで設定された固有の条件下で他のコントラクトともやり取りできるようになりました。
ERC20については以下を参考にしてください。
ただし、この設計ではEIP20のapprove関数がmsg.senderに依存しています。
つまり、EOAを起点に処理が実行実行される必要があります。
ユーザーがコントラクトとやり取りする必要がある場合、2つのトランザクション(approveとtransferFromの呼び出し)を実行する必要があります。
また、トランザクションのガスコストを支払うためにETHを持っている必要があります。
このERCは、EIP20標準を新しいpermit関数を使用することで、ユーザーがmsg.senderに依存せず署名されたメッセージを使用してallowanceマッピングを変更できるようにします。
ユーザーエクスペリエンスを向上させるために、署名データはEIP712に従って構造化されており、主要なRPCプロバイダーで広く採用されています。
EIP20は、トークンの所有者のアドレスが実際にコントラクトウォレットである場合を除き、EOAによって実行する必要があります。
コントラクトウォレットを使用する方法もありますが、現在はエコシステムでほとんど採用されていません。
コントラクトウォレットにはユーザーエクスペリエンスの問題があり、コントラクトウォレットのEOAオーナーとコントラクトウォレット自体(代理でアクションを実行しすべての資金を保持する)を分離するため、ユーザーインターフェースを特別に設計する必要があります。
permitパターンは、ユーザーインターフェースをほとんどまたはまったく変更せずに同じ処理を実行できます。
動機
EIP20トークンは、Ethereumエコシステムで広く使用されていますが、プロトコルの観点からは依然として2級のトークンの地位を持っています。
何を持っての2級なのかわからないです...。
Ethereum上でETHを保持せずに処理を実行できることは長い間の目標であり、多くのEIP(Ethereum Improvement Proposal)で議論されてきました。
これまでの提案の多くはほぼ採用されておらず、採用されたもの(例:EIP777)も、追加の機能を導入したためにコントラクトで予期しない動作を引き起こすことがありました。
ERC777については以下を参考にしてください。
このERCは、最小限の変更で、EIP20の問題である「approveメソッドの抽象化の不足」に対処する提案をしています。
EIP20の各関数に対して_by_signatureのような関数を導入することも考えられましたが、敢えてそれらを省略することにしました。
その理由は以下の2つです。
- この関数に必要な具体的な仕様は、
transfer_by_signatureの手数料やバッチ処理アルゴリズムなどが異なるなど使用ケースに依存してしまうため。 -
permitと追加のヘルパーコントラクトの組み合わせを使うことで固有の実装になり過ぎてしまうため。
このERCは、EIP20の抽象化を改善するために、最小限の変更を提案し単一の問題に焦点を当てています。
仕様
コンプライアンス対応のコントラクトは、EIP20に追加で3つの新しい関数を実装する必要があります。
それぞれの関数は以下のようになります。
permit
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external
以下の特定の条件を満たす場合に、トークンの承認を行います。
- 現在のブロックタイムが指定された
deadlineよりも小さいか等しい。 -
ownerアドレスがゼロでない。 - ステート更新前の
nonces[owner]の値が指定されたnonceと等しい。 -
r、s、vがメッセージの所有者であるownerによる有効なsecp256k1署名であること。
これらの条件を満たさない場合、permitの呼び出しは取り消されます(revert)。
nonces
function nonces(address owner) external view returns (uint)
指定されたownerアドレスの現在のnonce値を渡します。
DOMAIN_SEPARATOR
function DOMAIN_SEPARATOR() external view returns (bytes32)
コントラクト内で使用されるDOMAIN_SEPARATOR値を返します。
permit関数はトークンの承認を行うためのもので、特定の条件を満たす場合にのみ許可を与え、nonces関数はnonceの値を取得し、DOMAIN_SEPARATOR関数はコントラクトのドメインセパレーター値を提供します。
これらの関数は、トークン操作のセキュリティを向上させ、他のスマートコントラクトとの相互運用性を高めるために追加されました。
keccak256(abi.encodePacked(
hex"1901",
DOMAIN_SEPARATOR,
keccak256(abi.encode(
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
owner,
spender,
value,
nonce,
deadline))
))
DOMAIN_SEPARATORは、**EIP712(Ethereum Improvement Proposal 712)**に基づいて定義されているものです。
このDOMAIN_SEPARATORは、コントラクトとチェーンの間で一意である必要があり、他のドメインからのリプレイ攻撃を防ぐために使用されます。
リプレイ攻撃(リエントランシー攻撃)については以下を参考にしてください。
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
keccak256(bytes(name)),
keccak256(bytes(version)),
chainid,
address(this)
));
以下は、一般的なDOMAIN_SEPARATORの定義について詳細に説明します。
DOMAIN_SEPARATORは、以下の要素から構成されます。
-
'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'- ドメインの定義を表します。
- 各パラメータは次のとおりです。
-
name- コントラクトの名前。
-
version- バージョン。
-
chainId- チェーンのID。
-
verifyingContract- コントラクトのアドレス。
-
-
keccak256(bytes(name))- コントラクトの名前をバイト列に変換してハッシュ化します。
-
keccak256(bytes(version))- バージョンをバイト列に変換してハッシュ化します。
-
chainid- チェーンのID。
-
address(this)- コントラクト自体のアドレス。
これらの要素を組み合わせて、一意のDOMAIN_SEPARATORを生成します。
このDOMAIN_SEPARATORは、コントラクト内で署名検証やセキュリティ関連の操作に使用されます。
重要なのは、このDOMAIN_SEPARATORがコントラクトとチェーンに固有であるため、他のドメインからの不正な操作を防ぐのに役立つことです。
異なるドメインでは異なるDOMAIN_SEPARATORが生成され、リプレイ攻撃から保護されます。
DOMAIN_SEPARATORはセキュリティを強化し、コントラクトが他のコントラクトや外部エンティティとやり取りする時に、正当な操作かどうかを確認するのに役立つ情報を提供します。
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Permit": [
{
"name": "owner",
"type": "address"
},
{
"name": "spender",
"type": "address"
},
{
"name": "value",
"type": "uint256"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
}
],
},
"primaryType": "Permit",
"domain": {
"name": erc20name,
"version": version,
"chainId": chainid,
"verifyingContract": tokenAddress
},
"message": {
"owner": owner,
"spender": spender,
"value": value,
"nonce": nonce,
"deadline": deadline
}
}
この定義のどこにも、msg.senderに言及していないことに注意してください。
permit関数の呼び出し元はどのアドレスでも問題ないです。
補足
permit関数は、EIP20トークンにおいてトークンを使用してトランザクション手数料を支払う仕組みを提供します。
通常、EthereumトランザクションはETHを使用してガス(手数料)を支払いますが、permit関数を使用することで、ユーザーはトークンを使って手数料を支払うことができるようになります。
これは、トークンの所有者がトークンを保持しており、そのトークンを使用してトランザクションを実行できるため、便利でセキュアな方法です。
また、nonces(ノンス)マッピングは、リプレイ攻撃から保護するための仕組みです。
リプレイ攻撃は、同じトランザクションを複数回実行する攻撃の一種であり、noncesを使用することで同じPermitを複数回使用することを防ぎます。
各所有者には、ノンス(nonce)と呼ばれる一意の番号が割り当てられ、permit関数が呼び出されるたびにこの番号が増加します。
このため、同じPermitを複数回使用することはできません。
一般的な使用ケースとして、所有者がリレイヤー(代理人)によってPermitを提出する場合があります。
リレイヤーは所有者の代わりにトランザクションをPermitし、提出します。
この場合、リレーする側はPermitを提出または保留する選択肢を持っています。
しかし、所有者はPermitの有効期限を制限することができます。
deadline引数を設定することで、Permitの有効期限を設定し、一定の時間内に許可が提出されない場合、無効にできます。
deadlineをuint(-1)に設定すると、Permitが事実上期限切れにならないように設定できます。
最後に、EIP712型メッセージは、広く採用されているウォレットプロバイダーとの互換性を確保するために含まれています。
これにより、署名と認証プロセスがよりセキュアになり、トークンの操作が正当かどうかを確認するのに役立ちます。
後方互換性
実際のトークンコントラクトには、すでにいくつかのpermit関数が存在します。
特に、dai.solで導入されたpermit関数が広く注目されています。
しかし、ここで提供された仕様とは少し異なる点がいくつかあります。
-
value引数の代わりに、bool allowedを受け取り、approvalを0またはuint(-1)に設定します。- これは、トークンの
Permitを有効または無効にするためのboolフラグとして機能します。
- これは、トークンの
-
deadline引数の代わりに、expiryと呼ばれる引数を使用します。- これは、
Permitの有効期限を示し、署名メッセージのに含まれます。
- これは、
また、トークンStake(Ethereumアドレス0x0Ae055097C6d159879521C384F1D2123D1f195e6)には、daiと同じABIを持つ別のpermitの実装がありますが全く同じではありません。
この実装では、ユーザーは「有効期限がブロックのタイムスタンプよりも大きい場合にのみ」transferFromを許可する「有効期限付き許可」を発行できます。
ここで提供されているpermit関数の仕様は、Uniswap V2の実装と一致しています。
また、permitが無効な場合にはrevert(実行が中止される)する要件は、EIPが既に広く展開されていた段階で追加されました。
当時、この要件はすべての実装で一貫していました。
permit関数は、トークンの許可を有効または無効にするための方法を提供します。
しかし、その具体的な実装は異なる場合があり、実際のトークン契約によって異なることに留意する必要があります。
セキュリティ考慮事項
Permitの署名者は通常、特定のトランザクションが送信されることを意図しています。
しかし、常にそのトランザクションをフロントラン(先に実行)しようとしているユーザーもおり、意図したトランザクションよりも先にpermitを呼び出すことができます。
結果的に、Permitの署名者にとって結果は同じです。
これは、トランザクションを送信しようとする特定のアドレスが他のアドレスに対して優先権を持たないことを意味します。
ecrecoverの事前コンパイルは、不正なメッセージが提供された場合に無音で失敗し、署名者としてゼロアドレスを返します。
そのため、permitがゼロアドレスに属する資金(通常は使用できない「ゾンビ資金」)を使用して承認を作成しないように、「owner != address(0)」を確認することが重要です。
署名されたPermitメッセージは検閲可能です。
リレーする側は、Permitを受け取った後、提出オプションを保留することができます。
これに対処する1つの方法は、deadlineパラメータです。
署名者はPermitの有効期限を近い将来の値に設定することで、リレーに対処できます。
また、署名者がETHを保持している場合、彼らはPermitを自分で提出することもでき、これにより以前に署名されたPermitを無効にできます。
標準のEIP20では、承認に関する競合状態(SWC-114)が存在し、Permitでも同様の競合状態が発生する可能性があります。
DOMAIN_SEPARATORにchainIdが含まれ、コントラクトのデプロイ時に定義されている場合、将来のチェーン分割時に異なるチェーン間でのリプレイ攻撃のリスクがあることに注意が必要です。
つまり、将来のチェーンの分岐が起きた場合、同じDOMAIN_SEPARATORが異なるチェーンで使用され、リプレイ攻撃の可能性が生じる可能性があります。
Permitは設計上、いくつかのセキュリティリスクと競合条件に対処する必要があり、これらのポテンシャルリスクに対処するための注意が必要です。
引用
Martin Lundfall (@Mrchico), "ERC-2612: Permit Extension for EIP-20 Signed Approvals," Ethereum Improvement Proposals, no. 2612, April 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2612.
最後に
今回は「ERC712のsecp256k1署名を使用して、ERC20のapproveとtransferFromを1つのトランザクションで実行できる提案している規格であるERC2612」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!