はじめに
初めまして。
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の純粋な関数実装である。
最低の実行コストは236gasだが、サポートするインターフェースの数が増えるにつれて線形に増加します。
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に興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう!