Skip to content

feat(circle): Circle Paymaster v0.8 (USDC ガスレス) Phase 1#14

Merged
cipherwebllc merged 12 commits into
mainfrom
feat/circle-paymaster-phase1
May 30, 2026
Merged

feat(circle): Circle Paymaster v0.8 (USDC ガスレス) Phase 1#14
cipherwebllc merged 12 commits into
mainfrom
feat/circle-paymaster-phase1

Conversation

@cipherwebllc
Copy link
Copy Markdown
Owner

概要

USDC ガスレス決済の gas 支払いを Circle Paymaster v0.8(優先) に対応。Pimlico erc20 は fallback 保持、JPYC は現行 Pimlico sponsorship のまま。paymasterMode='erc20' は不変(法務 prose / C8 ドリフトガードを壊さない)。OpenPay 徴収は 0。

⚠️ feature flag NEXT_PUBLIC_ENABLE_CIRCLE_PAYMASTER は既定 OFF。OFF の間は全 chain が従来の Pimlico 経路に解決され、本番挙動は一切変わりませんresolveUsdcGaslessProvider が単一の真実点)。有効化は段階リリース(下記ゲート通過後)。

主な変更(chunk 1–6)

  • chunk1-2: lib/circlePaymaster.ts(per-chain hardcode allowlist = SoT・env override 不可・codehash/deploy 検証)+ lib/smartAccount/circleAccount.ts(viem/permissionless v0.8 client)+ provider 解決層・discriminated union。
  • chunk3(二重決済 FSM): lib/circlePending.ts(fail-closed localStorage FSM + read-back 検証)+ lib/smartAccount/circleSend.ts(permit→署名済 op 永続→submitting→raw broadcast→確定、応答ロスト時は 新規 op を作らず冪等 rebroadcast のみ、cross-invocation は callHash で dedup)。
  • chunk4(監査): lib/circleReceiptVerifier.tsper-UserOp scope で徴収 net 再計算・balanceOf 差分不採用・binding 不変条件 + EntryPoint 発火元検証 + success フラグ)+ HistoryEntry v1→v2 migration(legacy を null backfill で drop しない)+ paymentLog/API/stats/CSV/履歴 UI を end-to-end(verified ⇔ client-reported 分離)。
  • chunk5: hooks/useGasQuoteCircle.ts(per-chain surcharge + permitAmount を standard×安全係数で算定し過剰 allowance 回避)+ 法務 prose 拡張(Privacy §3 委託先に Circle 追記・UI から "Pimlico" 名排除)。
  • chunk6: docs/runbooks/circle-paymaster-release-gate.md(投入ゲート runbook)+ scripts/smoke-circle-crossswitch.mjs(cross-switch smoke)。

既存バグ修正(本 PR で発掘)

  • EntryPoint 整合: permissionless to7702SimpleSmartAccount は v0.8 専用。createPimlico を版引数化し各 builder を account に一致(simpleAccount/circle=v0.8、metamask/mav2=v0.7)。一律 v0.8 は metamask を壊すため per-builder。erc20 fallback の AA50 postOp revert を解消。

レビュー

Codex 2 巡 + Claude 多次元 adversarial review 2 巡(implement→review→fix→re-review)で収束。

  • 1 巡目: 8 件修正(二重決済ガード拡張 / testnet 二重徴収 / verifier 堅牢化 / API forge 防止 / over-allowance 圧縮 等)。
  • 2 巡目: 二重決済ガードの残 3 件(confirmed dedup の時間窓化、status 優先 sort、scan→reserve race は ERC-4337 nonce 一意性が最終 backstop と確認)。
  • Claude 総括 verdict = ship(残課題は全 LOW・hardening checklist 化)。

テスト

  • typecheck / lint clean、full suite 2505 passed / 0 failed(errored 6 は既存の OOM allowlist file・無関係)。
  • 新規: circlePending / circleSend / circleReceiptVerifier / useGasQuoteCircle / circleAccount + log/stats/legal/i18n。

本番投入前ゲート(マージ後・有効化前)

  1. 🔴 実機 cross-switch smokedocs/runbooks/circle-paymaster-release-gate.md)= 同一 EOA で Pimlico↔Circle 往復を receipt 付きで実証。testnet は実行済(PASS)、Base mainnet で fee 実測 + metamask erc20 regression が残。
  2. 段階投入: Base mainnet のみ flag ON → 拡大。fee config 未確認 chain は自動で Pimlico fallback。

hardening checklist(DEFER・低)

collector≠paymaster の mainnet 実測 / pre-submit 孤児 GC / unreconciled UI バッジ / POST size cap / binding-violation 分類の構造化 / producer⇔endpoint 型同期 / stats verified bucket(server verifier 実装まで常時 0)

🤖 Generated with Claude Code

cipherwebllc and others added 12 commits May 30, 2026 07:13
…undation)

USDC ガスレスを Circle Paymaster v0.8 に切替える Phase 1 の基盤層 (flag 既定 OFF
で本番挙動に影響なし)。chunk3 以降 (二重決済 FSM / receipt verifier+audit /
gas quote+法務 / cross-switch gate) は後続。

chunk1 — lib/circlePaymaster.ts:
- Circle Paymaster v0.8 アドレスの hardcode allowlist (SoT, C3 信頼境界)。
  公式 docs 由来 (mainnet 0x0578..700Ec / testnet 0x3BA9..8966、後者は spike 実証済)。
  WebSearch が返す v0.7 (0x6C97../0x31BE..) との取り違えを test で防止。env override 無し。
- resolveUsdcGaslessProvider = 単一の真実点。flag ON + USDC erc20 + allowlist +
  fee config 全て揃った時のみ circle、欠ければ pimlico erc20 fallback。
- Circle fee = gas の 10% (Arb/Base のみ docs 確認、他 chain は fee config 無し=Circle 無効)。
- permit/paymasterData ヘルパ (deadline=MAX, encodePacked encoding)、deploy/code guard。
- env flag NEXT_PUBLIC_ENABLE_CIRCLE_PAYMASTER (既定 OFF)。

chunk2 — discriminated union + Circle client builder:
- 既存 Pimlico bundle に provider:'pimlico'/entryPointVersion:'0.7' タグ付け (C6 union)。
- lib/smartAccount/circleAccount.ts: permissionless to7702SimpleSmartAccount (v0.8・
  walletClient owner 可・impl 0xe6Cae83 同一) + viem createBundlerClient。spike レシピ
  移植 (permit署名/pimlico gas price/estimate postOp>=15000/full-gas send)。

検証: typecheck / lint / vitest (circle 25 tests + 全体 2426 pass, 0 fail)。
codex review は利用上限/スタールでスキップ (memory 方針)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
二重決済 (C1) 対策の核。USDC ガスレス決済の送信を fail-closed pending store +
明示 FSM で段階管理し、応答ロスト/timeout 時も auto-fallback せず冪等 rebroadcast
のみで復旧する。

- lib/circlePending.ts: fail-closed localStorage 専用 store + FSM
  (reserved→awaiting_signature→signed→submitting→confirmed/failed/abandoned)。
  冪等キー=chainId+sender+paymentAttemptId+callHash (callHash は merchant calls のみ
  →quote 再生成で不変)。CAS 遷移・sender bound 認可・read-back 書込検証
  (private mode silent drop 検出)・findRecoverable (reload 後の submitting スキャン)。
- lib/smartAccount/circleAccount.ts: prepareAndSignCircleUserOp (prepare→postOp floor
  強制→sign→hash→formatUserOperationRequest)、broadcastCircleUserOp (raw
  eth_sendUserOperation・retryCount0・冪等 rebroadcast)、pollCircleReceipt。
  ※ viem sendUserOperation は account 有りで毎回 prepareUserOperation 再実行→別 op に
  なり冪等不可のため broadcast には raw RPC を使う。
- lib/smartAccount/circleSend.ts: executeCirclePayment オーケストレータ。
  cross-invocation recovery→reserve→permit(popup1)→prepare+sign(popup2)→署名済 op 永続
  →markSubmitting(broadcast前・fail-closed)→raw broadcast→確定。応答ロスト/timeout は
  CirclePendingError (失敗扱いせず submitting 維持)。submitting 後は fallback/新規 op 禁止。
- hooks/useSmartAccount.ts: pimlico-simple-7702 かつ provider==circle で Circle bundle へ
  routing。RQ key に provider+entryPointVersion 追加。
- hooks/useBatchPayment.ts: provider exhaustive 分岐。circle 枝は executeCirclePayment
  委譲・circlePermitAmount を params threading・gas ceiling 適用・reconcile は circle 除外。

permitAmount 算定は chunk5 (useGasQuoteCircle) 待ち、未指定なら circle 枝は送信拒否。
tests: circlePending 26 + circleSend 9 + useBatchPayment circle 3。
全 green (typecheck/lint clean、full suite 2464 passed/0 failed)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… verifier

監査基盤 (C2/C3/C4-hist) の 2 本柱。

- lib/history.ts: schemaVersion 1→2。Circle 監査フィールド追加 (provider /
  circlePaymasterAddress / circlePaymasterNetUsdc / circleVerification、すべて
  nullable)。MIGRATIONS[1] が legacy(v1) を null backfill (drop しない)。isValidEntry /
  buildHistoryEntry / BuildHistoryBase 更新。新フィールドは optional で既存 caller 無変更。
- lib/circleReceiptVerifier.ts: 徴収額 reconciliation (C2)。balanceOf 差分でなく
  tx receipt の **per-UserOp scope** で circlePaymasterNetUsdc を再計算。
  net = customer→paymaster − paymaster→customer 返金。bundle 内同一 sender 複数 UserOp は
  UserOperationEvent の log-index range で分離 (tx 全体合算しない)。binding 不変条件 (C3深):
  pending record の expected userOpHash/sender/paymaster が receipt の UserOperationEvent と
  一致して初めて verified、不一致は unreconciled。

tests: history v1→v2 backfill/circle entry、verifier の単一/2-UserOp scope/refund/binding 不一致/
汚染耐性。全 green (typecheck/lint clean、history 関連 164 + verifier 9)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…→UI)

provider / circlePaymasterNetUsdc / paymaster を決済 log から履歴 UI まで貫通させ、
verified(on-chain) と client-reported を区別する (C2/C3)。

- lib/paymentLog.ts: PaymentLogContext/Event/builder に provider/circlePaymasterAddress/
  circlePaymasterNetUsdc/circleVerification を追加 (gasless circle のみ)。
- app/api/log/payment/route.ts: 上記 4 field を allowlist 検証して受理 (未知 field は従来通り遮断)。
- app/api/log/payment/stats/route.ts: byProvider 集計を追加。circle の gas 徴収 net を
  verified / reported に分けて出力 (混同しない・dataSource 注記は従来通り client-reported)。
- hooks/useBatchPayment.ts: BatchPaymentResult に provider+circle 監査フィールド。circle 経路は
  確定後に verifyCircleReceiptOnChain で net を best-effort 再計算 (client-reported)、log へ。
- hooks/usePaymentHistory.ts: gasless success entry に provider/circle フィールドを記録。
- lib/historyCsv.ts: CSV 末尾に Paymaster種別 / Circleガス代USDC / 検証 列を追加 (既存列順維持)。
- components/HistoryRow.tsx + messages: circle 行に Circle ガス代を表示 (columnCircleGas)。

tests: route の circle field accept/reject、stats byProvider (verified/reported 分離)、
CSV circle 列、CheckoutForm mock の provider 補完。全 green (full suite 2483 passed/0 failed)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rose 拡張

USDC ガスレスが Circle に解決される場合の quote/permit 算定と、法務 prose を
provider 第一級次元に拡張 (C4)。

- hooks/useGasQuoteCircle.ts: 実費 (Pimlico getTokenQuotes の exchangeRate 流用) に
  per-chain surcharge (CIRCLE_GAS_SURCHARGE_BPS) を上乗せした表示 gasAmount と、
  gas ceiling ベースの permitAmount (tight upper bound、過剰 allowance 回避) を算定。
  useBatchPayment の circlePermitAmount 必須要件を満たす。postOp は Circle 下限 15000 反映。
- PaymentForm/CheckoutForm: provider が circle のとき activeQuote を circle quote に切替え、
  circlePermitAmount を mutate に渡す。gas help を gasInfoUsdcCircle (Circle 明示・約10%手数料・
  当社徴収0) に切替え、UI に 'Pimlico' 名が残らないようにする。flag OFF では従来通り (dormant)。
- Privacy §3 委託先に Circle Internet Financial を追加 (顧客が USDC で gas を Circle Paymaster に
  支払い・当社徴収0 を明示)。ja/en 両方。

tests: useGasQuoteCircle (surcharge/permit/postOp 下限)、legal §3 Circle 開示 (ja/en)、
i18n circle help text (Circle 明示・Pimlico 不在・当社徴収0)、form 各 test に circle quote mock。
全 green (full suite 2493 passed/0 failed)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…blocker)

同一 EOA で Pimlico(v0.7/JPYC) ↔ Circle(v0.8/USDC) を receipt 付きで往復実証する
手動ゲートを文書化。delegate アドレス一致だけでは EntryPoint/nonce/validation 差を
捕捉できないため、実機 testnet で send 成功を確認してから flag を有効化する。

- 前提 (disposable testnet EOA・Arb Sepolia USDC + JPYC sponsorship testnet)、
  手順 (A:Circle USDC / B:JPYC 往復 + 応答ロスト復旧)、受入基準チェックリスト、
  段階リリース (testnet→Base mainnet→拡大)、ロールバック手順。
- 本ゲート未通過のため NEXT_PUBLIC_ENABLE_CIRCLE_PAYMASTER は本番 OFF 維持。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scripts/smoke-circle-crossswitch.mjs: ローカル使い捨て鍵 1 つで同一 EOA に対し
Pimlico → Circle → Pimlico の 3 leg を送信し、receipt / 徴収 USDC / 委任先を検証して
PASS/FAIL を出す。MetaMask インポートや flag 再起動が不要 (ローカル鍵が 7702 委任を
自前 bootstrap)。Circle leg は spike の実証済みレシピ、Pimlico leg は本番 simpleAccount.ts
と同一スタック (permissionless createSmartAccountClient + ERC20 paymaster)。

調査確定: permissionless/viem の 7702 SimpleAccount は EntryPoint v0.8 専用。本番 Pimlico
経路も Circle 経路も同一 v0.8 EntryPoint + 同一 impl 0xe6Cae83 で、差は paymaster のみ
(単一 nonce 空間)。runbook の「v0.7↔v0.8」枠組みを「同一 v0.8・paymaster 往復」に訂正し、
スクリプトを最短実行として追記。SmartAccountBundle.entryPointVersion='0.7' は label の
名残で不正確 (runtime は provider 分岐のため無害・別途整理推奨) を明記。

node --check + 全 import 解決を keygen path で確認済 (on-chain leg は要 funded 鍵)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.7 client + v0.8 account の混在で erc20 paymaster の approve spender (v0.8 0x7777)
と paymaster 指定 (v0.7 0x8888) が食い違い AA50 postOp revert していた。Pimlico client を
entryPoint08 に揃える。本番 lib/pimlico.ts createPimlico も同根 (要修正・別 task)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cross-switch smoke で判明した既存バグの修正。permissionless to7702SimpleSmartAccount は
**EntryPoint v0.8** だが createPimlico は entryPoint07 固定だった。erc20 経路
(prepareUserOperationForErc20Paymaster) は getTokenQuotes を pimlicoActions decorator 経由で
呼び、decorator が entryPointAddress を client 設定 (v0.7) で上書きするため approve spender が
v0.7 paymaster、最終 getPaymasterData は account 由来 v0.8 paymaster になり mixed state →
postOp AA50 revert。mainnet USDC erc20 (simpleAccount 経路) が壊れていた (testnet は
sponsorship に倒れて露見せず)。

並列監査 workflow (6 agents・ライブラリ型/runtime/実機 receipt で裏取り) で各 builder の実
EntryPoint 版を確定: simpleAccount=v0.8 / circle=v0.8 / metamask=v0.7 (toolkit ハードワイヤ) /
mav2=v0.7。createPimlico 一律 v0.8 は metamask(v0.7) を鏡像で壊すため不可。

- lib/pimlico.ts: createPimlico(chainId, entryPointVersion='0.7') に版引数化。既定 '0.7' で
  metamask/mav2/既存呼出は挙動不変。
- lib/smartAccount/simpleAccount.ts: createPimlico(chainId,'0.8') に変更 (account v0.8 と一致)。
  SmartAccountBundle.entryPointVersion を '0.7'→'0.7'|'0.8' union 化し本経路は '0.8' (実体一致)。
  ラベルは provider 分岐に未使用 (discriminant は provider) なので consumer 無影響。
- metamask/mav2 は createPimlico(chainId) 既定 '0.7' のまま (account v0.7 と一致・変更不要)。

検証: typecheck/lint clean、full suite 2493 passed/0 failed、C8 guard 無影響。
残: mainnet metamask USDC erc20 の手動 regression smoke (audit verificationSteps)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
両レビュー (Codex 6 + Claude adversarial 14 確定/偽陽性 18 除去) を突き合わせ、実害確定分を修正。

A 二重決済 (最重要・両者一致): cross-invocation ガードが submitting しか見ず、pre-submit
  並行 (別タブ/popup 跨ぎ) や fast-confirm 後の再決済で同一 callHash の merchant 転送が二重に
  なりえた。findLiveByCallHash で同一 callHash の生き record を全状態スキャンし、confirmed→
  既支払い結果返却 / submitting→recover / pre-submit→CirclePendingError。reload 放置の stale
  orphan は abandon して恒久ブロックを回避。
B testnet 二重徴収: Circle 経路でも resolvePaymasterMode→sponsorship 倒しで gasReimbursement が
  feeReceiver へ加算され Circle の permit 徴収と二重 + 徴収0違反。isCircle 時は加算しない。
C 監査盲点: verify の unreconciled reason (paymaster/sender 不一致=信頼境界破れ) が握り潰され
  RPC flake と区別不能だった。binding 違反は logger.error、RPC 失敗は別 key で記録。
D verifier 堅牢化: UserOperationEvent の発火元が EntryPoint であることを必須化 (同名 event の
  scope 汚染防止)、success=false (revert) は unreconciled。
E API forge 防止: 未認証 endpoint が circleVerification='verified' を受理していた → reject
  (client は client-reported/unreconciled のみ。verified は server verifier 専用)。
F over-allowance: permitAmount を ceiling ベース (実費の数百〜数万倍) から standard×10 に圧縮。
  deadline=MAX の残余 allowance を実費の数倍に。送信時 spike は assertGasCeiling が abort。
G audit: CirclePendingError の userOpHash を onError 監査に載せ submitted op handle を保持。
H success: PendingRecord に receipt.success を永続し、resultFromConfirmed の receipt 取得失敗
  フォールバックで success:true 捏造 (revert を成功誤報告) をやめる。

tests: circleSend +4 (confirmed/pre-submit/submitting/stale guard)、verifier +3 (EntryPoint
poison/success=false)、log-payment +2 (verified reject)、circlePending/useGasQuoteCircle 追従。
全 green (typecheck/lint clean、full suite 2502 passed/0 failed)。
DEFER (低): collector≠paymaster の mainnet 実測、pre-submit GC、unreconciled UI バッジ、
POST size cap、gas tier コメント、recoverSubmitting エラー分類 (checklist 化)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex+Claude の修正分再レビューで判明した cross-invocation ガードの残ギャップを修正。

NEW-1 (Codex HIGH): confirmed-by-callHash が paymentAttemptId 非依存のため正規の同額リピート
  決済を恒久抑止していた → CONFIRMED_DEDUP_WINDOW_MS(90s) で時間窓化。窓内 = 偶発二重 (reload/
  再クリック) → 既支払い結果を返す。窓外 = 正規リピート → 新規決済を通す。
NEW-3 (Codex MED): live record を status 優先 (confirmed>submitting>pre-submit) で sort し、
  stale でない pre-submit が confirmed/submitting を CirclePendingError で masked するのを防止。
NEW-2 (Codex HIGH→実際は安全): scan→reserve の真の同時 race は localStorage の限界で残るが、
  両 attempt は同一 sender の key-0 sequential nonce を取り **ERC-4337 EntryPoint の nonce 一意性が
  最終 double-spend ガード** (後発は AA25 で revert、merchant 転送は 1 回)。コメントで明記
  (Claude 再レビューが指摘した backstop)。
LOW: stale orphan の abandon を try/catch で握り潰し (並行遷移時の PendingStateError 伝播を防止)。
  useGasQuoteCircle の docblock を ceiling→standard×係数 の実装に同期。

5/6 の元修正は両レビューとも solid 確認。Claude 総括 verdict=ship。
tests: circleSend +3 (時間窓/status 優先)。全 green (typecheck/lint、full suite 2505 passed/0 failed)。
DEFER (低・checklist): binding-violation 分類の構造化、producer/endpoint 型同期、stats verified
bucket の server verifier 実装、findRecoverable dead export 整理。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openpay Ready Ready Preview, Comment May 30, 2026 7:19am

@cipherwebllc cipherwebllc merged commit 60e52ed into main May 30, 2026
3 of 6 checks passed
@cipherwebllc cipherwebllc deleted the feat/circle-paymaster-phase1 branch May 30, 2026 07:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant