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

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、**ERC712**の`secp256k1`署名を使用して、**ERC20**の`approve`と`transferFrom`を1つのトランザクションで実行できる提案している規格である**ERC2612**についてまとめていきます！

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

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

今回の内容は以下の記事でよりわかりやすく解説しています。

https://chaldene.net/erc20permit

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

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

# 概要

このERCは、**ERC20**トークンを拡張して、**承認（approve）**と**transferFrom**という2つの関数に関連する提案をしています。
2つの関数の相互作用により、トークンは外部所有アカウント（**EOA**）間でだけでなく、トークンアクセス制御として使用される`msg.sender`を抽象化し、アプリケーションで設定された固有の条件下で他のコントラクトともやり取りできるようになりました。

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

https://chaldene.net/erc20

ただし、この設計では**EIP20**の`approve`関数が`msg.sender`に依存しています。
つまり、**EOA**を起点に処理が実行実行される必要があります。
ユーザーがコントラクトとやり取りする必要がある場合、2つのトランザクション（`approve`と`transferFrom`の呼び出し）を実行する必要があります。
また、トランザクションのガスコストを支払うために**ETH**を持っている必要があります。

このERCは、**EIP20**標準を新しい`permit`関数を使用することで、ユーザーが`msg.sender`に依存せず署名されたメッセージを使用して`allowance`マッピングを変更できるようにします。

ユーザーエクスペリエンスを向上させるために、署名データは**EIP712**に従って構造化されており、主要なRPCプロバイダーで広く採用されています。

:::note warn
**EIP20**は、トークンの所有者のアドレスが実際にコントラクトウォレットである場合を除き、**EOA**によって実行する必要があります。
コントラクトウォレットを使用する方法もありますが、現在はエコシステムでほとんど採用されていません。
コントラクトウォレットにはユーザーエクスペリエンスの問題があり、コントラクトウォレットの**EOA**オーナーとコントラクトウォレット自体（代理でアクションを実行しすべての資金を保持する）を分離するため、ユーザーインターフェースを特別に設計する必要があります。
`permit`パターンは、ユーザーインターフェースをほとんどまたはまったく変更せずに同じ処理を実行できます。
:::

# 動機

**EIP20**トークンは、Ethereumエコシステムで広く使用されていますが、プロトコルの観点からは依然として2級のトークンの地位を持っています。

:::note info
何を持っての2級なのかわからないです...。
:::

Ethereum上で**ETH**を保持せずに処理を実行できることは長い間の目標であり、多くのEIP（Ethereum Improvement Proposal）で議論されてきました。

これまでの提案の多くはほぼ採用されておらず、採用されたもの（例：**EIP777**）も、追加の機能を導入したためにコントラクトで予期しない動作を引き起こすことがありました。

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

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

このERCは、最小限の変更で、**EIP20**の問題である「`approve`メソッドの抽象化の不足」に対処する提案をしています。

**EIP20**の各関数に対して`_by_signature`のような関数を導入することも考えられましたが、敢えてそれらを省略することにしました。
その理由は以下の2つです。

1. この関数に必要な具体的な仕様は、`transfer_by_signature`の手数料やバッチ処理アルゴリズムなどが異なるなど使用ケースに依存してしまうため。
2. `permit`と追加のヘルパーコントラクトの組み合わせを使うことで固有の実装になり過ぎてしまうため。

:::note info
このERCは、**EIP20**の抽象化を改善するために、最小限の変更を提案し単一の問題に焦点を当てています。
:::

# 仕様

コンプライアンス対応のコントラクトは、**EIP20**に追加で3つの新しい関数を実装する必要があります。
それぞれの関数は以下のようになります。

- `permit`

```javascript
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external
```

以下の特定の条件を満たす場合に、トークンの承認を行います。

- 現在のブロックタイムが指定された`deadline`よりも小さいか等しい。
- `owner`アドレスがゼロでない。
- ステート更新前の`nonces[owner]`の値が指定された`nonce`と等しい。
- `r`、`s`、`v`がメッセージの所有者である`owner`による有効な`secp256k1`署名であること。

これらの条件を満たさない場合、`permit`の呼び出しは取り消されます（`revert`）。

- `nonces`

```javascript
function nonces(address owner) external view returns (uint)
```

指定された`owner`アドレスの現在の`nonce`値を渡します。

- `DOMAIN_SEPARATOR`

```javascript
function DOMAIN_SEPARATOR() external view returns (bytes32)
```
コントラクト内で使用される`DOMAIN_SEPARATOR`値を返します。

:::note info
`permit`関数はトークンの承認を行うためのもので、特定の条件を満たす場合にのみ許可を与え、`nonces`関数は`nonce`の値を取得し、`DOMAIN_SEPARATOR`関数はコントラクトのドメインセパレーター値を提供します。
これらの関数は、トークン操作のセキュリティを向上させ、他のスマートコントラクトとの相互運用性を高めるために追加されました。
:::

```javascript
keccak256(abi.encodePacked(
   hex"1901",
   DOMAIN_SEPARATOR,
   keccak256(abi.encode(
        keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
        owner,
        spender,
        value,
        nonce,
        deadline))
))
```

`DOMAIN_SEPARATOR`は、**EIP712（Ethereum Improvement Proposal 712）**に基づいて定義されているものです。
この`DOMAIN_SEPARATOR`は、コントラクトとチェーンの間で一意である必要があり、他のドメインからのリプレイ攻撃を防ぐために使用されます。

:::note info
リプレイ攻撃（リエントランシー攻撃）については以下を参考にしてください。
:::

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

```javascript
DOMAIN_SEPARATOR = keccak256(
    abi.encode(
        keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
        keccak256(bytes(name)),
        keccak256(bytes(version)),
        chainid,
        address(this)
));
```

以下は、一般的な`DOMAIN_SEPARATOR`の定義について詳細に説明します。

`DOMAIN_SEPARATOR`は、以下の要素から構成されます。

- `'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'`
    -  ドメインの定義を表します。
    -  各パラメータは次のとおりです。
        - `name`
            - コントラクトの名前。
        - `version`
            - バージョン。
        - `chainId`
            - チェーンのID。
        - `verifyingContract`
            - コントラクトのアドレス。
- `keccak256(bytes(name))`
    - コントラクトの名前をバイト列に変換してハッシュ化します。
- `keccak256(bytes(version))`
    - バージョンをバイト列に変換してハッシュ化します。
- `chainid`
    - チェーンのID。
- `address(this)`
    - コントラクト自体のアドレス。

これらの要素を組み合わせて、一意の`DOMAIN_SEPARATOR`を生成します。
この`DOMAIN_SEPARATOR`は、コントラクト内で署名検証やセキュリティ関連の操作に使用されます。
重要なのは、この`DOMAIN_SEPARATOR`がコントラクトとチェーンに固有であるため、他のドメインからの不正な操作を防ぐのに役立つことです。
異なるドメインでは異なる`DOMAIN_SEPARATOR`が生成され、リプレイ攻撃から保護されます。

:::note info
`DOMAIN_SEPARATOR`はセキュリティを強化し、コントラクトが他のコントラクトや外部エンティティとやり取りする時に、正当な操作かどうかを確認するのに役立つ情報を提供します。
:::

```yaml
{
  "types": {
    "EIP712Domain": [
      {
        "name": "name",
        "type": "string"
      },
      {
        "name": "version",
        "type": "string"
      },
      {
        "name": "chainId",
        "type": "uint256"
      },
      {
        "name": "verifyingContract",
        "type": "address"
      }
    ],
    "Permit": [
      {
        "name": "owner",
        "type": "address"
      },
      {
        "name": "spender",
        "type": "address"
      },
      {
        "name": "value",
        "type": "uint256"
      },
      {
        "name": "nonce",
        "type": "uint256"
      },
      {
        "name": "deadline",
        "type": "uint256"
      }
    ],
  },
  "primaryType": "Permit",
  "domain": {
    "name": erc20name,
    "version": version,
    "chainId": chainid,
    "verifyingContract": tokenAddress
  },
  "message": {
    "owner": owner,
    "spender": spender,
    "value": value,
    "nonce": nonce,
    "deadline": deadline
  }
}
```

この定義のどこにも、`msg.sender`に言及していないことに注意してください。
`permit`関数の呼び出し元はどのアドレスでも問題ないです。

# 補足

`permit`関数は、**EIP20**トークンにおいてトークンを使用してトランザクション手数料を支払う仕組みを提供します。
通常、Ethereumトランザクションは**ETH**を使用してガス（手数料）を支払いますが、`permit`関数を使用することで、ユーザーはトークンを使って手数料を支払うことができるようになります。
これは、トークンの所有者がトークンを保持しており、そのトークンを使用してトランザクションを実行できるため、便利でセキュアな方法です。

また、`nonces`（ノンス）マッピングは、リプレイ攻撃から保護するための仕組みです。
リプレイ攻撃は、同じトランザクションを複数回実行する攻撃の一種であり、`nonces`を使用することで同じ`Permit`を複数回使用することを防ぎます。
各所有者には、ノンス（`nonce`）と呼ばれる一意の番号が割り当てられ、`permit`関数が呼び出されるたびにこの番号が増加します。
このため、同じ`Permit`を複数回使用することはできません。

一般的な使用ケースとして、所有者がリレイヤー（代理人）によって`Permit`を提出する場合があります。
リレイヤーは所有者の代わりにトランザクションを`Permit`し、提出します。
この場合、リレーする側は`Permit`を提出または保留する選択肢を持っています。
しかし、所有者は`Permit`の有効期限を制限することができます。
`deadline`引数を設定することで、`Permit`の有効期限を設定し、一定の時間内に許可が提出されない場合、無効にできます。
`deadlineをuint(-1)`に設定すると、`Permit`が事実上期限切れにならないように設定できます。

最後に、**EIP712**型メッセージは、広く採用されているウォレットプロバイダーとの互換性を確保するために含まれています。
これにより、署名と認証プロセスがよりセキュアになり、トークンの操作が正当かどうかを確認するのに役立ちます。

# 後方互換性

実際のトークンコントラクトには、すでにいくつかの`permit`関数が存在します。
特に、`dai.sol`で導入された`permit`関数が広く注目されています。
しかし、ここで提供された仕様とは少し異なる点がいくつかあります。

1. `value`引数の代わりに、`bool allowed`を受け取り、`approval`を`0`または`uint(-1)`に設定します。
    - これは、トークンの`Permit`を有効または無効にするための`bool`フラグとして機能します。
2. `deadline`引数の代わりに、`expiry`と呼ばれる引数を使用します。
    - これは、`Permit`の有効期限を示し、署名メッセージのに含まれます。

また、トークン`Stake`（Ethereumアドレス`0x0Ae055097C6d159879521C384F1D2123D1f195e6`）には、**dai**と同じABIを持つ別の`permit`の実装がありますが全く同じではありません。
この実装では、ユーザーは「**有効期限がブロックのタイムスタンプよりも大きい場合にのみ**」`transferFrom`を許可する「**有効期限付き許可**」を発行できます。

ここで提供されている`permit`関数の仕様は、**Uniswap V2**の実装と一致しています。
また、`permit`が無効な場合には`revert`（実行が中止される）する要件は、EIPが既に広く展開されていた段階で追加されました。
当時、この要件はすべての実装で一貫していました。

:::note info
`permit`関数は、トークンの許可を有効または無効にするための方法を提供します。
しかし、その具体的な実装は異なる場合があり、実際のトークン契約によって異なることに留意する必要があります。
:::

# セキュリティ考慮事項

`Permit`の署名者は通常、特定のトランザクションが送信されることを意図しています。
しかし、常にそのトランザクションをフロントラン（先に実行）しようとしているユーザーもおり、意図したトランザクションよりも先に`permit`を呼び出すことができます。
結果的に、`Permit`の署名者にとって結果は同じです。
これは、トランザクションを送信しようとする特定のアドレスが他のアドレスに対して優先権を持たないことを意味します。

`ecrecover`の事前コンパイルは、不正なメッセージが提供された場合に無音で失敗し、署名者としてゼロアドレスを返します。
そのため、`permit`がゼロアドレスに属する資金（通常は使用できない「**ゾンビ資金**」）を使用して承認を作成しないように、「`owner != address(0)`」を確認することが重要です。

署名された`Permit`メッセージは検閲可能です。
リレーする側は、`Permit`を受け取った後、提出オプションを保留することができます。
これに対処する1つの方法は、`deadline`パラメータです。
署名者は`Permit`の有効期限を近い将来の値に設定することで、リレーに対処できます。
また、署名者が**ETH**を保持している場合、彼らは`Permit`を自分で提出することもでき、これにより以前に署名された`Permit`を無効にできます。

標準の**EIP20**では、承認に関する競合状態（**SWC-114**）が存在し、`Permit`でも同様の競合状態が発生する可能性があります。

`DOMAIN_SEPARATOR`に`chainId`が含まれ、コントラクトのデプロイ時に定義されている場合、将来のチェーン分割時に異なるチェーン間でのリプレイ攻撃のリスクがあることに注意が必要です。
つまり、将来のチェーンの分岐が起きた場合、同じ`DOMAIN_SEPARATOR`が異なるチェーンで使用され、リプレイ攻撃の可能性が生じる可能性があります。

:::note info
`Permit`は設計上、いくつかのセキュリティリスクと競合条件に対処する必要があり、これらのポテンシャルリスクに対処するための注意が必要です。
:::

# 引用

Martin Lundfall (@Mrchico), "ERC-2612: Permit Extension for EIP-20 Signed Approvals," Ethereum Improvement Proposals, no. 2612, April 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2612.

# 最後に

今回は「**ERC712**の`secp256k1`署名を使用して、**ERC20**の`approve`と`transferFrom`を1つのトランザクションで実行できる提案している規格である**ERC2612**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください！

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

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

https://chaldene.net/

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58


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