USDC 가스리스 전송
EIP-3009 transferWithAuthorization 함수의 작동 방식
transferWithAuthorization은 USDC의 핵심 기능으로, 서명만으로 제3자가 토큰 전송을 실행할 수 있게 합니다. x402 프로토콜의 기술적 기반이 되는 함수입니다.
USDC란?
USDC(USD Coin)는 Circle이 발행하는 스테이블코인으로, 1 USDC = 1 USD의 가치를 유지합니다. 달러 준비금으로 완전히 담보되어 있어 변동성이 없습니다.
$25B+
시가총액
15+
지원 블록체인
6
Decimals
왜 transferWithAuthorization인가?
일반 transfer의 문제
1.
보내는 사람이 직접 트랜잭션 전송 필요
2.
가스비(ETH)가 있어야 함
3.
2단계 승인 필요 (approve + transferFrom)
// 보내는 사람이 실행해야 함
USDC.transfer(to, amount)
USDC.transfer(to, amount)
transferWithAuthorization의 장점
1.
서명만 있으면 누구나 실행 가능
2.
보내는 사람 ETH 불필요 (가스비 대납)
3.
1단계로 완료 (승인 + 전송 동시)
// 누구나 실행 가능
USDC.transferWithAuthorization(
from, to, value, ..., signature
)
USDC.transferWithAuthorization(
from, to, value, ..., signature
)
함수 시그니처
// USDC 컨트랙트의 가스리스 전송 함수 (EIP-3009)
// 서명만 있으면 누구나 이 함수를 호출하여 토큰을 전송할 수 있습니다.
// 가스비는 함수를 호출하는 사람(퍼실리테이터)이 부담합니다.
function transferWithAuthorization(
address from, // 토큰을 보내는 지갑 주소
// 반드시 서명을 생성한 주소와 일치해야 함
address to, // 토큰을 받을 지갑 주소
// 콘텐츠 제공자의 지갑 주소
uint256 value, // 전송할 금액 (6 decimals 적용)
// 예: 10000 = $0.01, 1000000 = $1.00
uint256 validAfter, // 서명 유효 시작 시간 (Unix timestamp)
// 0이면 즉시 유효, 특정 시간 설정 가능
uint256 validBefore, // 서명 만료 시간 (Unix timestamp)
// 이 시간 이후 서명은 무효화됨
bytes32 nonce, // 32바이트 고유 식별자
// 같은 서명의 재사용 방지 (replay attack 차단)
uint8 v, // ECDSA 서명의 recovery id (27 또는 28)
bytes32 r, // ECDSA 서명의 r 값 (32바이트)
bytes32 s // ECDSA 서명의 s 값 (32바이트)
) external;파라미터 설명
| from | 서명자 주소. 이 주소에서 토큰이 차감됩니다. |
| to | 수신자 주소. 이 주소로 토큰이 전송됩니다. |
| value | 전송 금액. 6 decimals 적용 (1000000 = $1.00) |
| validAfter | 이 시간 이후에만 유효. 0이면 즉시 유효. |
| validBefore | 이 시간 이전에만 유효. 만료 시간. |
| nonce | 32바이트 고유 식별자. replay attack 방지. |
| v, r, s | EIP-712 서명 값. from 주소의 개인키로 서명. |
실행 흐름
1
서명 생성 (오프체인)
클라이언트가 EIP-712 형식으로 메시지에 서명합니다. 이 과정에서 가스비가 발생하지 않습니다.
2
서명 전달
서명을 서버 또는 퍼실리테이터에게 전달합니다. HTTP 헤더, API 요청 등 다양한 방식으로 전달 가능합니다.
3
트랜잭션 실행 (온체인)
퍼실리테이터가 transferWithAuthorization을 호출합니다. 가스비는 퍼실리테이터가 부담합니다.
4
토큰 전송
USDC 컨트랙트가 서명을 검증하고, from에서 to로 토큰을 전송합니다. 서명이 유효하지 않으면 전체 트랜잭션이 취소됩니다.
USDC 컨트랙트 주소
완전한 예제
import { ethers, Wallet, JsonRpcProvider, Signature } from 'ethers';
// ============================================================
// USDC 컨트랙트 ABI (필요한 함수만 포함)
// ============================================================
// 전체 ABI가 아닌 사용할 함수만 정의하면 번들 크기를 줄일 수 있습니다.
const USDC_ABI = [
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)"
];
/**
* 퍼실리테이터가 클라이언트의 서명을 사용해 결제를 실행합니다.
* 이 함수는 서버 측(퍼실리테이터)에서 실행됩니다.
*
* 흐름: 클라이언트가 서명 생성 → 서버가 이 함수로 온체인 실행
*/
async function executePayment(
provider: JsonRpcProvider, // ethers v6 Provider
// 블록체인 연결 (Alchemy, Infura 등)
executorWallet: Wallet, // 퍼실리테이터의 지갑
// 이 지갑이 가스비를 지불합니다
// ETH 잔액이 있어야 함!
usdcAddress: string, // USDC 컨트랙트 주소
// Base: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
from: string, // 토큰 보내는 사람 (클라이언트 지갑)
to: string, // 토큰 받는 사람 (콘텐츠 제공자)
value: string, // 전송 금액 (6 decimals)
validAfter: number, // 유효 시작 시간
validBefore: number, // 유효 종료 시간
nonce: string, // 고유 식별자
signature: string // 클라이언트가 EIP-712로 생성한 서명
// 65바이트 hex string
) {
// ============================================================
// 1단계: USDC 컨트랙트 인스턴스 생성
// ============================================================
// executorWallet을 연결하면 이 지갑으로 트랜잭션이 전송됩니다.
const usdc = new ethers.Contract(usdcAddress, USDC_ABI, executorWallet);
// ============================================================
// 2단계: 서명 분해 (v, r, s)
// ============================================================
// ethers v6에서는 Signature.from()을 사용합니다.
// EIP-712 서명은 65바이트로, r(32) + s(32) + v(1)로 구성됩니다.
const sig = Signature.from(signature);
// ============================================================
// 3단계: 온체인 트랜잭션 실행
// ============================================================
// transferWithAuthorization을 호출하면:
// - USDC 컨트랙트가 서명 검증
// - 유효하면 from → to로 토큰 전송
// - 가스비는 executorWallet에서 차감
const tx = await usdc.transferWithAuthorization(
from, // 서명자 주소 (토큰 출금 주소)
to, // 수신자 주소 (토큰 입금 주소)
value, // 전송 금액
validAfter, // 유효 시작
validBefore, // 유효 종료
nonce, // 고유 ID
sig.v, // 서명 v (recovery id: 27 또는 28)
sig.r, // 서명 r (32바이트)
sig.s // 서명 s (32바이트)
);
// ============================================================
// 4단계: 트랜잭션 확인 대기
// ============================================================
// wait()는 트랜잭션이 블록에 포함될 때까지 대기합니다.
// Base에서는 보통 2-3초 소요됩니다.
const receipt = await tx.wait();
// 성공 결과 반환
// hash로 블록 익스플로러에서 트랜잭션 확인 가능
return {
success: true,
txHash: receipt.hash // ethers v6: transactionHash → hash
};
}