# [ERC4494] ERC2612をNFTに適用してUX良くNFTを送付する仕組みを理解しよう！

> この規格は、ERC2612で提案されているPermit承認フローをERC721に適用する仕組みを提案しています。

- URL: https://cardene.dev/articles/f80c6b2f938fa1f54868
- 公開日: 2024-07-01
- 更新日: 2024-07-01
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、**ERC2612**の仕組みを**ERC721**形式のNFTに適用して、`approve` + `transferFrom`のUXを向上させる仕組みを提案している**ERC4494**についてまとめていきます！

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

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

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

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

## 概要

この規格は、**ERC2612**で提案されている**Permit**承認フローを**ERC721**に適用する仕組みを提案しています。

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

https://chaldene.net/erc721

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

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

**ERC2612**では、**Permit**という仕組みを提案していて、**ERC20**トークンをガスレス（ガスコストなし）で送ることなどができました。
通常、**ERC20**トークンを別アドレス（運営アドレスなど）に`transfer`してもらう時、`approve`と`transferFrom`を実行する必要があります。
`approve`をユーザーが実行して、運営のアドレスなどに`transfer`の権限を与え、その権限をもとに運営のアドレスから`transferFrom`を実行して**ERC20**トークンを送るという流れです。
この場合、ユーザーはガス代を支払うために少額でもETHを持っていないといけないです。
これは特にWeb3に不慣れなユーザーにとってはUXが悪く改善の必要がありました。
そこで提案されたのが、**Permit**です。
**Permit**では、ユーザーが実行する必要がある**approve**を署名だけ実行すればよく、`approve`と`transferFrom`の実行を運営のアドレスなどに託すことができるような仕組みを提案しています。
署名だけであればガス代は不要なので、ユーザーはETHを持っていなくても運営のアドレスなどにガス代を負担してもらってトークンのMintなどができるようになりました。

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

https://chaldene.net/erc20

**ERC20**トークンと**ERC721**トークンでは構造が異なるため、別のEIPが必要になります。
**ERC20**では、`approve`されるトークン量と所有者のアドレスの`nonce`によって**Permit**が作成されますが、**ERC721**ではNFTの`tokenId`と`nonce`をもとに**Permit**が作成されます。

## 動機

**ERC2612**で提案されている**Permit**の構造は、署名されたメッセージ（**ERC712**）を使用して`approve`を作成します。

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

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

通常の`approve`してトークンを`transfer`するフローでは、先ほど述べたように`approve`と`transferFrom`の2つの関数を実行するための、2つのトランザクションが必要になります。
ただ、これはUXが悪くユーザーを混乱させる可能性があります。
一方、**Permit**スタイルのフローでは、`approve`部分が署名だけで良くなり、トランザクションも1回で済むためUXが良くなります。


また、**ERC2612**では**ERC20**トークンの**Permit**アーキテクチャのみ説明されていますが、この規格では署名されたメッセージベースの`approve`フローのメリットを享受できる`approve`アーキテクチャを含む**ERC721**形式のNFTのアーキテクチャを提案しています。

## 仕様

**ERC721**に以下の3つの関数を追加する必要があります。

```js
pragma solidity 0.8.10;

import "./IERC165.sol";

///
/// @dev Interface for token permits for ERC-721
///
interface IERC4494 is IERC165 {
  /// ERC165 bytes to add to interface array - set in parent contract
  ///
  /// _INTERFACE_ID_ERC4494 = 0x5604e225

  /// @notice Function to approve by way of owner signature
  /// @param spender the address to approve
  /// @param tokenId the index of the NFT to approve the spender on
  /// @param deadline a timestamp expiry for the permit
  /// @param sig a traditional or EIP-2098 signature
  function permit(address spender, uint256 tokenId, uint256 deadline, bytes memory sig) external;
  /// @notice Returns the nonce of an NFT - useful for creating permits
  /// @param tokenId the index of the NFT to get the nonce of
  /// @return the uint256 representation of the nonce
  function nonces(uint256 tokenId) external view returns(uint256);
  /// @notice Returns the domain separator used in the encoding of the signature for permits, as defined by EIP-712
  /// @return the bytes32 domain separator
  function DOMAIN_SEPARATOR() external view returns(bytes32);
}
```

#### permit

トークン所有者が署名を使用して指定した`spender`に`tokenId`のトークンを使用許可する関数。
`deadline`は署名の有効期限を示すタイムスタンプです。
sigは所有者の署名（**secp256k1**または**EIP2098**形式）です。

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

[\[ERC2098\] Ethereumの署名をよりコンパクトにする仕組みを理解しよう！](/articles/2fb8f63af3b22eb72e33)

#### nonces

指定された`tokenId`のトークンの`nonce`（使い捨てカウンター）を返す関数。
これにより、署名の一意性が保証されて使い回しを防止できます。

#### DOMAIN_SEPARATOR

**EIP712**に従って署名のエンコーディングに使用されるドメインセパレータを返す関数。

### 署名の検証
署名の検証は以下の条件を満たす場合にのみ成功します。

- 現在のブロックタイムが`deadline`以下である。
- トークンの所有者がゼロアドレスでない。
- 指定された`tokenId`の`nonce`が現在のノンスと一致する。
- 署名`sig`が正しい形式で、所有者の`tokenId`に対応する。

### 署名の計算
署名は以下の方法で計算されます。

```js
keccak256(abi.encodePacked(
  hex"1901",
  DOMAIN_SEPARATOR,
  keccak256(abi.encode(
    keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"),
    spender,
    tokenId,
    nonce,
    deadline))
));
```

### DOMAIN_SEPARATORの定義

**DOMAIN_SEPARATOR**は**EIP712**に従って以下のように定義されます。

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

- `name`
    - コントラクトの名前。
- `version`
    - コントラクトのバージョン。
- `chainId`
    - チェーンID。
- `address(this)`
    - コントラクトアドレス。

これにより、異なるドメインからのリプレイ攻撃を防ぐためにユニークなドメインセパレータが生成されます。

### 署名メッセージ

署名メッセージは以下の**ERC712**形式の構造になります。

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

上記に加えて以下の条件があります。
特定の`tokenId`の`nonce(nonces[tokenId])`は、`tokenId`に紐付くNFTの`transfer`時にインクリメントされる必要があります。

:::message
同じ署名を作成されることを防止するためです。
:::

また、`permit`関数は署名者がゼロアドレスでないことをチェックする必要があります。

この仕組みでは、`msg.sender`（関数実行アドレス）を参照する仕組みがなく、`permit`関数の呼び出しもとは任意のアドレスにすることができます。

### EIP165
この規格には**EIP165**が必要になります。
**EIP165**を使用することで、他のNFTコントラクトがこのEIPを実装しているか簡単に検証できるようになります。
この規格で提案されているコントラクトが**EIP165**で返されるインターフェースIDは`0x5604e225`です。

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

[\[ERC165\] コントラクトが特定のインターフェースをサポートしているか確認する仕組みを理解しよう！](/articles/541184977a5cba51b759)

## 補足

`permit`関数は追加のトランザクションを必要とせずに、`safeTransferFrom`を実行できるようになります。
このフォーマットは予期しないコードへの呼び出しを避けるように設計されています。
また、リプレイ保護のために`nonces`マッピングが提供されています。

一般的な`permit`の使用ケースとして、リレーヤーが所有者に代わって**Permit**を提出するシナリオがあります。
この場合、リレーヤーは**Permit**を提出するかどうかの選択権を持つことになります。
この点が懸念される場合、所有者は`deadline`を近い将来に設定することで、**Permit**の有効期間を制限できます。
また、`deadline`引数を`uint(-1)`に設定することで有効期限のない**Permit**を作成することもできます。

**ERC712**形式のメッセージは**ERC2612**での使用を考慮しており、多くのウォレットプロバイダで広く採用されているためこのEIPでも採用されています。

**ERC2612**が`approve`される`value`に焦点を当てているのに対し、このEIPは`permit`によって`approve`されるNFTの`tokenId`に焦点を当てています。
これにより、**ERC20**や**ERC1155**トークンでは実現できない柔軟性が実現され、特定のNFTに複数の**Permit**を与えることができます。
**ERC721**トークンは非代替性という特徴を持つため、**permit**を与えても各トークンの所有者であるという`owner`の権限は変わりません。

**ERC2612**では署名が`v`, `r`, `s`のコンポーネントに分割されていますが、このEIPでは可変長のバイト配列を使用して**EIP2098**署名（`64`バイト）をサポートします。
**EIP2098**署名は`r`, `s`, `v`コンポーネント（`65`バイト）から簡単に分離・再構成することができません。

## 互換性
`permit`を使用した**ERC721**コントラクトはすでにいくつか存在しており、有名なのが**UniswapV3**です。
ただ、既存の実装とは以下の点で異なります。

- `permit`アーキテクチャが`owner`ベースである。
- `nonce`が`permit`が作成された時点でインクリメントされる。
- `permit`関数はNFT所有者によって呼び出される必要があり、`owner`として設定される。
- 署名がバイト配列ではなく`r, s, v`に分割されている

## テスト

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

https://github.com/dievardump/erc721-with-permits/tree/main/test

## 参考実装

以下に参考実装コードが格納されています。

https://github.com/dievardump/erc721-with-permits

## セキュリティ

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

`permit`と`transfer`関数を1つの関数内で使用する場合には、無効な`permit`が使用されないように注意する必要があります。
特に自動化されたNFTプラットフォームにおいて、脆弱性のある実装がユーザー資産の損失を引き起こす可能性があります。

以下は**ERC2612**からの引用であり、軽微な適応を加えていますが、同様に重要です：

`Permit`の署名者が特定の第三者にトランザクションの提出を依頼する場合でも、他の第三者が先回りしてこのトランザクションを実行して想定していた第三者の前に`permit`を呼び出すことができます。
`Permit`の署名者にとっての最終結果は同じですが、このことを認識しておくことが重要です。

`ecrecover`プリコンパイルが不正なメッセージを受け取った場合、サイレントに失敗して署名者としてゼロアドレスを返すため、`ownerOf(tokenId) != address(0)`を確認し、**Permit**が設定されていない`tokenId`に対して`permit`が`approve`を作成しないようにする。

署名された`Permit`メッセージは検閲可能です。
リレーする第三者は`Permit`を受け取った後、それを実行しないことでずっと**pending**状態にすることが可能です。
この問題の緩和策として、`deadline`パラメータが使用されます。
署名者がETHを保有している場合、自身で`Permit`を提出することも可能であり、これにより以前に署名された`Permit`を無効にすることができます。

**ERC20**の`approve`に関する競合状態は、`permit`にも適用されます。

:::message
例えば以下のような状況です。
**ERC20**トークンの所有者（Alice）が、第三者（Bob）にトークンを使用する許可を与えるために、`approve`関数を呼び出します。
このとき、Bobは特定の`value`（例：`100`トークン）を使用する許可を得ます。
Bobは`transferFrom`関数を呼び出して、Aliceのアカウントから自分のアカウントにトークンを`transfer`します。

AliceがBobに`100`トークンの使用許可を与えた後、Aliceが許可を取り消して、別の`value`（例：`50`トークン）を再度許可するために`approve`関数を呼び出す場合、競合状態が発生します。
Bobがまだ最初の`100`トークンの許可を使用していない間に、Aliceが許可を変更すると以下のような問題が発生します。
 - Bobは`100`トークンの許可を使い切る前に、`50`トークンの新しい許可が設定される。
 - Bobが`100`トークンの`transferFrom`を呼び出そうとすると、`50`トークンに変更された新しい許可と競合し、予期せぬ挙動になります。

#### 回避策

この問題を回避するには、`approve`関数を使う前に既存の`approve`をゼロにリセットする方法があります。

1. まず、既存の`approce`をゼロにリセットします。

```js
approve(spender, 0);
```

2. その後、新しい`approve`を設定します。

```solidity
approve(spender, newAmount);
```
:::

`DOMAIN_SEPARATOR`が`chainId`を含んでコントラクトのデプロイ時に定義され、各署名のたびに再構築されない場合、将来のチェーン分岐時にチェーン間でリプレイ攻撃のリスクがあります。

## 引用

Simon Fremaux (@dievardump), William Schwab (@wschwab), "ERC-4494: Permit for ERC-721 NFTs [DRAFT]," Ethereum Improvement Proposals, no. 4494, November 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4494.

## 最後に

今回は「**ERC2612**の仕組みを**ERC721**形式のNFTに適用して、`approve` + `transferFrom`のUXを向上させる仕組みを提案している**ERC4494**」についてまとめてきました！
いかがだったでしょうか？

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