리서치 이유
멀티토큰(ERC-1155)을 SBT로 만들어보고 싶었는데 관련 eip문서를 보던 중 ERC-165에 대한 언급이 나왔고 살펴보았다.
전에 ERC-20 토큰의 표준에 만족하는지에 대한 여부를 체크할 수 있는 프로그램을 허접하게 만들었는데 해당 로직은 바이트코드에 ERC-20 표준 함수셀렉터들이 모두 포함되어 있는 지를 검사하는 과정이였다. 그런데 ERC-165를 리서치하다보니 이 표준을 통해서도 ERC-20 표준 여부 만족을 체크할 수도 있겠다는 생각을 하였고 실제로 가능한지 확인해보았다.
목적
스마트 컨트랙트가 구현하는 인터페이스를 게시하고 감지하는 표준을 생성
기능
인터페이스를 식별하는 방법
검색 컨트랙트가 구현하는 인터페이스를 게시하는 방법
컨트랙트가 ERC-165를 구현하는지 감지하는 방법
컨트랙트가 특정 인터페이스를 구현하는 지 감시하는 방법
사양
함수셀렉터란?
스마트 컨트랙트의 함수 아이디라고 생각하면 되는데 ERC-20의 balanceOf(address)함수의 아이디는
“balanceOf(address)”를 keccak256으로 돌리면 나오는 값의 앞에 8자리를 함수셀렉터라고 한다.
func | transfer(address,uint256) | a9059cbb |
---|---|---|
func | approve(address,uint256) | 095ea7b3 |
func | balanceOf(address) | 70a08231 |
func | transferFrom(address,address,uint256) | 23b872dd |
func | allowance(address,address) | dd62ed3e |
func | totalSupply() | 18160ddd |
스마트 컨트랙트 내에서 셀렉터를 구하는 방법은 두 가지로 keccak256을 쓰거나
this.balance.selector를 사용하면 된다.
구한 셀렉터들을 xor연산(^)을 해주면 인터페이스 식별자 아이디를 구할 수 있게 된다.
erc165를 구현하는 컨트랙트는 다음과 같은 인터페이스를 구현해야 한다.
pragma solidity^0.4.20;
interface ERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID)externalviewreturns (bool);
}
이 인터페이스의 인터페이스 식별자는 0x01ffc9a7이다. → bytes4(keccak256('supportsInterface(bytes4)'))
interfaceID가 0x01ffc9a7(EIP165 인터페이스)인 경우 true
interfaceID가 0xffffffff인 경우 false
소스 컨트랙트는 입력 데이터와 함께 대상 주소로 STATICCALL을 만든다.
입력데이터 ⇒ 0x01ffc9a701ffc9a7000000000000000000000000000000000000000000000000( contract.supportsInterface(0x01ffc9a7)) 및 가스 30,000.
호출이 실패하거나 거짓을 반환하면 대상 컨트랙트는 ERC-165를 구현하지 않는다..
호출이 참을 반환하면 입력 데이터 0x01ffc9a7ffffff00000000000000000000000000000000000000000000으로 두 번째 호출이 수행된다.
두 번째 호출이 실패하거나 참을 반환하면 대상 컨트랙트는 ERC-165를 구현하지 않고
그렇지 않으면 ERC-165를 구현해야 한다.
컨트랙트가 ERC-165를 구현하는지 확실하지 않은 경우, 위의 절차를 통해 확인
ERC-165를 구현하지 않는다면 구식 방식으로 어떤 메소드를 사용하는지 확인해야 함(해당 방법은 복잡하고 에러가 나올 확률도 있음)
ERC-165를 구현하는 컨트랙트라면 supportsInterface(interfaceID)를 호출하여 사용할 수 있는 인터페이스를 구현하는지 확인하면 됨.
컨트랙트가 어떤 인터페이스를 제공하였는지 확인하는 테스트 코드
// SPDX-License-Identifier: MIT
pragma solidity^0.4.20;
contract ERC165Query {
bytes4 InvalidID= 0xffffffff;
bytes4 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
}
}
}
결론
로컬 테스트넷에서 테스트를 해봤는데
토큰과 nft를 테스트 해본 결과 토큰은 잘 찾아내지 못하고 nft는 잘 찾아냈다.
그래서 혹시 몰라 네트워크를 폴리곤에서 테스트 하였을 때도 NFT는 잘 찾아내지만 ERC20은 찾아내지 못했다.
그래서 공식문서를 보았을 때 erc20은 165에 대한 언급이 없지만 721은 있는 걸로 봐서 721들은 구현이 되었기에 찾을 수 있었던 것으로 확인되었다.
결론은 165를 통해서 토큰의 표준을 만족하는지에 대한 여부를 코드에 반영하는 것은 힘들것 같다.