[ERC3005] 複数のメタトランザクション処理を一括で送付する仕組みを理解しよう！

# はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、メタトランザクションをバッチで送付する仕組みを提案している**ERC3005**についてまとめていきます！

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

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

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

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

# 概要

**ERC3005**では、**ERC20**などのFungible Token（代替可能トークン）規格を拡張するための新しい関数 `processMetaBatch` を定義しています。
この関数は、複数のMeta Transaction（メタトランザクション）を一括で受け取り、検証・処理するために使用されます。

Meta Transactionとは、送信者自身がガス代（手数料）を支払うことなく、第三者（Relayer）を通じてトランザクションを実行する仕組みです。
この関数により、複数の送信者からのMeta Transactionを1つのオンチェーントランザクションとしてまとめて処理することが可能になります。

`processMetaBatch` 関数は、各トランザクションの署名検証やデータの整合性確認を行ったうえで、トークンの転送を実行します。
これにより、Relayerは1件ずつトランザクションを送信する必要がなくなり、ガスコストの削減が期待できます。

:::note info
Meta Transactionについては以下の記事を参考にしてください。
:::

https://qiita.com/cardene/items/6809173b2ac428b1ccd0

# 動機

Ethereum上では、多くのアカウントがETH（イーサ）を持っていないが**ERC20**トークンは保有しているという状況があります。
このアカウントが自分のトークンを他者に送るためには、ガス代を支払うETHが必要となりそれが障壁になっていました。

この問題を解決する手段として登場したのがMeta Transactionです。
現在の実装では、1つのMeta Transactionしか中継できなかったり、同一の送信者からの複数のMeta Transactionをバッチ処理するものはあっても、異なる複数の送信者からのMeta Transactionをバッチ処理できる仕組みは存在していませんでした。

**ERC3005**の目的は、複数の送信者のMeta Transactionを一括で処理する仕組みを実現することにあります。
これにより、Relayerは1回のトランザクションで多くの処理をまとめて実行できるようになり、全体のガスコストが削減され、より効率的なMeta Transactionの運用が可能となります。

![meta-txs-directly-to-token-smart-contract.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/4e496a06-f571-45ad-ac08-0befd3db65fc.png)

# 仕様

**ERC3005**では、**ERC20**などのFungible Token（代替可能トークン）に、複数の送信者からのMeta Transactionを一括で処理できるようにする `processMetaBatch` 関数の導入を提案しています。

## Metaトランザクションのデータ内容

`processMetaBatc)` 関数がトランザクションを正しく検証し、トークンを転送するためには、以下の情報を処理する必要があります。

* 送信者アドレス
* 受信者アドレス
* トークンの数量
* リレイヤー手数料
* ノンス（`nonce`）
* 有効期限（ブロック番号またはタイムスタンプ）
* トークンコントラクトのアドレス
* リレイヤーのアドレス
* 署名

これらのうち、関数に直接渡す必要があるのは一部で、残りはコントラクトのストレージから取得可能です。

## `processMetaBatch` の引数として必要な情報

関数に渡すべきデータは以下のとおりです。

* 送信者アドレス
* 受信者アドレス
* トークンの数量
* リレイヤー手数料
* 有効期限（ブロック番号またはタイムスタンプ）
* 署名（V, R, Sの形式）

一方、`nonce`やトークンアドレス、リレイヤーアドレスは関数内で取得できるため省略可能です。

## データハッシュの計算方法

各Metaトランザクションは以下の形式でハッシュ化されます。

```js
keccak256(address(sender)
       ++ address(recipient)
       ++ uint256(amount)
       ++ uint256(relayerFee)
       ++ uint256(nonce)
       ++ uint256(expirationDate)
       ++ address(tokenContract)
       ++ address(relayer)
)
```

このハッシュに署名されたデータが送信され、署名者の検証に用いられます。

## 検証ルール

* ノンスは前回のノンスより1だけ大きい必要があります。
* 送信者・受信者が `0x0` の場合は無効。
* 有効期限を過ぎているトランザクションは処理不可。
* 残高がトークン数量＋手数料より小さい場合も無効。
* 一部のトランザクションが無効であっても全体はrevertせず、無効な分だけスキップして処理継続。

## インターフェース

```js
function processMetaBatch(
    address[] memory senders,
    address[] memory recipients,
    uint256[] memory amounts,
    uint256[] memory relayerFees,
    uint256[] memory blocks,
    uint8[] memory sigV,
    bytes32[] memory sigR,
    bytes32[] memory sigS
) public returns (bool);
```

## `nonce`管理

```js
mapping (address => uint256) private _metaNonces;

function nonceOf(address account) public view returns (uint256);
```

各ユーザーのノンスを記録してリプレイ攻撃を防止します。

## トークン転送処理

検証に成功したトランザクションに対しては以下の処理が行われます。

* `nonce` の更新
* 受信者へトークン送信
* リレイヤー（`msg.sender`）に手数料送信

## 変数

### `_metaNonces`

```js
mapping (address => uint256) private _metaNonces;
```

各アドレスに対するMeta Transactionのノンスを保持する配列。
Meta Transactionごとにノンスを使用することで、同じ署名が繰り返し使用される「リプレイ攻撃」を防止するための仕組みです。

**パラメータ**

* `address`
    * アカウントのアドレス。
* `uint256`
    * 対応するノンスの数値。

## 関数

### `nonceOf`

```js
function nonceOf(address account) public view returns (uint256) {
    return _metaNonces[account];
}
```

指定したアカウントの現在の `nonce` を返す関数。
Meta Transactionの整合性チェックやリプレイ防止のために、各アカウントの `nonce` 値を確認するために使用されます。

**引数**

* `account`
    * `nonce` を取得したい対象のアカウントアドレス。

**戻り値**

* `uint256`
    * 対象アカウントの現在の `nonce` 値。

### `processMetaBatch`

```js
function processMetaBatch(
    address[] memory senders,
    address[] memory recipients,
    uint256[] memory amounts,
    uint256[] memory relayerFees,
    uint256[] memory blocks,
    uint8[] memory sigV,
    bytes32[] memory sigR,
    bytes32[] memory sigS
) public returns (bool);
```

複数のMeta Transactionを一括で処理する関数。
この関数は、異なる送信者からの署名付きMeta Transactionをバッチ処理するもので、有効性の検証後にトークンの送信およびリレイヤーへの報酬支払いを行います。
無効なトランザクションはスキップされ、処理全体が`revert`されることはありません。

**引数**

* `senders`
    * 各Meta Transactionの送信者アドレスの配列。
* `recipients`
    * 各Meta Transactionの受信者アドレスの配列。
* `amounts`
    * 各送信者が送るトークンの数量の配列。
* `relayerFees`
    * 各送信者が支払うリレイヤー手数料の配列。
* `blocks`
    * 有効期限として使うブロック番号（またはタイムスタンプ）の配列。
* `sigV`, `sigR`, `sigS`
    * 署名を構成する3つの配列。各トランザクションに対する署名を格納。

**戻り値**

* `bool`
    * トランザクションの一括処理が完了したかどうかを示す真偽値。

# 補足

## オールインワン設計

従来のメタトランザクション実装（例えば **GSN（Gas Station Network）**）は、複数のコントラクトを連携させる設計となっており、構造が複雑でガスコストも高くなりがちです。
一方、**ERC3005**は、全ての処理を1つの関数 `processMetaBatch` に集約することで、シンプルな構成とガスコストの削減を実現しています。

この関数は、メタトランザクションの受信、検証、トークン送信をすべて一括で行います。

## 関数のパラメータ設計

`processMetaBatch` 関数では、以下の複数の配列を受け取ります。

* 送信者アドレスの配列
* 受信者アドレスの配列
* トークン数量の配列
* リレイヤー手数料の配列
* 有効期限（ブロック番号）の配列
* 署名に使われる `v`, `r`, `s` の各配列

これらの配列は、それぞれのメタトランザクションごとに同じインデックスで関連付けられている必要があります。
順序がずれていると署名検証が失敗するため順序は重要です。

## 代替案：構造体ではなく配列で渡す理由

現在の設計では、各データを個別の配列で渡す方式を採用していますが、代わりにビットパック（bitpack）して1つの配列にまとめる方法もあります。
各メタトランザクションのデータを1つの構造に圧縮し、それを配列として渡せば関数の引数数を減らすことができます。

ただし、スマートコントラクト内でのデータのアンパック処理が必要になり、実装は複雑になります。

## なぜ `nonce` を関数の引数に含めないのか？

署名検証に使用する `msgHash` の構築には `nonce` が含まれますが、`nonce` は明示的に引数で渡す必要はありません。
なぜなら、ノンスは常に「**前回のノンス＋1**」でなければならないため、現在の状態から推測できるからです。

これにより、関数の引数数を減らすことができ、Solidity特有の「Stack too deep」エラーの回避にもつながります。

#### ERC2612の `nonces` マッピングは再利用できるか？

**ERC2612** の `permit` 関数もノンスマッピングを使用しています。
**ERC3005**の `nonce` マッピングと同じ仕組みを再利用できる可能性がありますが、セキュリティ上の影響を慎重に検討する必要があります。
現時点では再利用可否について結論は出ていません。

## トークンの転送最適化

OpenZeppelinの**ERC20**実装では `_transfer` 関数を使う方法もありますが、それを使うと以下の問題が発生します。

* 無効なメタトランザクションがあった場合に全体が `revert` される。
* `SSTORE` コールが多発し、ガスコストが増える。

**ERC3005**のように有効なものだけを処理し、最後にリレイヤーへの報酬を一括送信する設計のほうが、全体のガスコストを抑えつつ安全性も確保できます。

## 参考実装

```js
function processMetaBatch(address[] memory senders,
                          address[] memory recipients,
                          uint256[] memory amounts,
                          uint256[] memory relayerFees,
                          uint256[] memory blocks,
                          uint8[] memory sigV,
                          bytes32[] memory sigR,
                          bytes32[] memory sigS) public returns (bool) {
    
    address sender;
    uint256 newNonce;
    uint256 relayerFeesSum = 0;
    bytes32 msgHash;
    uint256 i;

    // loop through all meta txs
    for (i = 0; i < senders.length; i++) {
        sender = senders[i];
        newNonce = _metaNonces[sender] + 1;

        if(sender == address(0) || recipients[i] == address(0)) {
            continue; // sender or recipient is 0x0 address, skip this meta tx
        }

        // the meta tx should be processed until (including) the specified block number, otherwise it is invalid
        if(block.number > blocks[i]) {
            continue; // if current block number is bigger than the requested number, skip this meta tx
        }

        // check if meta tx sender's balance is big enough
        if(_balances[sender] < (amounts[i] + relayerFees[i])) {
            continue; // if sender's balance is less than the amount and the relayer fee, skip this meta tx
        }

        // check if the signature is valid
        msgHash = keccak256(abi.encode(sender, recipients[i], amounts[i], relayerFees[i], newNonce, blocks[i], address(this), msg.sender));
        if(sender != ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", msgHash)), sigV[i], sigR[i], sigS[i])) {
            continue; // if sig is not valid, skip to the next meta tx
        }

        // set a new nonce for the sender
        _metaNonces[sender] = newNonce;

        // transfer tokens
        _balances[sender] -= (amounts[i] + relayerFees[i]);
        _balances[recipients[i]] += amounts[i];
        relayerFeesSum += relayerFees[i];
    }

	// give the relayer the sum of all relayer fees
    _balances[msg.sender] += relayerFeesSum;

    return true;
}
```

# セキュリティ

## メタトランザクションの偽造

リレイヤーが勝手にメタトランザクションを作成・偽造することを防ぐため、送信者自身が秘密鍵で署名する必要があります。
その署名は `ecrecover` 関数により検証され、送信者と署名者が一致しない場合、そのメタトランザクションは無効としてスキップされます。

## リプレイ攻撃（Replay Attacks）

**ERC3005**では以下の2つのリプレイ攻撃に対して対策が講じられています。

- **同一のメタトランザクションを同じコントラクトで複数回使用する攻撃**

各送信者に対してノンス（`nonce`）を追跡し、1度処理されたノンスは再利用できないようにしています。

- **同一のメタトランザクションを異なるコントラクトで再利用する攻撃**

署名にコントラクトアドレス（`address(this)`）を含めることで、異なるコントラクトでは署名検証に失敗し、処理されません。

## 署名の検証
全てのメタトランザクションは署名の正当性検証が必須です。
不正な署名だった場合、そのメタトランザクションはスキップされ、全体のトランザクションは `revert` されません。
これにより、一部に不備があるだけで他のトランザクション処理が止まってしまうことを防いでいます。

なお、リレイヤーが不正なメタトランザクションを送信しても、報酬（リレイヤーフィー）は得られないため、事前検証が推奨されます。

## 悪意あるリレイヤーによる過剰支出の強制

悪意あるリレイヤーが、ユーザーが同一内容のトークン送信をオンチェーンで自ら実行した後に、同じメタトランザクションを遅延送信することでトークンを二重に消費させる可能性があります。

この問題への対策として、メタトランザクションには「**有効期限（ブロック番号）**」が必須とされています。
`block.number` がこの値を超えると、そのメタトランザクションは無効としてスキップされます。

## フロントランニング攻撃

悪意あるリレイヤーがメタトランザクションを盗み、元のリレイヤーより先に送信するフロントランニング攻撃も想定されています。

これを防ぐため、メタトランザクションの署名ハッシュにはリレイヤーのアドレス（`msg.sender`）が含まれています。
本来のリレイヤー以外が送信しても、署名が一致しないため、署名検証に失敗してスキップされます。

## 同一ノンスの多重リレー

ユーザーが同一ノンスのメタトランザクションを複数のリレイヤーに同時に送信するケースも想定されています。
この場合、先にオンチェーンで処理したリレイヤーのみが報酬を得られ、他は失敗となるため、トランザクションの衝突が起きます。

対策としては、リレイヤー同士がメタトランザクション情報（送信者アドレス・トークンアドレス・`nonce`）を共有する仕組み（インフォメーションメンプール）を持つことが推奨されます。

## 不当に大きな有効期限の設定

リレイヤーがユーザーを騙して、メタトランザクションの有効期限を極端に遠い将来（例：10年後）に設定させることで、不当な支配権を得る可能性もあります。

この問題への対策は2通りあります。

- コントラクトレベルで、有効期限を現在のブロックから最大10万ブロック以内**（約17日）に制限する。
- 実装者が自由に上限を設けない場合は、リレイヤー側でブロック番号が適切かどうかを検証してユーザーに警告する方法。

このように、意図しない長期拘束を回避する手段も考慮されています。

# 引用

Matt (@defifuture), "ERC-3005: Batched meta transactions [DRAFT]," Ethereum Improvement Proposals, no. 3005, September 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3005.

# 最後に

今回は「メタトランザクションをバッチで送付する仕組みを提案している**ERC3005**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下の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