# [ERC6120] ERC20やERC721などのトークンをapproveなしにtransferする仕組みを理解しよう！

> イーサリアムのトランザクションでは、本来「transferしてコールする」（transfer-and-call）というのが基本的な動作です。

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

## はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、ルーターコントラクトにより、トークンを`approve`してからコールするのではなく、`transfer`してからコールする**Universal Token Router（UTR）**の仕組みを提案している**ERC6120**についてまとめていきます！

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

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

:::message alert
**6120**は現在（2024年1月17日）では「**Review**」段階です。
:::

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

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

## 概要

イーサリアムのトランザクションでは、本来「**`transfer`してコールする**」（`transfer-and-call`）というのが基本的な動作です。
しかし、**ERC20** やその他のトークン規格はこの方式を前提としていないため、すでに存在するトークンコントラクトに新しい規格を適用するのは難しいです。

その結果、アプリケーションやルータコントラクトでは、「**`approve`してからコールする**」（`approve-then-call`）というパターンを使う必要があります。
この方法では、各コントラクト、トークン、アカウントに追加の承認（`approve` または `permit`）が必要となり、ユーザー体験の低下、手数料やネットワークストレージの消費、セキュリティリスクの増大などの問題があります。
承認されるコントラクトが未監査や未検証、アップグレード可能なプロキシコントラクトの場合、リスクはさらに高まります。
また、この`approve-then-call` パターンはエラーが発生しやすく、許可関連のバグや脆弱性が多く見つかっています。

**Universal Token Router（UTR）** は、これらの問題を解決するために設計されました。
**UTR**はトークンの許可をアプリケーションロジックから分離し、ETHと同じ方法で他のアプリケーションコントラクトの承認なしに、任意のトークンをコントラクトコールで使用できるようにします。

**UTR**に承認されたトークンは、所有者が直接署名したトランザクションでのみ使用できます。
トークンの移動は明確で、トークンのタイプ（**ETH**、**ERC20**、**ERC721**、**ERC1155**）、`amountIn`、`amountOutMin`、`recipient`などが明確に表示されます。

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

https://chaldene.net/erc20

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

https://chaldene.net/erc721

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

https://chaldene.net/erc1155

**Universal Token Router** コントラクトは、**EIP1014** の **SingletonFactory** コントラクトを使用して、アドレス `0x8Bd6072372189A12A2889a56b6ec982fD02b0B87` で全てのEVM互換ネットワークにデプロイされています。
これにより、新しいトークンコントラクトは、インタラクティブな使用時に承認トランザクションを必要とせず、**UTR**を信頼できるアドレスとして事前に設定できます。
これは、ユーザー体験の向上とセキュリティの強化に大きく貢献します。

:::message
#### `transfer-and-call`と`approve-then-call`

**Transfer-and-Call**
まず、Ethereumのスマートコントラクトは一般的にEther（**ETH**）やトークンといったデジタルアセットを管理するために使用されます。
`Transfer-and-Call`は、スマートコントラクトが他のアドレスに一定数量のトークンを送信し、そのトークンの送信後に他のスマートコントラクトの特定の関数を呼び出すプロセスを指します。
通常、この手順は次のようになります。
     1. トークンを送信するスマートコントラクトが、所定の数量のトークンを別のアドレスに送信（`transfer`）します。
     2. トークンを受け取ったアドレスには、受け取ったトークンに対して何らかの操作を行うスマートコントラクトが存在します。
     3. トークンを受け取ったアドレスのスマートコントラクトは、そのトークンを受け取ると特定の関数を実行します。

**Approve-then-Call**
`Approve-then-Call`は、トークンの所有者（通常はスマートコントラクト）が、他のスマートコントラクトに対してトークンの操作を許可し、その後に特定の関数を呼び出すプロセスです。
この手順は通常、以下のように進行します。
     1. トークンの所有者（スマートコントラクト）は、特定のアドレスに対してトークン操作を許可する（`approve`）。
     2. トークンを操作したい別のスマートコントラクトが、許可を受けて指定されたトークン操作を実行します。
     3. 許可を受けたスマートコントラクトは、指定されたトークン操作を実行し、その後に特定の関数を呼び出すことができます。

要するに、`Transfer-and-Call`はトークンを直接送信してから別のスマートコントラクトを呼び出す方法であり、`Approve-then-Call`はトークンの所有者から許可を受けてから操作を行い、その後に関数を呼び出す方法です。
どちらの方法も、トークンの操作を安全に他のスマートコントラクトと組み合わせるための方法です。
:::

## 動機

**UTR**は、ユーザーがトークンをコントラクトに承認する時のセキュリティを強化するために設計されています。
ユーザーは、以下の２つの重要なセキュリティ条件が満たされていることを信頼します。
1. トークンは、ユーザーの許可（`msg.sender` や `ecrecover` 経由）を得てのみ使用される。
2. `delegatecall`（例えば、アップグレード可能なプロキシなど）は使用されない。

これらのセキュリティ条件を守ることで、**UTR**は様々なインタラクティブアプリケーション間で共有され、既存のトークンのほとんどの承認トランザクションや、新しいトークンのすべての承認トランザクションを実行する必要がなくなります。

以前のEIPでは、ユーザーは承認されたトークンを使ってトランザクションに署名する時、フロントエンドがトランザクションを正確に送信することを完全に信頼していました。
しかし、これはフィッシングサイトによる大きなリスクを伴います。

**UTR**の関数引数は、トランザクションに署名する時のユーザーのマニフェストとして機能し、ウォレットのサポートを受けて、ユーザーは期待されるトークンの挙動を確認してレビューできます。
これにより、フィッシングサイトをより簡単に検出し、回避することが可能になります。

多くのアプリケーションコントラクトは既にUTRと互換性があり、以下のような利点を受け取れます。
- 他のアプリケーションとユーザートークンの許可を安全に共有する。
- 周辺コントラクトを頻繁に更新することが可能。
- ルータコントラクトの開発とセキュリティ監査に関するコストを削減。

さらに、UTRは「**プロセスによるセキュリティ**」ではなく「**結果によるセキュリティ**」を推進し、トークン残高の変更を直接クエリして出力を検証することで、誤ったコントラクトや悪意のあるコントラクトとの相互作用でも、ユーザートランザクションの安全性を確保します。
トークン以外の結果に関しては、アプリケーションヘルパーコントラクトが**UTR**の出力検証のために追加の結果チェック機能を提供することができます。

## 仕様

UTRコントラクトのメインインターフェース。

```javascript
interface IUniversalTokenRouter {
    function exec(
        Output[] memory outputs,
        Action[] memory actions
    ) payable;
}
```

### 出力検証

```javascript
struct Output {
    address recipient;
    uint eip;           // token standard: 0 for ETH or EIP number
    address token;      // token contract address
    uint id;            // token id for ERC-721 and ERC-1155
    uint amountOutMin;
}
```

**UTR**では、トークンバランスの変化を検証するために「`Output`」という構造体が定義されています。
この構造体には以下のフィールドが含まれます。
- `recipient`
    - 受取人のアドレス。
- `eip`
    - トークン標準（`0`は**ETH**、それ以外はEIP番号）。
- `token`
    - トークンコントラクトのアドレス。
- `id`
    - **ERC721** や **ERC1155** のトークンID。
- `amountOutMin`
    - 最小出力量。

`exec` 関数の実行中、出力ごとに受取アドレスのトークンバランスが記録され、開始時と終了時のバランスが比較されます。
もしバランスの変化が `amountOutMin` より少なければ、トランザクションは `INSUFFICIENT_OUTPUT_AMOUNT` というエラーで`revert`します。

特別なID `ERC_721_BALANCE` は **ERC721** 用に予約されており、出力アクションで使用して、受取アドレスが所有するすべてのIDの合計数を検証することができます。
このIDは、`keccak256('UniversalTokenRouter.ERC_721_BALANCE')` で計算されます。

```javascript
ERC_721_BALANCE = keccak256('UniversalTokenRouter.ERC_721_BALANCE')
```
### Action

```javascript
struct Action {
    Input[] inputs;
    address code;       // contract code address
    bytes data;         // contract input data
}
```

「`Action`」構造体は、トークンの入力とコントラクトの呼び出しを定義します。
この構造体には以下のフィールドが含まれています。

- `inputs`
    - 入力されるトークンの配列。
- `code`
    - コントラクトコードのアドレス。
- `data`
    - コントラクトの入力データ。

**UTR**によって呼び出されるためには、アクションコードコントラクトは **ERC165** インターフェースをID `0x61206120` で実装していなければなりません。
このインターフェースチェックにより、**UTR**によるトークンの`allowance-spending`関数（例えば、`transferFrom`）の直接的な呼び出しが防がれます。
したがって、新しいトークンコントラクトは、このインターフェースIDを実装してはいけません。

`NotToken` という抽象コントラクトは **ERC165** を実装しており、`supportsInterface` 関数を通じて特定のインターフェースIDのサポートを宣言します。
この関数は、ID `0x61206120` または他のサポートされているインターフェースIDが入力された場合に `true` を返します。

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

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

例として、`Application` というコントラクトは `NotToken` を継承しており、UTRとの互換性を持つコントラクトとして利用することができます。

```javascript
abstract contract NotToken is ERC165 {
    // IERC165-supportsInterface
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return
            interfaceId == 0x61206120 ||
            super.supportsInterface(interfaceId);
    }
}

contract Application is NotToken {
    // this contract can be used with the UTR
}
```

### Input

```javascript
struct Input {
    uint mode;
    address recipient;
    uint eip;           // token standard: 0 for ETH or EIP number
    address token;      // token contract address
    uint id;            // token id for ERC-721 and ERC-1155
    uint amountIn;
}
```

「`Input`」構造体は、アクションコントラクトの実行前に準備または`transfer`する入力トークンを定義します。

「`Input`」構造体には次のフィールドが含まれています。
- `mode`
    - 処理モード。
- `recipient`
    - 受取アドレス。
- `eip`
    - トークン標準（`0`は**ETH**、それ以外はEIP番号）。
- `token`
    - トークンコントラクトのアドレス。
- `id`
    - **ERC721** または **ERC1155** のトークンID。
- `amountIn`
    - 入力量。

`mode` は以下の値のいずれかを取ります。
- `PAYMENT = 0`
    - トークンを `msg.sender` から受取アドレスに`transfer`するための支払いを保留する。
    - 同一トランザクション内のどこからでもUTRの `pay` を呼び出すことで実行されます。
- `TRANSFER = 1`
    - トークンを直接 `msg.sender` から受取アドレスに`transfer`する。
- `CALL_VALUE = 2`
    - アクションに渡す**ETH**量を記録する。

`inputs` 引数内の各入力は順番に処理されます。
簡単化のため、重複する `PAYMENT` や `CALL_VALUE` 入力は有効ですが、最後の `amountIn` 値のみが使用されます。

#### Payment Input

**Universal Token Router（UTR）** における「`PAYMENT`」という入力モードについて説明です。
これは、特に「`transfer-in-callback`」パターンを使用するアプリケーションコントラクト（例：フラッシュローンコントラクト、**Uniswap/v3-core**、**Derivable**など）に推奨されるモードです。

「`PAYMENT`」モードを使用する各入力について、`msg.sender` から受取アドレスにトークンを`transfer`するために、同一トランザクション内のどこからでもUTRの `pay` を呼び出すことができます。
ただし、`transfer`されるトークンの量は `amountIn` までとなります。

**UTR**の処理フローは次のようになります。
1. UTRは `PAYMENT` モードの支払いをUTRの `pay` で保留します。
2. アクションコードがアプリケーションコントラクトを呼び出します。
3. アプリケーションコントラクトから**UTR**の `pay` が呼び出されます。
4. **UTR**は保留されていたすべての支払いをクリアします。
5. トランザクションは終了します。

```
UTR
 |
 | PAYMENT
 | (payments pended for UTR.pay)
 |
 |                                  Application Contracts
action.code.call ---------------------> |
                                        |
UTR.pay <----------------------- (call) |
                                        |
 | <-------------------------- (return) |
 |
 | (clear all pending payments)
 |
END
```

トークンの許可（`allowance`）と `PAYMENT` は本質的に異なります。
- 許可（`allowance`）
    - 特定のアドレスがいつでも、任意の人にトークンを`transfer`することを許可します。
- `PAYMENT`
    - トランザクション内でのみ、誰でも特定の受取アドレスにトークンを`transfer`することを許可します。

##### 支払いの使用（Spend Payment）

```javascript
interface IUniversalTokenRouter {
    function pay(bytes memory payment, uint amount);
}
```

`IUniversalTokenRouter` インターフェースには `pay` という関数があります。
この関数を呼び出すためには、`payment` パラメータを次のようにエンコードする必要があります。

```plaintext
payment = abi.encode(
    payer,      // address
    recipient,  // address
    eip,        // uint256
    token,      // address
    id          // uint256
);
```

`payment` バイトは、カスタム支払いロジックを実行するためのコンテキストやペイロードを渡すために、アダプターUTRコントラクトによっても使用されることがあります。

##### 支払いの破棄（Discard Payment）

場合によっては、`transfer`を実行するのではなく支払いを破棄することが有用です。
例えば、アプリケーションコントラクトが `payment.payer` から自身のトークンを`burn`したい場合などです。
以下の関数を使用すると、呼び出し元のアドレスに対する支払いを検証し、その一部を破棄することができます。

```javascript
interface IUniversalTokenRouter {
    function discard(bytes memory payment, uint amount);
}
```

##### 支払いの有効期間（Payment Lifetime）

支払いは**UTR**ストレージに記録され、そのトランザクション内のみで `input.action` 外部呼び出しによって使用されることを意図しています。
すべての支払いストレージは、**UTR**の `exec` が終了する前にクリアされます。

### ネイティブトークン Transfer

**UTR**は、ユーザーが実行ロジックで**ETH**を`transfer`する必要がある場合に備えて、`receive()` 関数を持つべきです。
ルーターに`transfer`された `msg.value` は、異なるアクションにまたがる複数の入力で使用することができます。
ETHのルーター内での移動については、呼び出し元が全責任を負いますが、`exec` 関数は、関数が終了する前に残ったETHを返金するべきです。

### 使用例

#### Uniswap V2 Router

##### 従来の関数（Legacy Function）

`UniswapV2Router01` の `swapExactTokensForTokens` 関数は以下のように定義されています。

```js
UniswapV2Router01.swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
)
```

##### 改良されたバージョン

`UniswapV2Helper01.swapExactTokensForTokens` は、トークン`transfer`部分を除いた上記関数の改良版です。


ユーザーが従来の関数の代わりにスワップを実行するために署名するトランザクションは以下のようになります。

```js
UniversalTokenRouter.exec([{
    recipient: to,
    eip: 20,
    token: path[path.length-1],
    id: 0,
    amountOutMin,
}], [{
    inputs: [{
        mode: TRANSFER,
        recipient: UniswapV2Library.pairFor(factory, path[0], path[1]),
        eip: 20,
        token: path[0],
        id: 0,
        amountIn: amountIn,
    }],
    code: UniswapV2Helper01.address,
    data: encodeFunctionData("swapExactTokensForTokens", [
        amountIn,
        amountOutMin,
        path,
        to,
        deadline,
    ]),
}])
```

このトランザクションでは、**UTR**を使用してトークンの`transfer`を行い、`UniswapV2Helper01` コントラクトによって実際のスワップロジックが実行されます。
これにより、従来の方法と比べてより効率的かつ安全にトークンスワップを行うことができます。

#### Uniswap V3 Router

##### 従来のルータコントラクト（Legacy Router Contract）

```js
contract SwapRouter {
    // this function is called by pool to pay the input tokens
    function pay(
        address token,
        address payer,
        address recipient,
        uint256 value
    ) internal {
        ...
        // pull payment
        TransferHelper.safeTransferFrom(token, payer, recipient, value);
    }
}
```

`SwapRouter` というコントラクトには、プールが入力トークンを支払うために呼び出す `pay` 関数が含まれています。
この関数内で、`TransferHelper.safeTransferFrom` を使用してトークンの支払いを行います。

##### UTRを使用するヘルパーコントラクト

```js
contract SwapHelper {
    // this function is called by pool to pay the input tokens
    function pay(
        address token,
        address payer,
        address recipient,
        uint256 value
    ) internal {
        ...
        // pull payment
        bytes memory payment = abi.encode(payer, recipient, 20, token, 0);
        UTR.pay(payment, value);
    }
}
```

`SwapHelper` というコントラクトにも同様に `pay` 関数がありますが、こちらではUTRの `pay` 関数を使用してトークンの支払いを行います。
この関数は、`abi.encode` を使用してトークンの支払い情報をエンコードし、UTRに渡します。

##### UTRを使用したトランザクションの実行

ユーザーが `exactInput` 機能を `PAYMENT` モードで実行するために署名するトランザクションは以下のようになります。

```js
UniversalTokenRouter.exec([{
    eip: 20,
    token: tokenOut,
    id: 0,
    amountOutMin: 1,
    recipient: to,
}], [{
    inputs: [{
        mode: PAYMENT,
        eip: 20,
        token: tokenIn,
        id: 0,
        amountIn: amountIn,
        recipient: pool.address,
    }],
    code: SwapHelper.address,
    data: encodeFunctionData("exactInput", [...]),
}])
```

このトランザクションでは、**UTR**を使用してトークンの支払いを行い、`SwapHelper` コントラクトによって実際のスワップロジックが実行されます。
これにより、従来の方法と比べてより効率的かつ安全にトークンスワップを行うことができます。

#### Allowance Adapter

##### Allowance Adapter コントラクト

```js
contract AllowanceAdapter is ReentrancyGuard {
    struct Input {
        address token;
        uint amountIn;
    }

    function approveAndCall(
        Input[] memory inputs,
        address spender,
        bytes memory data,
        address leftOverRecipient
    ) external payable nonReentrant {
        for (uint i = 0; i < inputs.length; ++i) {
            Input memory input = inputs[i];
            IERC20(input.token).approve(spender, input.amountIn);
        }

        (bool success, bytes memory result) = spender.call{value: msg.value}(data);
        if (!success) {
            assembly {
                revert(add(result, 32), mload(result))
            }
        }

        for (uint i = 0; i < inputs.length; ++i) {
            Input memory input = inputs[i];
            // clear all allowance
            IERC20(input.token).approve(spender, 0);
            uint leftOver = IERC20(input.token).balanceOf(address(this));
            if (leftOver > 0) {
                TransferHelper.safeTransfer(input.token, leftOverRecipient, leftOver);
            }
        }
    }
}
```

`AllowanceAdapter` コントラクトは、許可（`allowance`）を使用するアプリケーションやルータコントラクトのための、再入可能性（reentrancy）を考慮した **ERC20** アダプタです。
このコントラクトには `approveAndCall` 関数が含まれており、以下のような構造です。
- 入力として複数のトークンとその量を受け取ります。
- 各トークンに対して、指定された支出者（`spender`）に対する承認（`approve`）を行います。
- その後、支出者のコントラクトにデータと**ETH**（`msg.value`）を送信します。
- トランザクションが成功しない場合は、エラーを`revert`します。
- 最後に、すべての許可をクリアし、残ったトークンを指定された受取人（`leftOverRecipient`）に転送します。

##### UTRを使用したトランザクションの構築

```js
const { data: routerData } = await uniswapRouter.populateTransaction.swapExactTokensForTokens(
    amountIn,
    amountOutMin,
    path,
    to,
    deadline,
)

const { data: adapterData } = await adapter.populateTransaction.approveAndCall(
    [{
        token: path[0],
        amountIn,
    }],
    uniswapRouter.address,
    routerData,
    leftOverRecipient,
)

await utr.exec([], [{
    inputs: [{
        mode: TRANSFER,
        recipient: adapter.address,
        eip: 20,
        token: path[0],
        id: 0,
        amountIn,
    }],
    code: adapter.address,
    data: adapterData,
}])
```

**UTR**を使用して**Uniswap V2 Router**とやりとりするために、以下のようなトランザクションが構築されます。
1. `uniswapRouter` から `swapExactTokensForTokens` 関数のトランザクションデータを取得します。
2. `AllowanceAdapter` から `approveAndCall` 関数のトランザクションデータを取得します。
3. UTRの `exec` 関数を呼び出して、上記のデータを使用してトランザクションを実行します。

このプロセスにより、**UTR**を介して**Uniswap V2 Router**とのトランザクションを実行する時に、トークンを直接承認することなく、セキュアで効率的な取引を可能にします。

## 補足

このテキストは、Universal Token Router（UTR）における「**Permitタイプの署名**」のサポートに関する方針について説明しています。

**Universal Token Router**は、新しいトークンに対するすべてのインタラクティブな承認（`approve`）署名を排除し、古いトークンのほとんどについても同様に排除することを目的としています。
このため、`Permit`タイプの署名はサポートされていません。

`Permit`タイプの署名は、**ERC20** トークンなどで使用される、ユーザーがトークンの承認を署名によって行う方法です。
しかし、**UTR**はこのようなインタラクティブな承認プロセスを不要にすることで、より効率的かつ安全なトークン取引を可能にすることを目指しています。
そのため、`Permit`タイプの署名は**UTR**ではサポートされていないのです。

## 互換性

### トークン

```js
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/**
 * @dev Implementation of the {ERC20} token standard that support a trusted ERC6120 contract as an unlimited spender.
 */
contract ERC20WithUTR is ERC20 {
    address immutable UTR;

    /**
     * @dev Sets the values for {name}, {symbol} and ERC6120's {utr} address.
     *
     * All three of these values are immutable: they can only be set once during
     * construction.
     *
     * @param utr can be zero to disable trusted ERC6120 support.
     */
    constructor(string memory name, string memory symbol, address utr) ERC20(name, symbol) {
        UTR = utr;
    }

    /**
     * @dev See {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        if (spender == UTR && spender != address(0)) {
            return type(uint256).max;
        }
        return super.allowance(owner, spender);
    }

    /**
     * Does not check or update the allowance if `spender` is the UTR.
     */
    function _spendAllowance(address owner, address spender, uint256 amount) internal virtual override {
        if (spender == UTR && spender != address(0)) {
            return;
        }
        super._spendAllowance(owner, spender, amount);
    }
}
```

#### 古いトークンコントラクト（**ERC20**、**ERC721**、**ERC1155**）
これらのコントラクトは、各アカウントごとに一度、**UTR**に対する承認が必要です。
これは、**UTR**がトークンを移動させるために必要な承認を得るための手順です。

#### 新しいトークンコントラクト
新しいトークンコントラクトは、**UTR**を信頼できるアドレス（`spender`）として事前に設定することができます。
これにより、インタラクティブな使用のための承認トランザクションは必要ありません。

`ERC20WithUTR` は、**UTR**を無制限の支出者としてサポートする **ERC20** トークン標準の実装です。
このコントラクトは、以下の特徴を持っています。
- `UTR` は不変（`immutable`）で、コンストラクタで一度設定されます。
- `allowance` 関数では、`spender` が**UTR**である場合、無限の許可を返します。
- `_spendAllowance` 関数では、`spender` が**UTR**であれば、承認をチェックまたは更新しません。

このアプローチにより、新しいトークンコントラクトは**UTR**を使用して効率的かつ安全にトークンを移動させることができます。
これにより、トークンの承認プロセスが簡略化され、ユーザーエクスペリエンスが向上します。

### アプリケーション

#### UTRと非互換なアプリケーションコントラクト
**UTR**と互換性がない唯一のアプリケーションコントラクトは、内部ストレージで `msg.sender` を受取アドレスとして使用し、所有権の移転機能がないものです。
これは、**UTR**を使用する時に、受取アドレスが `msg.sender` とは異なる可能性があるためです。

#### UTRと互換性のあるアプリケーションコントラクト
受取アドレス（または `to`）引数を受け入れるすべてのアプリケーションコントラクトは、そのまま**UTR**と互換性があります。

#### トークン`transfer`に関するアダプターの必要性
**ERC20**、**ERC721**、**ERC1155** トークンを `msg.sender` に`transfer`するアプリケーションコントラクトは、その機能に受取アドレスを追加するための追加のアダプターが必要です。

##### アダプターコントラクトの例
`WethAdapter` というコントラクトは、**WETH**トークンに対するサンプルアダプターです。
このコントラクトは `deposit` 関数を持っており、`IWETH(WETH).deposit()` を呼び出した後に `TransferHelper.safeTransfer` を使用してトークンを指定された受取アドレスに`transfer`します。

```js
// sample adapter contract for WETH
contract WethAdapter {
    function deposit(address recipient) external payable {
        IWETH(WETH).deposit(){value: msg.value};
        TransferHelper.safeTransfer(WETH, recipient, msg.value);
    }
}
```

##### ヘルパーとアダプターコントラクトの追加
追加のヘルパーやアダプターコントラクトが必要な場合もあります。
これらのコントラクトはトークンや許可（allowance）を保持せず、頻繁に更新される可能性があり、核となるアプリケーションコントラクトのセキュリティへの影響はほとんどもしくは全くありません。

## 実装

```js
/// @title The implemetation of the EIP-6120.
/// @author Derivable Labs
contract UniversalTokenRouter is ERC165, IUniversalTokenRouter {
    uint256 constant PAYMENT       = 0;
    uint256 constant TRANSFER      = 1;
    uint256 constant CALL_VALUE    = 2;

    uint256 constant EIP_ETH       = 0;

    uint256 constant ERC_721_BALANCE = uint256(keccak256('UniversalTokenRouter.ERC_721_BALANCE'));

    /// @dev transient pending payments
    mapping(bytes32 => uint256) t_payments;

    /// @dev accepting ETH for user execution (e.g. WETH.withdraw)
    receive() external payable {}

    /// The main entry point of the router
    /// @param outputs token behaviour for output verification
    /// @param actions router actions and inputs for execution
    function exec(
        Output[] memory outputs,
        Action[] memory actions
    ) external payable virtual override {
    unchecked {
        // track the expected balances before any action is executed
        for (uint256 i = 0; i < outputs.length; ++i) {
            Output memory output = outputs[i];
            uint256 balance = _balanceOf(output);
            uint256 expected = output.amountOutMin + balance;
            require(expected >= balance, 'UTR: OUTPUT_BALANCE_OVERFLOW');
            output.amountOutMin = expected;
        }

        address sender = msg.sender;

        for (uint256 i = 0; i < actions.length; ++i) {
            Action memory action = actions[i];
            uint256 value;
            for (uint256 j = 0; j < action.inputs.length; ++j) {
                Input memory input = action.inputs[j];
                uint256 mode = input.mode;
                if (mode == CALL_VALUE) {
                    // eip and id are ignored
                    value = input.amountIn;
                } else {
                    if (mode == PAYMENT) {
                        bytes32 key = keccak256(abi.encode(sender, input.recipient, input.eip, input.token, input.id));
                        t_payments[key] = input.amountIn;
                    } else if (mode == TRANSFER) {
                        _transferToken(sender, input.recipient, input.eip, input.token, input.id, input.amountIn);
                    } else {
                        revert('UTR: INVALID_MODE');
                    }
                }
            }
            if (action.code != address(0) || action.data.length > 0 || value > 0) {
                require(
                    ERC165Checker.supportsInterface(action.code, 0x61206120),
                    "UTR: NOT_CALLABLE"
                );
                (bool success, bytes memory result) = action.code.call{value: value}(action.data);
                if (!success) {
                    assembly {
                        revert(add(result,32),mload(result))
                    }
                }
            }
            // clear all transient storages
            for (uint256 j = 0; j < action.inputs.length; ++j) {
                Input memory input = action.inputs[j];
                if (input.mode == PAYMENT) {
                    // transient storages
                    bytes32 key = keccak256(abi.encodePacked(
                        sender, input.recipient, input.eip, input.token, input.id
                    ));
                    delete t_payments[key];
                }
            }
        }

        // refund any left-over ETH
        uint256 leftOver = address(this).balance;
        if (leftOver > 0) {
            TransferHelper.safeTransferETH(sender, leftOver);
        }

        // verify balance changes
        for (uint256 i = 0; i < outputs.length; ++i) {
            Output memory output = outputs[i];
            uint256 balance = _balanceOf(output);
            // NOTE: output.amountOutMin is reused as `expected`
            require(balance >= output.amountOutMin, 'UTR: INSUFFICIENT_OUTPUT_AMOUNT');
        }
    } }
    
    /// Spend the pending payment. Intended to be called from the input.action.
    /// @param payment encoded payment data
    /// @param amount token amount to pay with payment
    function pay(bytes memory payment, uint256 amount) external virtual override {
        discard(payment, amount);
        (
            address sender,
            address recipient,
            uint256 eip,
            address token,
            uint256 id
        ) = abi.decode(payment, (address, address, uint256, address, uint256));
        _transferToken(sender, recipient, eip, token, id, amount);
    }

    /// Discard a part of a pending payment. Can be called from the input.action
    /// to verify the payment without transfering any token.
    /// @param payment encoded payment data
    /// @param amount token amount to pay with payment
    function discard(bytes memory payment, uint256 amount) public virtual override {
        bytes32 key = keccak256(payment);
        require(t_payments[key] >= amount, 'UTR: INSUFFICIENT_PAYMENT');
        unchecked {
            t_payments[key] -= amount;
        }
    }

    // IERC165-supportsInterface
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return
            interfaceId == type(IUniversalTokenRouter).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    function _transferToken(
        address sender,
        address recipient,
        uint256 eip,
        address token,
        uint256 id,
        uint256 amount
    ) internal virtual {
        if (eip == 20) {
            TransferHelper.safeTransferFrom(token, sender, recipient, amount);
        } else if (eip == 1155) {
            IERC1155(token).safeTransferFrom(sender, recipient, id, amount, "");
        } else if (eip == 721) {
            IERC721(token).safeTransferFrom(sender, recipient, id);
        } else {
            revert("UTR: INVALID_EIP");
        }
    }

    function _balanceOf(
        Output memory output
    ) internal view virtual returns (uint256 balance) {
        uint256 eip = output.eip;
        if (eip == 20) {
            return IERC20(output.token).balanceOf(output.recipient);
        }
        if (eip == 1155) {
            return IERC1155(output.token).balanceOf(output.recipient, output.id);
        }
        if (eip == 721) {
            if (output.id == ERC_721_BALANCE) {
                return IERC721(output.token).balanceOf(output.recipient);
            }
            try IERC721(output.token).ownerOf(output.id) returns (address currentOwner) {
                return currentOwner == output.recipient ? 1 : 0;
            } catch {
                return 0;
            }
        }
        if (eip == EIP_ETH) {
            return output.recipient.balance;
        }
        revert("UTR: INVALID_EIP");
    }
}
```

## セキュリティ

### ERC-165 Tokens

トークンコントラクトは、ID `0x61206120` の **ERC165** インターフェースを決してサポートしてはいけません。
このIDは、**UTR**を使用して呼び出される非トークンコントラクト用に予約されています。
**UTR**に承認され、インターフェースID `0x61206120` を持つ任意のトークンは、制限なしに誰でも使用することができます。

つまり、トークンコントラクトがこの特定のインターフェースIDをサポートしていると、**UTR**を通じてそのトークンが無制限に移動されるリスクがあります。
これはセキュリティ上の大きな問題を引き起こす可能性があるため、トークンコントラクトはこのインターフェースIDをサポートすべきではありません。
**UTR**との互換性を確保するためには、このIDのサポートを避ける必要があります。

### Reentrancy

#### UTRコントラクトへのトークン`transfer`
**UTR**コントラクトに`transfer`されたトークンは永久に失われます。
これは、一度**UTR**コントラクトに`transfer`されたトークンを外部に`transfer`する方法がないためです。
トークンを一時的に保持する必要があるアプリケーションは、安全な実行のために再入可能性ガード（`reentrancy guard`）を備えた独自のヘルパーコントラクトを使用するべきです。

#### UTRコントラクトへのETH`transfer`
アクションコールで使われる前に、**ETH**は**UTR**コントラクトに`transfer`される必要があります（`CALL_VALUE`モードを使用して）。
この**ETH**の値は、アクションコード内の再入可能なコールや不正なトークン関数を使用して**UTR**から抜き取られる可能性があります。
ただし、ユーザーがそのトランザクションで使うよりも多くの**ETH**を`transfer`しない場合、このような抜き取りは不可能になります。

#### 例
```js
// transfer 100 in, but spend only 60,
// so at most 40 wei can be exploited in this transaction
UniversalTokenRouter.exec([
    ...
], [{
    inputs: [{
        mode: CALL_VALUE,
        eip: 20,
        token: 0,
        id: 0,
        amountIn: 60,   // spend 60
        recipient: AddressZero,
    }],
    ...
}], {
    value: 100,   // transfer 100 in
})
```

この例では、**UTR**コントラクトに`100 wei`を`transfer`し、そのうち`60 wei`のみをアクションコールで使用します。
これにより、最大`40 wei`が抜き取られる可能性があることを示しています。
トランザクションで使う量以上の**ETH**を`transfer`しないことで、このようなリスクを避けることができます。

### 支払破棄

#### `pay` 関数の実行結果の確認
**UTR**コントラクトの `pay` 関数の結果は、コール後のバランスを照会することで確認することができます。
これにより、**UTR**コントラクトを信頼できない状況でも安全に呼び出すことが可能です。

#### `discard` 関数の使用に関する注意
`discard` 関数の実行を検証する方法がないため、この関数は信頼できる**UTR**コントラクトとのみ使用するべきです。
`discard` 関数は、特定の支払いを破棄するために使われますが、その実行が適切に行われたかを外部から検証することはできません。
したがって、この関数は慎重に、信頼できる状況でのみ使用する必要があります。

## 引用

Derivable (@derivable-labs), Zergity (@Zergity), Ngo Quang Anh (@anhnq82), BerlinP (@BerlinP), Khanh Pham (@blackskin18), Hal Blackburn (@h4l), "ERC-6120: Universal Token Router [DRAFT]," Ethereum Improvement Proposals, no. 6120, December 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6120.

## 最後に

今回は「ルーターコントラクトにより、トークンを`approve`してからコールするのではなく、`transfer`してからコールする**Universal Token Router（UTR）**の仕組みを提案している**ERC6120**」についてまとめてきました！
いかがだったでしょうか？

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