EIP-712는 사람이 의미를 구분할 수 있는 구조체 타입(struct type)과 필드를 정해진 규칙으로 인코딩하고, name·version·chainId·verifyingContract 등을 담은 도메인 구분자(domain separator)와 합쳐 0x1901 || domainSeparator || hashStruct(message)를 서명 다이제스트로 만듭니다. 덕분에 지갑은 알아보기 힘든 헥스 문자열 대신 spender·amount·deadline 같은 값을 구조적으로 보여 줄 바탕을 얻습니다. 하지만 화면이 모든 필드를 정확히 보여 준다는 보장은 아닙니다. 계약은 타입 해시와 도메인, 논스·기한을 정확히 검증하고, 사용자는 체인·계약·행위·무제한 금액 여부를 확인해야 합니다.
두 해시를 최종 다이제스트에 합칩니다
EIP-712 메시지 해시는 hashStruct(s)=keccak256(typeHash || encodeData(s))입니다. typeHash는 정규 encodeType 문자열의 해시이고, 사용자 정의 구조체가 다른 구조체를 참조하면 의존 타입을 이름순으로 이어 붙입니다. 구현마다 타입 문자열 순서가 다르면 화면에 같은 데이터가 보여도 다이제스트는 달라집니다.
도메인 역시 EIP712Domain 구조체의 hashStruct이며, 흔히 쓰는 필드는 name, version, chainId, verifyingContract, salt입니다. 최종 접두사 0x19 0x01은 EIP-191의 버전 0x01 영역을 씁니다. 모든 도메인 필드가 필수는 아니지만, 빼 버린 경계에서 생기는 재사용 위험은 응용 프로그램이 책임져야 합니다.
타입 데이터는 사용자가 읽을 재료와 계약이 검증할 구조를 맞춰 주지만, 그 내용이 사용자에게 유리한 승인인지 판단해 주지는 않습니다.
JOBCOIN 해설

그림은 이 주제의 공통 개념을 단순화한 설명입니다. 아래 항목에서 이 글의 구체적인 조건과 예외를 함께 읽어보세요.
- 두 해시를 최종 다이제스트에 합칩니다
EIP-712 메시지 해시는 hashStruct(s)=keccak256(typeHash || encodeData(s))입니다.
- 정적 값과 동적 값은 인코딩 방식이 다릅니다
uint·address·bytes32 같은 기본 값은 32바이트 ABI 형태로 encodeData에 들어갑니다.
- 퍼밋 화면을 계약 다이제스트와 맞춥니다
토큰 퍼밋에서 지갑은 owner, spender, value, nonce, deadline을 보여 줄 수 있습니다.
설명을 위한 개념도 · 실제 가격·거래 내역·통계가 아닙니다.
정적 값과 동적 값은 인코딩 방식이 다릅니다
uint·address·bytes32 같은 기본 값은 32바이트 ABI 형태로 encodeData에 들어갑니다. string과 bytes는 내용의 keccak256 해시를 쓰고, 구조체 필드는 그 구조체의 hashStruct를 넣습니다. 배열도 원소별 인코딩을 이어 붙여 해시하므로 일반 abi.encodePacked로 멋대로 바꿔 쓰면 안 됩니다.
다이제스트 순서는 JSON-RPC 요청 객체의 필드 순서로 정해지지 않습니다. types에 선언된 멤버 순서와 정규 의존 타입 정렬이 기준입니다. 숫자를 자바스크립트 부동소수점으로 다루다가 큰 uint 값을 바꾸거나, 체크섬이 붙은 주소 표기와 실제 20바이트 값을 다르게 처리하지 않도록 시험 벡터로 확인합니다.
| 요소 | 역할 | 확인할 위험 |
|---|---|---|
| types | 필드 이름·형식 선언 | 숨은 권한 필드 |
| primaryType | 서명할 최상위 구조체 | 다른 타입 선택 |
| 도메인 name/version | 앱·버전 구분 | 같은 이름 사칭 |
| chainId/계약 | 배포 경계 | 다른 체인에서의 재사용 |
| 메시지 nonce/deadline | 1회·시간 제한 | 재사용·장기 승인 |
퍼밋 화면을 계약 다이제스트와 맞춥니다
토큰 퍼밋에서 지갑은 owner, spender, value, nonce, deadline을 보여 줄 수 있습니다. value가 uint 최댓값이면 사실상 무제한 승인이고, spender가 라우터처럼 보여도 verifyingContract는 토큰 계약입니다. 사용자는 토큰과 spender 두 주소, 그리고 체인을 함께 확인해야 합니다.
계약은 현재 nonces[owner] 값을 메시지와 대조하고 성공하면 올립니다. 도메인 구분자를 배포 당시 chainId로 고정해 캐시하면 체인이 갈라진 뒤 재사용 조건이 생길 수 있으므로, 구현이 chainId 변경을 어떻게 처리하는지 확인합니다. 프록시에서는 verifyingContract가 구현 계약이 아니라 실제 호출과 상태가 있는 프록시 주소인지 검증합니다.
- 기본 타입과 모든 의존 타입 문자열을 출력합니다.
- 도메인의 name·version·chainId·계약을 표시합니다.
- 주소·uint·동적 필드의 인코딩을 시험 벡터로 비교합니다.
- 논스·기한·권한 금액을 사용자 화면에서 빼지 않습니다.
- 계약과 지갑이 만든 다이제스트를 바이트 단위로 대조합니다.
구조화됐다고 해서 안전한 승인은 아닙니다
악성 탈중앙 앱도 EIP-712로 무제한 토큰 사용 한도, NFT 전체 운영자 권한, 마켓플레이스 주문을 깔끔한 형식으로 요청할 수 있습니다. 지갑이 기본 타입 제목만 크게 보여 주고 중요한 중첩 필드를 접어 두면 사용자는 위험을 놓칩니다. 사람이 읽을 수 있다는 말은 정보를 보여 줄 수 있다는 뜻이지 정책 보증이 아닙니다.
로그인 메시지와 자산 이전 권한을 같은 시각적 등급으로 보여 주면 피싱이 쉬워집니다. 지갑은 알려진 스키마의 의미, spender·운영자 권한, 만료, 시뮬레이션 결과를 덧붙여 설명할 수 있지만, 계약 업그레이드나 이후 호출까지 완전히 내다볼 수는 없습니다.
버전·재사용 방지와 실패 증거를 함께 남깁니다
백엔드가 타입 데이터를 만들고 프런트엔드가 서명할 때 JSON 원본만 보관하면, 직렬화 도구 차이 때문에 재현이 어려울 수 있습니다. 정규 타입 문자열, domainSeparator, structHash, 최종 다이제스트, 복구된 서명자를 함께 기록하되 개인 데이터 원본은 최소한만 남깁니다.
검증 시험에는 필드 순서 변경, 타입 이름의 대소문자, chainId·계약 변경, 기한 만료, 이미 쓴 논스, 큰 s 값 서명을 넣습니다. 지갑 미리보기 화면 캡처만으로 계약이 같은 다이제스트를 검증했다고 주장하지 말고, 체인 위 호출 결과와 논스 변화를 확인합니다.
자주 묻는 질문
EIP-712 서명이면 피싱이 불가능한가요?
아닙니다. 내용을 구조적으로 보여 줄 수 있을 뿐, 악성 권한이나 화면의 필드 생략, 사칭을 자동으로 막지는 않습니다.
JSON 필드 순서를 바꾸면 서명이 달라지나요?
다이제스트는 선언된 구조체 멤버 순서와 정규 타입 규칙을 따릅니다. JSON 속성의 임의 순서에 기대면 안 됩니다.
도메인에 chainId와 계약을 넣으면 논스는 필요 없나요?
아닙니다. 도메인은 배포 경계를, 논스는 같은 경계 안의 반복 실행을 제한하는 별도 장치입니다.
더 깊이 읽기
본문에서 다룬 개념과 확인 절차를 다음 글에서 이어서 살펴보세요.



