Skip to content

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

Open
fanhousanbu wants to merge 2 commits into
mainfrom
feat/gtokenauth-eip3009
Open

feat(core,x402): GTokenAuthorization EIP-3009 ABI + signing helpers#23
fanhousanbu wants to merge 2 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,请在对应签名路径加上防护。

Copy link
Copy Markdown
Member

@jhfnetboy jhfnetboy left a comment

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 校验守卫)需要处理后合并。

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