はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、ERC2612で提案されているにapprove
+ transferFrom
のメタトランザクションを、より安全に実行する仕組みを提案しているERC3009についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIPについてまとめています。
この規格はステージがStagnant(6ヶ月以上更新がない状態)なので、暫く更新されておらず提案された当時の状態と現在の状態と異なる部分があります。
EIPの各ステージについては以下を参考にしてください。
概要
この規格では、署名されたapprove
を使用してトークンをtransfer
可能にするインターフェースを提案しています。
EIP712という、ERC20に準拠した署名によってアトミック性を持ったメタトランザクションを可能にする仕組みを使用することで、以下のようなことができます。
イーサリアム上のトランザクションを実行するには、ガス代をETHで支払う必要があります。
メタトランザクションを使うことで、ユーザーが直接ガス料金を支払う必要がなくなり、他のアドレスがガス代を支払うことができます。
- ガス代のデリゲート
- 他のアドレスにガス代の支払いを委任できます。
- トークンでのガス代支払い
- ネイティブトークンであるETHではなく、任意のトークンでガス代を支払うことできます。
- アトミックなトランザクション
- 1回のトランザクションで複数のトークンを送付したり、その他の操作をまとめて実行できます。
- アトミックというのは、1つでも処理が失敗したらそのトランザクションの処理を全て失敗にするということです。
- トランザクション送信の委任
- ガス代の支払いの委任に通じる部分ですが、トランザクションの送信を他のアドレスにしてもらうことが可能です。
- バッチトランザクション
- 複数のトランザクションを一括で処理できます。
-
nonce
の再利用防止-
nonce
とは、特定のアドレスがこれまで送ってきたトランザクションの数です。 - トランザクションをが成功/失敗すると、この
nonce
値に1
加算されるのですが、このnonce
が1
加算されないと、同じトランザクションを2度送れてしまいます。 - これを防止する仕組みを実装しつつ複数のトランザクションを実行できます。
-
ERC20については以下の記事を参考にしてください。
動機
EIP2612という既存の規格によりメタトランザクションは可能になっています。
この規格とEIP2612の違いは以下になります。
-
EIP2612は連続した
nonce
(トランザクションの一意の識別子)を使用しますが、この規格ではランダムな32
バイトのnonce
を使用します。 -
EIP2612はERC20の
approve
+transferFrom
パターンに依存しています。
EIP2612については以下の記事を参考にしてください。
連続したnonce
を使用する場合、以下の理由から複数トランザクションの同時実行ができません。
- Dappsがブロックチェーン上で処理されていない
nonce
を再利用する可能性がある。- ガス代の急激な高騰により、トランザクションがmempool(実行予定のトランザクションの溜まり場)に長期間溜まったままになってしまうことで起きる可能性があります。
一方、この規格で提案されているように、非連続のnonce
を使用することでユーザーは好きなだけトランザクションを送ることができます。
現状、アドレスごとnonce
の管理がされているため、コントラクト側でnonce
の管理をしてもアドレスの方のnonce
管理は意識する必要があります。
ここで提案されているのは、コントラクトで管理しているnonce
が連番でないため、トランザクションの順番を気にせず実行することができるということです。
また、ガス代についてもEIP1559の導入により、急激な高騰を防ぐ仕組みが導入されています。
EIP1559については以下の記事を参考にしてください。
ERC20のallowance
メカニズムには以下の問題点があります。
- 複数回の引き出し攻撃に弱い
リエントランシー攻撃のことです。
複数回同じ関数を実行し続けることができてしまう脆弱性のことです。
より詳しくは以下の記事を参考にしてください。
- トランザクションの順番が変わる
トランザクションは作成した順番通りに実行されるとは限りません。
例えば、より多くのガス代を支払うことで、先にトランザクションを通すことができてしまいます。
これにより、トランザクションを盗み見るということができてしまいます。
より詳しくは以下の記事を参考にしてください。
- 無限
allowance
-
ERC20の
approve
実行時に、極端に大きい値を設定することでほぼ無限に設定したアドレスがトークンの操作権限を持ってしまう。
-
ERC20の
アップグレード可能なコントラクトの普及により、上記の攻撃が発生しやすくなっています。
なぜ発生しやすくなっているかはわからないですが、アップグレード可能なので注意力が落ちているということかなと思いました。
ERC20のallowance
の問題点を解決するために、ERC777やERC677などの代替トークン標準が提案されました。
しかし、互換性やセキュリティの問題からこれらの標準はあまり普及していません。
ERC677については以下の記事を参考にしてください。
ERC777については以下の記事を参考にしてください。
仕様
イベント
event AuthorizationUsed(
address indexed authorizer,
bytes32 indexed nonce
);
// keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267;
// keccak256("ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8;
/**
* @notice Returns the state of an authorization
* @dev Nonces are randomly generated 32-byte data unique to the authorizer's
* address
* @param authorizer Authorizer's address
* @param nonce Nonce of the authorization
* @return True if the nonce is used
*/
function authorizationState(
address authorizer,
bytes32 nonce
) external view returns (bool);
/**
* @notice Execute a transfer with a signed authorization
* @param from Payer's address (Authorizer)
* @param to Payee's address
* @param value Amount to be transferred
* @param validAfter The time after which this is valid (unix time)
* @param validBefore The time before which this is valid (unix time)
* @param nonce Unique nonce
* @param v v of the signature
* @param r r of the signature
* @param s s of the signature
*/
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
/**
* @notice Receive a transfer with a signed authorization from the payer
* @dev This has an additional check to ensure that the payee's address matches
* the caller of this function to prevent front-running attacks. (See security
* considerations)
* @param from Payer's address (Authorizer)
* @param to Payee's address
* @param value Amount to be transferred
* @param validAfter The time after which this is valid (unix time)
* @param validBefore The time before which this is valid (unix time)
* @param nonce Unique nonce
* @param v v of the signature
* @param r r of the signature
* @param s s of the signature
*/
function receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
AuthorizationUsed
event AuthorizationUsed(
address indexed authorizer,
bytes32 indexed nonce
);
特定の認証が使用された時に発行されるイベント。
-
authorizer
- 認証を行ったユーザーのアドレス。
-
nonce
- 認証に関連付けられた一意の値。
TRANSFER_WITH_AUTHORIZATION_TYPEHASH
bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267;
TransferWithAuthorization
関数のための型ハッシュ値。
EIP712標準に基づき、構造化データの署名に使用されます。
EIP712については以下の記事を参考にしてください。
`RECEIVE_WITH_AUTHORIZATION_TYPEHASH
bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8;
ReceiveWithAuthorization
関数のための型ハッシュ値。
EIP712標準に基づき、構造化データの署名に使用されます。
authorizationState
function authorizationState(
address authorizer,
bytes32 nonce
) external view returns (bool);
指定されたnonce
が使用済みか確認する関数。
-
引数:
-
authorizer
- 認証を行うユーザーのアドレス。
-
nonce
- 認証に関連付けられた一意の値。
-
-
戻り値
-
nonce
が使用済みであればtrue
、そうでなければfalse
。
-
transferWithAuthorization
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
署名された認証を使用してトークンをtransfer
する関数。
-
引数
-
from
- トークンを支払うユーザーのアドレス(認証者)。
-
to
- トークンを受け取るユーザーのアドレス。
-
value
-
transfer
するトークンの量。
-
-
validAfter
- この時刻以降に有効となる(UNIX時間)。
-
validBefore
- この時刻以前に有効となる(UNIX時間)。
-
nonce
- 一意の値。
-
v
,r
,s
- 署名パーツ。
-
receiveWithAuthorization
function receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
署名された認証を使用してトークンを受け取る関数。
セキュリティ上の理由から、トークンを受け取るアドレスがこの関数を呼び出す必要があります。
-
引数
-
from
- トークンを支払うユーザーのアドレス(認証者)。
-
to
- トークンを受け取るユーザーのアドレス。
-
value
-
transfer
するトークンの量。
-
-
validAfter
- この時刻以降に有効となる(UNIX時間)。
-
validBefore
- この時刻以前に有効となる(UNIX時間)。
-
nonce
- 一意の値。
-
v
,r
,s
- 署名パーツ。
-
オプション
event AuthorizationCanceled(
address indexed authorizer,
bytes32 indexed nonce
);
// keccak256("CancelAuthorization(address authorizer,bytes32 nonce)")
bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = 0x158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a1597429;
/**
* @notice Attempt to cancel an authorization
* @param authorizer Authorizer's address
* @param nonce Nonce of the authorization
* @param v v of the signature
* @param r r of the signature
* @param s s of the signature
*/
function cancelAuthorization(
address authorizer,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
AuthorizationCanceled
event AuthorizationCanceled(
address indexed authorizer,
bytes32 indexed nonce
);
特定の認証がキャンセルされた時に発行されるイベント。
-
authorizer
- 認証を行ったユーザーのアドレス。
-
nonce
- 認証に関連付けられた一意のノンス。
bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = 0x158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a1597429;
cancelAuthorization
関数のための型ハッシュ値。
EIP712標準に基づき、構造化データの署名に使用されます。
cancelAuthorization
function cancelAuthorization(
address authorizer,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
署名された認証をキャンセルする関数。
これにより、まだ使用されていない認証を無効化することができます。
-
引数:
-
authorizer
- 認証を行うユーザーのアドレス。
-
nonce
- 認証に関連付けられた一意のノンス。
-
v
,r
,s
- 署名の部分。
-
EIP712による署名の取得方法
EIP712の仕様に基づいて、署名部分(v
, r
, s
)を取得するためには以下のような手順を取ります。
-
ドメインセパレータの生成
- ドメインセパレータは、署名の一意性を保証するために使用されるデータで以下のように生成します。
bytes32 DomainSeparator = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("USD Coin"), // name keccak256("2"), // version 1, // chainId 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 // verifyingContract ));
-
メッセージハッシュの生成
- キャンセルする認証に関するデータをハッシュ化します。
bytes32 structHash = keccak256(abi.encode( CANCEL_AUTHORIZATION_TYPEHASH, authorizer, nonce ));
-
EIP712ハッシュの生成
- ドメインセパレータとメッセージハッシュを組み合わせて、EIP712のハッシュを生成します。
bytes32 digest = keccak256(abi.encodePacked( "\x19\x01", DomainSeparator, structHash ));
-
署名部分の取得
-
digest
を署名することで、署名部分v
,r
,s
を取得します。 - これを使って認証をキャンセルします。
-
上記の内容を以下のように説明します。
ドメインセパレータとTypeHash
ドメインセパレータとTypeHashを使って、特定のメッセージに対するハッシュを生成してユーザーの秘密鍵で署名できます。
ドメインセパレータ
ドメインセパレータは以下のように生成されます。
bytes32 DomainSeparator = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256("USD Coin"), // name
keccak256("2"), // version
1, // chainId
0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 // verifyingContract
));
各メッセージのTypeHashとパラメータ
以下は各メッセージのTypeHashとパラメータの例です。
TransferWithAuthorization
bytes32 TypeHash = keccak256(
"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
);
Params := { From, To, Value, ValidAfter, ValidBefore, Nonce };
ReceiveWithAuthorization
bytes32 TypeHash = keccak256(
"ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
);
Params := { From, To, Value, ValidAfter, ValidBefore, Nonce };
CancelAuthorization
bytes32 TypeHash = keccak256(
"CancelAuthorization(address authorizer,bytes32 nonce)"
);
Params := { Authorizer, Nonce };
ダイジェストの生成
各メッセージに対して、ダイジェストを生成します。
bytes32 Digest = keccak256(
abi.encodePacked(
"\x19\x01",
DomainSeparator,
keccak256(abi.encode(TypeHash, Params...))
)
);
このダイジェストは、トークンホルダーの秘密鍵で署名されます。
{ v, r, s } := Sign(Digest, PrivateKey);
receiveWithAuthorization
の簡略化
スマートコントラクトの関数は、receiveWithAuthorization
コールの全ての引数をバイト型の1つの引数として受け取ることで、引数の数を減らすことができます。
例
bytes4 private constant _RECEIVE_WITH_AUTHORIZATION_SELECTOR = 0xef55bec6;
function deposit(address token, bytes calldata receiveAuthorization)
external
nonReentrant
{
(address from, address to, uint256 amount) = abi.decode(
receiveAuthorization[0:96],
(address, address, uint256)
);
require(to == address(this), "Recipient is not this contract");
(bool success, ) = token.call(
abi.encodePacked(
_RECEIVE_WITH_AUTHORIZATION_SELECTOR,
receiveAuthorization
)
);
require(success, "Failed to transfer tokens");
// additional logic...
}
-
_RECEIVE_WITH_AUTHORIZATION_SELECTOR
は、receiveWithAuthorization
関数の最初の4バイトのセレクタです。 -
deposit
関数は、receiveAuthorization
のバイトデータをデコードし、from
,to
,amount
を抽出します。 - **
token.call
**は、receiveWithAuthorization
関数を呼び出すために、エンコードされたデータを使用します。
Web3プロバイダーでの使用
approve
の署名はWeb3プロバイダーのeth_signTypedData{_v4}
を使用して取得できます。
例
const data = {
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
],
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
},
domain: {
name: tokenName,
version: tokenVersion,
chainId: selectedChainId,
verifyingContract: tokenAddress,
},
primaryType: "TransferWithAuthorization",
message: {
from: userAddress,
to: recipientAddress,
value: amountBN.toString(10),
validAfter: 0,
validBefore: Math.floor(Date.now() / 1000) + 3600, // Valid for an hour
nonce: Web3.utils.randomHex(32),
},
};
const signature = await ethereum.request({
method: "eth_signTypedData_v4",
params: [userAddress, JSON.stringify(data)],
});
const v = "0x" + signature.slice(130, 132);
const r = signature.slice(0, 66);
const s = "0x" + signature.slice(66, 130);
補足
ユニークなランダムnonce
の重要性
順序付きノンスの問題点
順序付きnonce
は、トランザクションの順序付けに良いとされますが、メタトランザクションにおいては以下の問題点があります。
-
高すぎるノンスの問題
- イーサリアムのネイティブトランザクションでは、高すぎる
nonce
を持つトランザクションは、低い未使用ノンスのトランザクションが実行されるまで保留されます。 - しかし、メタトランザクションでは、高すぎる
nonce
を持つトランザクションは即座に失敗してガス代が無駄になります。
- イーサリアムのネイティブトランザクションでは、高すぎる
-
マイナーのトランザクション再順序付け
- マイナーはトランザクションを任意の順序でブロックに含めることができるため、正しい
nonce
を使用してもメタトランザクションが失敗する可能性があります。 - 例えば、ユーザーが
nonce
を3
、4
、5
のトランザクションを送信しても、マイナーが4
、5
、3
の順で含めた場合、nonce
が3
のトランザクションのみが成功します。
- マイナーはトランザクションを任意の順序でブロックに含めることができるため、正しい
-
アプリケーション間での
nonce
管理- 異なるアプリケーションを同時に使用する場合、オフチェーンの
nonce
トラッカーがない限り次のnonce
を正しく決定することが難しいです。 - ガス価格が高いと、トランザクションがプール内で長時間処理されないことがあり、その間に同じ
nonce
が意図せず再利用される可能性があります。
- 異なるアプリケーションを同時に使用する場合、オフチェーンの
これらの理由から、トランザクションの順序付けを保証する唯一の方法は、リレイヤーが各トランザクションの確認を待ってから次のトランザクションを順次送信することであり、順序付きnonce
の利点は薄れます。
有効期間(Valid After and Valid Before)
リレイヤーにトランザクションの送信を依頼する場合、トランザクション送信のタイミングを正確に制御することは難しいです。
validAfter
とvalidBefore
のパラメータを使用することで、トランザクションが未来の特定の時刻以降、または特定の期限前のみ有効となるようにスケジュールできます。
これにより、トランザクションの実行タイミングをコントトールできます。
EIP712の重要性
EIP712は、署名が特定のトークンコントラクトのインスタンスに対してのみ有効であり、異なるネットワーク(異なるチェーンID)で再利用されることを防ぎます。
これを実現するために、コントラクトアドレスとチェーンIDを含むKeccak-256ハッシュダイジェスト(ドメインセパレータ)を生成します。
ドメインセパレータを生成する時には、verifyingContract
とchainId
のフィールドを含めることが推奨されます。
互換性
新しく作成されるコントラクトは、アトミックトランザクションを作成するために、EIP3009を直接利用できるメリットがあり、既存コントラクトは従来のapprove
+ transferFrom
パターンを使用することができます。
EIP3009を既存のERC20に追加するには、フォワーダーコントラクトを構築する必要があります。
-
approve
からユーザーと入金額を抽出。 -
receiveWithAuthorization
を呼び出して、指定された資金をユーザーからフォワーダーコントラクトにtransfer
。 - 親コントラクトがフォワーダーからトークンを操作できるように
allowance
を設定。 - 親コントラクト上のメソッドを呼び出して、フォワーダーから設定された
allowance
分のトークンを使用。 - 得られたトークンの所有権をユーザーに戻す。
コントラクト例
IDeFiToken
インターフェース
interface IDeFiToken {
function deposit(uint256 amount) external returns (uint256);
function transfer(address account, uint256 amount) external returns (bool);
}
-
deposit
- 指定された量のトークンを入金し、新しく発行されたトークンの数を返す関数。
-
transfer
- 指定されたアカウントにトークンを送付する関数。
DepositForwarder
コントラクト
contract DepositForwarder {
bytes4 private constant _RECEIVE_WITH_AUTHORIZATION_SELECTOR = 0xef55bec6;
IDeFiToken private _parent;
IERC20 private _token;
constructor(IDeFiToken parent, IERC20 token) public {
_parent = parent;
_token = token;
}
function deposit(bytes calldata receiveAuthorization)
external
nonReentrant
returns (uint256)
{
(address from, address to, uint256 amount) = abi.decode(
receiveAuthorization[0:96],
(address, address, uint256)
);
require(to == address(this), "Recipient is not this contract");
(bool success, ) = address(_token).call(
abi.encodePacked(
_RECEIVE_WITH_AUTHORIZATION_SELECTOR,
receiveAuthorization
)
);
require(success, "Failed to transfer to the forwarder");
require(
_token.approve(address(_parent), amount),
"Failed to set the allowance"
);
uint256 tokensMinted = _parent.deposit(amount);
require(
_parent.transfer(from, tokensMinted),
"Failed to transfer the minted tokens"
);
uint256 remainder = _token.balanceOf(address(this));
if (remainder > 0) {
require(
_token.transfer(from, remainder),
"Failed to refund the remainder"
);
}
return tokensMinted;
}
}
-
コンストラクタ
-
DepositForwarder
コントラクトは、IDeFiToken
(親コントラクト)とIERC20
(トークンコントラクト)のアドレスを受け取ります。 - これにより、フォワーダーが親コントラクトのメソッドを呼び出し、トークンの
transfer
を管理できるようになります。
-
-
deposit
関数-
認証データのデコード
-
receiveAuthorization
からfrom
(送信者)、to
(受信者)、amount
(金額)をデコードします。
-
-
トークンの
transfer
-
receiveWithAuthorization
を呼び出して、指定されたトークンをユーザーからフォワーダーコントラクトにtransfer
します。
-
-
allowance
の設定- フォワーダーコントラクトが親コントラクトに対してトークンを使用できるように
allowance
を設定します。
- フォワーダーコントラクトが親コントラクトに対してトークンを使用できるように
-
親コントラクトへの入金
- 親コントラクトの
deposit
関数を呼び出して、トークンを入金して新しく発行されたトークンを受け取ります。
- 親コントラクトの
-
発行されたトークンの
transfer
- 新しく発行されたトークンを送信者に
transfer
します。
- 新しく発行されたトークンを送信者に
-
残高の返金
- フォワーダーに残っているトークンがあれば、それを送信者に送付します。
-
認証データのデコード
この方法により、既存のERC20allowance
パターンを使用するコントラクトに対しても、EIP3009の機能を追加することができます。
テスト
以下にテストコードが格納されています。
実装
EIP3009.sol
abstract contract EIP3009 is IERC20Transfer, EIP712Domain {
// keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267;
// keccak256("ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8;
mapping(address => mapping(bytes32 => bool)) internal _authorizationStates;
event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce);
string internal constant _INVALID_SIGNATURE_ERROR = "EIP3009: invalid signature";
function authorizationState(address authorizer, bytes32 nonce)
external
view
returns (bool)
{
return _authorizationStates[authorizer][nonce];
}
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external {
require(now > validAfter, "EIP3009: authorization is not yet valid");
require(now < validBefore, "EIP3009: authorization is expired");
require(
!_authorizationStates[from][nonce],
"EIP3009: authorization is used"
);
bytes memory data = abi.encode(
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
from,
to,
value,
validAfter,
validBefore,
nonce
);
require(
EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == from,
"EIP3009: invalid signature"
);
_authorizationStates[from][nonce] = true;
emit AuthorizationUsed(from, nonce);
_transfer(from, to, value);
}
}
IERC20Transfer.sol
abstract contract IERC20Transfer {
function _transfer(
address sender,
address recipient,
uint256 amount
) internal virtual;
}
EIP712Domain.sol
abstract contract EIP712Domain {
bytes32 public DOMAIN_SEPARATOR;
}
EIP712.sol
library EIP712 {
// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
bytes32 public constant EIP712_DOMAIN_TYPEHASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;
function makeDomainSeparator(string memory name, string memory version)
internal
view
returns (bytes32)
{
uint256 chainId;
assembly {
chainId := chainid()
}
return
keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(bytes(name)),
keccak256(bytes(version)),
address(this),
bytes32(chainId)
)
);
}
function recover(
bytes32 domainSeparator,
uint8 v,
bytes32 r,
bytes32 s,
bytes memory typeHashAndData
) internal pure returns (address) {
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
domainSeparator,
keccak256(typeHashAndData)
)
);
address recovered = ecrecover(digest, v, r, s);
require(recovered != address(0), "EIP712: invalid signature");
return recovered;
}
}
EIP3009を完全に実装するには、以下のリポジトリ内のコードとEIP2612のコード、上記のEIP712のコードを実装する必要があります。
セキュリティ
以下は、提供された内容についての説明です。
セキュリティ考慮事項
receiveWithAuthorization
の使用推奨
他のコントラクトから呼び出す場合は、以下の理由からtransferWithAuthorization
ではなくreceiveWithAuthorization
を使用することが推奨されます。
-
フロントランニング攻撃のリスク
- トランザクションプールを監視する攻撃者が、
transferWithAuthorization
のapprove
を抽出し、トランザクションをフロントランして、ラッパー関数を呼び出さずにtransfer
を実行する可能性があります。 - これにより、未処理のままトークンがロックされてしまいます。
-
receiveWithAuthorization
は、追加のチェックを行い、呼び出し元が受け取りアドレスであることを確認することでこの問題を防ぎます。
- トランザクションプールを監視する攻撃者が、
nonce
のリーダーバイト
複数のコントラクトの関数がapprove
を受け取る場合、他の関数で同じapprove
が使用されないようにする必要があります。
そこで、nonce
の先頭バイトを特定の識別子として使用し間違った実行をしないようにします。
複数のtransfer
を同時に送信する場合の注意点
リレイヤーやマイナーがトランザクションの処理順序を決定します。
これは、トランザクションが相互に依存していない場合には問題ないです。
しかし、相互に依存するトランザクションの場合、署名されたapprove
を一度に1つずつ送信することが推奨されます。
ecrecover
の使用におけるゼロアドレスの拒否
ecrecover
を使用する時、ゼロアドレスを拒否する必要があります。
これは、不正なtransfer
やゼロアドレスからのトークンのapprove
を防ぐためです。
組み込みのecrecover
は、不正な署名が提供された場合にゼロアドレスを返すためチェックが重要です。
引用
Peter Jihoon Kim (@petejkim), Kevin Britz (@kbrizzle), David Knott (@DavidLKnott), "ERC-3009: Transfer With Authorization [DRAFT]," Ethereum Improvement Proposals, no. 3009, September 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3009.
最後に
今回は「ERC2612で提案されているにapprove
+ transferFrom
のメタトランザクションを、より安全に実行する仕組みを提案しているERC3009」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!