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

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、スマートコントラクトがどのインターフェースを実装しているか示し、確認できる標準的な実装を提案している**ERC165**についてまとめていきます！

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

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

# 概要
以下のことを標準化します。

- インターフェースの識別方法。
- コントラクトが実装しているインターフェースを公開する方法。
- コントラクトが**ERC165**を実装しているか確認する方法。
- コントラクトが特定のインターフェースを実装しているか確認する方法。

# 動機
**ERC20**や**ERC721**などの「**標準的なインターフェース**」では、コントラクトがそのインターフェースをサポートしているかどうか、サポートしている場合にどのバージョンのインターフェースをサポートしているかを確認できることは役に立ちます。
特に**ERC20**の場合、既にバージョン識別子が提案されています。
この提案はインターフェースの概念を標準化し、インターフェースの識別（命名）を標準化します。

:::note info
識別子とは、他と区別するための名前や番号、記号のことです。
:::

# 仕様

## インターフェースの識別方法
この標準では、インターフェースはEthereum ABIによって定義される関数セレクターを使用します。

:::note info
関数セレクターとは、関数の名前と引数の型を文字列にし、keccak256ハッシュ関数で生成されたハッシュ値の先頭4byteのことです。
:::

これはSolidityのインターフェースの概念と異なり、返り値の型、変更可能性、およびイベントなどは定義されません。

インターフェース識別子は、インターフェース内のすべての関数セレクターのXOR（排他的論理和）として定義されます。
以下のコード例は、インターフェース識別子を計算する方法を示しています。

```javascript
pragma solidity ^0.4.20;

interface Solidity101 {
    function hello() external pure;
    function world(int) external pure;
}

contract Selector {
    function calculateSelector() public pure returns (bytes4) {
        Solidity101 i;
        return i.hello.selector ^ i.world.selector;
    }
}
```

:::note warn
インターフェースはオプション機能を許可していないため、インターフェースIDにはオプション機能は含まれない。
:::

## コントラクトが実装するインターフェースの公開方法
**ERC165**に準拠するコントラクトは、以下のインターフェース（**ERC165.sol**）を実装しなければならない。

```javascript
pragma solidity ^0.4.20;

interface ERC165 {
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
```

### supportsInterface

あるコントラクトが指定されたインターフェースを実装しているか確認できる関数。
`interfaceID`は、**ERC165**で指定されたインターフェース識別子です。
`interfaceID`で指定されたインターフェースが実装されていれば`true`を返し、実装されていなければ`false`を返します。

:::note warn
この関数は30,000ガス以下で実行されることが想定されています。
:::

**ERC165**のインターフェースの識別子は`0x01ffc9a7`です。
これは、`bytes4(keccak256('supportsInterface(bytes4)'))`を実行するか、`Selector`コントラクトを使用して計算できます。

実装するコントラクトは、以下のような`supportsInterface`関数を持つ必要があります。

- `interfaceID`が`0x01ffc9a7`（**ERC165**インターフェース）の場合、`true`を返す。
- `interfaceID`が`0xffffffff`の場合、`false`を返す。
- コントラクトが実装している他のすべての`interfaceID`に対して、`true`を返す。
- 上記以外の（コントラクトで実装していない）すべての`interfaceID`に対して、`false`を返す。
- `bool`型を返し、最大で`30,000`ガスまで使用できる。

## その他仕様

### ERC165をコントラクトが実装しているかどうかを確認する方法

1. ソースコントラクトは、`0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000`という入力データと`30,000`ガスで宛先アドレスに対して`STATICCALL`を行います。
    - これは`contract.supportsInterface(0x01ffc9a7)`に相当します。
2. 呼び出しが失敗するか、`false`を返した場合、対象のコントラクトは**ERC165**を実装していないと判断されます。
3. `true`を返した場合、2回目の呼び出しを行います。
    - このときの入力データは`0x01ffc9a7ffffffff00000000000000000000000000000000000000000000000000000000`です。
4. 2回目の呼び出しが失敗したか、`true`を返した場合、対象のコントラクトは**ERC165**を実装していないと判断されます。
5. 上記のいずれでもない場合、対象のコントラクトは**ERC165**を実装しています。

### 任意のインターフェースをコントラクトが実装しているかどうかを確認する方法

1. コントラクトが**ERC165**を実装しているかどうかわからない場合、以下の手順を使用して確認します。
2. もし**ERC165**を実装していない場合は、従来の方法でどのようなメソッドを使用しているかを確認する必要があります。
3. もし**ERC165**を実装している場合は、単に`supportsInterface(interfaceID)`を呼び出すことで、利用可能なインターフェースを実装しているかを判断できます。

# 補足
この仕様はできるだけシンプルに保つように心がけました。

# 後方互換性
また、この実装は現在のSolidityバージョンと互換性があります。

過去の大部分のコントラクトに対して（`0xffffffff`を使用した方法で）もこの仕組みは機能するはずであり、既に**ENS**（Ethereum Name Service）は以下のEIPを実装しています。

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

# テストケース
テストケースについては、他のコントラクトがどのようなインターフェースを実装しているかを検出するためのコントラクトが提供されています。

```javascript
pragma solidity ^0.4.20;

contract ERC165Query {
    bytes4 constant InvalidID = 0xffffffff;
    bytes4 constant ERC165ID = 0x01ffc9a7;

    function doesContractImplementInterface(address _contract, bytes4 _interfaceId) external view returns (bool) {
        uint256 success;
        uint256 result;

        (success, result) = noThrowCall(_contract, ERC165ID);
        if ((success==0)||(result==0)) {
            return false;
        }

        (success, result) = noThrowCall(_contract, InvalidID);
        if ((success==0)||(result!=0)) {
            return false;
        }

        (success, result) = noThrowCall(_contract, _interfaceId);
        if ((success==1)&&(result==1)) {
            return true;
        }
        return false;
    }

    function noThrowCall(address _contract, bytes4 _interfaceId) constant internal returns (uint256 success, uint256 result) {
        bytes4 erc165ID = ERC165ID;

        assembly {
                let x := mload(0x40)               // Find empty storage location using "free memory pointer"
                mstore(x, erc165ID)                // Place signature at beginning of empty storage
                mstore(add(x, 0x04), _interfaceId) // Place first argument directly next to signature

                success := staticcall(
                                    30000,         // 30k gas
                                    _contract,     // To addr
                                    x,             // Inputs are stored at location x
                                    0x24,          // Inputs are 36 bytes long
                                    x,             // Store output over input (saves space)
                                    0x20)          // Outputs are 32 bytes long

                result := mload(x)                 // Load the result
        }
    }
}
```

他のコントラクトが特定のインターフェースを実装しているか確認する機能を提供するコントラクト。

## doesContractImplementInterface

指定されたコントラクトが特定のインターフェースを実装しているか確認する関数。

- `_contract (address)`
    - 確認したいコントラクトのアドレス。
- `_interfaceId (bytes4)`
    - 確認したいインターフェースの識別子。

指定されたコントラクトが指定されたインターフェースを実装していれば`true`を返し、実装されていない場合は`false`を返す。
`noThrowCall`関数を使用して、`STATICCALL`を実行してインターフェースの実装を確認します。

:::note info
`STATICCALL`とは、コントラクトの状態を変更せずに関数を呼び出す方法です。
関数の状態を変更する場合は、関数の呼び出しに失敗します。
:::

https://medium.com/coinmonks/call-staticcall-and-delegatecall-1f0e1853340

具体的な確認方法は以下になります。

1. まず、指定されたコントラクトに対して、**ERC165**のインターフェースが実装されているか確認します。
    - `noThrowCall`関数を実行して対象のコントラクト対し、**ERC165**の関数セレクターを引数として`STATICCALL`を実行し、結果が`true`か`0`以外か確認します。
2. コントラクトに対して、無効なIDを引数として`STATICCALL`を実行し、結果が`false`か`0`か確認し、**ERC165**のインターフェースが実装されていないことを確認します。
3. 指定されたインターフェースIDを引数にして`STATICCALL`を実行し、結果が`true`か`0`以外か確認して指定されたインターフェースを実装しているか確認します。

## noThrowCall

アセンブリコードを使用して、指定されたコントラクトに対して`STATICCALL`を実行し、結果と成功フラグを取得します。

# 実装
`supportsInterface`のビュー関数実装を使用します、
実行コストは、どの入力に対しても`586`ガスです。
しかし、コントラクトの初期化には各インターフェースを保存する必要がある（`SSTORE`は`20,000`ガス）。
`ERC165MappingImplementation`コントラクトは汎用的で再利用可能です。

```javascript
pragma solidity ^0.4.20;

import "./ERC165.sol";

contract ERC165MappingImplementation is ERC165 {
    /// @dev You must not set element 0xffffffff to true
    mapping(bytes4 => bool) internal supportedInterfaces;

    function ERC165MappingImplementation() internal {
        supportedInterfaces[this.supportsInterface.selector] = true;
    }

    function supportsInterface(bytes4 interfaceID) external view returns (bool) {
        return supportedInterfaces[interfaceID];
    }
}

interface Simpson {
    function is2D() external returns (bool);
    function skinColor() external returns (string);
}

contract Lisa is ERC165MappingImplementation, Simpson {
    function Lisa() public {
        supportedInterfaces[this.is2D.selector ^ this.skinColor.selector] = true;
    }

    function is2D() external returns (bool){}
    function skinColor() external returns (string){}
}
```

## ERC165MappingImplementation

### supportedInterfaces

**ERC165**インターフェースを実装するために使用されるマッピング。
インターフェースIDをキーとして、そのインターフェースがサポートされているかどうかを`true`、`false`の`bool`値で表現します。

### ERC165MappingImplementation

コントラクトのデプロイ時に`supportsInterface`関数のセレクターを`supportedInterfaces`マッピングに`true`として登録する関数。

### supportsInterface

コントラクトが指定されたインターフェースをサポートしているかどうかを返す関数。

## ERC165MappingImplementation

### Lisa

コントラクトのデプロイ時に、`is2D`関数と`skinColor`関数セレクターを`supportedInterfaces`マッピングに`true`として登録します。
これにより、`Lisa`コントラクトが`Simpson`インターフェースをサポートしていることを示します。

## Homer

以下は`supportsInterface`の純粋な関数実装である。
最低の実行コストは`236`gasだが、サポートするインターフェースの数が増えるにつれて線形に増加します。

```javascript
pragma solidity ^0.4.20;

import "./ERC165.sol";

interface Simpson {
    function is2D() external returns (bool);
    function skinColor() external returns (string);
}

contract Homer is ERC165, Simpson {
    function supportsInterface(bytes4 interfaceID) external view returns (bool) {
        return
          interfaceID == this.supportsInterface.selector || // ERC165
          interfaceID == this.is2D.selector
                         ^ this.skinColor.selector; // Simpson
    }

    function is2D() external returns (bool){}
    function skinColor() external returns (string){}
}
```

３つ以上のサポートされたインターフェース（**ERC165**自体を必要とするサポートされたインターフェースを含む）がある場合、マッピングを使用するアプローチは最悪の場合でもピュアなアプローチよりもガスコストが低いです。

# 最後に

今回は「スマートコントラクトがどのインターフェースを実装しているか示し、確認できる標準的な実装を提案している**ERC165**」についてまとめてきました！
いかがだったでしょうか？

実装については今後追記していきます。

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

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

# 採用強化中！

CryptoGamesでは一緒に働く仲間を大募集中です。

https://www.wantedly.com/companies/cryptogames

この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。
「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう！