EIP-712 서명
EIP-712는 이더리움의 타입화된 구조적 데이터 해싱 및 서명 표준입니다. x402에서는 USDC의 transferWithAuthorization 함수를 호출하기 위한 서명을 생성하는 데 사용됩니다.
왜 EIP-712인가?
기존 서명 방식의 문제
eth_sign("0x9a8b7c6d5e...")
// 사용자가 보는 것:
// "서명하시겠습니까?"
// 0x9a8b7c6d5e4f3a2b...
//
// 무엇에 서명하는지 알 수 없음!일반 해시 서명은 사용자에게 의미 없는 16진수만 보여줍니다. 악의적인 트랜잭션에 서명할 위험이 있습니다.
EIP-712의 해결책
eth_signTypedData_v4
// 사용자가 보는 것:
// Domain: USDC
// Action: Transfer With Authorization
// From: 0x390e4Ce3...
// To: 0x742d35Cc...
// Amount: 0.01 USDC
//
// 정확히 무엇에 서명하는지 확인 가능!구조화된 데이터를 사람이 읽을 수 있는 형태로 보여줍니다. 서명 전에 정확한 내용을 확인할 수 있습니다.
EIP-712 구조
1. Domain Separator
서명이 어떤 컨트랙트/애플리케이션을 위한 것인지 식별합니다. 다른 앱의 서명을 재사용하는 것을 방지합니다.
// USDC의 EIP-712 Domain Separator
// 이 정보는 서명이 "어떤 컨트랙트를 위한 것인지" 명시합니다.
// 다른 앱에서 만든 서명을 재사용하는 크로스-프로토콜 공격을 방지합니다.
{
name: "USD Coin", // USDC 컨트랙트의 공식 이름
// "USDC"가 아닌 "USD Coin"이어야 함!
version: "2", // USDC 컨트랙트 버전 (Circle이 정의)
chainId: 84532, // 체인 ID (Base Sepolia 테스트넷)
// Base Mainnet은 8453
verifyingContract: "0x036CbD53..." // USDC 컨트랙트 배포 주소
// 반드시 해당 체인의 공식 주소 사용
}주의: Domain의 name과 version은 반드시 USDC 컨트랙트에 정의된 값과 일치해야 합니다. 잘못된 값을 사용하면 서명이 무효화됩니다.
2. Type Definitions
서명할 데이터의 구조를 정의합니다. USDC의 transferWithAuthorization은 아래 타입을 사용합니다.
// EIP-712 타입 정의
// 서명할 데이터의 "스키마"를 정의합니다.
// USDC 컨트랙트가 기대하는 정확한 필드명과 타입을 사용해야 합니다.
const types = {
// "TransferWithAuthorization" - USDC의 가스리스 전송 함수 이름
TransferWithAuthorization: [
{ name: "from", type: "address" }, // 토큰을 보내는 지갑 주소
{ name: "to", type: "address" }, // 토큰을 받는 지갑 주소
{ name: "value", type: "uint256" }, // 전송할 금액 (6 decimals)
{ name: "validAfter", type: "uint256" }, // 서명 유효 시작 시간
{ name: "validBefore", type: "uint256" },// 서명 만료 시간
{ name: "nonce", type: "bytes32" } // 중복 사용 방지용 고유 식별자
]
};3. Message (실제 데이터)
서명할 실제 값입니다. 타입 정의에 맞춰 작성해야 합니다.
// 실제 서명할 메시지 데이터
// 이 값들이 사용자의 지갑에 표시되어 서명 전 확인할 수 있습니다.
const message = {
from: "0x390e4Ce3...", // 보내는 사람 주소 (서명자 본인의 지갑)
// 이 주소에서 USDC가 차감됩니다
to: "0x742d35Cc...", // 받는 사람 주소 (콘텐츠 제공자의 지갑)
// 이 주소로 USDC가 입금됩니다
value: "10000", // 전송할 금액 (단위: 마이크로 달러)
// USDC는 6 decimals이므로:
// 10000 = $0.01 (1센트)
// 1000000 = $1.00 (1달러)
validAfter: 0, // 서명 유효 시작 시간 (Unix timestamp)
// 0 = 즉시 유효 (제한 없음)
// 특정 시간 이후에만 실행되게 하려면 값 지정
validBefore: 1768474060, // 서명 만료 시간 (Unix timestamp)
// 이 시간이 지나면 서명이 무효화됩니다
// 보안을 위해 현재시간 + 60초 권장
nonce: "0xabc123..." // 32바이트 고유 식별자
// 같은 서명의 재사용(replay attack)을 방지
// crypto.randomBytes(32)로 생성 권장
};코드 예제
import { ethers, Signer } from 'ethers';
/**
* USDC transferWithAuthorization을 위한 EIP-712 서명을 생성합니다.
* 이 서명은 가스비 없이 USDC를 전송할 수 있게 해줍니다.
*
* @param signer - ethers.js v6 Signer 인스턴스
* @param to - 받는 사람 지갑 주소
* @param amount - 전송할 금액 (6 decimals, "10000" = $0.01)
* @param usdcAddress - 해당 체인의 USDC 컨트랙트 주소
* @param chainId - 블록체인 네트워크 ID (Base Sepolia: 84532)
*/
async function signTransferWithAuthorization(
signer: Signer,
to: string,
amount: string,
usdcAddress: string,
chainId: number
) {
// ========================================
// 1단계: Domain Separator 정의
// ========================================
// 서명이 USDC 컨트랙트용임을 증명합니다.
// 이 값들은 USDC 컨트랙트에 하드코딩된 값과 일치해야 합니다.
const domain = {
name: "USD Coin", // USDC v2 컨트랙트의 공식 이름
version: "2", // USDC 컨트랙트 버전
chainId: chainId, // 체인 ID (다른 체인 서명 재사용 방지)
verifyingContract: usdcAddress // USDC 컨트랙트 주소
};
// ========================================
// 2단계: Type 정의
// ========================================
// USDC 컨트랙트의 transferWithAuthorization 함수가
// 기대하는 정확한 파라미터 구조입니다.
const types = {
TransferWithAuthorization: [
{ name: "from", type: "address" }, // 보내는 사람
{ name: "to", type: "address" }, // 받는 사람
{ name: "value", type: "uint256" }, // 금액
{ name: "validAfter", type: "uint256" }, // 유효 시작
{ name: "validBefore", type: "uint256" }, // 유효 종료
{ name: "nonce", type: "bytes32" } // 고유 ID
]
};
// ========================================
// 3단계: 보안 파라미터 생성
// ========================================
// nonce: 32바이트 랜덤 값으로 replay attack 방지
// ethers v6에서는 ethers.randomBytes() 사용
const nonce = ethers.hexlify(ethers.randomBytes(32));
// validBefore: 현재 시간 + 60초
// 서명이 1분 후 만료되어 도용 위험 최소화
const validBefore = Math.floor(Date.now() / 1000) + 60;
// ========================================
// 4단계: 메시지 구성
// ========================================
const signerAddress = await signer.getAddress();
const message = {
from: signerAddress, // 서명자 = 토큰 보내는 사람
to: to, // 파라미터로 받은 수신자
value: amount, // 파라미터로 받은 금액
validAfter: 0, // 즉시 유효 (시작 제한 없음)
validBefore: validBefore,// 1분 후 만료
nonce: nonce // 랜덤 생성된 고유 ID
};
// ========================================
// 5단계: EIP-712 서명 생성
// ========================================
// ethers v6에서는 signer.signTypedData() 사용
// domain + types + message를 해시하고 서명합니다.
// 결과: 65바이트 서명 (v, r, s 포함)
const signature = await signer.signTypedData(domain, types, message);
// 서명과 메시지 데이터를 함께 반환
// 서버/퍼실리테이터가 이 정보로 온체인 트랜잭션 실행
return {
signature, // 65바이트 서명 문자열
authorization: message // 서명된 메시지 (검증용)
};
}해시 계산 과정
1. Domain Separator Hash
domainSeparator = keccak256(encode(EIP712Domain, domain))
2. Struct Hash
structHash = keccak256(encode(typesHash, message))
3. Final Message Hash
messageHash = keccak256("\x19\x01" + domainSeparator + structHash)
4. Signature
(v, r, s) = sign(messageHash, privateKey)
보안 고려사항
Nonce 관리
각 서명에 고유한 nonce를 사용해야 합니다. 같은 nonce를 재사용하면 replay attack에 취약해집니다. 랜덤 바이트 생성을 권장합니다.
시간 제한
validBefore를 현재 시간 + 1분 정도로 설정하세요. 너무 긴 유효 기간은 서명 도용 위험을 높입니다.
금액 확인
서명 전에 반드시 value(금액)를 확인하세요. USDC는 6 decimals를 사용하므로 1000000 = $1.00, 10000 = $0.01입니다.