ERC-1820 의사 인트로스펙션 등록소 컨트랙트 (Pseudo-introspection Registry Contract)

Final · 기타 · 2019-03-04 작성

컨트랙트든 일반 계정이든 어떤 주소가 어떤 인터페이스를 지원하고 그 구현을 어느 컨트랙트가 맡는지 등록하고 조회하는 공용 등록소를 정의합니다. 모든 체인에서 같은 주소를 씁니다.

왜 필요한가

스마트 컨트랙트끼리 안전하게 주고받으려면 상대가 어떤 기능(인터페이스)을 갖췄는지 먼저 알아야 합니다. 기존 방식인 ERC-165는 컨트랙트가 스스로 답하는 구조라 일반 계정은 쓸 수 없습니다. 이후 나온 ERC-672는 역방향 ENS를 이용했는데, 원문은 이 방식이 불필요하게 복잡하고 ENS 자체가 다중 서명 지갑이 관리하는 중앙화된 컨트랙트라는 점을 문제로 꼽습니다. 그 다중 서명이 이론상 시스템을 바꿀 수 있기 때문입니다. 또 체인마다 등록소 주소가 다르면 어느 주소가 진짜인지 매번 확인해야 합니다. ERC-1820은 이 문제들을 한 번에 풉니다. 일반 계정도 대리 컨트랙트를 지정해 인터페이스를 등록할 수 있고 구조가 단순하며 누구도 통제하지 않고 어느 체인에서나 같은 주소를 갖습니다. 이 표준은 Solidity 0.5 업데이트로 생긴 ERC-165 호환 버그를 고친 것으로, 기능은 ERC-820과 같지만 ERC-820 대신 반드시 이것을 써야 합니다.

구현해야 하는 것

심층 설명

등록소의 저장 구조

등록소는 세 개의 매핑을 둡니다. interfaces 는 주소와 인터페이스 해시(bytes32)에서 구현자 주소로 가는 매핑입니다. managers 는 주소에서 관리자 주소로 가는 매핑으로, 값이 0이면 그 주소 자신이 관리자입니다. erc165Cached 는 주소와 ERC-165 인터페이스 ID(bytes4)마다 캐시 여부를 담는 플래그입니다. 상수로는 ERC-165 무효 ID인 INVALID_ID(0xffffffff), supportsInterface(bytes4) 의 메서드 ID인 ERC165ID(0x01ffc9a7), 대리 구현 승인에 쓰는 ERC1820_ACCEPT_MAGIC 이 있습니다. 이벤트는 구현자가 바뀔 때 InterfaceImplementerSet, 관리자가 바뀔 때 ManagerChanged 두 가지입니다. 컨트랙트는 Solidity 0.5.3 으로 작성됐고 주석에 0x1820 으로 시작하는 주소를 얻기 위한 IV 값 53759 가 적혀 있습니다.

인터페이스 이름과 해시 규칙

인터페이스는 이름 문자열을 keccak256 으로 해시한 bytes32 값으로 식별합니다. 등록소의 interfaceHash 함수가 이 해시를 온체인에서 계산해 줍니다. 승인된 ERC의 인터페이스는 반드시 ERC###XXXXX 형식으로 이름을 짓습니다. ### 은 ERC 번호, XXXXX 는 카멜케이스로 쓴 인터페이스 이름이며 그 의미는 해당 ERC에서 정의해야 합니다. 원문 예시는 keccak256("ERC20Token"), keccak256("ERC777Token"), keccak256("ERC777TokensSender"), keccak256("ERC777TokensRecipient") 입니다. 해시의 마지막 28바이트가 모두 0이면 ERC-165 인터페이스로 간주하며 앞 4바이트가 ERC-165 인터페이스 ID 가 됩니다. 사용자가 직접 정한 비공개 인터페이스 이름도 쓸 수 있지만 위의 예약된 이름 규칙과 충돌해서는 안 됩니다.

setInterfaceImplementer 의 검증 순서

구현자 등록은 setInterfaceImplementer(_addr, _interfaceHash, _implementer) 로 합니다. _addr 가 0이면 msg.sender 로 간주합니다. 이 기본값 덕분에 다중 서명 지갑은 인스턴스 주소와 상관없이 같은 트랜잭션 데이터에 서명할 수 있습니다. 함수는 먼저 getManager(addr) 가 msg.sender 와 같은지 확인하고 아니면 "Not the manager" 로 되돌립니다. 다음으로 _interfaceHash 가 ERC-165 형식(마지막 28바이트가 0)이면 "Must not be an ERC165 hash" 로 되돌립니다. _implementer 가 0 도 아니고 msg.sender 도 아니면 _implementer 의 canImplementInterfaceForAddress(_interfaceHash, addr) 를 호출해 ERC1820_ACCEPT_MAGIC 이 돌아오는지 확인하고 아니면 "Does not implement the interface" 로 되돌립니다. 모두 통과하면 interfaces[addr][_interfaceHash] 에 저장하고 InterfaceImplementerSet 이벤트를 냅니다. _implementer 가 0이면 대리 확인 없이 0 이 저장됩니다.

getInterfaceImplementer 와 ERC-165 전달

조회는 getInterfaceImplementer(_addr, _interfaceHash) 로 하며 누구나 호출할 수 있습니다. _addr 가 0이면 역시 msg.sender 로 간주합니다. _interfaceHash 의 마지막 28바이트가 0이면 등록소는 매핑을 보지 않고 앞 4바이트를 ERC-165 인터페이스 ID 로 잘라 _addr 컨트랙트에 직접 물어봅니다. 이때 implementsERC165Interface 를 거치므로 캐시가 있으면 캐시를 씁니다. 지원하면 _addr 자신을, 아니면 0 을 돌려줍니다. ERC-165 형식이 아니면 interfaces[addr][_interfaceHash] 값을 그대로 돌려주고 등록이 없으면 0 입니다. 이렇게 한 함수로 ERC-165 와 ERC-1820 을 모두 조회할 수 있어 원문은 두 표준을 함께 구현해도 충돌하지 않는다고 밝힙니다.

대리 구현자와 ERC1820_ACCEPT_MAGIC

어떤 주소를 대신해 인터페이스를 구현하는 컨트랙트는 그 인터페이스 자체를 구현해야 하고 추가로 ERC1820ImplementerInterface 를 구현해야 합니다. 이 인터페이스의 유일한 함수 canImplementInterfaceForAddress(interfaceHash, addr) 는 해당 주소와 해시 조합을 맡을 때만 ERC1820_ACCEPT_MAGIC 을 돌려줘야 하고 맡지 않으면 절대 이 값을 돌려주면 안 됩니다. ERC1820_ACCEPT_MAGIC 은 문자열 "ERC1820_ACCEPT_MAGIC" 의 keccak256 해시입니다. 불리언 대신 매직값을 쓰는 이유는 원문에 설명돼 있습니다. 이 함수를 구현하지 않았지만 되돌리지 않는 폴백 함수가 있는 컨트랙트는 호출이 폴백으로 넘어가 1 을 돌려줄 수 있고 그러면 true 를 돌려준 것처럼 보입니다. 특정 해시 값을 요구하면 이런 오판을 막을 수 있습니다. 함수 식별자는 f0083250 입니다.

관리자(manager) 권한

주소마다 관리자가 있고 그 주소의 인터페이스 구현자를 등록할 수 있는 유일한 주체입니다. 기본값은 주소 자신입니다. getManager(_addr) 는 managers[_addr] 가 0 이면 _addr 를, 아니면 저장된 관리자를 돌려줍니다. 관리자는 setManager(_addr, _newManager) 로 역할을 넘길 수 있으며 현재 관리자만 호출할 수 있습니다. _newManager 에 0x0 을 넣으면 관리자가 _addr 자신으로 초기화됩니다. 구현을 보면 _newManager 가 _addr 와 같을 때도 0 으로 저장해 기본 상태로 돌립니다. 변경 때마다 ManagerChanged(addr, newManager) 이벤트가 납니다. 원문은 외부 소유 계정에는 프록시 컨트랙트에 이렇게 위임하는 일이 꼭 필요하고 다중 서명이나 DAO 같은 기존 컨트랙트를 다시 배포하지 않아도 되게 해 준다고 설명합니다.

ERC-165 조회와 수동 캐시

등록소는 ERC-165 캐시 역할도 합니다. implementsERC165Interface(_contract, _interfaceId) 는 erc165Cached 플래그가 없으면 직접 조회합니다. 있으면 interfaces[_contract][_interfaceId] 가 _contract 와 같은지로 답합니다. 직접 조회인 implementsERC165InterfaceNoCache 는 noThrowCall 로 세 번 staticcall 을 보내며 각 호출의 가스는 30000 입니다. 첫째, supportsInterface(0x01ffc9a7) 호출이 성공하고 0 이 아닌 값을 돌려줘야 합니다. 둘째, 무효 ID 0xffffffff 에 대해 성공하고 0 을 돌려줘야 합니다. 셋째, 대상 _interfaceId 에 대해 성공하고 정확히 1 을 돌려줘야 true 입니다. 캐시는 자동으로 갱신되거나 무효화되지 않습니다. 인터페이스를 동적으로 바꾸는 컨트랙트는 반드시 updateERC165Cache(_contract, _interfaceId) 로 캐시를 직접 갱신해야 하며 이상적으로는 컨트랙트가 스스로 갱신하지만 누구나 대신 호출할 수 있습니다. 이 함수는 조회 결과에 따라 interfaces 에 _contract 또는 0 을 쓰고 erc165Cached 를 true 로 둡니다. 이 부분은 William Entriken 과 함께 설계했다고 원문에 적혀 있습니다.

키 없는 배포와 고정 주소, ERC-820 대체

등록소는 Nick 의 방법이라 불리는 키 없는 배포 방식으로 올립니다. 배포 트랜잭션은 어느 체인에서나 유효하도록 EIP-155 를 쓰지 않고 가스 가격은 100 Gwei 로 높게 잡습니다. 서명값은 v 가 27, r 과 s 는 모두 1820 이 16번 반복된 값입니다. 사람이 정한 이 패턴에서 서명자 주소를 역산하면 0xa990077c3205cbDf861e17Fa532eeB069cE9fF96 이 나오며 이 계정의 개인키는 아무도 모릅니다. 배포하려면 이 계정에 먼저 정확히 0.08 ether 를 보낸 뒤 원문에 실린 원시 트랜잭션을 그대로 브로드캐스트합니다. 그 결과 등록소 주소는 모든 체인에서 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24 로 같고 누구도 그 주소에 다른 컨트랙트를 올릴 수 없습니다. 이 표준은 ERC-820 을 대체합니다. Solidity 0.5 업데이트로 생긴 ERC-165 로직의 비호환을 고친 것이며 그 외 기능은 ERC-820 과 같습니다. 원문은 ERC-820 대신 반드시 ERC-1820 을 쓰라고 명시합니다. 테스트와 구현은 0xjac/ERC1820 저장소에 있으며 상태는 Final 입니다.

ERC 원문 · ← ERCs