[ERC3009] より安全にメタトランザクションを実行する仕組みを理解しよう！

# はじめに

初めまして。
**CryptoGames**というブロックチェーンゲーム企業でエンジニアをしている **cardene（かるでね）** です！
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

https://cryptogames.co.jp/

代表的なゲームは**クリプトスペルズ**というブロックチェーンゲームです。

https://cryptospells.jp/

今回は、**ERC2612**で提案されているに`approve` + `transferFrom`のメタトランザクションを、より安全に実行する仕組みを提案している**ERC3009**についてまとめていきます！

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

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

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

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

:::note warn
この規格はステージが**Stagnant**（6ヶ月以上更新がない状態）なので、暫く更新されておらず提案された当時の状態と現在の状態と異なる部分があります。
EIPの各ステージについては以下を参考にしてください。
:::

https://eips.ethereum.org/

https://qiita.com/cardene/items/24768d24a8d222a07107#eip%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC

# 概要

この規格では、署名された`approve`を使用してトークンを`transfer`可能にするインターフェースを提案しています。

**EIP712**という、**ERC20**に準拠した署名によってアトミック性を持ったメタトランザクションを可能にする仕組みを使用することで、以下のようなことができます。

:::note info
イーサリアム上のトランザクションを実行するには、ガス代をETHで支払う必要があります。
メタトランザクションを使うことで、ユーザーが直接ガス料金を支払う必要がなくなり、他のアドレスがガス代を支払うことができます。
:::

- ガス代のデリゲート
    - 他のアドレスにガス代の支払いを委任できます。
- トークンでのガス代支払い
    - ネイティブトークンであるETHではなく、任意のトークンでガス代を支払うことできます。
- アトミックなトランザクション
    - 1回のトランザクションで複数のトークンを送付したり、その他の操作をまとめて実行できます。
    - アトミックというのは、1つでも処理が失敗したらそのトランザクションの処理を全て失敗にするということです。
- トランザクション送信の委任
    - ガス代の支払いの委任に通じる部分ですが、トランザクションの送信を他のアドレスにしてもらうことが可能です。
- バッチトランザクション
    - 複数のトランザクションを一括で処理できます。
- `nonce`の再利用防止
    - `nonce`とは、特定のアドレスがこれまで送ってきたトランザクションの数です。
    - トランザクションをが成功/失敗すると、この`nonce`値に`1`加算されるのですが、この`nonce`が`1`加算されないと、同じトランザクションを2度送れてしまいます。
    - これを防止する仕組みを実装しつつ複数のトランザクションを実行できます。

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

https://chaldene.net/erc20

# 動機

**EIP2612**という既存の規格によりメタトランザクションは可能になっています。
この規格と**EIP2612**の違いは以下になります。

- **EIP2612**は連続した`nonce`（トランザクションの一意の識別子）を使用しますが、この規格ではランダムな`32`バイトの`nonce`を使用します。
- **EIP2612**は**ERC20**の`approve` + `transferFrom`パターンに依存しています。

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

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

連続した`nonce`を使用する場合、以下の理由から複数トランザクションの同時実行ができません。

- Dappsがブロックチェーン上で処理されていない`nonce`を再利用する可能性がある。
    - ガス代の急激な高騰により、トランザクションがmempool（実行予定のトランザクションの溜まり場）に長期間溜まったままになってしまうことで起きる可能性があります。

一方、この規格で提案されているように、非連続の`nonce`を使用することでユーザーは好きなだけトランザクションを送ることができます。

:::note info
現状、アドレスごと`nonce`の管理がされているため、コントラクト側で`nonce`の管理をしてもアドレスの方の`nonce`管理は意識する必要があります。
ここで提案されているのは、コントラクトで管理している`nonce`が連番でないため、トランザクションの順番を気にせず実行することができるということです。
:::

:::note info
また、ガス代についても**EIP1559**の導入により、急激な高騰を防ぐ仕組みが導入されています。
**EIP1559**については以下の記事を参考にしてください。
:::

https://qiita.com/cardene/items/ce77124940010280717b

**ERC20**の`allowance`メカニズムには以下の問題点があります。

- 複数回の引き出し攻撃に弱い

:::note info
リエントランシー攻撃のことです。
複数回同じ関数を実行し続けることができてしまう脆弱性のことです。

より詳しくは以下の記事を参考にしてください。
:::

https://chaldene.net/solidity-reentrancy-attack

- トランザクションの順番が変わる

:::note info
トランザクションは作成した順番通りに実行されるとは限りません。
例えば、より多くのガス代を支払うことで、先にトランザクションを通すことができてしまいます。
これにより、トランザクションを盗み見るということができてしまいます。

より詳しくは以下の記事を参考にしてください。
:::

https://swcregistry.io/docs/SWC-114/

- 無限`allowance`
    - **ERC20**の`approve`実行時に、極端に大きい値を設定することでほぼ無限に設定したアドレスがトークンの操作権限を持ってしまう。

アップグレード可能なコントラクトの普及により、上記の攻撃が発生しやすくなっています。

:::note info
なぜ発生しやすくなっているかはわからないですが、アップグレード可能なので注意力が落ちているということかなと思いました。
:::

**ERC20**の`allowance`の問題点を解決するために、**ERC777**や**ERC677**などの代替トークン標準が提案されました。
しかし、互換性やセキュリティの問題からこれらの標準はあまり普及していません。

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

https://qiita.com/cardene/items/a33541bc7ae02bdc4b2a

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

https://qiita.com/cardene/items/c0fcb8f3122f66985226

# 仕様

## イベント

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

/**
 * @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`

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

特定の認証が使用された時に発行されるイベント。
- `authorizer`
    - 認証を行ったユーザーのアドレス。
- `nonce`
    - 認証に関連付けられた一意の値。

#### `TRANSFER_WITH_AUTHORIZATION_TYPEHASH`

```js
bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267;
```
`TransferWithAuthorization`関数のための型ハッシュ値。
**EIP712**標準に基づき、構造化データの署名に使用されます。

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

https://qiita.com/cardene/items/f54bc85cb66de5a54257

#### `RECEIVE_WITH_AUTHORIZATION_TYPEHASH

```js
bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8;
```
`ReceiveWithAuthorization`関数のための型ハッシュ値。
**EIP712**標準に基づき、構造化データの署名に使用されます。

#### `authorizationState`

```js
function authorizationState(
    address authorizer,
    bytes32 nonce
) external view returns (bool);
```
指定された`nonce`が使用済みか確認する関数。
- **引数**:
    - `authorizer`
        - 認証を行うユーザーのアドレス。
    - `nonce`
        - 認証に関連付けられた一意の値。
- **戻り値**
    - `nonce`が使用済みであれば`true`、そうでなければ`false`。

#### `transferWithAuthorization`

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

```js
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`
        - 署名パーツ。

### オプション

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

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

特定の認証がキャンセルされた時に発行されるイベント。
- `authorizer`
    - 認証を行ったユーザーのアドレス。
- `nonce`
    - 認証に関連付けられた一意のノンス。

```js
bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = 0x158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a1597429;
```
`cancelAuthorization`関数のための型ハッシュ値。
**EIP712**標準に基づき、構造化データの署名に使用されます。

#### `cancelAuthorization`
```solidity
function cancelAuthorization(
    address authorizer,
    bytes32 nonce,
    uint8 v,
    bytes32 r,
    bytes32 s
) external;
```

署名された認証をキャンセルする関数。
これにより、まだ使用されていない認証を無効化することができます。

- **引数**:
    - `authorizer`
        - 認証を行うユーザーのアドレス。
    - `nonce`
        - 認証に関連付けられた一意のノンス。
    - `v`, `r`, `s`
        - 署名の部分。

### EIP712による署名の取得方法

**EIP712**の仕様に基づいて、署名部分（`v`, `r`, `s`）を取得するためには以下のような手順を取ります。

1. **ドメインセパレータの生成**
    - ドメインセパレータは、署名の一意性を保証するために使用されるデータで以下のように生成します。
   
    ```js
    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
    ));
    ```

2. **メッセージハッシュの生成**
    - キャンセルする認証に関するデータをハッシュ化します。
   
    ```js
    bytes32 structHash = keccak256(abi.encode(
       CANCEL_AUTHORIZATION_TYPEHASH,
       authorizer,
       nonce
    ));
    ```

3. **EIP712ハッシュの生成**
    - ドメインセパレータとメッセージハッシュを組み合わせて、**EIP712**のハッシュを生成します。
   
    ```js
    bytes32 digest = keccak256(abi.encodePacked(
       "\x19\x01",
       DomainSeparator,
       structHash
    ));
    ```

4. **署名部分の取得**
   - `digest`を署名することで、署名部分`v`, `r`, `s`を取得します。
   - これを使って認証をキャンセルします。

上記の内容を以下のように説明します。

### ドメインセパレータとTypeHash

**ドメインセパレータ**と**TypeHash**を使って、特定のメッセージに対するハッシュを生成してユーザーの秘密鍵で署名できます。

#### ドメインセパレータ
ドメインセパレータは以下のように生成されます。

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

```js
bytes32 TypeHash = keccak256(
  "TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
);
Params := { From, To, Value, ValidAfter, ValidBefore, Nonce };
```

#### `ReceiveWithAuthorization`

```js
bytes32 TypeHash = keccak256(
  "ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
);
Params := { From, To, Value, ValidAfter, ValidBefore, Nonce };
```

#### `CancelAuthorization`

```js
bytes32 TypeHash = keccak256(
  "CancelAuthorization(address authorizer,bytes32 nonce)"
);
Params := { Authorizer, Nonce };
```

### ダイジェストの生成
各メッセージに対して、ダイジェストを生成します。

```js
bytes32 Digest = keccak256(
  abi.encodePacked(
    "\x19\x01",
    DomainSeparator,
    keccak256(abi.encode(TypeHash, Params...))
  )
);
```

このダイジェストは、トークンホルダーの秘密鍵で署名されます。

```js
{ v, r, s } := Sign(Digest, PrivateKey);
```

### `receiveWithAuthorization`の簡略化
スマートコントラクトの関数は、`receiveWithAuthorization`コールの全ての引数をバイト型の1つの引数として受け取ることで、引数の数を減らすことができます。

#### 例

```js
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}`を使用して取得できます。

例

```js
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**に追加するには、フォワーダーコントラクトを構築する必要があります。

1. `approve`からユーザーと入金額を抽出。
2. `receiveWithAuthorization`を呼び出して、指定された資金をユーザーからフォワーダーコントラクトに`transfer`。
3. 親コントラクトがフォワーダーからトークンを操作できるように`allowance`を設定。
4. 親コントラクト上のメソッドを呼び出して、フォワーダーから設定された`allowance`分のトークンを使用。
5. 得られたトークンの所有権をユーザーに戻す。

### コントラクト例

#### `IDeFiToken` インターフェース

```js
interface IDeFiToken {
    function deposit(uint256 amount) external returns (uint256);
    function transfer(address account, uint256 amount) external returns (bool);
}
```
- **`deposit`**
    - 指定された量のトークンを入金し、新しく発行されたトークンの数を返す関数。
- **`transfer`**
    - 指定されたアカウントにトークンを送付する関数。

#### `DepositForwarder` コントラクト

```js
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`します。
    - **残高の返金**
        - フォワーダーに残っているトークンがあれば、それを送信者に送付します。

この方法により、既存の**ERC20**`allowance`パターンを使用するコントラクトに対しても、**EIP3009**の機能を追加することができます。

# テスト

以下にテストコードが格納されています。

https://github.com/CoinbaseStablecoin/eip-3009/blob/master/test/EIP3009.test.ts

# 実装

## `EIP3009.sol`

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

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

## `EIP712Domain.sol`

```js
abstract contract EIP712Domain {
    bytes32 public DOMAIN_SEPARATOR;
}
```

## `EIP712.sol`

```js
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**のコードを実装する必要があります。

https://github.com/CoinbaseStablecoin/eip-3009/blob/master/contracts/lib/EIP3009.sol

https://github.com/CoinbaseStablecoin/eip-3009/blob/master/contracts/lib/EIP2612.sol

# セキュリティ

以下は、提供された内容についての説明です。

### セキュリティ考慮事項

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

[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