Skip to content

feat(core,x402): GTokenAuthorization EIP-3009 ABI + signing helpers#23

Merged
fanhousanbu merged 4 commits into
mainfrom
feat/gtokenauth-eip3009
May 24, 2026
Merged

feat(core,x402): GTokenAuthorization EIP-3009 ABI + signing helpers#23
fanhousanbu merged 4 commits into
mainfrom
feat/gtokenauth-eip3009

Conversation

@fanhousanbu

Copy link
Copy Markdown
Contributor

Summary

  • @aastar/core: 新增 GTokenAuthorization.json ABI(EIP-3009 无 gas 转账合约,v2.2.0);导出 GTokenAuthorizationABI / GTokenAuthorizationArtifact;新增 gTokenAuthorizationActions() viem action factory
  • @aastar/x402: 新增 ReceiveWithAuthorizationCancelAuthorization EIP-712 typed-data schema;新增 signReceiveWithAuthorization()signCancelAuthorization() 签名 helper;新增 GTOKEN_EIP712_DOMAIN 常量

关键约束

约束 说明
MAX_AUTH_VALIDITY validBefore - validAfter < 300s(5 分钟)
receiveWithAuthorization msg.sender 必须等于 to(CallerMustBeRecipient);relay 无法代提交
xPNTsToken 字段 不纳入签名(relay 在 RC-2 阶段提供)

Files Changed

  • packages/core/src/abis/GTokenAuthorization.json — 新增 ABI
  • packages/core/src/abis/abi.config.json — ABI 完整性 hash 注册
  • packages/core/src/abis/index.ts — 导出 GTokenAuthorizationABI
  • packages/core/src/actions/gTokenAuthorization.ts — viem actions
  • packages/core/src/actions/index.ts — 重导出
  • packages/x402/src/eip3009.ts — 新增类型定义和签名函数
  • packages/x402/src/index.ts — 新增导出

Test Plan

  • pnpm --filter @aastar/core test — 单测通过(含 gTokenAuthorization.test.ts)
  • pnpm --filter @aastar/x402 test — EIP-3009 authorization 测试通过
  • pnpm -r build — 构建通过,无类型错误
  • 对 GTokenAuthorization 合约手动调用 receiveWithAuthorization,确认签名有效

GTokenAuthorization v2.2.0 (SuperPaymaster main, 2026-05-21):
- Add GTokenAuthorization.json ABI to @aastar/core
- Export GTokenAuthorizationABI / GTokenAuthorizationArtifact
- Add gTokenAuthorization viem action factory (gTokenAuthorizationActions)

EIP-3009 signing (x402/eip3009):
- Add ReceiveWithAuthorization and CancelAuthorization typed-data schemas
- Add signReceiveWithAuthorization(): msg.sender must equal `to` on-chain
- Add signCancelAuthorization(): signed by original authorizer
- Add GTOKEN_EIP712_DOMAIN constant (name='GToken', version='1')

Constraints:
- MAX_AUTH_VALIDITY = 5 min (validBefore - validAfter < 300s)
- receiveWithAuthorization: relay cannot submit, only recipient can
- xPNTsToken is NOT included in signature (relay-supplied hint for RC-2)
@fanhousanbu
fanhousanbu requested a review from jhfnetboy as a code owner May 21, 2026 16:34
@jhfnetboy

Copy link
Copy Markdown
Member

🤖 Round 1 — Claude Code Review (Local Model)

Reviewer: Claude Code (claude-sonnet-4-6)
Scope: GTokenAuthorization.json ABI + gTokenAuthorization.ts viem actions + eip3009.ts signing helpers


🔴 Critical

None.


🟠 High

H1 — xPNTsToken excluded from EIP-712 signature without documenting trust model

On-chain receiveWithAuthorization takes 8 params including xPNTsToken, but the EIP-712 typed data only commits to 6 fields (no xPNTsToken). The relay supplies xPNTsToken freely after the user signs. The PR description says this is intentional ("relay 在 RC-2 阶段提供"), but:

  1. The function docstring says "relay-supplied hint for RC-2" — if RC-2 uses xPNTsToken for access control decisions (e.g., "only members of this xPNTs community can receive"), a malicious relay could supply a token address that bypasses checks
  2. The SDK surface (gTokenAuthorizationActions.receiveWithAuthorization) exposes xPNTsToken as a caller-controlled field with no warning that it's not authenticated by the signature

Required: Add a comment at the type definition level that explicitly states the trust boundary: "the signer does not commit to xPNTsToken — the caller/relay supplies this and must be trusted."

If xPNTsToken influences access control (not just query logic), this must be re-examined and either included in the signature or hardcoded in the contract.


🟡 Medium

M1 — No client-side MAX_AUTH_VALIDITY validation in signing helpers

The contract enforces validBefore - validAfter < 300 (throws AuthorizationWindowTooLong). Neither signReceiveWithAuthorization nor the existing signTransferWithAuthorization validates this client-side. A user signs a 1-hour window; the authorization is permanently unusable.

// Suggested guard at top of signReceiveWithAuthorization:
if (params.validBefore - params.validAfter > 300n) {
  throw new Error(`Authorization window ${params.validBefore - params.validAfter}s exceeds MAX_AUTH_VALIDITY (300s)`);
}
if (params.validBefore <= params.validAfter) {
  throw new Error('validBefore must be greater than validAfter');
}

Note: signTransferWithAuthorization also lacks this — recommend adding to both.

M2 — GTOKEN_EIP712_DOMAIN hardcoded but signReceiveWithAuthorization takes tokenName/tokenVersion as params

export const GTOKEN_EIP712_DOMAIN = { name: 'GToken', version: '1' } as const;

If a caller passes GTOKEN_EIP712_DOMAIN.name and GTOKEN_EIP712_DOMAIN.version to signReceiveWithAuthorization, they'll sign with the correct domain. But if a future GTokenAuthorization v2 changes its name or version, callers using the constant will produce invalid signatures silently. The constant needs a clear link to the deployed contract version it was extracted from.

Suggest renaming to GTOKEN_AUTHORIZATION_V2_EIP712_DOMAIN to make version explicit.

M3 — writeContract type cast swallows transaction hash type

const writeContract = (...) => (client as any).writeContract({ ... });

The cast to any means TypeScript won't catch misuse (wrong arg count, wrong types). Other action factories in the codebase use the same pattern — this is a pre-existing issue — but since this is a new file it's worth noting that Hash return type on the method signatures is unchecked.


🔵 Low

L1 — ReceiveWithAuthorization type schema is identical to TransferWithAuthorization — test only checks length/field names

The test:

it('ReceiveWithAuthorization has the same 6 fields as TransferWithAuthorization', () => {
    expect(receive).toHaveLength(transfer.length);
    expect(fieldNames(receive)).toEqual(fieldNames(transfer));
});

This passes only because the schemas happen to be identical. It doesn't verify the actual type values or that the schemas produce different typehashes (the key EIP-3009 replay-protection invariant). Consider adding:

expect(RECEIVE_TYPEHASH).not.toEqual(TRANSFER_TYPEHASH);

L2 — Timestamp in ABI export comment will rot

// GTokenAuthorization v2.2.0 — EIP-3009 gasless transfers (SuperPaymaster main, 2026-05-21)

Deployment date in source comment will be misleading after maintenance deploys or version bumps.

L3 — generateNonce() behavior is not tested in the new test file
The function is imported and used but its properties (32-byte hex format, randomness) are untested.


✅ Verdict

Approve with requests. Clean implementation overall — good test coverage for the new actions and signing helpers. H1 (xPNTsToken trust model documentation) and M1 (time window validation) are the items worth addressing before merge.


Round 2 (Codex) will follow.

@jhfnetboy

Copy link
Copy Markdown
Member

🤖 Round 2 — Codex Review

Reviewer: Codex (via codex:rescue subagent)
⚠️ Meta-note: Codex 读到的是本地 chore/sync-sepolia-v5.3.2-addresses 工作树,PR 分支文件不在 checkout 中。gTokenAuthorization.tsGTOKEN_EIP712_DOMAIN、新测试等均无法直接验证。以下发现基于已在本地可访问的代码(eip3009.ts 现有部分 + ABI JSON)。


🔴 Critical

无可验证发现(代码不可访问)。


🟠 High

H1 — 现有 signTransferWithAuthorization 也缺少授权窗口验证(确认了 Claude M1 并扩展)

通过读取可访问的 packages/x402/src/eip3009.ts:56 及测试 packages/x402/__tests__/index.test.ts:151,172,确认现有 helper 传入 validBefore: 9999999999n 而不报错。对于 GToken 合约(MAX_AUTH_VALIDITY = 300sAuthorizationWindowTooLong error),这会导致签名后授权永远无法在链上执行。

新 PR 的 GToken 专用 helper(signReceiveWithAuthorizationsignCancelAuthorization)应在调用 signTypedData 前显式验证

  • validBefore - validAfter <= 300n
  • validBefore > validAfter

通用 x402/USDC helper 不需要此约束(合约不同)。


🟡 Medium

M1 — validBefore <= validAfter 边界未验证 (与 Claude M1 一致)

即使不考虑 MAX_AUTH_VALIDITY,validBefore == validAftervalidBefore < validAfter 也会让授权立即无效。无客户端快速失败。


🔵 Low / N/A

以下条目因代码不可访问无法验证:

  • xPNTsToken relay trust model(Claude H1)
  • (client as any).writeContract 类型安全
  • ReceiveWithAuthorization 测试覆盖
  • CallerMustBeRecipient SDK 预检

⚠️ Verdict(受限)

无法完整评估。Codex 只能确认 Claude M1 发现(授权窗口验证缺失),其余关键安全问题(xPNTsToken relay trust、类型安全)依赖 Claude 的分析。


参考 Claude Round 1 review 获取完整分析。

@jhfnetboy

Copy link
Copy Markdown
Member

⚔️ PK 对比 — Claude vs Codex (PR #23)

维度 Claude Codex
裁决 ✅ Approve with requests ⚠️ 受限(分支不可访问)
H1 发现 xPNTsToken 未纳入签名的 trust model 缺文档 无法验证(代码不可访问)
M1 一致 MAX_AUTH_VALIDITY 客户端未验证 ✅ 确认,且指出现有 signTransferWithAuthorization 也有此问题
Codex 增量 明确指出验证应仅对 GToken helper 加(通用 x402 helper 不适用)

🏆 PK 结论

Claude 覆盖更完整(Codex 受分支访问限制)。Codex 贡献了一个有价值的补充:M1 时间窗口验证只应加在 GToken 专用 helper 上,不应加在通用 signTransferWithAuthorization(不同合约约束)。

📋 必须修复后 merge

优先级 行动
[建议,非阻塞] signReceiveWithAuthorizationvalidBefore - validAfter <= 300nvalidBefore > validAfter 验证(仅 GToken helper,不改通用 helper)
[建议,非阻塞] GTokenAuthorizationActions.receiveWithAuthorization 类型定义处添加注释,说明 xPNTsToken 不在签名范围内(relay-supplied),以及对 relay 的信任要求
[低] GTOKEN_EIP712_DOMAIN 常量名加版本后缀(GTOKEN_AUTHORIZATION_V2_EIP712_DOMAIN

H1 (xPNTsToken trust) 是设计决策而非 bug,文档化即可,不阻塞 merge。


立即开始 PR #20 review。

…trust model

- signReceiveWithAuthorization now validates validBefore > validAfter and
  window <= 300s before signing, mirroring GTokenAuthorization RC-1 on-chain check
- GTokenAuthorizationActions: expand xPNTsToken docs to explain it is NOT part
  of the EIP-712 signature (relay-supplied) and the RC-2 trust implications;
  add JSDoc on each arg for discoverability
@jhfnetboy

Copy link
Copy Markdown
Member

🔄 Re-Review Round 2 — PR #23 (feat/gtokenauth-eip3009)

Claude Round 2

Fix commit 4e5972f verified via gh pr diff.

[Claude H1] signReceiveWithAuthorization 校验 ✅ FIXED
packages/x402/src/eip3009.ts 中新增两个 guard:

if (params.validBefore <= params.validAfter) {
    throw new Error('validBefore must be greater than validAfter');
}
if (params.validBefore - params.validAfter > 300n) {
    throw new Error(`Authorization window ${...}s exceeds MAX_AUTH_VALIDITY (300s)`);
}

✓ 无效窗口在链上提交前即被拦截。

[Claude M1] xPNTsToken 信任模型文档 ✅ FIXED
gTokenAuthorization.ts 中的注释清晰说明 xPNTsToken 不在 EIP-712 签名范围内,并告知调用者在不受信任的 relay 场景应传 address(0)。✓


Codex Round 2

[NEW H1] signTransferWithAuthorization 缺少相同的校验守卫

GTokenAuthorization 合约同样暴露 transferWithAuthorization,且根据 ABI 中 MAX_AUTH_VALIDITY 常量推断,该函数在链上同样强制 300s 窗口限制。但 signTransferWithAuthorization(已存在函数)在本 PR 中没有添加等效的 validBefore > validAfter> 300n 校验,用户使用该函数给 GToken transfer 签名时,若传入无效窗口,签名不会在客户端被拒绝,只会在链上 revert。

建议修复方案(二选一):

  • 方案 A:在 signTransferWithAuthorization 中添加相同的两个 guard(如果该函数专门用于 GTokenAuthorization)
  • 方案 B:新增 signGTokenTransferWithAuthorization 包装器,保留通用函数不变,在 GToken 专用入口添加校验(推荐,不影响已有调用者)

[Low] 校验逻辑的负向测试缺失
eip3009-authorization.test.ts 仅覆盖正常路径,建议补充:validBefore <= validAfter 应抛出、window > 300n 应抛出的单测。


PK Summary

Finding Claude Codex
H1 signReceiveWithAuthorization 校验 ✅ FIXED ✅ FIXED
M1 xPNTsToken 信任模型文档 ✅ FIXED ✅ FIXED
NEW H1 signTransferWithAuthorization 缺少校验 ➕ 新发现 ⚠️ 明确指出
Low 负向测试缺失 同意 同意

原始两个 issue 均已修复。但 新 H1 需要处理后才建议合并:若 GTokenAuthorization 对 transfer 同样执行 MAX_AUTH_VALIDITY,请在对应签名路径加上防护。

@jhfnetboy jhfnetboy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

原始两个 blocking issue 均已修复。新发现的 H1(signTransferWithAuthorization 缺少 MAX_AUTH_VALIDITY 校验守卫)需要处理后合并。

…IDITY guard

GTokenAuthorization enforces 300s on-chain for both transfer and receive.
Add a GToken-specific signGTokenTransferWithAuthorization wrapper that
validates the authorization window before signing, while keeping the
generic signTransferWithAuthorization unchanged for non-GToken callers.

Also add negative test cases for both GToken signing helpers covering
validBefore <= validAfter and window > 300s rejection paths.
@jhfnetboy

Copy link
Copy Markdown
Member

🥊 对抗式双重 Review:Claude(Round 1)+ Codex(Round 2)

PR: feat/gtokenauth-eip3009 — EIP-3009 GTokenAuthorization 实现
Reviewer: Claude (local model, gh pr diff) + Codex (subagent, PR branch diff)


Round 1:Claude 审查

🟡 Medium

signGTokenTransferWithAuthorization 缺少 walletClient.account guard

  • 位置:packages/x402/src/eip3009.tssignGTokenTransferWithAuthorization 函数
  • signReceiveWithAuthorizationsignCancelAuthorization 都有:
    const account = walletClient.account;
    if (!account) throw new Error('WalletClient must have an account');
  • signGTokenTransferWithAuthorization 直接 delegate 给 signTransferWithAuthorization,没有 account guard。
  • 调用方传入无 account 的 walletClient,会得到运行时的模糊错误而非明确提示。

🔵 Low

  1. 缺少对应测试signGTokenTransferWithAuthorization 没有"如果 walletClient 无 account 则 throw"的测试用例,而 signReceiveWithAuthorization 有(eip3009-authorization.test.ts)。
  2. GTOKEN_EIP712_DOMAIN 硬编码name: 'GToken', version: '1' 直接写死,如果合约升级改 domain 版本,需要单独追踪。注释已有说明,但建议在函数文档注明使用前应与部署合约实际 domain 核对。

✅ 正面评价

  • 正确的设计选择:新增 GToken 特定包装函数而非修改通用 signTransferWithAuthorization,保持了向后兼容性。
  • window guard 完整:300s MAX_AUTH_VALIDITY 校验在 transfer 和 receive 两条路径都实现了。
  • xPNTsToken trust model 文档充分:代码注释清楚说明了 xPNTsToken 不进入 EIP-712 签名的设计理由。
  • 测试覆盖率高:ABI 覆盖、负向测试、域名形状验证都有。

Round 2:Codex 审查

🟡 Medium

signer/authorizer 地址不做客户端校验——可导致 InvalidSignature revert

  • 所有三个签名函数(signGTokenTransferWithAuthorizationsignReceiveWithAuthorizationsignCancelAuthorization)均不校验 walletClient.account.address === from / === authorizer
  • 用户以错误账户签名 → 生成签名 → 链上 InvalidSignature revert,消耗 Gas,调试困难。
  • 建议在每个函数中加:
    if (account.address.toLowerCase() !== params.from.toLowerCase()) {
      throw new Error(`Signer ${account.address} does not match from ${params.from}`);
    }

🔵 Low

  1. receiveWithAuthorization 链上 msg.sender == to 约束只有注释,action factory 无 runtime guard:调用方用错误账户会白白消耗 Gas 得到 CallerMustBeRecipient revert。建议在 gTokenAuthorizationActions 中对 receiveWithAuthorization 加调用账户与 to 的一致性校验。
  2. 测试覆盖缺口
    • 无 EIP-3009 typehash 常量断言(防止字段顺序变更不被发现)
    • 无 signer/authorizer 地址不一致的拒绝测试
    • 无跨函数 replay 防护测试(同一 nonce 不能同时用于 transfer 和 cancel)
    • 未显式确认 xPNTsToken 不进入签名(只有注释,无测试断言)

⚔️ PK 对比表

维度 Claude Codex 是否有分歧
signGTokenTransferWithAuthorization account guard 🟡 Medium — 缺少 🟡 也发现(作为 signer/from 校验的前置) 同向,Codex 更激进
signer === from 客户端校验 未独立提出 🟡 Medium — 缺少 Codex 额外发现
receiveWithAuthorization CallerMustBeRecipient guard 未独立提出 🔵 Low — 无 runtime guard Codex 额外发现
测试覆盖 🔵 Low — signGTokenTransfer 无 account 测试 🔵 Low — 4 处缺口(typehash / signer mismatch / replay / xPNTs) 同向,Codex 列举更细
设计正确性(wrapper 方案) ✅ 明确好评 无异议 一致
window guard ✅ 正确 无异议 一致

🏁 联合结论

结论:Request Changes(需修改后合并)

必须修复(阻塞合并):

  1. signGTokenTransferWithAuthorization 添加 account guard(Claude Medium + Codex 前置条件):

    export async function signGTokenTransferWithAuthorization(walletClient, params): Promise<Hex> {
      const account = walletClient.account;
      if (!account) throw new Error('WalletClient must have an account'); // ← 加这行
      if (params.validBefore <= params.validAfter) throw new Error('...');
      if (params.validBefore - params.validAfter > 300n) throw new Error('...');
      return signTransferWithAuthorization(walletClient, params);
    }
  2. 添加对应测试signGTokenTransferWithAuthorization 需要"无 account → throw"测试,与其他两个函数保持对称。

建议修复(不阻塞但强烈推荐):

  1. signer/from 地址一致性校验(Codex Medium):三个签名函数都应校验签名账户地址与参数中的 from/authorizer 一致。这不是 EIP-3009 标准要求,但显著改善调试体验,避免消耗 Gas 后才得到 InvalidSignature
  2. receiveWithAuthorization action 中加 msg.sender == to 的提前校验(Codex Low)。

可合并后跟进(下次迭代):

  1. typehash 常量的断言测试
  2. GTOKEN_EIP712_DOMAIN 版本注释增强

两位 reviewer 在核心问题上方向一致:account guard 不对称是真实缺陷,需要修复。Codex 提出的 signer/from 校验是一个额外的防御层,推荐采纳。整体实现质量高,修复上述 2 个必修项即可合并。

Claude: local model review via gh pr diff | Codex: subagent review via PR branch

@jhfnetboy jhfnetboy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes per combined Claude + Codex review — see detailed comment above.

…oken signers

signGTokenTransferWithAuthorization was missing the walletClient.account
guard that signReceiveWithAuthorization and signCancelAuthorization both
have, creating an asymmetric API.

Also add signer-address-must-match-from/authorizer checks to all three
GToken signing functions: wrong account produces a clear client-side
error instead of consuming gas on a guaranteed-to-fail tx.

Tests updated to match: happy paths use the correct signing account,
new negative cases cover no-account and signer-mismatch paths.

@jhfnetboy jhfnetboy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - all required and suggested fixes addressed. account guard + signer/from consistency checks added to all three signing functions, tests updated across the board. Ready to merge.

@fanhousanbu
fanhousanbu merged commit a2ce75c into main May 24, 2026
1 check passed
@fanhousanbu
fanhousanbu deleted the feat/gtokenauth-eip3009 branch May 24, 2026 13:49
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.

2 participants