ERC-5267 EIP-712 도메인 조회 표준 (Retrieval of EIP-712 domain)

Final · 서명·인증 · 2022-07-14 작성

컨트랙트가 자신의 EIP-712 서명 도메인을 어떤 필드와 값으로 구성하는지 공개하는 함수를 정합니다. 앱과 지갑은 이를 읽어 도메인 구분자를 직접 만들 수 있습니다.

왜 필요한가

EIP-712는 구조화된 메시지에 서명하는 방식입니다. 서명이 다른 곳에서 다시 쓰이는 재전송 공격(replay attack)을 막고 피싱을 줄이기 위해, 서명을 특정 컨트랙트 같은 도메인에 묶어 주는 도메인 구분자(domain separator)를 씁니다. 도메인은 이름, 버전, 체인 ID, 검증 컨트랙트 주소, 솔트 같은 선택 필드로 이루어지는데, 정작 EIP-712는 컨트랙트가 어떤 필드를 어떤 값으로 쓰는지 알릴 방법을 정하지 않았습니다. 그래서 앱은 컨트랙트마다 도메인을 따로 맞춰 넣어야 했고 범용 연동이 불가능했습니다. 원문은 사용자 경험을 크게 개선한다고 알려진 EIP-2612(permit)조차 이 때문에 널리 쓰이지 못했다고 짚습니다. EIP-2612의 DOMAIN_SEPARATOR 함수는 해시값만 돌려주기 때문에, 서명 전에 사용자에게 도메인 내용을 보여 주고 검증하는 데는 쓸 수 없습니다. 이 표준은 앱이 컨트랙트의 도메인 정의를 읽어 올 수 있는 인터페이스를 정해 이 빈틈을 메웁니다.

구현해야 하는 것

심층 설명

eip712Domain 함수 선언과 반환 규칙

규격을 따르는 컨트랙트는 eip712Domain 함수를 원문에 선언된 형태 그대로 정의해야 합니다(MUST). 반환값은 fields, name, version, chainId, verifyingContract, salt, extensions 일곱 개입니다. 쓰지 않는 필드가 있어도 지정된 값을 모두 돌려줘야 합니다. 클라이언트가 반환 데이터를 올바르게 디코딩하려면 형태가 항상 같아야 하기 때문입니다. 이 반환값은 컨트랙트가 EIP-712 서명을 검증할 때 실제로 쓰는 도메인 구분자를 설명해야 하며 EIP712Domain 구조체의 형태(어떤 선택 필드와 확장이 있는지)와 각 필드의 값을 함께 나타냅니다.

fields 비트맵 읽는 법

fields는 bytes1 하나로 된 비트맵입니다. 비트 i가 1이면 도메인 필드 i가 있다는 뜻이고 i는 0에서 4까지입니다. 비트는 최하위부터 최상위 순으로 읽고 필드 순서는 EIP-712가 정한 순서이자 함수 반환 타입에 나열된 순서와 같습니다. 즉 비트 0은 name, 1은 version, 2는 chainId, 3은 verifyingContract, 4는 salt입니다. 참조 구현은 name, chainId, verifyingContract만 쓰므로 fields가 2진수 01101, 16진수 0d입니다. JavaScript 참조 구현은 fieldNames 배열을 돌며 fields & (1 << i)가 0인 필드를 도메인 객체에서 지우는 방식으로 해석합니다.

name·version·chainId·verifyingContract·salt의 의미

이 다섯 값은 fields에 따라 해당 필드가 있을 때 EIP712Domain의 그 필드 값입니다. 필드가 없으면 값은 정해지지 않은 것으로 보고 무시합니다. 각 필드의 의미는 EIP-712가 정한 그대로입니다. chainId를 쓰는 경우 그 값은 실제 체인의 EIP-155 체인 ID를 따라 바뀌어야 합니다(SHOULD). 참조 구현은 chainId에 block.chainid를, verifyingContract에 address(this)를 넣어 컨트랙트가 배포된 체인과 자기 주소를 그대로 알립니다. 이 컨트랙트가 이더리움 메인넷의 0x0000000000000000000000000000000000000001 주소에 있다면, 설명하는 도메인은 name이 Example, chainId가 1, verifyingContract가 그 주소인 세 필드로 구성됩니다.

extensions 목록

extensions는 uint256 배열로 된 EIP 번호 목록입니다. 각 번호는 EIP-712에 새 도메인 필드를 추가하는 EIP를 가리켜야 하며(MUST), 그 EIP에는 필드 값을 얻는 방법과 필요하면 포함 조건이 함께 정의되어 있어야 합니다. fields 비트맵은 확장 필드의 포함 여부에 영향을 주지 않습니다. 확장을 EIP 번호로 표현하는 이유는 EIP-712가 새 필드는 EIP 절차를 거쳐 제안하라고 정했기 때문입니다. 참조 구현은 가상의 EIP-XYZ가 bytes32 타입 subdomain 필드와 getSubdomain() 함수를 정의했다고 가정하고 extensions에 그 번호가 있으면 getSubdomain()을 불러 도메인 객체에 넣는 예를 보여 줍니다. 모르는 번호가 오면 오류를 냅니다. 이때 EIP712Domain 구조체 타입에도 subdomain 필드를 더해야 하지만 그 부분은 참조 구현 범위 밖입니다.

도메인 변경과 EIP712DomainChanged 이벤트

eip712Domain의 반환값, 곧 컨트랙트의 EIP-712 도메인은 컨트랙트 수명 동안 바뀔 수 있습니다(MAY). 다만 자주 바뀌어서는 안 됩니다(SHOULD NOT). chainId 필드를 쓴다면 실제 체인의 EIP-155 ID를 그대로 반영해 바뀌어야 합니다. 도메인이 바뀌었을 수 있음을 알리기 위해 컨트랙트는 인자가 없는 EIP712DomainChanged 이벤트를 낼 수 있습니다(MAY). 이벤트를 받은 앱은 eip712Domain을 다시 호출해 최신 도메인을 읽으면 됩니다.

DOMAIN_SEPARATOR 해시만으로 부족한 이유

EIP-2612(permit)는 bytes32 값을 돌려주는 DOMAIN_SEPARATOR 함수를 정합니다. 이 값은 hashStruct(eip712Domain)의 결과, 즉 실제 도메인 구분자입니다. 그런데 EIP-712가 정한 RPC 메서드는 해시가 아니라 도메인을 설명하는 객체를 받습니다. 이는 RPC 설계의 결함이 아닙니다. 서명 과정에서 도메인을 검증하고 사용자에게 알려야 한다는 보안 전제의 일부입니다. 해시는 내용을 알 수 없는 불투명한 값이라 이 일을 할 수 없습니다. 이 표준은 EIP-712와 EIP-2612 양쪽의 이 빈틈을 메웁니다.

이 표준을 구현하지 못한 기존 컨트랙트 다루기

이 표준은 EIP-712의 선택 확장이라 하위 호환 문제를 일으키지 않습니다. EIP-712 서명을 쓰는 업그레이드 가능 컨트랙트는 업그레이드해서 이 표준을 구현할 수 있습니다(MAY). 반면 불변이라 업그레이드할 수 없는 컨트랙트도 있으므로, 이 표준을 쓰는 사용자 에이전트와 앱은 그런 컨트랙트도 함께 지원해야 합니다(SHOULD). 가장 간단한 방법은 컨트랙트 주소와 체인 ID를 기준으로 흔한 도메인을 하드코딩하는 것입니다. 더 일반적인 방법도 있습니다. 알려진 정보로 몇 가지 흔한 패턴에 따라 가능한 도메인 후보를 만들고 그 해시가 컨트랙트의 DOMAIN_SEPARATOR 또는 domainSeparator 함수 값과 일치하는 것을 고르는 방식입니다.

쓸 때 주의할 점

ERC 원문 · ← ERCs