ERC-165 표준 인터페이스 감지 (Standard Interface Detection)

Final · 기타 · 2018-01-23 작성

컨트랙트가 자신이 어떤 인터페이스(함수 묶음)를 구현했는지 공개하고 다른 컨트랙트나 앱이 호출 한 번으로 그 사실을 확인할 수 있게 하는 표준입니다.

왜 필요한가

이더리움 컨트랙트는 서로를 호출하며 동작하지만 상대 컨트랙트에 어떤 함수가 있는지 미리 물어볼 표준 방법이 없었습니다. 예를 들어 ERC-20 토큰 인터페이스처럼 널리 쓰이는 표준 인터페이스를 다룰 때, 상대가 그 인터페이스를 지원하는지, 지원한다면 어느 버전인지 알아야 호출 방식을 맞출 수 있습니다. 실제로 ERC-20에는 버전 식별자가 따로 제안되기도 했습니다. 이런 확인 수단이 없으면 호출하는 쪽은 상대 컨트랙트의 코드를 직접 들여다보거나, 일단 호출해 보고 실패하는지 지켜보는 방식에 기댈 수밖에 없습니다. 이 제안은 인터페이스라는 개념 자체를 표준화하고 각 인터페이스에 이름(식별자)을 붙이는 방법을 정해 이 문제를 풉니다.

구현해야 하는 것

심층 설명

인터페이스 식별자는 함수 선택자의 XOR

이 표준에서 인터페이스는 이더리움 ABI가 정의한 함수 선택자(function selector)의 집합입니다. Solidity의 interface 키워드는 반환 타입, 가변성, 이벤트까지 정의하지만 여기서는 그 가운데 함수 선택자만 봅니다. 인터페이스 식별자는 인터페이스에 속한 모든 함수 선택자를 XOR한 bytes4 값입니다. 원문 예시의 Solidity101 인터페이스라면 i.hello.selector ^ i.world.selector가 식별자가 됩니다. 인터페이스에는 선택 구현 함수가 허용되지 않으므로 식별자에도 선택 함수는 들어가지 않습니다.

supportsInterface가 지켜야 할 반환 규칙

ERC-165를 따르는 컨트랙트는 ERC165 인터페이스, 즉 supportsInterface(bytes4 interfaceID) 함수를 구현합니다. 이 인터페이스 자체의 식별자는 0x01ffc9a7이며 bytes4(keccak256('supportsInterface(bytes4)'))로 계산할 수 있습니다. 반환 규칙은 네 가지로, interfaceID가 0x01ffc9a7이면 true, 0xffffffff이면 false, 그 밖에 이 컨트랙트가 구현한 식별자이면 true, 나머지는 false입니다. 이 함수는 반드시 bool을 반환해야 하고 가스를 최대 30,000까지만 써야 합니다. 원문은 구현 방법이 여러 가지라며 예시 구현과 가스 사용 논의를 참고하라고 적습니다.

ERC-165 지원 여부를 판별하는 두 번의 STATICCALL

먼저 대상 주소에 입력 데이터 0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000과 가스 30,000으로 STATICCALL을 보냅니다. supportsInterface(0x01ffc9a7)에 해당합니다. 호출이 실패하거나 false가 돌아오면 ERC-165를 구현하지 않은 것입니다. true가 돌아오면 입력 데이터 0x01ffc9a7ffffffff00000000000000000000000000000000000000000000000000000000으로 두 번째 호출을 합니다. 이 호출이 실패하거나 true를 돌려주면 역시 미구현으로 판정합니다. 두 조건을 모두 통과해야 ERC-165 구현으로 봅니다. 원문은 0xffffffff를 쓰는 이 방식이 표준 이전에 만들어진 대부분의 컨트랙트에 대해 미구현을 올바르게 가려낸다고 설명합니다.

특정 인터페이스 지원 여부 확인

상대가 ERC-165를 구현했는지 확실하지 않으면 먼저 위 절차로 확인합니다. ERC-165를 구현하지 않은 컨트랙트라면 어떤 함수를 쓰는지 예전 방식대로 직접 알아내야 합니다. ERC-165를 구현했다면 supportsInterface(interfaceID)를 호출하는 것만으로 사용하려는 인터페이스의 지원 여부를 알 수 있습니다. 원문은 명세를 되도록 단순하게 유지했고 당시 Solidity 버전과도 호환되도록 설계했다고 밝힙니다.

구현 방식 두 가지와 가스 비용 비교

원문은 두 가지 구현 예를 보여 줍니다. 첫째는 mapping(bytes4 => bool) supportedInterfaces에 지원 인터페이스를 저장하고 view 함수로 조회하는 ERC165MappingImplementation입니다. 어떤 입력이든 실행 비용은 586 가스이지만 생성 시 인터페이스마다 SSTORE(20,000 가스)로 저장해야 합니다. 이 방식에서는 0xffffffff를 true로 설정하면 안 됩니다. 둘째는 pure 함수 안에서 식별자를 직접 비교하는 방식(Homer 예시)으로, 최악의 경우 236 가스이지만 지원 인터페이스 수에 비례해 비용이 늘어납니다. ERC-165 자체를 포함해 지원 인터페이스가 3개 이상이면 mapping 방식이 pure 방식의 최악 비용보다 언제나 쌉니다.

테스트 케이스와 수정 이력

원문 Test Cases에는 다른 컨트랙트의 인터페이스 지원 여부를 판별하는 ERC165Query 컨트랙트가 있습니다. doesContractImplementInterface(address, bytes4)가 위 판별 절차를 그대로 수행합니다. 내부의 noThrowCall이 어셈블리로 가스 30,000짜리 staticcall을 보내 실패해도 되돌리지 않고 성공 여부와 결과를 돌려줍니다. 입력 길이는 선택자 4바이트와 인자 32바이트를 합친 36바이트(0x24)입니다. 2019년 1월 23일 확정된 PR 1640은 이 값을 이전의 32바이트에서 36바이트로 바로잡았습니다. 예전 코드는 Solidity 0.4.x에서는 조용히 동작했지만 0.5.0의 새 동작에서는 깨졌기 때문입니다. 원문 기준으로 ENS는 이미 이 표준을 구현하고 있습니다.

ERC 원문 · ← ERCs