EIP-712는 사람이 의미를 구분할 수 있는 struct type과 fields를 정규 encoding하고, name·version·chainId·verifyingContract 등을 담는 domain separator와 결합해 0x1901 || domainSeparator || hashStruct(message)를 서명 digest로 만듭니다. 이는 opaque hex보다 지갑이 spender·amount·deadline 같은 값을 구조적으로 표시할 기반을 주지만 UI가 모든 field를 정확히 보여 준다는 보장은 아닙니다. Contract는 type hash와 domain, nonce·deadline을 정확히 검증하고 사용자는 chain·contract·행위·무제한 금액을 확인해야 합니다.
두 개의 hash를 최종 digest에 결합합니다
EIP-712 message hash는 hashStruct(s)=keccak256(typeHash || encodeData(s))입니다. typeHash는 canonical encodeType 문자열의 hash이고 custom struct가 다른 struct를 참조하면 dependencies를 이름순으로 이어 붙입니다. 구현마다 type 문자열 순서가 다르면 같은 화면 데이터도 다른 digest가 됩니다.
Domain도 EIP712Domain struct의 hashStruct이며 일반 fields에는 name, version, chainId, verifyingContract와 salt가 있습니다. 최종 prefix 0x19 0x01은 EIP-191 version 0x01 영역을 사용합니다. 모든 domain field가 필수는 아니지만 생략한 경계의 replay 가능성을 application이 책임져야 합니다.
Typed data는 사용자가 읽을 재료와 계약이 검증할 구조를 맞춰 주지만, 그 내용이 사용자에게 유리한 승인인지 판단해 주지는 않습니다.
JOBCOIN 해설

그림은 이 주제의 공통 개념을 단순화한 설명입니다. 아래 항목에서 이 글의 구체적인 조건과 예외를 함께 읽어보세요.
- 두 개의 hash를 최종 digest에 결합합니다
EIP-712 message hash는 hashStruct(s)=keccak256(typeHash || encodeData(s))입니다.
- 정적 값과 동적 값을 다르게 encoding합니다
uint·address·bytes32 같은 atomic 값은 32바이트 ABI 형태로 encodeData에 놓입니다.
- Permit 화면을 계약 digest와 맞춥니다
Token permit에서 지갑은 owner, spender, value, nonce, deadline을 보여 줄 수 있습니다.
AI로 제작한 개념도 · 실제 가격·거래 내역·통계가 아닙니다.
정적 값과 동적 값을 다르게 encoding합니다
uint·address·bytes32 같은 atomic 값은 32바이트 ABI 형태로 encodeData에 놓입니다. string과 bytes는 내용의 keccak256 hash를 사용하고 struct field는 그 struct의 hashStruct를 넣습니다. Array도 각 원소 encoding을 이어 hash하므로 일반 abi.encodePacked를 임의로 대체하면 안 됩니다.
JSON-RPC 요청 객체의 field 순서가 digest 순서를 정하는 것이 아닙니다. types에 선언된 member order와 canonical dependency ordering이 기준입니다. 숫자를 JavaScript 부동소수점으로 다뤄 큰 uint를 바꾸거나 address checksum 표시와 실제 20바이트를 다르게 처리하지 않게 test vector를 사용합니다.
| 요소 | 역할 | 확인할 위험 |
|---|---|---|
| types | 필드 이름·형식 선언 | 숨은 권한 필드 |
| primaryType | 서명할 root struct | 다른 type 선택 |
| domain name/version | 앱·버전 구분 | 동일 이름 사칭 |
| chainId/contract | 배포 경계 | cross-chain replay |
| message nonce/deadline | 1회·시간 제한 | 재사용·장기 승인 |
Permit 화면을 계약 digest와 맞춥니다
Token permit에서 지갑은 owner, spender, value, nonce, deadline을 보여 줄 수 있습니다. Value가 uint 최대값이면 사실상 무제한 승인이고 spender가 router처럼 보이더라도 verifyingContract는 token 계약입니다. 사용자는 token·spender 두 주소와 chain을 함께 확인해야 합니다.
Contract는 현재 nonces[owner]를 message와 대조하고 성공 시 증가시킵니다. Domain separator를 deployment chainId에 고정 캐시하면 chain split 뒤 replay 조건이 생길 수 있어 구현이 chainId 변경을 어떻게 처리하는지 확인합니다. Proxy에서는 verifyingContract가 implementation이 아니라 호출·상태 주소인 proxy인지 검증합니다.
- Primary type과 모든 dependency type 문자열을 출력합니다.
- Domain name·version·chainId·contract를 표시합니다.
- 주소·uint·dynamic fields의 encoding을 test vector로 비교합니다.
- Nonce·deadline·권한 금액을 사용자 화면에 생략하지 않습니다.
- Contract와 wallet이 만든 digest를 byte 단위로 대조합니다.
구조화됐다는 사실이 안전한 승인을 뜻하지 않습니다
악성 dApp도 EIP-712로 무제한 token allowance, NFT 전체 운영자 권한이나 marketplace order를 명확한 형식으로 요청할 수 있습니다. Wallet이 primary type 제목만 크게 보여 주고 중요한 nested field를 접으면 사용자는 위험을 놓칩니다. Human-readable은 정보 표현 가능성이지 정책 보증이 아닙니다.
Sign-in 메시지와 자산 이전 권한을 같은 시각적 등급으로 보여 주면 phishing이 쉬워집니다. Wallet은 알려진 schema 의미, spender·operator 권한, 만료와 simulation을 추가로 설명할 수 있지만 contract upgrade와 이후 호출까지 완전히 예측할 수는 없습니다.
버전·재생 방지와 실패 증거를 함께 남깁니다
Backend가 typed data를 만들고 frontend가 서명할 때 JSON 원본만 보관하면 serializer 차이로 재현이 어려울 수 있습니다. Canonical type strings, domainSeparator, structHash, final digest와 recovered signer를 함께 기록하되 raw private data는 최소화합니다.
검증 test에는 field 순서 변경, type 이름 대소문자, chainId·contract 변경, 만료, 이미 사용한 nonce, high-s signature를 포함합니다. Wallet preview screenshot만으로 계약이 같은 digest를 검증했다고 주장하지 않고 on-chain call result와 nonce 변화를 확인합니다.
자주 묻는 질문
EIP-712 서명이면 피싱이 불가능한가요?
아닙니다. 내용을 구조적으로 표시할 수 있을 뿐 악성 권한이나 UI의 필드 생략, 사칭을 자동 차단하지 않습니다.
JSON field 순서를 바꾸면 서명이 달라지나요?
Digest는 선언된 struct member order와 canonical type 규칙을 따릅니다. 임의 JSON property order에 의존하면 안 됩니다.
Domain에 chainId와 contract를 넣으면 nonce는 불필요한가요?
아닙니다. Domain은 배포 경계를, nonce는 같은 경계 안에서 반복 실행을 제한하는 별도 장치입니다.
더 깊이 읽기
본문에서 다룬 개념과 확인 절차를 다음 글에서 이어서 살펴보세요.



