---
title: "EIP-3009（ERC-3009）とは？transferWithAuthorizationの仕組みと署名実装・チェーン別対応表【2026年11月】"
url: "https://www.issoh.co.jp/tech/details/9717/"
published: 2025-11-10
updated: 2026-11-08
categories: ["ブロックチェーン"]
publisher: "株式会社一創"
---

# EIP-3009（ERC-3009）とは？transferWithAuthorizationの仕組みと署名実装・チェーン別対応表【2026年11月】

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の仕様ページ](https://eips.ethereum.org/EIPS/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リポジトリ](https://github.com/ethereum/ERCs/blob/master/ERCS/erc-3009.md)で管理されています。

ステータスはDraftのままで、比較されることの多い[EIP-2612](https://eips.ethereum.org/EIPS/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](https://github.com/circlefin/stablecoin-evm/blob/master/CHANGELOG.md)（2023年11月9日）で、署名を `bytes signature` で受け取るオーバーロードを追加しました。

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

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

比較に使うのは端末の時計ではなくブロックの時刻です。`validBefore` を現在時刻の1秒前にした署名は、USDCへの `eth_call` では有効として通りました。評価に使う最新ブロックの時刻が端末より前だったためです。期限切れの判定を端末側で済ませず、Ethereumの12秒のスロット間隔と収録待ちを見込んだ余裕を持たせてください。

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

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チェーン対応](/tech/details/11180/)）。Paxos系のPYUSDとUSDPは、仕様に無い `transferWithAuthorizationBatch` も公開していますが（[PYUSDのリリース履歴](https://github.com/paxosglobal/pyusd-contract/releases)）、複数トークン共通のリレイヤーでは仕様の単発関数に揃える方が安全です。

Circle Gatewayの少額決済は別の扱いです。[Circleの署名手順](https://developers.circle.com/gateway-nanopayments/howtos/eip-3009-signing)では、同じ型の認可でもドメイン名は `GatewayWalletBatched`、`verifyingContract` はGatewayWallet、`validBefore` は3日以上先と定めています。

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

### ドメインをチェーンから取得する署名コード

次のコードは、残高ゼロの使い捨て鍵で署名し、EthereumメインネットのUSDCに `eth_call` で検証させるものです。資金は動きません。ethers v6.17.0とNode.js v26.5.0で実行しました。`npm i ethers@6.17.0` のあと、`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の使い方と安全対策](/tech/details/1495/)を参照）。

### 改ざん・期限切れ・呼び出し元違いで返る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](https://eips.ethereum.org/EIPS/eip-7702)のアカウントには `0xef0100` で始まる委任コードが置かれ、`EXTCODESIZE` は23を返します。一方、[FiatTokenの署名検証](https://github.com/circlefin/stablecoin-evm/blob/master/contracts/util/SignatureChecker.sol)はコードの有無で経路を分け、コードがあれば `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のソース](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.7.0/contracts/token/ERC20/extensions/draft-ERC3009.sol)）。同じ「ERC-3009対応」でも、署名の受け取り方は実装ごとに異なります。

## リレイヤーの送信前検査とガス代の見積もり

リレイヤーは失敗したトランザクションのガスも負担します。送信前に確かめる順序は、実装の手間と損失の大きさで決めます。

1. `staticCall` で実行をシミュレーションする。署名・期限・nonce・残高の不備をまとめて落とせます。
2. 同じ認可の多重送信を防ぐ。`(from, nonce)` の処理中フラグをリレイヤー側で持ち、`authorizationState` と併用します。
3. ブロックリストの宛先を除外する。[Arcのリレイヤー手順](https://docs.arc.io/integrate/relayers-and-paymasters/eip-3009-relayer)は、ブロックリスト対象の `from`・`to` はrevertしてガスだけを消費すると注意しています。

今回の条件ではコストは通常の送金の約1.9倍でした。USDCをEthereumフォークで実送金して測ると、`transferWithAuthorization` は85,601ガス、`transfer` は44,908ガスでした（残高のある宛先への送金）。署名検証とnonceの書き込みの分だけ増え、差の40,693ガスはリレイヤーの負担です。

EIP-3009自体には手数料を支払う仕組みがないため、ガス代の回収はサービス側で設計します。商品代金の認可とは別に、リレイヤーを受取人、手数料を金額とする認可を受け取る方法があり、両方の支払いを保証するなら同一トランザクションで処理します。自社の決済基盤にステーブルコイン受付を組み込む場合は、[決済・サブスクリプションシステム開発](/service/system/payment/)もご相談いただけます。

## 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の使い方](/tech/details/12466/)を参照）。

```
npm i -D hardhat@2.29.1 @nomicfoundation/hardhat-ethers@3 ethers@6.17.0 @openzeppelin/contracts@5.7.0
```

`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](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.7.0/contracts/token/ERC20/extensions/ERC20TransferAuthorization.sol) を継承します。`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](https://github.com/x402-foundation/x402/blob/main/specs/schemes/exact/scheme%5Fexact%5Fevm.md)で管理され、`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の関係](/tech/details/9716/)、HTTP 402の別系統は[MPPとx402の違い](/tech/details/11520/)で扱っています。

## 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` を短めにしておけば、取り消さなくても期限切れで無効になります。

## 関連記事

- [ステーブルコインとは？仕組み・3類型と国内の購入経路、日米欧の規制](/tech/details/9918/)
- [USDC Bridge（CCTP）とは｜仕組み・手数料・送金時間と2026年12月のV1停止](/tech/details/11859/)
- [Circle Agent Stackとは？Agent Wallets・Nanopaymentsの仕組みとCLIでの使い方【CLI 1.2.0対応】](/tech/details/12120/)
- [AP2（Agent Payments Protocol）とは？v0.2のマンデートとx402・ACP・UCPとの違い](/tech/details/9716/)
- [Solidity入門｜開発環境の作り方・基本構文・Sepoliaデプロイ手順【2026年10月】](/tech/details/8629/)

---

出典: [EIP-3009（ERC-3009）とは？transferWithAuthorizationの仕組みと署名実装・チェーン別対応表【2026年11月】](<https://www.issoh.co.jp/tech/details/9717/>)（株式会社一創）
