はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、スマートコントラクトがどのインターフェースを実装しているか示し、確認できる標準的な実装を提案しているERC165についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
概要
以下のことを標準化します。
- インターフェースの識別方法。
- コントラクトが実装しているインターフェースを公開する方法。
- コントラクトがERC165を実装しているか確認する方法。
- コントラクトが特定のインターフェースを実装しているか確認する方法。
動機
ERC20やERC721などの「標準的なインターフェース」では、コントラクトがそのインターフェースをサポートしているかどうか、サポートしている場合にどのバージョンのインターフェースをサポートしているかを確認できることは役に立ちます。
特にERC20の場合、既にバージョン識別子が提案されています。
この提案はインターフェースの概念を標準化し、インターフェースの識別(命名)を標準化します。
識別子とは、他と区別するための名前や番号、記号のことです。
仕様
インターフェースの識別方法
この標準では、インターフェースはEthereum ABIによって定義される関数セレクターを使用します。
関数セレクターとは、関数の名前と引数の型を文字列にし、keccak256ハッシュ関数で生成されたハッシュ値の先頭4byteのことです。
これはSolidityのインターフェースの概念と異なり、返り値の型、変更可能性、およびイベントなどは定義されません。
インターフェース識別子は、インターフェース内のすべての関数セレクターのXOR(排他的論理和)として定義されます。
以下のコード例は、インターフェース識別子を計算する方法を示しています。
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;
}
}
インターフェースはオプション機能を許可していないため、インターフェースIDにはオプション機能は含まれない。
コントラクトが実装するインターフェースの公開方法
ERC165に準拠するコントラクトは、以下のインターフェース(ERC165.sol)を実装しなければならない。
pragma solidity ^0.4.20;
interface ERC165 {
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
supportsInterface
あるコントラクトが指定されたインターフェースを実装しているか確認できる関数。
interfaceID
は、ERC165で指定されたインターフェース識別子です。
interfaceID
で指定されたインターフェースが実装されていればtrue
を返し、実装されていなければfalse
を返します。
この関数は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をコントラクトが実装しているかどうかを確認する方法
- ソースコントラクトは、
0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000
という入力データと30,000
ガスで宛先アドレスに対してSTATICCALL
を行います。- これは
contract.supportsInterface(0x01ffc9a7)
に相当します。
- これは
- 呼び出しが失敗するか、
false
を返した場合、対象のコントラクトはERC165を実装していないと判断されます。 -
true
を返した場合、2回目の呼び出しを行います。- このときの入力データは
0x01ffc9a7ffffffff00000000000000000000000000000000000000000000000000000000
です。
- このときの入力データは
- 2回目の呼び出しが失敗したか、
true
を返した場合、対象のコントラクトはERC165を実装していないと判断されます。 - 上記のいずれでもない場合、対象のコントラクトはERC165を実装しています。
任意のインターフェースをコントラクトが実装しているかどうかを確認する方法
- コントラクトがERC165を実装しているかどうかわからない場合、以下の手順を使用して確認します。
- もしERC165を実装していない場合は、従来の方法でどのようなメソッドを使用しているかを確認する必要があります。
- もしERC165を実装している場合は、単に
supportsInterface(interfaceID)
を呼び出すことで、利用可能なインターフェースを実装しているかを判断できます。
補足
この仕様はできるだけシンプルに保つように心がけました。
後方互換性
また、この実装は現在のSolidityバージョンと互換性があります。
過去の大部分のコントラクトに対して(0xffffffff
を使用した方法で)もこの仕組みは機能するはずであり、既にENS(Ethereum Name Service)は以下のEIPを実装しています。
テストケース
テストケースについては、他のコントラクトがどのようなインターフェースを実装しているかを検出するためのコントラクトが提供されています。
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
を実行してインターフェースの実装を確認します。
STATICCALL
とは、コントラクトの状態を変更せずに関数を呼び出す方法です。
関数の状態を変更する場合は、関数の呼び出しに失敗します。
具体的な確認方法は以下になります。
- まず、指定されたコントラクトに対して、ERC165のインターフェースが実装されているか確認します。
-
noThrowCall
関数を実行して対象のコントラクト対し、ERC165の関数セレクターを引数としてSTATICCALL
を実行し、結果がtrue
か0
以外か確認します。
-
- コントラクトに対して、無効なIDを引数として
STATICCALL
を実行し、結果がfalse
か0
か確認し、ERC165のインターフェースが実装されていないことを確認します。 - 指定されたインターフェースIDを引数にして
STATICCALL
を実行し、結果がtrue
か0
以外か確認して指定されたインターフェースを実装しているか確認します。
noThrowCall
アセンブリコードを使用して、指定されたコントラクトに対してSTATICCALL
を実行し、結果と成功フラグを取得します。
実装
supportsInterface
のビュー関数実装を使用します、
実行コストは、どの入力に対しても586
ガスです。
しかし、コントラクトの初期化には各インターフェースを保存する必要がある(SSTORE
は20,000
ガス)。
ERC165MappingImplementation
コントラクトは汎用的で再利用可能です。
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だが、サポートするインターフェースの数が増えるにつれて線形に増加します。
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){}
}
3つ以上のサポートされたインターフェース(ERC165自体を必要とするサポートされたインターフェースを含む)がある場合、マッピングを使用するアプローチは最悪の場合でもピュアなアプローチよりもガスコストが低いです。
最後に
今回は「スマートコントラクトがどのインターフェースを実装しているか示し、確認できる標準的な実装を提案しているERC165」についてまとめてきました!
いかがだったでしょうか?
実装については今後追記していきます。
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
採用強化中!
CryptoGamesでは一緒に働く仲間を大募集中です。
この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。
「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう!