# [ERC7758] 安全にERC20トークンをメタトランザクションで送付する仕組みを理解しよう！

> ERC7758では、EIP712を活用してERC20トークンのメタトランザクションを可能にする仕組みを提案しています。この仕組みを活用することで以下を実現できます。

- URL: https://cardene.dev/articles/9ead3db11831a24e37e8
- 公開日: 2025-02-17
- 更新日: 2025-02-17
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

https://amzn.asia/d/gxvJ0Pw

今回は、安全に**ERC20**トークンをメタトランザクション（`approve` + `transferFrom`）で送付する仕組みを提案している**ERC7758**についてまとめていきます！

:::message
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
:::

https://eips.ethereum.org/EIPS/eip-7758

他にも様々なEIPについてまとめています。

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

## 概要

**ERC7758**では、**EIP712**を活用して**ERC20**トークンのメタトランザクションを可能にする仕組みを提案しています。
この仕組みを活用することで以下を実現できます。

- ガス代を他者に負担してもらう。
- ETHの代わりに**ERC20**トークンでガス代を支払う。
- 複数のトークン送付や処理を1回のトランザクションでまとめて実行する。
- トークンを送る時に受信者がトランザクションを送信する仕組みの実装。
- 複数のトランザクションを効率的に処理して計算効率の向上や手数料の負担軽減。
- トランザクションの失敗を防ぐためにnonce管理を改善。

:::message
**ERC20**については以下の記事を参考にしてください。
:::

https://chaldene.net/erc20

:::message
**ERC712**については以下の記事を参考にしてください。
:::

[\[EIP712\] 型付き構造化データのハッシュ化と署名の仕組みを理解しよう！](/articles/f54bc85cb66de5a54257)

## 動機

### メタトランザクションとは？

通常、EVM互換のブロックチェーンでトランザクションを実行するには、送信者（トランザクションを実行したい人）がガス代をETHで支払う必要があります
しかし、メタトランザクションを使うことで、送信者が直接トランザクションを送るのではなく、別の誰か（リレーサービスや運営のEOAアドレスなど）がトランザクションをブロックチェーンに送信し、ガス代を支払うことが可能になります。

:::message

#### 具体例

AliceがBobにERC20トークンを送りたいとします。
しかし、AliceはETHを持っていないためトランザクションを送ることができない状態です。
そこで、Aliceは「署名付きメッセージ」（**EIP712**に準拠）を作成してBob に送ります。
Bobがそのメッセージを使ってブロックチェーン上でトランザクションを送信し、Aliceの代わりにガス代を支払います。

この方法を利用すると、ユーザーがETHを持っていなくても別のアドレスがガス代を負担しつつトランザクションを実行して、**ERC20**トークンのやりとりが可能になります。
:::

### ERC2612との違い

**ERC2612**もメタトランザクションを可能にする仕組みですが、今回の提案とは以下の点で異なります。

:::message
**ERC2612**については以下の記事を参考にしてください。
:::

[\[ERC2612\] ERC20のapproveとtransferFromを1つのトランザクションで実行できる仕組みを理解しよう！](/articles/6809173b2ac428b1ccd0)

#### Nonceの管理

**ERC2612**では 連番の`nonce`を使用しますが、本提案ではランダムな32バイトの`nonce`を使用します。

:::message
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**の代替として広く使用されませんでした。

## 仕様

### インターフェース

```js
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`

```js
event AuthorizationUsed(
    address indexed authorizer,
    bytes32 indexed nonce
);
```

**概要**  
**EIP712準拠の署名を使用してトークン転送が実行された時に発行されるイベント。  

**詳細**  
このイベントは、`transferWithAuthorization`関数や`receiveWithAuthorization`関数を通じてトークンが送付されたときに発行され、トランザクションの署名が使用されたことを示します。
これにより、同じ署名の再利用を防ぐことができます。  

**パラメータ**  
- `authorizer`
    - 署名を行ったアドレス（トークンの送信者）。  
- `nonce`
    - 署名に関連付けられた一意の識別子（ランダムな32バイトの値）。  

---

#### `TRANSFER_WITH_AUTHORIZATION_TYPEHASH`

```js
bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267;
```

**概要**  
**EIP712**に準拠した`transferWithAuthorization`関数のデータ型（型ハッシュ）を表す定数。  

**詳細**  
**EIP712**では、メッセージの構造を明確に定義してハッシュを用いて署名を行います。
この変数は `transferWithAuthorization`関数のデータ構造を`keccak256`によってハッシュ化したもので、署名の検証時にトランザクションの整合性をチェックするために使用されます。

---

#### `RECEIVE_WITH_AUTHORIZATION_TYPEHASH`

```js
bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8;
```

**概要**  
**EIP712**に準拠した`receiveWithAuthorization`関数のデータ型（型ハッシュ）を表す定数。  

**詳細**  
**EIP712**では、メッセージの型を定義してそのハッシュを使用して署名を行います。
この変数は`receiveWithAuthorization`関数のデータ構造を`keccak256`によってハッシュ化したもので、署名の検証時に`receiveWithAuthorization`関数のリクエストが正当なものかチェックするために使用されます。  

---

#### `authorizationState`

```js
function authorizationState(
    address authorizer,
    bytes32 nonce
) external view returns (bool);
```

**概要**  
指定された`nonce`がすでに使用されたかどうかを確認する関数。  

**詳細**  
この関数は、`transferWithAuthorization`関数や`receiveWithAuthorization`関数を実行する時に使用された`nonce`が、すでに使用されたかどうかをチェックします。  
一度使用された`nonce`は再利用できないため、リプレイ攻撃を防ぐ役割を果たします。  

**引数**  
- `authorizer`  
	- 署名を行ったアドレス（トークンの送信者）。  
- `nonce`  
	- 一意の識別子（ランダムな32バイトの値）。  

**戻り値**  
- `bool`  
	- `true`の場合、`nonce`はすでに使用済み。  
	- `false`の場合`nonce`は未使用であり、トランザクション実行が可能。  

---

#### `transferWithAuthorization`

```js
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`

```js
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`

```js
event AuthorizationCanceled(
    address indexed authorizer,
    bytes32 indexed nonce
);
```

**概要**  
`cancelAuthorization`によって署名（**EIP712**に準拠）が取り消されたときに発行されるイベント。  

**詳細**  
このイベントは、未使用の署名を取り消した時に発行され、`nonce`がキャンセルされたことを記録します。  
これにより、不要になった署名が悪用されるのを防ぐことができます。  

**引数**  
- `authorizer`  
	- 署名を行ったアドレス（トークンの所有者）。  
- `nonce`  
	- 一意の識別子（キャンセルされたランダムな32バイトの値）。  

---

#### `CANCEL_AUTHORIZATION_TYPEHASH`

```js
bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = 0x158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a1597429;
```

**概要** 

**EIP712**に準拠した`cancelAuthorization`関数のデータ型を表す定数。 

**詳細**  
**EIP712**では、メッセージの型を定義してそのハッシュを使用して署名を行います。  
この変数は`cancelAuthorization`関数のデータ構造を`keccak256`によってハッシュ化したもので、署名の検証時に`cancelAuthorization`関数のリクエストが正当なものかチェックするために使用されます。  

---

#### `cancelAuthorization`

```js
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）の生成

```js
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`）の計算

```js
// 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`）を処理するための重要な仕組み**。

:::message

**型ハッシュ（TypeHash）とは？**  

各関数（`TransferWithAuthorization`、`ReceiveWithAuthorization`、`CancelAuthorization`）ごとに固定のデータ型を定義し、そのハッシュ値を事前に計算します。  
これを利用することで、署名の対象となるデータの種類を明確に区別できるようになります。 
:::


---

##### トランザクションデータのハッシュ化と署名

```js
// "‖" 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`をラップするコントラクト

```js
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`関数をラップし、簡潔に署名済みトランザクションを処理する関数。  

1. `receiveAuthorization`関数のデータをデコードし、`from`、`to`、`amount`を取得します。  
2. `to`（受取アドレス）がコントラクト自身かどうかチェック。  
3. `receiveWithAuthorization`関数を実行してトークン送付。  
4. 成功しなければエラーを返す。

このように、署名済みデータを1つの`bytes`型として渡してトランザクションを簡潔に実行します。


### Web3プロバイダー

**EIP712**の署名をWeb3プロバイダー（Metamaskなど）を利用して取得するサンプルコードも記載されています。  

---

#### EIP712メッセージ構造の定義

```javascript
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 メインネット）
   - **`verifyingContract`**
       - 処理を行うコントラクトアドレス

- **メッセージデータ（`message`）**
   - **`from`**
       - 送金者のアドレス
   - **`to`**
       - 受取アドレス
   - **`value`**
       - 送金するトークンの量（BigNumber形式から文字列に変換）
   - **`validAfter`**
       - この署名が有効になる開始時間（`0`は即時有効）
   - **`validBefore`**
       - 有効期限（現在の時刻から`+1時間`）
   - **`nonce`**
       - 32バイトのランダムな値（署名の再利用を防ぐ）

---

#### `eth_signTypedData_v4`を使って署名を取得

```javascript
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`に分割

```javascript
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`は、メタトランザクションを使ってトークンを預け入れできるようにするラッパーコントラクトです。

### 処理の流れ

1. 署名付きの`receiveAuthorization`データを受け取る。
2. `receiveWithAuthorization`を実行して、送信者（`from`）からフォワードコントラクトへトークンを送付する。
3. 送付されたトークンを`approve`して、親コントラクトに引き渡す。
4. 親コントラクトの`deposit`メソッドを呼び出し、トークンを預け入れ（ステーキングなど）を行う。
5. 親コントラクトでミントされたトークンを元の送信者（`from`）に返す。
6. 万が一、余剰トークンが残っていれば元の送信者（`from`）に返金。

## 参考実装

```js:EIP7758.sol
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);
    }
}
```

```js:IERC20Transfer.sol
abstract contract IERC20Transfer {
    function _transfer(
        address sender,
        address recipient,
        uint256 amount
    ) internal virtual;
}
```

```js:EIP712Domain.sol
abstract contract EIP712Domain {
    bytes32 public DOMAIN_SEPARATOR;
}
```

```js: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)),
                    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` を使用する理由

フロントランニング攻撃を防ぐため。

:::message
#### フロントランニングとは

誰かがトランザクション（トランザクションA）を送信する前に、別の誰か（攻撃者）がそれを事前に察知して別のトランザクション（トランザクションB）を先に処理する攻撃。
Ethereumでは、トランザクションは「Mempool」と呼ばれる待機領域に一時的に保管されます。
攻撃者はこのメモリプールを監視し、特定のトランザクション（例：`transferWithAuthorization`）を自分が先に実行することで、意図しない結果を引き起こすことが可能になります。
:::

#### 攻撃シナリオ
1. Aliceが`transferWithAuthorization`関数を実行する署名付きデータを作成し、リレイヤーを介して実行しようとする。
2. 攻撃者がメモリプールを監視し、Aliceの`transferWithAuthorization`のデータを取得。
3. 攻撃者は、自分のアカウントを`to`に変更し、Aliceの代わりに`transferWithAuthorization`を先に実行。
4. その結果、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`を行うと、ゼロアドレスのアカウントが勝手にトークンを取得できる可能性があります。

#### 解決策

以下のような仕組みでゼロアドレスのチェックします。

```js
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などからお気軽に質問してください！

[Twitter @cardene777](https://twitter.com/cardene777)

他の媒体でも情報発信しているのでぜひ他も見ていってください！

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
