EIP-3009は、ERC-20トークンの保有者がEIP-712形式の署名だけで「この相手に、この金額を、この期間内に1回だけ送る」と認可できる規格です。署名を受け取った第三者が transferWithAuthorization を呼び出してガス代を払うため、保有者はETHを持たなくても送金できます。USDCが採用し、AIエージェント決済のx402もこの関数を既定の送金方式にしています。
この記事では仕様の要点とEIP-2612・Permit2との違いを整理したうえで、2026年11月8日に各チェーンのコントラクトへ問い合わせて確かめた署名ドメインと対応状況、EIP-7702で委任したアカウントの署名が失敗する条件、OpenZeppelin Contracts 5.7.0での実装手順を示します。
まとめ:EIP-3009の要点と実装判断
- EIP-3009は2020年9月28日作成のERCで、2026年11月8日時点のステータスはDraftです。EIP-3009とERC-3009は同じ規格を指します。
- 必須の関数は
transferWithAuthorization・receiveWithAuthorization・authorizationStateの3つで、cancelAuthorizationは任意の拡張です。 - nonceはランダムな32バイト値なので、複数の認可を順不同で実行できます。他のコントラクトから呼ぶときは、フロントランを避けるため
receiveWithAuthorizationを使います。 - 署名ドメインはチェーンで変わります。主要6チェーンのUSDCは「USD Coin」、Sepolia系は「USDC」、PolygonのUSDC.eは
chainIdの代わりにsaltを使います。 - EthereumのUSDTとDAIは非対応ですが、ArbitrumとOptimismのUSDT0は対応しています。USDC.eはArbitrumとOptimismで非対応です。
- EIP-7702で委任済みのアカウントは、委任先がERC-1271を実装していないとUSDCで
invalid signatureになります。 - 自前トークンにはOpenZeppelinの
ERC3009が使えますが、npm i @openzeppelin/contractsで入る5.6.1には含まれないため、5.7.0を指定します。
EIP-3009(ERC-3009)の仕様と規格上の位置付け
ステータスDraft・EIP-20とEIP-712に依存する規格
EIP-3009の仕様ページによると、タイトルは「Transfer With Authorization」、著者はPeter Jihoon Kim、Kevin Britz、David Knottの3名、作成日は2020年9月28日です。種別はStandards TrackのERCで、EIP-20とEIP-712に依存します。仕様の原文は現在 ethereum/ERCsリポジトリで管理されています。
ステータスはDraftのままで、比較されることの多いEIP-2612はFinalです。それでもUSDCをはじめ複数の本番トークンが同じインターフェースで実装しており、事実上の標準として扱えます。EIPは提案の通し番号、ERCはアプリケーション層の規格という分類名なので、どちらの表記でも指す規格は同じです。
必須の関数・イベントと任意のcancelAuthorization
| 要素 | 区分 | 役割 |
|---|---|---|
transferWithAuthorization |
必須 | 署名済みの認可で送金を実行 |
receiveWithAuthorization |
必須 | 受取人本人だけが実行できる版 |
authorizationState |
必須 | nonceが使用済みかを返す |
AuthorizationUsed |
必須イベント | 認可の使用を記録 |
cancelAuthorization |
任意 | 未使用の認可を署名で無効化 |
AuthorizationCanceled |
任意イベント | 無効化を記録 |
署名対象の型文字列は次の3つです。1文字でも違うとハッシュが変わり、署名検証に失敗します。
TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)
ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)
CancelAuthorization(address authorizer,bytes32 nonce)
transferWithAuthorizationの引数・有効期間・nonce
from・to・valueなど6つの値と署名の渡し方
transferWithAuthorization は、from(支払者)、to(受取人)、value(金額)、validAfter・validBefore(有効期間)、nonce の6つの値と署名を受け取ります。仕様上の署名は uint8 v, bytes32 r, bytes32 s の3引数です。
SafeなどのコントラクトウォレットはERC-1271の isValidSignature で署名を検証します。署名の長さはウォレットの実装や設定によって異なり、この3引数で渡せる65バイトに収まらない場合があります。そこでCircleはFiatToken 2.2.0(2023年11月9日)で、署名を bytes signature で受け取るオーバーロードを追加しました。
validAfter・validBeforeの境界とブロック時刻のずれ
有効期間の判定は、Circleの実装もOpenZeppelinの実装も block.timestamp > validAfter かつ block.timestamp < validBefore で、等号を含みません。validAfter を0にすれば即時有効、未来の時刻にすれば「今署名して、指定日以降に実行できる」予約送金になります。
比較に使うのは端末の時計ではなくブロックの時刻です。validBefore を現在時刻の1秒前にした署名は、USDCへの eth_call では有効として通りました。評価に使う最新ブロックの時刻が端末より前だったためです。期限切れの判定を端末側で済ませず、Ethereumの12秒のスロット間隔と収録待ちを見込んだ余裕を持たせてください。
nonceはクライアントが生成するランダムな32バイト値です。コントラクトは (from, nonce) の組を使用済みとして記録し、同じ組での2回目の実行をrevertさせます。事前に確かめるには authorizationState(from, nonce) を呼びます。
仕様はランダムnonceの理由として、連番nonceでは番号が先の取引を送るとrevertしてガスが無駄になる点を挙げています。順不同で実行できることは、後半のHardhatの検証で確かめます。
receiveWithAuthorizationによるフロントラン対策
transferWithAuthorization は署名さえあれば誰でも実行できます。問題になるのは、決済コントラクトが内部でこの関数を呼ぶ場合です。攻撃者がメンプールから署名を抜き出して先に直接呼ぶと、送金だけが完了し、決済コントラクト側の後続処理は実行されません。
仕様は、他のスマートコントラクトから呼ぶときは receiveWithAuthorization を使うよう求めています。この関数は「呼び出し元 = 受取人 to」を追加で検査し、署名の型文字列も別なので、transferWithAuthorization 用の署名は流用できません。
受取人がEOAでリレイヤーが送金だけを代行するなら transferWithAuthorization、受取人がコントラクトで入金と同じトランザクション内で処理を続けるなら receiveWithAuthorization を選びます。
EIP-2612(permit)・Permit2との違いと選び方
| 項目 | EIP-3009 | EIP-2612 permit | Permit2(SignatureTransfer) |
|---|---|---|---|
| 署名が許可するもの | 1回の送金 | allowanceの設定 | Permit2経由の送金 |
| nonce | ランダム32バイト | 連番(nonces[owner]) | ビットマップ |
| 実行後の許可残り | 残らない | 上限まで残る | 署名単位で消費 |
| トークン側の対応 | 必要 | 必要 | 不要(初回approve) |
| 仕様ステータス | Draft | Final | Uniswapのコントラクト |
EIP-2612の署名は approve の代替で、通ると spender にallowanceが付き、上限まで何度でも transferFrom できます。EIP-3009の署名は受取人と金額が固定された1回分の送金にしか使えません。
継続的な引き落としならEIP-2612、1回ごとに金額が決まる支払いならEIP-3009を選びます。どちらにも非対応のトークンにはUniswapのPermit2が使えますが、最初に1回、Permit2コントラクトへの approve がオンチェーンで必要です。
チェーン別のEIP-712ドメインと対応トークン(2026年11月実測)
USDCのチェーン・トークン種別ごとの署名ドメイン
EIP-712署名の失敗で最初に疑うのがドメインです。各チェーンの公開RPCへ name()・version()・DOMAIN_SEPARATOR() を問い合わせ、ローカルで計算したハッシュが一致することを確かめた結果です。
| チェーン | トークン | name | version |
|---|---|---|---|
| Ethereum・Base・Arbitrum・Optimism・Polygon・Avalanche | USDC | USD Coin | 2 |
| Sepolia・Base Sepolia | USDC | USDC | 2 |
| Ethereum | EURC | Euro Coin | 2 |
| Base | EURC | EURC | 2 |
| Polygon | USDC.e(PoS) | USD Coin (PoS) | 1(salt) |
テストネットで動いた署名コードでも、このドメインの差を反映しないと、本番で invalid signature になります。同じEURCでもEthereumとBaseでnameが違います。ドメインはハードコードせず、対象アドレスから読み取ります。
PolygonのUSDC.eのchainIdを省略したsalt型ドメイン
PolygonのUSDC.e(0x2791...4174)は、ドメインに chainId を含みません。代わりに salt へチェーンIDを32バイトで入れます。標準形式で署名すると GasAbstraction: invalid signature で失敗し、次の形式なら署名検証を通過しました。関数も一部しかなく、receiveWithAuthorization とbytes署名版はありません。
const domain = {
name: "USD Coin (PoS)",
version: "1",
verifyingContract: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
salt: ethers.zeroPadValue(ethers.toBeHex(137), 32),
};
Polygonで新規に実装するなら、標準どおりのネイティブUSDC(0x3c49...3359)を使う方が分岐を増やさずに済みます。
主要ステーブルコインの対応状況とUSDT0の例外
次の表は、バイトコード内の関数セレクタと eth_call の応答を併用した結果です(Ethereumはブロック26,369,638時点)。○は関数あり、×は関数なしです。USDGはセレクタ検索では検出できず、authorizationState の応答と署名エラーで判定しました。バイトコードの検索だけで「非対応」と結論しないでください。
| トークン(チェーン) | v,r,s版 | bytes版 | receive | permit |
|---|---|---|---|---|
| USDC(上記の主要6チェーン) | ○ | ○ | ○ | ○ |
| EURC(Ethereum・Base) | ○ | ○ | ○ | ○ |
| PYUSD・USDP・USDG(Ethereum) | ○ | ○ | ○ | ○ |
| FDUSD(Ethereum) | ○ | ○ | ○ | ○ |
| USDT0(Arbitrum・Optimism) | ○ | ○ | ○ | ○ |
| USD1(Ethereum) | ○ | × | ○ | ○ |
| JPYC(Ethereum・Polygon) | ○ | × | ○ | ○ |
| USDC.e(Polygon) | ○ | × | × | ○ |
| USDC.e(Arbitrum) | × | × | × | ○ |
| USDC.e(Optimism)・USDbC(Base) | × | × | × | × |
| USDT0(Polygon) | × | × | × | ○ |
| USDT(Ethereum) | × | × | × | × |
| DAI(Ethereum) | × | × | × | 独自形式 |
| USDS・RLUSD・USDe・GHO(Ethereum) | × | × | × | ○ |
同じ名前でも別実装のことがあるので、USDC以外の主なアドレスを記します。
| トークン(チェーン) | コントラクトアドレス |
|---|---|
| USDT0(Arbitrum) | 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9 |
| USDT0(Optimism) | 0x01bFF41798a0BcF287b996046Ca68b395DbC1071 |
| USD1(Ethereum) | 0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d |
| JPYC(Ethereum・Polygon) | 0xE7C3D8C9a439feDe00D2600032D5dB0Be71C3c29 |
| USDC.e(Polygon) | 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 |
| USDC.e(Arbitrum) | 0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8 |
「USDTは使えない」と一括りにはできません。EthereumのUSDTは非対応ですが、ArbitrumとOptimismのUSDT0はドメイン名「USD₮0」、version「1」で署名検証を通過します。Polygonは同じUSDT0の名前でも transferWithAuthorization がなく、permitのみでした。
USD1とJPYCのv,r,s版は、ERC-1271のウォレットを from にすると EIP3009: invalid signature で拒否されました(Ethereumフォークで確認)。同じ条件でFDUSDは署名検証を通過し、cancelAuthorization だけを持っていません(JPYCの詳細はJPYCの仕組みと4チェーン対応)。Paxos系のPYUSDとUSDPは、仕様に無い transferWithAuthorizationBatch も公開していますが(PYUSDのリリース履歴)、複数トークン共通のリレイヤーでは仕様の単発関数に揃える方が安全です。
Circle Gatewayの少額決済は別の扱いです。Circleの署名手順では、同じ型の認可でもドメイン名は GatewayWalletBatched、verifyingContract はGatewayWallet、validBefore は3日以上先と定めています。
メインネットUSDCで検証したEIP-712署名の組み立て
ドメインをチェーンから取得する署名コード
次のコードは、残高ゼロの使い捨て鍵で署名し、EthereumメインネットのUSDCに eth_call で検証させるものです。資金は動きません。ethers v6.17.0とNode.js v26.5.0で実行しました。npm i [email protected] のあと、verify.mjs に保存して node verify.mjs で起動します。
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://ethereum-rpc.publicnode.com");
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const usdc = new ethers.Contract(USDC, [
"function name() view returns (string)",
"function version() view returns (string)",
"function DOMAIN_SEPARATOR() view returns (bytes32)",
"function transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)",
], provider);
const payer = ethers.Wallet.createRandom(); // 残高ゼロの使い捨て鍵
const relayer = ethers.Wallet.createRandom().address;
const domain = {
name: await usdc.name(), // "USD Coin"
version: await usdc.version(), // "2"
chainId: (await provider.getNetwork()).chainId, // 1n
verifyingContract: USDC,
};
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" },
],
};
const message = {
from: payer.address,
to: "0x000000000000000000000000000000000000dEaD",
value: 1_000_000n, // 1 USDC(decimals=6)
validAfter: 0,
validBefore: Math.floor(Date.now() / 1000) + 600,
nonce: ethers.hexlify(ethers.randomBytes(32)),
};
console.log(ethers.TypedDataEncoder.hashDomain(domain) === await usdc.DOMAIN_SEPARATOR());
const sig = ethers.Signature.from(await payer.signTypedData(domain, types, message));
try {
await usdc.transferWithAuthorization.staticCall(
message.from, message.to, message.value, message.validAfter,
message.validBefore, message.nonce, sig.v, sig.r, sig.s, { from: relayer });
} catch (e) {
console.log(e.reason);
}
1行目の出力は true で、ドメインセパレーターがUSDCの返す値と一致しました。ブラウザでは、ウォレットに eth_signTypedData_v4 で同じ domain・types・message を使い、types に EIP712Domain を加えて primaryType を TransferWithAuthorization としたJSONを渡すと、利用者に内容が表示されたうえで署名されます(ウォレットの基本はMetaMaskの使い方と安全対策を参照)。
改ざん・期限切れ・呼び出し元違いで返るrevert理由
| 条件 | revert理由 |
|---|---|
| 正しい署名(支払者の残高0) | ERC20: transfer amount exceeds balance |
署名後に value を書き換え |
FiatTokenV2: invalid signature |
validBefore を1時間前にして署名 |
FiatTokenV2: authorization is expired |
validAfter を1時間後にして署名 |
FiatTokenV2: authorization is not yet valid |
受取人以外が receiveWithAuthorization |
FiatTokenV2: caller must be the payee |
残高不足で止まるのは、署名の検証を通過した証拠です。本番で invalid signature が出たら、まずドメインのnameとversion、次にchainIdと型文字列を確認します。有効期間やnonceを変えた場合は、署名検証より先に期限や使用済み認可の検査で失敗することがあります。
EIP-7702で委任したアカウントの署名が通らない条件
EIP-7702で委任したEOAは秘密鍵を持ったままなので、従来どおりECDSAで署名できます。ところがUSDCでは、この署名が拒否される場合があります。EIP-7702のアカウントには 0xef0100 で始まる委任コードが置かれ、EXTCODESIZE は23を返します。一方、FiatTokenの署名検証はコードの有無で経路を分け、コードがあれば isValidSignature だけを呼びます。
Ethereumメインネットをanvilでフォークし、同じ鍵のECDSA署名を委任の状態だけ変えて検証した結果です。
| 支払者の状態 | v,r,s版・bytes版の結果 |
|---|---|
| 委任なし | 署名検証を通過(残高不足で停止) |
| ERC-1271なしの実装へ委任 | FiatTokenV2: invalid signature |
| ERC-1271ありの実装へ委任 | 署名検証を通過(残高不足で停止) |
| 委任解除(ゼロアドレスへ委任) | 署名検証を通過(残高不足で停止) |
ウォレットやx402のクライアントを実装するなら、署名前に次の判定を入れます。委任中のアカウントについては、委任先のコントラクトが isValidSignature を実装しているかを確認してください。
const code = await provider.getCode(payer.address);
const delegated = code.startsWith("0xef0100");
OpenZeppelin 5.7.0の基底 ERC3009 は ECDSA.recover のみで検証するため、この分岐の影響を受けません。そのかわり、コントラクトウォレットの署名は受け取れません(5.7.0のソース)。同じ「ERC-3009対応」でも、署名の受け取り方は実装ごとに異なります。
リレイヤーの送信前検査とガス代の見積もり
リレイヤーは失敗したトランザクションのガスも負担します。送信前に確かめる順序は、実装の手間と損失の大きさで決めます。
staticCallで実行をシミュレーションする。署名・期限・nonce・残高の不備をまとめて落とせます。- 同じ認可の多重送信を防ぐ。
(from, nonce)の処理中フラグをリレイヤー側で持ち、authorizationStateと併用します。 - ブロックリストの宛先を除外する。Arcのリレイヤー手順は、ブロックリスト対象の
from・toはrevertしてガスだけを消費すると注意しています。
今回の条件ではコストは通常の送金の約1.9倍でした。USDCをEthereumフォークで実送金して測ると、transferWithAuthorization は85,601ガス、transfer は44,908ガスでした(残高のある宛先への送金)。署名検証とnonceの書き込みの分だけ増え、差の40,693ガスはリレイヤーの負担です。
EIP-3009自体には手数料を支払う仕組みがないため、ガス代の回収はサービス側で設計します。商品代金の認可とは別に、リレイヤーを受取人、手数料を金額とする認可を受け取る方法があり、両方の支払いを保証するなら同一トランザクションで処理します。自社の決済基盤にステーブルコイン受付を組み込む場合は、決済・サブスクリプションシステム開発もご相談いただけます。
OpenZeppelin 5.7.0のERC3009で自前トークンに実装する手順
OpenZeppelin 5.7.0のバージョン指定とインストール
OpenZeppelin Contractsは5.7.0(2026年7月29日公開)で token/ERC20/extensions/draft-ERC3009.sol を追加しました。2026年11月8日時点でも、npmのdist-tagは latest が5.6.1、dev が5.7.0です。npm i @openzeppelin/contracts では5.6.1が入り、ERC3009はimportできません。
Hardhatも同様で、npm i hardhat は3系を入れます。ここではHardhat 2.29.1で、hardhat-ethersプラグインを読み込み、Solidity 0.8.30・evmVersion pragueを設定して動かしました(Hardhat 3の導入はHardhatの使い方を参照)。
npm i -D [email protected] @nomicfoundation/hardhat-ethers@3 [email protected] @openzeppelin/[email protected]
hardhat.config.js は次のとおりです。コントラクトは contracts/DemoToken.sol に、後述のスクリプトは scripts/run.js に保存し、先頭で const { ethers } = require("hardhat"); を読み込んで全体をasync関数で包み、npx hardhat run scripts/run.js で起動します。
require("@nomicfoundation/hardhat-ethers");
module.exports = {
solidity: { version: "0.8.30", settings: { evmVersion: "prague", optimizer: { enabled: true, runs: 200 } } },
};
トークン本体は、ERC3009 を継承して EIP712 のコンストラクターにドメインのnameとversionを渡すだけで作れます。コンパイルは上の設定で通りました。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ERC3009} from "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC3009.sol";
contract DemoToken is ERC3009 {
constructor() ERC20("Demo USD", "DUSD") EIP712("Demo USD", "1") {
_mint(msg.sender, 1_000_000 * 10 ** decimals());
}
}
5.7.0には独自拡張があり、validAfter と validBefore の両方でビット47を立てると、時刻ではなくブロック番号で期間を判定します。フラグが無ければ仕様どおりタイムスタンプで判定します。
ERC-1271対応のERC20TransferAuthorizationとnonceの制約
コントラクトウォレットの署名を受けるなら、同じ5.7.0の ERC20TransferAuthorization を継承します。bytes signature 版の関数が加わり、ERC-1271のウォレットからの認可もHardhatで実行できました。
ただし、nonceの扱いがEIP-3009の説明と変わります。32バイトを上位192ビットのキーと下位64ビットの連番に分けるため、ランダムな32バイトを渡すと InvalidAccountNonce でrevertしました。同じキーで連番を飛ばしても同じ結果です。並行して実行したい認可にはキーを変え、下位64ビットを0から順に使います。USDC向けのクライアントをそのまま流用すると、この実装では最初の送金で失敗します。
ETHを持たない支払者の送金と順不同の実行をHardhatで確認
Hardhat 2.29.1のローカルネットワークで、ETH残高0の支払者が署名した2件の認可を、リレイヤーが後に署名した方から実行しました。実行したスクリプトの抜粋です。
const [deployer, relayer, shop] = await ethers.getSigners();
const payer = ethers.Wallet.createRandom().connect(ethers.provider); // ETHを持たない
const token = await ethers.deployContract("DemoToken");
await token.transfer(payer.address, ethers.parseUnits("100", 18));
// types は上のUSDC例と共通。domain はDemoToken用に作る
const domain = {
name: "Demo USD", version: "1",
chainId: (await ethers.provider.getNetwork()).chainId,
verifyingContract: await token.getAddress(),
};
const now = (await ethers.provider.getBlock("latest")).timestamp;
const sign = async (value) => {
const m = { from: payer.address, to: shop.address, value,
validAfter: 0, validBefore: now + 3600,
nonce: ethers.hexlify(ethers.randomBytes(32)) };
const s = ethers.Signature.from(await payer.signTypedData(domain, types, m));
return [m.from, m.to, m.value, m.validAfter, m.validBefore, m.nonce, s.v, s.r, s.s];
};
const a = await sign(ethers.parseUnits("10", 18)); // 先に署名
const b = await sign(ethers.parseUnits("20", 18)); // 後に署名
await (await token.connect(relayer).transferWithAuthorization(...b)).wait();
await (await token.connect(relayer).transferWithAuthorization(...a)).wait();
console.log(ethers.formatEther(await ethers.provider.getBalance(payer.address))); // 0.0
console.log(ethers.formatUnits(await token.balanceOf(shop.address), 18)); // 30.0
支払者のETH残高は0.0のまま、受取人に30トークンが届きました。同じ認可を再送すると ERC3009UsedAuthorization でrevertします。USDCの文字列メッセージとは形式が違うので、リレイヤーでエラーを判定するときはトークンごとにrevert形式を確認してください。
x402がEIP-3009を既定の送金方式にした理由と非対応トークンの扱い
x402は、HTTPの402 Payment Requiredを使って、APIやAIエージェントがその場で支払う決済プロトコルです。仕様はx402-foundation/x402で管理され、coinbase/x402 はそのforkです。
EVM向けの exact スキームは送金方式を3つ定義し、assetTransferMethod の指定がなければ "eip3009" を使います。
| 方式 | 対象 | 仕様上の位置付け |
|---|---|---|
| EIP-3009 | transferWithAuthorization を持つトークン |
Recommended(最も単純で真にガスレス) |
| Permit2 | EIP-3009を持たないERC-20 | Universal Fallback |
| ERC-7710 | 委任に対応したスマートアカウント | Smart Account Option |
EIP-3009が既定なのは、エージェントが承認トランザクションを送らず署名だけで済むからです。参照実装の @x402/evm 2.30.0では、既定資産31件のうち25件がEIP-3009、6件がPermit2(MegaUSDやSBCなどEIP-3009を持たないトークン)でした。Permit2は事前の approve が前提で、ガスレスにするには追加の手当てが要ります。エージェント決済の全体像はAP2のマンデートとx402の関係、HTTP 402の別系統はMPPとx402の違いで扱っています。
EIP-3009を採用しない方がよい場面と失敗パターン
- 既存トークンをアップグレードできない:規格はトークン本体への実装が前提で、プロキシでないコントラクトには後付けできません。Permit2かERC-4337のPaymasterに切り替えます。
- 毎月同じ相手に引き落とさせたい:認可は1回で使い切りです。期日ごとに署名を先に作っても、未使用分の失効には任意拡張の
cancelAuthorizationを各nonceでオンチェーン実行する必要があります。上限付きの反復引き出しならEIP-2612のallowanceにします。 - コントラクトウォレットの利用者が多いのに、トークンの署名検証がERC-1271に対応していない:USD1・JPYC・OpenZeppelin 5.7.0の基底
ERC3009がこれに当たります。 - 決済コントラクトから
transferWithAuthorizationを呼ぶ設計:フロントランで後続処理を飛ばされます。receiveWithAuthorizationにします。
よくある質問
EIP-3009とERC-3009は違うものですか?
同じ規格です。EIPはイーサリアム改善提案の通し番号で、そのうちトークンなどアプリケーション層の規格をERCと呼びます。仕様ページのタイトルは「ERC-3009: Transfer With Authorization」で、URLは eips.ethereum.org/EIPS/eip-3009 です。検索や実装では両方の表記が使われ、指している関数やイベントは同じです。2026年11月8日時点のステータスはDraftです。
EIP-3009とEIP-2612の違いは何ですか?
EIP-2612は署名で approve を代替し、spenderに上限付きの引き出し許可(allowance)を与えます。許可は実行後も上限まで残ります。EIP-3009は受取人と金額を固定した1回分の送金を認可し、実行後に許可は残りません。nonceも、EIP-2612は連番、EIP-3009はランダムな32バイト値です。定期的な引き落としにはEIP-2612、都度払いにはEIP-3009が向きます。
USDCはEIP-3009に対応していますか?
対応しています。Ethereum・Base・Arbitrum・Optimism・Polygon・Avalancheのネイティブ版USDCは、関数がそろい、署名ドメインはnameが「USD Coin」、versionが「2」です。注意が必要なのはブリッジ版で、ArbitrumとOptimismのUSDC.eは関数がなく、PolygonのUSDC.eは chainId の代わりに salt を使う別形式です。他のチェーンは対象アドレスごとに確認してください。
transferWithAuthorizationのガス代は誰が払いますか?
関数を呼び出したアカウントが払います。署名するのは支払者ですが、トランザクションを送るのはリレイヤーや受取人、x402ならファシリテーターなので、支払者はETHを持つ必要がありません。今回のEthereumフォークでの測定では、通常の transfer の約1.91倍のガスを使いました。必要ガス量は送金先の残高状態などで変わります。EIP-3009の仕様にガス代を回収する仕組みはなく、手数料の徴収方法はサービス側で設計します。
署名した認可を取り消すことはできますか?
トークンが任意拡張の cancelAuthorization を実装していれば、CancelAuthorization(address authorizer,bytes32 nonce) に署名して同じnonceを使用済みにできます。USDCとOpenZeppelin 5.7.0の基底 ERC3009 は対応し、FDUSDは非対応です。ERC20TransferAuthorization では、同じキーの次のnonceだけを取り消せます。取り消しもトランザクションなので、送金より先にオンチェーンで実行される必要があります。validBefore を短めにしておけば、取り消さなくても期限切れで無効になります。