はじめに
初めまして。
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などからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!