はじめに
『DApps開発入門』という本や色々記事を書いているかるでねです。
今回は、安全にERC20トークンをメタトランザクション(approve
+ transferFrom
)で送付する仕組みを提案しているERC7758についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIPについてまとめています。
概要
ERC7758では、EIP712を活用してERC20トークンのメタトランザクションを可能にする仕組みを提案しています。
この仕組みを活用することで以下を実現できます。
- ガス代を他者に負担してもらう。
- ETHの代わりにERC20トークンでガス代を支払う。
- 複数のトークン送付や処理を1回のトランザクションでまとめて実行する。
- トークンを送る時に受信者がトランザクションを送信する仕組みの実装。
- 複数のトランザクションを効率的に処理して計算効率の向上や手数料の負担軽減。
- トランザクションの失敗を防ぐためにnonce管理を改善。
ERC20については以下の記事を参考にしてください。
ERC712については以下の記事を参考にしてください。
動機
メタトランザクションとは?
通常、EVM互換のブロックチェーンでトランザクションを実行するには、送信者(トランザクションを実行したい人)がガス代をETHで支払う必要があります
しかし、メタトランザクションを使うことで、送信者が直接トランザクションを送るのではなく、別の誰か(リレーサービスや運営のEOAアドレスなど)がトランザクションをブロックチェーンに送信し、ガス代を支払うことが可能になります。
ERC2612との違い
ERC2612もメタトランザクションを可能にする仕組みですが、今回の提案とは以下の点で異なります。
ERC2612については以下の記事を参考にしてください。
Nonceの管理
ERC2612では 連番のnonce
を使用しますが、本提案ではランダムな32バイトのnonce
を使用します。
EOAアドレスでトランザクションを作成するとき、連番されるユニークな値がトランザクション内に含まれます。
この値をnonce
といい、同じトランザクションが複数回実行されないようにしています。
ERC2612では トランザクションが順番に処理されることが前提となっています。
もし未処理のトランザクションがあれば、それが処理されるまで次のトランザクションが実行できません。
また、DAppsが意図せず同じnonce
を再利用してしまったり、トランザクションの処理順序が原因で意図しない順番でトランザクションが処理される可能性があります。
この問題により、ガス価格が高騰しトランザクションが詰まりやすい環境では、ユーザーが複数のトランザクションを同時に処理しようとすると失敗しやすくなるという課題があります。
ERC7758により、ランダムなnonce
を使用することで、複数のトランザクションを同時に発行しても競合が発生しなくなります。
また、並列処理が可能となり、送信者が複数のトランザクションを安全に送れます。
承認(Approve)と送金(TransferFrom)の仕組み
ERC2612は、ERC20の「approve
→ transferFrom
」パターンを利用しますが、本提案ではそのパターンを使用しません。
理由としては、ERC20の「approve
→ transferFrom
」パターンにはセキュリティ上の問題があるためです。
approve + transferFromパターンの問題点
Multiple Withdrawal Attack
ERC20のapprove
を使ってトークンの送金許可を実行すると、悪意のあるアカウントがその許可を利用して何度も送金できる可能性があります。
例えば、DAppがapprove
を実行した直後に攻撃者がtransferFrom
を複数回実行すれば、ユーザーが意図しないトークンの流出が起きてしまいます。
Infinite Allowance
多くのDAppsは、利便性を考えて「Infinite Allowance」(approve
を一度発行すれば無制限にトークンを使用できる)を推奨しています。
しかし、もしDAppやスマートコントラクトがハッキングされた場合、攻撃者は無限にトークンを盗むことが可能になります。
これらの問題を回避するため、本提案では「approve
→ transferFrom
」の仕組みを利用しないことを推奨しています。
ERC777はなぜ普及しなかったのか?
ERC777は、ERC20のapprove
の問題を解決するために登場した新しいトークン標準ですが普及しませんでした。
理由としては以下があります。
ERC20との互換性の問題
既存のDAppsやウォレットがERC777に対応していませんでした。
ERC777には「Hooks」という新しい概念があり、古いコントラクトがそれに対応できませんでした。
セキュリティの懸念
Hooksの仕組みを悪用すれば、特定のトランザクションを妨害したり攻撃者が意図しない処理をさせる可能性があります。
特に「Reentrancy Attack」のリスクが高まります。
そのため、ERC777は理論的には優れたトークン標準であるものの、実際の導入は進まずERC20の代替として広く使用されませんでした。
仕様
インターフェース
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;
AuthorizationUsed
event AuthorizationUsed(
address indexed authorizer,
bytes32 indexed nonce
);
概要
**EIP712準拠の署名を使用してトークン転送が実行された時に発行されるイベント。
詳細
このイベントは、transferWithAuthorization
関数やreceiveWithAuthorization
関数を通じてトークンが送付されたときに発行され、トランザクションの署名が使用されたことを示します。
これにより、同じ署名の再利用を防ぐことができます。
パラメータ
-
authorizer
- 署名を行ったアドレス(トークンの送信者)。
-
nonce
- 署名に関連付けられた一意の識別子(ランダムな32バイトの値)。
TRANSFER_WITH_AUTHORIZATION_TYPEHASH
bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267;
概要
EIP712に準拠したtransferWithAuthorization
関数のデータ型(型ハッシュ)を表す定数。
詳細
EIP712では、メッセージの構造を明確に定義してハッシュを用いて署名を行います。
この変数は transferWithAuthorization
関数のデータ構造をkeccak256
によってハッシュ化したもので、署名の検証時にトランザクションの整合性をチェックするために使用されます。
RECEIVE_WITH_AUTHORIZATION_TYPEHASH
bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8;
概要
EIP712に準拠したreceiveWithAuthorization
関数のデータ型(型ハッシュ)を表す定数。
詳細
EIP712では、メッセージの型を定義してそのハッシュを使用して署名を行います。
この変数はreceiveWithAuthorization
関数のデータ構造をkeccak256
によってハッシュ化したもので、署名の検証時にreceiveWithAuthorization
関数のリクエストが正当なものかチェックするために使用されます。
authorizationState
function authorizationState(
address authorizer,
bytes32 nonce
) external view returns (bool);
概要
指定されたnonce
がすでに使用されたかどうかを確認する関数。
詳細
この関数は、transferWithAuthorization
関数やreceiveWithAuthorization
関数を実行する時に使用されたnonce
が、すでに使用されたかどうかをチェックします。
一度使用されたnonce
は再利用できないため、リプレイ攻撃を防ぐ役割を果たします。
引数
-
authorizer
- 署名を行ったアドレス(トークンの送信者)。
-
nonce
- 一意の識別子(ランダムな32バイトの値)。
戻り値
-
bool
-
true
の場合、nonce
はすでに使用済み。 -
false
の場合nonce
は未使用であり、トランザクション実行が可能。
-
transferWithAuthorization
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
概要
EIP712署名を利用してトークンを転送する関数。
詳細
送金者(from
)が事前にオフチェーンで署名したデータを、受取アドレス(to
)または第三者がブロックチェーンに送信することでトークンを送付できる。
ETHを持っていなくてもトークン送金が可能になり、ガス代を他者に負担してもらうことができます。
また、nonce
を用いることで署名の再利用を防ぎます。
引数
-
from
- 送金者(署名を行ったトークンの所有者)。
-
to
- 受取アドレス(トークンを受け取るアドレス)。
-
value
- 送付するトークンの金額。
-
validAfter
- この署名が有効になる開始時間(UNIXタイム)。
-
validBefore
- この署名が無効になる時間(UNIXタイム)。
-
nonce
- 一意の識別子(ランダムな32バイトの値)。
-
v
- 署名の
v
コンポーネント(EIP712の署名要素)。
- 署名の
-
r
- 署名の
r
コンポーネント(EIP712の署名要素)。
- 署名の
-
s
- 署名の
s
コンポーネント(EIP712の署名要素)。
- 署名の
receiveWithAuthorization
function receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
概要
EIP712署名を利用してトークンを送付するが、受取アドレス自身がトランザクションを実行することを保証する関数。
詳細
transferWithAuthorization
関数とは異なり、この関数では受取アドレス(to
)が msg.sender
と一致している必要があります。
この仕様により、第三者がフロントランニング攻撃(取引を先回りして処理する攻撃)を防ぐことができます。
また、他の関数と同様にnonce
を利用することで、署名の再利用を防ぎます。
引数
-
from
- 送金者(署名を行ったトークンの所有者)。
-
to
- 受取アドレス(トークンを受け取るアドレス)。
-
value
- 送付するトークンの金額。
-
validAfter
- この署名が有効になる開始時間(UNIXタイム)。
-
validBefore
- この署名が無効になる時間(UNIXタイム)。
-
nonce
- 一意の識別子(ランダムな32バイトの値)。
-
v
- 署名の
v
コンポーネント(EIP712の署名要素)。
- 署名の
-
r
- 署名の
r
コンポーネント(EIP712の署名要素)。
- 署名の
-
s
- 署名の
s
コンポーネント(EIP712の署名要素)。
- 署名の
オプション機能
AuthorizationCanceled
event AuthorizationCanceled(
address indexed authorizer,
bytes32 indexed nonce
);
概要
cancelAuthorization
によって署名(EIP712に準拠)が取り消されたときに発行されるイベント。
詳細
このイベントは、未使用の署名を取り消した時に発行され、nonce
がキャンセルされたことを記録します。
これにより、不要になった署名が悪用されるのを防ぐことができます。
引数
-
authorizer
- 署名を行ったアドレス(トークンの所有者)。
-
nonce
- 一意の識別子(キャンセルされたランダムな32バイトの値)。
CANCEL_AUTHORIZATION_TYPEHASH
bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = 0x158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a1597429;
概要
EIP712に準拠したcancelAuthorization
関数のデータ型を表す定数。
詳細
EIP712では、メッセージの型を定義してそのハッシュを使用して署名を行います。
この変数はcancelAuthorization
関数のデータ構造をkeccak256
によってハッシュ化したもので、署名の検証時にcancelAuthorization
関数のリクエストが正当なものかチェックするために使用されます。
cancelAuthorization
function cancelAuthorization(
address authorizer,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
概要
**未使用の署名をキャンセル(無効化)する関数。
詳細
トランザクションが実行される前であれば、署名を取り消すことが可能です。
これにより、万が一署名が漏洩した場合でも不正利用を防ぐことができます。
注意点として、nonce
をキャンセルするとそのnonce
は今後使用できなくなります。
引数
-
authorizer
- 署名を行ったアドレス(トークンの所有者)。
-
nonce
- キャンセルする一意の識別子(ランダムな32バイトの値)。
-
v
- 署名の
v
コンポーネント(EIP712の署名要素)。
- 署名の
-
r
- 署名の
r
コンポーネント(EIP712の署名要素)。
- 署名の
-
s
- 署名の
s
コンポーネント(EIP712の署名要素)。
- 署名の
具体例
ドメインセパレーター(DomainSeparator)の生成
DomainSeparator := Keccak256(ABIEncode(
Keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
Keccak256("USD Coin"), // name
Keccak256("2"), // version
1, // chainId
0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 // verifyingContract
))
EIP712の署名では、署名の一意性を保証するために「EIP712Domain」という構造体(DomainSeparator
)を定義します。
DomainSeparator
は、対象となるコントラクトやトークンごとに異なるハッシュ値を生成し、異なるコントラクトでの署名の混同や異なるブロックチェーンでのリプレイ攻撃を防ぎます。
-
name
(例:"USD Coin"
)- トークンの名前。
-
version
(例:"2"
)- スマートコントラクトのバージョン。
-
chainId
(例:1
)- トランザクションを実行するブロックチェーンのチェーンID(Ethereum メインネット = 1)。
-
verifyingContract
(例:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
)- トランザクションを処理する コントラクトのアドレス。
型ハッシュ(TypeHash
)の計算
// Transfer With Authorization
TypeHash := Keccak256(
"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
)
Params := { From, To, Value, ValidAfter, ValidBefore, Nonce }
// ReceiveWithAuthorization
TypeHash := Keccak256(
"ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
)
Params := { From, To, Value, ValidAfter, ValidBefore, Nonce }
// CancelAuthorization
TypeHash := Keccak256(
"CancelAuthorization(address authorizer,bytes32 nonce)"
)
Params := { Authorizer, Nonce }
トランザクションの種類ごとに型ハッシュ(TypeHash
)を作成し、どの種類のトランザクションかを識別します。
これは EIP712の型付きメッセージ(Typed Message
)を処理するための重要な仕組み**。
型ハッシュ(TypeHash)とは?
各関数(TransferWithAuthorization
、ReceiveWithAuthorization
、CancelAuthorization
)ごとに固定のデータ型を定義し、そのハッシュ値を事前に計算します。
これを利用することで、署名の対象となるデータの種類を明確に区別できるようになります。
トランザクションデータのハッシュ化と署名
// "‖" denotes concatenation.
Digest := Keccak256(
0x1901 ‖ DomainSeparator ‖ Keccak256(ABIEncode(TypeHash, Params...))
)
{ v, r, s } := Sign(Digest, PrivateKey)
以下のトランザクションデータをkeccak256
でハッシュ化し、そのハッシュをユーザーの秘密鍵で署名します。
-
0x1901
(固定プレフィックス) -
DomainSeparator
(ドメインセパレーター) -
Keccak256(ABIEncode(TypeHash, Params...))
(型ハッシュとデータのエンコード)
最終的な署名結果としては取得できる、v, r, s
の署名データをコントラクトで検証することで、オフチェーン署名が改ざんされていないことを保証できます。
receiveWithAuthorization
をラップするコントラクト
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");
...
}
receiveWithAuthorization
関数をラップし、簡潔に署名済みトランザクションを処理する関数。
-
receiveAuthorization
関数のデータをデコードし、from
、to
、amount
を取得します。 -
to
(受取アドレス)がコントラクト自身かどうかチェック。 -
receiveWithAuthorization
関数を実行してトークン送付。 - 成功しなければエラーを返す。
このように、署名済みデータを1つのbytes
型として渡してトランザクションを簡潔に実行します。
Web3プロバイダー
EIP712の署名をWeb3プロバイダー(Metamaskなど)を利用して取得するサンプルコードも記載されています。
EIP712メッセージ構造の定義
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, // 有効期限を1時間後に設定
nonce: Web3.utils.randomHex(32),
},
};
このコードでは、EIP712の型付きメッセージ(Typed Message
)を定義し、トランザクションの署名データを作成しています。
詳細
-
EIP712のメッセージ構造を定義
-
types
- 型情報の定義
-
domain
- 署名の一意性を保証するドメイン情報
-
primaryType
- 今回のメッセージの主な型
-
message
- 署名するデータ本体
-
-
ドメイン情報(
domain
)-
name
- トークンの名前(例: "USD Coin")
-
version
- トークンのバージョン(例: "2")
-
chainId
- 対象のブロックチェーンID(例:
1
= Ethereum メインネット)
- 対象のブロックチェーンID(例:
-
verifyingContract
- 処理を行うコントラクトアドレス
-
-
メッセージデータ(
message
)-
from
- 送金者のアドレス
-
to
- 受取アドレス
-
value
- 送金するトークンの量(BigNumber形式から文字列に変換)
-
validAfter
- この署名が有効になる開始時間(
0
は即時有効)
- この署名が有効になる開始時間(
-
validBefore
- 有効期限(現在の時刻から
+1時間
)
- 有効期限(現在の時刻から
-
nonce
- 32バイトのランダムな値(署名の再利用を防ぐ)
-
eth_signTypedData_v4
を使って署名を取得
const signature = await ethereum.request({
method: "eth_signTypedData_v4",
params: [userAddress, JSON.stringify(data)],
});
概要
eth_signTypedData_v4
を使用して、EIP712形式の署名を取得しています。
eth_signTypedData_v4
とは、EthereumでEIP712署名を取得するためのメソッドです。
MetamaskなどのWeb3プロバイダーを介して呼び出してユーザーがウォレットで署名を承認すると、署名データを取得できます。
引数
-
userAddress
- 署名を行うユーザーのアドレス。
-
JSON.stringify(data)
- 署名するEIP712メッセージデータ。
戻り値
-
signature
-
v, r, s
を含む署名データ(0x
で始まる 130 文字の16進数)
-
取得した署名をv, r, s
に分割
const v = "0x" + signature.slice(130, 132);
const r = signature.slice(0, 66);
const s = "0x" + signature.slice(66, 130);
eth_signTypedData_v4
で取得した署名データをコントラクトで利用できる v, r, s
に分割しています。
Ethereum の署名(65バイト
= 130文字の16進数
)は以下のフォーマットになっています。
領域 | バイト数 | 取得方法 |
---|---|---|
r |
32バイト | signature.slice(0, 66) |
s |
32バイト | signature.slice(66, 130) |
v |
1バイト | signature.slice(130, 132) |
-
r
-
signature
の最初の32バイト0〜65文字)
-
-
s
-
signature
の次の32バイト(66〜129文字)
-
-
v
-
signature
の最後の1バイト130〜131文字) -
v
は0x1b(27)
または0x1c(28)
の値を持つ。
-
署名をコントラクトで利用
この署名(v, r, s
)は、コントラクトのtransferWithAuthorization
関数やreceiveWithAuthorization
関数の引数として使用します。
補足
連番のnonce
の問題点とランダムnonce
の採用
通常、Ethereum のトランザクションには連番のnonce
が使われますが、メタトランザクションではこれが機能せず、ランダムnonce
を採用する必要があります。
例えば、nonce = 3
のトランザクションが処理される前にnonce = 4
のトランザクションを送ると、nonce = 4
は保留 (pending
) となり、nonce = 3
が確定するまで実行されません。
しかし、メタトランザクションではこの仕組みが機能しません。
メタトランザクションでは、ユーザー自身ではなくリレイヤー(第三者)がトランザクションを送信するため、異なるリレイヤー間で順序が保証されません。
これにより以下の問題点があります。
順番通りに処理されない
例えば、ユーザーがnonce = 3, 4, 5
のトランザクションを発行したとしても、リレイヤーの送信順やトランザクションがブロックに含まれる順番によって「4 → 5 → 3」のように順番がバラバラになります。
この結果、3
は有効なnonce
なのに処理されないということがおきます。
- 高すぎる
nonce
のトランザクションは即座に失敗
通常のEthereumトランザクションでnonce
が高すぎる場合は「保留状態」になりますが、メタトランザクションでは即座に失敗してガス代が無駄になります。
異なるアプリケーションでの競合
複数のアプリを使う場合、アプリごとにnonce
の管理方法が異なるため、未承認のノンスが発生して重複する可能性があります。
高ガス価格環境での問題
ガス価格が高騰してトランザクションが詰まると、メタトランザクションが長時間保留され、同じnonce
が別のトランザクションで誤って再利用される可能性があります。
解決策
この解決策として、ランダムnonce
を採用する方法があります。
nonce
をランダムな32バイトの値にすることで、トランザクションの順序の影響を受けなくします。
これにより、リレイヤーが異なる順番で処理しても問題がなく、ガスの無駄やトランザクションの失敗を防ぎます。
validAfter
とvalidBefore
の必要性
メタトランザクションでは、ユーザーが直接トランザクションを送信するのではなく、リレイヤーが送信するため送信タイミングを制御できません。
そのため、以下の問題点を防ぐために「いつからいつまで有効か」を指定するvalidAfter
とvalidBefore
が必要です。
意図しないタイミングでの送信
例えば、「給料日(30日)になったらトークンを送信する」つもりで署名を作成しても、リレイヤーが29日に送信してしまう可能性があります。
意図しない遅すぎる送信
例えば、「30分以内に送金しないと取引が無効になる」ような場合、リレイヤーが 31分後に送信すると不要な取引が発生してしまいます。
解決策
トランザクションが有効になる「開始時間」であるvalidAfter
とトランザクションが無効になる「終了時間」であるvalidBefore
を定義します。
EIP712による署名の安全性の確保
EIP712は、Ethereumの署名データにコントラクトのアドレスやチェーンIDを含めることで、署名の安全性を向上させる技術です。
Ethereum の基本的なeth_sign
署名はどのコントラクトやどのネットワークでも再利用できるため、リプレイ攻撃のリスクがあります。
EIP712では以下の対策をしています。
Domain Separatorを使う
verifyingContract
とchainId
を含めることで、特定のコントラクト・チェーン以外では署名が無効になります。
JSON構造化データを利用
eth_signTypedData_v4
を使って、ユーザーが「何に署名しているのか」を確認できます。
互換性
既存のERC20トークンはapprove
+ transferFrom
の仕組みに依存しているため、EIP712メタトランザクションに直接対応できていません。
そのため、既存のコントラクトと互換性を持たせるための「Forwarder Contract」が必要です
DepositForwarder
というフォワードコントラクトを導入することで、EIP712メタトランザクションを利用して既存のERC20コントラクトと連携できるようになります。
DepositForwarder
は、メタトランザクションを使ってトークンを預け入れできるようにするラッパーコントラクトです。
処理の流れ
- 署名付きの
receiveAuthorization
データを受け取る。 -
receiveWithAuthorization
を実行して、送信者(from
)からフォワードコントラクトへトークンを送付する。 - 送付されたトークンを
approve
して、親コントラクトに引き渡す。 - 親コントラクトの
deposit
メソッドを呼び出し、トークンを預け入れ(ステーキングなど)を行う。 - 親コントラクトでミントされたトークンを元の送信者(
from
)に返す。 - 万が一、余剰トークンが残っていれば元の送信者(
from
)に返金。
参考実装
abstract contract EIP7758 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 = "EIP7758: 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, "EIP7758: authorization is not yet valid");
require(now < validBefore, "EIP7758: authorization is expired");
require(
!_authorizationStates[from][nonce],
"EIP7758: 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,
"EIP7758: invalid signature"
);
_authorizationStates[from][nonce] = true;
emit AuthorizationUsed(from, nonce);
_transfer(from, to, value);
}
}
abstract contract IERC20Transfer {
function _transfer(
address sender,
address recipient,
uint256 amount
) internal virtual;
}
abstract contract EIP712Domain {
bytes32 public DOMAIN_SEPARATOR;
}
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)),
bytes32(chainId),
address(this)
)
);
}
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;
}
}
セキュリティ
transferWithAuthorization
ではなくreceiveWithAuthorization
を使用する理由
フロントランニング攻撃を防ぐため。
攻撃シナリオ
- Aliceが
transferWithAuthorization
関数を実行する署名付きデータを作成し、リレイヤーを介して実行しようとする。 - 攻撃者がメモリプールを監視し、Aliceの
transferWithAuthorization
のデータを取得。 - 攻撃者は、自分のアカウントを
to
に変更し、Aliceの代わりにtransferWithAuthorization
を先に実行。 - その結果、Aliceの意図したラッパー関数(フォワードコントラクト)が実行されず、資金が適切に処理されない可能性がある。
解決策
receiveWithAuthorization
関数を使用する。
receiveWithAuthorization
は、送信者 (from
)ではなく、受取アドレス(to
) がトランザクションを実行する必要があるため、攻撃者が勝手にトランザクションを横取りできません。
msg.sender
がto
と一致しているかどうかをチェックすることでフロントランニングを防ぎます。
追加の対策
-
nonce
の先頭バイトを識別子として使用する。- 例:用途ごとに異なる
nonce
プレフィックスを定義。
- 例:用途ごとに異なる
- これにより、異なる目的の
receiveWithAuthorization
関数の誤用を防ぐ。
複数のトランザクションを同時に送信する時の注意点
通常のEthereumトランザクションでは、nonce
によって実行順序が決まるが、メタトランザクションではnonce
がランダムなので、順序を保証できません。
また、異なるリレイヤーが異なる順序で送信する可能性があるため、依存関係のあるトランザクションを同時に送ると失敗するリスクがあります。
解決策
依存関係のあるトランザクションは1つずつ送信する
例えば、「A → B → C」という順序が重要な場合、Aが確定するまでBを送信しない。
オフチェーンで順序管理を行う
署名データに「順番情報」を持たせ、リレイヤー側で順番に処理する仕組みを作る。
異なるアプリ間でのnonce
管理に注意
未確定のnonce
を使ったトランザクションがあると、異なるアプリが同じnonce
を再利用する可能性があります。
そのため、各アプリケーションがオフチェーンでnonce
を管理することで解決できます。
ecrecover
のゼロアドレス(0x0
)チェックの必要性
ecrecover
を使用する時に、ゼロアドレス(0x0
)を拒否する必要があります。
ecrecover
は、Ethereumの公開鍵復元関数で、署名データ(v, r, s
)から元の署名者アドレスを復元する ために使用されます。
しかし、不正な署名データを渡すと、ゼロアドレス(0x0000000000000000000000000000000000000000
)が返ってくることがあります。
ここでもしecrecover
の結果を直接使用してtransfer
やapprove
を行うと、ゼロアドレスのアカウントが勝手にトークンを取得できる可能性があります。
解決策
以下のような仕組みでゼロアドレスのチェックします。
address recovered = ecrecover(digest, v, r, s);
require(recovered != address(0), "Invalid signature");
参考
Peter Jihoon Kim (@petejkim), Kevin Britz (@kbrizzle), David Knott (@DavidLKnott), Dongri Jin (@dongri), "ERC-7758: Transfer With Authorization [DRAFT]," Ethereum Improvement Proposals, no. 7758, September 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7758.
最後に
今回は「安全にERC20トークンをメタトランザクション(approve
+ transferFrom
)で送付する仕組みを提案しているERC7758」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!