ERC-4834 계층형 도메인 (Hierarchical Domains)
Final · 이름·메타데이터 · 2022-02-22 작성
컨트랙트 자체를 도메인으로 삼아 사람이 읽기 쉬운 이름을 주소로 바꿔 주는 최소한의 이름 해석(name resolution) 표준입니다. 계층마다 원하는 접근 제어를 붙일 수 있습니다.
왜 필요한가
이더리움 주소는 긴 16진수 문자열이라 사람이 읽거나 기억하기 어렵습니다. 그래서 주소에 이름을 붙이는 ENS(ERC-137) 같은 체계가 쓰이지만 원문은 ENS의 구조가 비교적 복잡하고 표준화된 접근 제어가 없다는 점을 문제로 듭니다. 이름마다 누가 만들고 바꾸고 지울 수 있는지를 정하는 방식이 표준으로 정해져 있지 않으면, 계층마다 다른 관리 방식을 쓰고 싶어도 공통 인터페이스가 없어 앱과 지갑이 일관되게 다루기 어렵습니다. 이 표준은 이름 해석에 필요한 함수 두 개만으로 된 최소 인터페이스를 정의하고 루트와 최상위 도메인(TLD), 서브도메인까지 모든 도메인을 똑같은 컨트랙트로 구현하게 해서 DNS처럼 단순한 반복 알고리즘으로 이름을 풀 수 있게 합니다.
구현해야 하는 것
function hasDomain(string[] memory name) external view returns (bool);: 주어진 이름(오른쪽에서 왼쪽 순서)의 서브도메인이 있는지를 true/false로 알려 줍니다.function getDomain(string[] memory name) external view returns (address);: 주어진 이름의 서브도메인 주소를 돌려줍니다. hasDomain이 false인 이름이면 되돌려야(revert) 합니다.event SubdomainCreate(address indexed sender, string name, address subdomain);: 새 서브도메인이 만들어질 때 반드시 발생해야 하는 이벤트입니다.event SubdomainUpdate(address indexed sender, string name, address subdomain, address oldSubdomain);: 도메인이 가리키는 주소가 바뀔 때 반드시 발생해야 하는 이벤트로, 이전 주소도 함께 기록합니다.event SubdomainDelete(address indexed sender, string name, address subdomain);: 도메인 연결이 해제될 때 반드시 발생해야 하는 이벤트입니다.function createDomain(string memory name, address subdomain) external payable;: 주어진 이름으로 서브도메인을 만듭니다. 권한이 없거나 이미 존재하면 되돌려야 합니다.function setDomain(string memory name, address subdomain) external;: 기존 서브도메인이 가리키는 주소를 바꿉니다. 권한이 없거나 도메인이 없으면 되돌려야 합니다.function deleteDomain(string memory name) external;: 서브도메인을 삭제합니다. 도메인이 없거나 권한이 없으면 되돌려야 합니다.function canCreateDomain(address updater, string memory name, address subdomain) external view returns (bool);: 어떤 계정이 해당 이름의 서브도메인을 만들 수 있는지 알려 줍니다. 이미 존재하는 이름이면 반드시 false를 돌려줍니다.function canSetDomain(address updater, string memory name, address subdomain) external view returns (bool);: 어떤 계정이 서브도메인을 변경할 수 있는지 알려 줍니다. 대상 서브도메인이 접근 제어 확장을 구현했다면 그쪽의 canMoveSubdomain 판단도 반영해야 합니다.function canDeleteDomain(address updater, string memory name) external view returns (bool);: 어떤 계정이 서브도메인을 삭제할 수 있는지 알려 줍니다. 대상이 접근 제어 확장을 구현했다면 canDeleteSubdomain 판단도 반영해야 합니다.function subdomainByIndex(uint256 index) external view returns (string memory);: 인덱스로 서브도메인 이름을 조회합니다. 개수를 알 수 없거나 무한이면 되돌려야 합니다.function totalSubdomains() external view returns (uint256);: 서브도메인의 총 개수를 돌려줍니다. 개수를 알 수 없거나 무한이면 되돌려야 합니다.function canMoveSubdomain(address updater, string memory name, IDomain parent, address newSubdomain) external view returns (bool);: 어떤 계정이 이 서브도메인을 현재 부모에서 다른 곳으로 옮길 수 있는지를 서브도메인 스스로 판단합니다. 부모의 canSetDomain이 호출할 수 있습니다.function canDeleteSubdomain(address updater, string memory name, IDomain parent) external view returns (bool);: 어떤 계정이 이 도메인의 서브도메인 연결을 해제할 수 있는지를 서브도메인 스스로 판단합니다. 부모의 canDeleteDomain이 호출할 수 있습니다.
심층 설명
모든 계층이 똑같은 도메인 컨트랙트입니다
이 표준에서 도메인은 IDomain 인터페이스를 구현한 컨트랙트입니다. 서브도메인, TLD, 심지어 루트까지 전부 같은 방식의 도메인으로 구현됩니다. 그래서 이름 해석이 DNS와 비슷한 단순 반복 알고리즘이 됩니다. 필수 인터페이스는 hasDomain과 getDomain 두 개뿐이며 두 함수 모두 이름을 오른쪽에서 왼쪽 순서의 문자열 배열(string[])로 받습니다. getDomain은 hasDomain(name)이 false일 때 되돌려야 합니다.
이름 해석 알고리즘
"a.b.c" 같은 이름을 구분자로 쪼개 ["a", "b", "c"]를 만들고 domain을 루트로, path를 빈 목록으로 초기화합니다. 배열의 마지막 원소를 꺼내 path에 추가한 뒤 domain.hasDomain(path)를 호출합니다. false면 해석이 실패하고 true면 domain을 domain.getDomain(path)로 바꿉니다. 쪼갠 조각이 모두 없어질 때까지 반복합니다. 중첩 깊이에 제한이 없어 원문은 루트가 z를 담고 z가 y를 담는 식으로 이어지면 36단계 이름도 유효하다는 예를 듭니다. 해석 경로 위의 컨트랙트들이 해석을 전적으로 통제하므로, 시간대에 따라 다른 주소를 돌려주는 도메인 같은 유연한 구현도 가능합니다.
선택 확장 Registerable: 등록과 변경
서브도메인의 생성(createDomain)·변경(setDomain)·삭제(deleteDomain)와 세 가지 이벤트(SubdomainCreate, SubdomainUpdate, SubdomainDelete)를 정의합니다. 각 쓰기 함수는 대응하는 조회 함수(canCreateDomain, canSetDomain, canDeleteDomain)가 false이거나 존재 조건이 맞지 않으면 되돌려야 합니다. canCreateDomain은 hasDomain(name)이 true면 반드시 false를, canSetDomain과 canDeleteDomain은 hasDomain(name)이 false면 반드시 false를 돌려줍니다. createDomain은 payable로 선언되어 있어 등록에 수수료를 받는 구현도 가능합니다.
선택 확장 Enumerable: 목록 조회
subdomainByIndex(uint256 index)로 인덱스에 해당하는 서브도메인 이름을, totalSubdomains()로 전체 개수를 조회합니다. 두 함수 모두 서브도메인 개수를 알 수 없거나 무한일 때는 반드시 되돌려야 합니다. 이 확장을 구현하면 외부에서 도메인이 가진 서브도메인 목록을 순회할 수 있습니다.
선택 확장 Access Control: 서브도메인 쪽 권한
서브도메인이 자기 자신에 대한 권한 판단을 스스로 하게 하는 확장입니다. canMoveSubdomain은 어떤 계정이 이 서브도메인을 현재 부모에서 떼어내 다른 주소로 바꿀 수 있는지, canDeleteSubdomain은 연결을 해제할 수 있는지를 돌려줍니다. 부모 도메인의 canSetDomain과 canDeleteDomain이 이 함수들을 호출하도록 설계되어 서브도메인이 부모의 접근 제어에만 묶이지 않습니다. 원문 Rationale은 이 함수들이 제안 성격이라는 점도 밝힙니다. 예컨대 소유자가 있는 도메인은 키 분실 시 복구할 수 있도록 소유자 우선권(owner override)을 구현할 수 있습니다.
왜 단순한 ownable을 쓰지 않았나
최대한 범용적으로 설계하기 위해 단순한 소유자(ownable) 인터페이스를 채택하지 않았습니다. 소유자 방식이 필요하면 그렇게 구현하면 됩니다. 이 구조 덕분에 계층마다 다른 거버넌스를 둘 수 있습니다. 원문은 루트 도메인은 양도 불가 토큰을 균등하게 나눈 DAO가, TLD는 지분 토큰 DAO가, 그 아래 도메인은 단일 소유자가, 서브도메인은 NFT와 연결된 소유자가 다스리는 식의 조합을 예로 듭니다.
ENS와의 관계
이 표준은 ENS(ERC-137)와 비슷한 목적을 갖지만 접근이 다릅니다. ENS의 해석 알고리즘과 달리 이 표준의 이름 해석은 해석 경로 위의 컨트랙트들이 완전히 통제합니다. 원문은 이 동작이 사용자에게 더 직관적이라고 설명합니다. 호환성에 대해서는 이 표준이 ENS를 지원할 만큼 범용적이지만 ENS는 이 표준을 지원할 만큼 범용적이지 않다고 밝힙니다. 이 표준은 Final 상태로 확정된 표준입니다.
쓸 때 주의할 점
- 악성 canMoveSubdomain (블랙홀): setDomain으로 서브도메인을 옮기는 일은 위험할 수 있습니다. 악성 서브도메인이 canMoveSubdomain에서 예상치 못하게 false를 돌려주면 도메인 소유권이 사실상 잠길 수 있고 반대로 예상 밖에 true를 돌려주는 백도어라면 그 컨트랙트 소유자가 도메인을 가로챌 수 있습니다. 클라이언트는 새 서브도메인의 canMoveSubdomain이나 canDeleteSubdomain이 false로 바뀌면 경고해야 하지만 이들은 함수라서 연결 전후로 값이 바뀔 여지가 있습니다. 따라서 setDomain을 호출하기 전에 새 서브도메인의 소스 코드를 항상 감사하는 것이 권장됩니다.
- 부모 도메인의 해석 통제: 부모 도메인은 서브도메인의 이름 해석을 완전히 통제합니다. 어떤 도메인이 a.b.c에 연결되어 있어도, b.c는 코드에 따라 a.b.c를 임의의 도메인으로 바꿀 수 있고, c는 b.c 자체를 임의의 도메인으로 바꿀 수 있습니다. 미리 연결된 도메인을 취득하기 전에는 그 컨트랙트와 루트까지의 모든 부모를 함께 감사하는 것이 권장됩니다.