EIP-3009(ERC-3009)とは?transferWithAuthorizationの仕組みと署名実装・対応トークン

EIP-3009(ERC-3009)とは?transferWithAuthorizationの仕組みと署名実装・対応トークン

EIP-3009は、ERC-20トークンの保有者がEIP-712形式の署名だけで「この相手に、この金額を、この期間内に1回だけ送る」と認可できる規格です。署名を受け取った第三者が transferWithAuthorization を呼び出してガス代を払うため、保有者はETHを持たなくても送金できます。USDCが採用し、近年はAIエージェント決済のx402がこの関数を決済の既定経路にしています。

この記事では、仕様の関数と有効期間・nonceの扱い、EIP-2612(permit)やPermit2との違いを整理したうえで、メインネットのUSDCに対して実際に署名を検証させたコードと、OpenZeppelin Contracts 5.7.0で自前トークンに組み込む手順を示します。

まとめ:EIP-3009の要点と実装判断

  • EIP-3009は2020年9月28日作成のERC(2026年9月時点でステータスはDraft)。EIP-3009とERC-3009は同じ規格を指します。
  • 必須の関数は transferWithAuthorization・receiveWithAuthorization・authorizationState の3つで、cancelAuthorization は任意の拡張です。
  • nonceは連番ではなくランダムな32バイト値のため、複数の認可を順不同で実行できます。EIP-2612のpermitは連番nonceで、allowanceを残す方式です。
  • 他のコントラクトから呼ぶときは、フロントランを避けるため receiveWithAuthorization を使います。
  • USDC・EURC・PYUSD・USDP・FDUSDはメインネットで対応しています。USDT・DAIは非対応です。
  • 自前トークンに組み込むなら、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(ERC-20)とEIP-712(型付き構造化データの署名)です。

2026年9月17日時点のステータスは Draft のままです。一方、比較されることの多いEIP-2612はFinalです。Draftであっても、USDCをはじめ複数の本番トークンが同じインターフェースで実装しており、事実上の標準として扱われています。「EIP-3009」と「ERC-3009」の2つの表記が混在しますが、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(32バイト)の6つの値と、署名を受け取ります。仕様上の署名は uint8 v, bytes32 r, bytes32 s の3引数です。

コントラクトウォレット(Safe等)は秘密鍵による署名を持たず、ERC-1271の isValidSignature で署名を検証します。これに対応するため、CircleはFiatToken 2.2.0(2023年11月9日)で、署名を bytes signature で受け取るオーバーロードを追加しました。PaxosのPYUSDもv2.1.0で同様のbytes版を追加しています。コントラクトウォレットの利用者を想定するなら、トークンが必要な署名形式を受け取り、ERC-1271で検証する実装になっているかを確認してください。

validAfter・validBeforeの境界とブロック時刻のずれ

有効期間の判定は、Circleの実装もOpenZeppelinの実装も block.timestamp > validAfter かつ block.timestamp < validBefore の不等号(等号を含まない)です。validAfter を0にすれば即時有効、未来の時刻にすれば「今署名して、指定日以降に実行できる」予約送金になります。

比較に使うのは端末の時計ではなくブロックの時刻です。後述のUSDC検証では、validBefore を「現在時刻の1秒前」にした署名が、eth_call では有効として通りました。eth_call が評価に使う最新ブロックの時刻が、端末の現在時刻より前だったためです。期限には、Ethereumの12秒のスロット間隔に加え、ブロック生成の欠落やトランザクションの収録待ちも見込んだ余裕を持たせ、「端末で期限切れ=オンチェーンでも失敗」とは考えないでください。

32バイトのランダムnonceとauthorizationStateによる二重実行防止

nonceは連番ではなく、クライアントが生成するランダムな32バイト値です。コントラクトは (from, nonce) の組を使用済みとして記録し、同じ組での2回目の実行をrevertさせます。実行前に使用済みかを確かめるには authorizationState(from, nonce) を呼びます。

仕様はランダムnonceを選んだ理由として、連番nonceのメタトランザクションでは「番号が先の取引を送ると保留されずにrevertし、ガスが無駄になる」点を挙げています。ランダムnonceなら、先に署名した認可と後に署名した認可のどちらから実行してもかまいません。この性質は、後半のOpenZeppelin版の検証で実際に確かめます。

receiveWithAuthorizationによるフロントラン対策

transferWithAuthorization は、署名さえあれば誰でも実行できます。これが問題になるのは、決済コントラクト(たとえば「入金を受けたら商品の権利を発行する」コントラクト)が内部でこの関数を呼ぶ場合です。攻撃者がメンプールから署名を抜き出して先に直接呼ぶと、送金だけが完了し、決済コントラクト側の後続処理は実行されません。

仕様は、他のスマートコントラクトから呼ぶときは receiveWithAuthorization を使うよう求めています。この関数は「呼び出し元 = 受取人 to」を追加で検査するため、受取人である決済コントラクト以外は実行できません。署名の型文字列も ReceiveWithAuthorization と別なので、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 できます。署名は1枚でも、許可は残ります。EIP-3009の署名は受取人と金額が固定された1回分の送金にしか使えず、実行後に許可が残りません。

継続的な引き落とし(DEXのルーターやサブスクの定期課金)ならEIP-2612、1回ごとに金額が決まる支払いならEIP-3009が向きます。トークンがどちらにも対応していない場合は、UniswapのPermit2が受け皿になります。ただし最初に1回、Permit2コントラクトへの approve をオンチェーンで実行する必要があり、そのガスは保有者か代行者が負担します。

メインネットUSDCで検証したEIP-712署名の組み立て

ドメイン名「USD Coin」とversion「2」をチェーンから取得するコード

EIP-712署名で最も間違えやすいのはドメイン(name・version・chainId・verifyingContract)です。USDCのシンボルは「USDC」ですが、ドメイン名は「USD Coin」、versionは「2」です。推測で書かず、コントラクトから読み取ります。次のコードは、残高ゼロの使い捨て鍵で署名し、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の DOMAIN_SEPARATOR() と一致しました。3種類の型文字列から計算したハッシュも、USDCの TRANSFER_WITH_AUTHORIZATION_TYPEHASH() などが返す値とすべて一致しています。

ブラウザでは、MetaMaskなどのウォレットに eth_signTypedData_v4 で同じ domain・types・message を渡すと、利用者に内容が表示されたうえで署名されます(ウォレットの基本はMetaMask(メタマスク)とは?仕組み・使い方・安全性を実務目線で解説を参照)。

改ざん・期限切れ・呼び出し元違いで返るrevert理由

同じスクリプトで条件を変え、USDC(FiatTokenV2_2)が返す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と型文字列を疑ってください。署名後にvalueを変えると、この検証条件では署名エラーになります。ただし、有効期間やnonceを変えた場合は、署名検証より先に期限や使用済み認可の検査で失敗することがあります。

OpenZeppelin 5.7.0のERC3009で自前トークンに実装する手順

OpenZeppelin 5.7.0のバージョン指定とインストール

OpenZeppelin Contractsは5.7.0(2026年7月29日公開)で token/ERC20/extensions/draft-ERC3009.sol を追加しました。ただし2026年9月17日時点で、npmのdist-tagは latest が5.6.1、dev が5.7.0です。npm i @openzeppelin/contracts では5.6.1が入り、ERC3009はimportできません。バージョンを明示します。

npm i @openzeppelin/[email protected]

ファイル名の draft- は、元の規格がDraftであることを示しています。トークン本体は、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());
    }
}

このコントラクトはsolc 0.8.30(evmVersion prague)でコンパイルできました。cancelAuthorization も含まれています。5.7.0の実装には独自拡張もあり、validAfter と validBefore の両方でビット47を立てると、時刻ではなくブロック番号で期間を判定します。フラグを立てなければ仕様どおりタイムスタンプで判定します。

ETHを持たない支払者の送金と、順不同の実行をHardhatで確認

Hardhat 2.29.1のローカルネットワークで、ETH残高0の支払者が署名した2件の認可を、リレイヤーが後に署名した方から実行しました(Hardhatの基本はHardhatの基本概要とEthereum開発環境として選ばれる理由を参照)。次のコードは実行したスクリプトの抜粋です。

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

await token.connect(relayer).transferWithAuthorization(...a); // 再送
// => revert: ERC3009UsedAuthorization

支払者のETH残高は0.0のまま、受取人に30トークンが届きました。後に署名した20トークン分を先に実行しても失敗しません。連番nonceのEIP-2612ではこの順序入れ替えはできません。同じ認可の再送は、OpenZeppelin版ではカスタムエラー ERC3009UsedAuthorization でrevertします。USDCの文字列メッセージとは形式が違うため、リレイヤー側でエラーを判定するときは、トークンごとにrevert形式を確認してください。

EIP-3009対応トークンの見分け方と主要ステーブルコインの対応状況

2026年9月17日に、Ethereumメインネットの各コントラクトへ eth_call を送り、transferWithAuthorization が存在するかを確かめました。関数を持つトークンは、署名エラーなどのrevert理由を返しました。理由データのないrevertだけでは関数の不存在を断定できないため、USDTとDAIは公開ソース・ABIに該当関数が無いことも確認しています。

トークン 発行体 v,r,s版 bytes版 補足
USDC Circle あり あり ドメイン名 USD Coin/version 2
EURC Circle あり あり USDCと同じFiatToken実装
PYUSD Paxos あり あり バッチ関数あり
USDP Paxos あり あり バッチ関数あり
FDUSD First Digital あり あり –
USDT Tether なし なし permitもなし
DAI Sky(旧MakerDAO) なし なし 独自形式のpermit

Paxosのコントラクト(PYUSD・USDP・USDG)は、仕様に無い transferWithAuthorizationBatch を公開しており、複数の認可を1トランザクションで実行できます。PYUSDはv2.0.0でEIP-3009とEIP-2612を追加しました。バッチ関数はPaxos独自の拡張なので、複数トークン共通のリレイヤーを作るときは、仕様の単発関数だけを使う方が安全です。

ブリッジ経由のUSDC(L2上の「USDC.e」等)は、同じEIP-3009を持つとは限りません。チェーンごとに対象アドレスで authorizationState を呼ぶだけでなく、公開実装・ABIで送金関数と署名形式を確認し、対象チェーンのドメインで署名した認可をシミュレーションしてから実装してください。

x402がEIP-3009を決済の既定経路に選んだ理由と非対応トークンの扱い

x402は、HTTPの402 Payment Requiredを使って、APIやAIエージェントがその場で支払う決済プロトコルです。仕様リポジトリはCoinbaseからx402-foundation/x402へ移り、coinbase/x402 は現在forkになっています。

EVM向けの exact スキームでは、送金方式 assetTransferMethod を指定しなければ "eip3009" が既定になります。クライアント(エージェント)は TransferWithAuthorization に署名してHTTPヘッダーで送り、ファシリテーターがその署名で transferWithAuthorization を実行します。仕様はこの方式を「最も単純で、真にガスレス」と位置付けています。エージェントはETHを持たず、承認トランザクションも送らずに済むためです。

EIP-3009を持たないERC-20には、Permit2を使う方式が「Universal Fallback」として用意されています。この場合は、事前にPermit2コントラクトへの approve が必要です。承認の手段としては、保有者自身がガスを払う方法、ファシリテーターがガス代を送ってからまとめて実行する方法、トークンがEIP-2612に対応していればpermit署名で済ませる方法の3つが定義されています。エージェント決済の全体像はAP2 (Agent Payments Protocol)とは何か? 新たな決済プロトコルの概要と意義と背景で扱っています。

EIP-3009を採用しない方がよい場面と失敗パターン

EIP-3009は万能のガスレス手段ではありません。次の条件に当てはまるなら、別の方式を選びます。

  • 既存トークンをアップグレードできない:規格はトークン本体への実装が前提です。プロキシでないコントラクトに後付けはできません。この場合はPermit2か、ERC-4337のPaymasterでガスを肩代わりする方が現実的です。
  • 毎月同じ相手に引き落とさせたい:認可は1回で使い切りです。定期課金を組むには、期日ごとに validAfter をずらした署名を先に何枚も作っておく必要があり、発行済みの未使用認可を期限前に失効させるには、トークンが任意拡張の cancelAuthorization に対応している場合、各nonceの取消を先にオンチェーンで実行する必要があります。上限付きで何度も引き出させるなら、EIP-2612のallowanceの方が素直です。
  • コントラクトウォレットの利用者が多いのに、トークンがv,r,s版しか持たない:ERC-1271で検証できず、署名が通りません。bytes版の有無を先に確認します。
  • 決済コントラクトから transferWithAuthorization を呼ぶ設計:前述のフロントランで、後続処理を飛ばされます。必ず receiveWithAuthorization にします。

リレイヤーを自前で運用する場合、ガス代の回収方法も決めておく必要があります。EIP-3009自体にはリレイヤーへの手数料を払う仕組みが無いため、商品代金の認可とは別に、リレイヤーを受取人、手数料を金額とする認可を受け取るなど、アプリケーション側で設計します。両方の支払いを保証する場合は、同一トランザクションで処理します。

よくある質問

EIP-3009とERC-3009は違うものですか?

同じ規格です。EIPはイーサリアム改善提案の通し番号で、そのうちトークンなどアプリケーション層の規格をERCと呼びます。仕様ページのタイトルは「ERC-3009: Transfer With Authorization」で、URLは eips.ethereum.org/EIPS/eip-3009 です。検索や実装では両方の表記が使われますが、指している関数やイベントは同じです。2026年9月時点のステータスは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メインネットでは、署名ドメインのnameが「USD Coin」、versionが「2」です。CircleのFiatToken 2.1.0(2021年2月)で receiveWithAuthorization が加わり、2.2.0(2023年11月)でコントラクトウォレット向けのbytes署名版が追加されました。同じ実装のEURCも対応しています。他チェーンのUSDCは、アドレスごとに関数の有無を確認してください。

transferWithAuthorizationのガス代は誰が払いますか?

関数を呼び出したアカウントが払います。署名するのは支払者ですが、トランザクションを送るのはリレイヤーや受取人、x402ならファシリテーターなので、支払者はETHを持つ必要がありません。ただしEIP-3009の仕様にはガス代を回収する仕組みが無く、手数料の徴収方法はサービス側で設計します。

署名した認可を取り消すことはできますか?

トークンが任意拡張の cancelAuthorization を実装していれば、CancelAuthorization(address authorizer,bytes32 nonce) に署名して同じnonceを使用済みにできます。USDCとOpenZeppelin 5.7.0の実装は対応しています。取り消しもトランザクションなので、送金より先にオンチェーンで実行される必要があります。先に送信しても、送金が先に処理されれば取り消せません。validBefore を短めに設定しておけば、取り消しを送らなくても期限切れで無効になります。

関連記事

お気に入りに入れた記事の一覧

資料請求

RELATED POSTS 関連記事

目次