From b3c1379270aa218d18e7c33d6b31b0d421015455 Mon Sep 17 00:00:00 2001 From: namedfarouk Date: Sat, 4 Apr 2026 10:12:12 +0100 Subject: [PATCH] fix: stop swallowing facilitator verification errors --- .../evm/src/exact/facilitator/eip3009.ts | 47 ++++- .../evm/src/exact/facilitator/permit2.ts | 33 +++- .../evm/test/unit/exact/facilitator.test.ts | 173 ++++++++++++++++++ .../svm/src/exact/facilitator/scheme.ts | 17 +- 4 files changed, 255 insertions(+), 15 deletions(-) diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts index e0f3f72..e42d2be 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts @@ -9,6 +9,10 @@ import { authorizationTypes, eip3009ABI } from "../../constants"; import { FacilitatorEvmSigner } from "../../signer"; import { ExactEIP3009Payload } from "../../types"; +function toErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + export interface EIP3009FacilitatorConfig { /** * If enabled, the facilitator will deploy ERC-4337 smart wallets @@ -101,7 +105,9 @@ export async function verifyEIP3009( payer, }; } - } catch { + } catch (error) { + console.warn("EIP-3009 signature verification threw an error:", toErrorMessage(error)); + // Signature verification failed - could be an undeployed smart wallet // Check if smart wallet is deployed const signature = eip3009Payload.signature!; @@ -110,11 +116,35 @@ export async function verifyEIP3009( if (isSmartWallet) { const payerAddress = eip3009Payload.authorization.from; - const bytecode = await signer.getCode({ address: payerAddress }); + let bytecode: Hex; + try { + bytecode = await signer.getCode({ address: payerAddress }); + } catch (getCodeError) { + const message = toErrorMessage(getCodeError); + console.warn("Failed to inspect smart wallet bytecode during verification:", message); + return { + isValid: false, + invalidReason: "invalid_exact_evm_payload_signature_verification_error", + invalidMessage: message, + payer, + }; + } if (!bytecode || bytecode === "0x") { // Wallet is not deployed. Check if it's EIP-6492 with deployment info. - const erc6492Data = parseErc6492Signature(signature); + let erc6492Data: ReturnType; + try { + erc6492Data = parseErc6492Signature(signature); + } catch (parseError) { + const message = toErrorMessage(parseError); + console.warn("Failed to parse EIP-6492 signature payload:", message); + return { + isValid: false, + invalidReason: "invalid_exact_evm_payload_signature", + invalidMessage: message, + payer, + }; + } const hasDeploymentInfo = erc6492Data.address && erc6492Data.data && @@ -192,8 +222,15 @@ export async function verifyEIP3009( payer, }; } - } catch { - // If we can't check balance, continue with other validations + } catch (error) { + const message = toErrorMessage(error); + console.warn("EIP-3009 balance check failed during verification:", message); + return { + isValid: false, + invalidReason: "invalid_exact_evm_payload_balance_check_failed", + invalidMessage: message, + payer, + }; } // Verify amount is sufficient diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts index b627453..660edb0 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts @@ -15,6 +15,10 @@ import { import { FacilitatorEvmSigner } from "../../signer"; import { ExactPermit2Payload } from "../../types"; +function toErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + // ERC20 allowance ABI for checking Permit2 approval const erc20AllowanceABI = [ { @@ -167,10 +171,13 @@ export async function verifyPermit2( payer, }; } - } catch { + } catch (error) { + const message = toErrorMessage(error); + console.warn("Permit2 signature verification threw an error:", message); return { isValid: false, - invalidReason: "invalid_permit2_signature", + invalidReason: "invalid_permit2_signature_verification_error", + invalidMessage: message, payer, }; } @@ -191,8 +198,15 @@ export async function verifyPermit2( payer, }; } - } catch { - // If we can't check allowance, continue - settlement will fail if insufficient + } catch (error) { + const message = toErrorMessage(error); + console.warn("Permit2 allowance check failed during verification:", message); + return { + isValid: false, + invalidReason: "permit2_allowance_check_failed", + invalidMessage: message, + payer, + }; } // Check balance @@ -212,8 +226,15 @@ export async function verifyPermit2( payer, }; } - } catch { - // If we can't check balance, continue with other validations + } catch (error) { + const message = toErrorMessage(error); + console.warn("Permit2 balance check failed during verification:", message); + return { + isValid: false, + invalidReason: "permit2_balance_check_failed", + invalidMessage: message, + payer, + }; } return { diff --git a/typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts b/typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts index 487040a..a0cf183 100644 --- a/typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts @@ -460,6 +460,148 @@ describe("ExactEvmScheme (Facilitator)", () => { expect(result.invalidReason).toBe("invalid_permit2_recipient_mismatch"); expect(result.payer).toBe(mockClientSigner.address); }); + + it("should fail Permit2 verification when allowance check throws", async () => { + const requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { name: "USDC", version: "2", assetTransferMethod: "permit2" }, + }; + + mockFacilitatorSigner.readContract = vi + .fn() + .mockRejectedValue(new Error("allowance rpc timeout")); + + const permit2Payload: PaymentPayload = { + x402Version: 2, + payload: { + signature: "0xmocksignature", + permit2Authorization: { + from: mockClientSigner.address, + permitted: { + token: requirements.asset, + amount: requirements.amount, + }, + spender: x402ExactPermit2ProxyAddress, + nonce: "12345", + deadline: "999999999999", + witness: { + to: requirements.payTo, + validAfter: "0", + extra: "0x", + }, + }, + }, + accepted: requirements, + resource: { url: "", description: "", mimeType: "" }, + }; + + const result = await facilitator.verify(permit2Payload, requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("permit2_allowance_check_failed"); + expect(result.invalidMessage).toContain("allowance rpc timeout"); + expect(result.payer).toBe(mockClientSigner.address); + }); + + it("should fail Permit2 verification when balance check throws", async () => { + const requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { name: "USDC", version: "2", assetTransferMethod: "permit2" }, + }; + + mockFacilitatorSigner.readContract = vi + .fn() + .mockResolvedValueOnce(BigInt("10000000000")) + .mockRejectedValueOnce(new Error("balance rpc timeout")); + + const permit2Payload: PaymentPayload = { + x402Version: 2, + payload: { + signature: "0xmocksignature", + permit2Authorization: { + from: mockClientSigner.address, + permitted: { + token: requirements.asset, + amount: requirements.amount, + }, + spender: x402ExactPermit2ProxyAddress, + nonce: "12345", + deadline: "999999999999", + witness: { + to: requirements.payTo, + validAfter: "0", + extra: "0x", + }, + }, + }, + accepted: requirements, + resource: { url: "", description: "", mimeType: "" }, + }; + + const result = await facilitator.verify(permit2Payload, requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("permit2_balance_check_failed"); + expect(result.invalidMessage).toContain("balance rpc timeout"); + expect(result.payer).toBe(mockClientSigner.address); + }); + + it("should return a specific code when Permit2 signature verification throws", async () => { + const requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { name: "USDC", version: "2", assetTransferMethod: "permit2" }, + }; + + mockFacilitatorSigner.verifyTypedData = vi + .fn() + .mockRejectedValue(new Error("typed-data verifier crashed")); + + const permit2Payload: PaymentPayload = { + x402Version: 2, + payload: { + signature: "0xmocksignature", + permit2Authorization: { + from: mockClientSigner.address, + permitted: { + token: requirements.asset, + amount: requirements.amount, + }, + spender: x402ExactPermit2ProxyAddress, + nonce: "12345", + deadline: "999999999999", + witness: { + to: requirements.payTo, + validAfter: "0", + extra: "0x", + }, + }, + }, + accepted: requirements, + resource: { url: "", description: "", mimeType: "" }, + }; + + const result = await facilitator.verify(permit2Payload, requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_signature_verification_error"); + expect(result.invalidMessage).toContain("typed-data verifier crashed"); + expect(result.payer).toBe(mockClientSigner.address); + }); }); describe("Permit2 settlement", () => { @@ -556,6 +698,37 @@ describe("ExactEvmScheme (Facilitator)", () => { }); describe("Error cases", () => { + it("should fail EIP-3009 verification when balance check throws", async () => { + const requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { name: "USDC", version: "2" }, + }; + + mockFacilitatorSigner.readContract = vi + .fn() + .mockRejectedValue(new Error("balance rpc unavailable")); + + const paymentPayload = await client.createPaymentPayload(2, requirements); + + const fullPayload: PaymentPayload = { + ...paymentPayload, + accepted: requirements, + resource: { url: "", description: "", mimeType: "" }, + }; + + const result = await facilitator.verify(fullPayload, requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_evm_payload_balance_check_failed"); + expect(result.invalidMessage).toContain("balance rpc unavailable"); + expect(result.payer).toBe(mockClientSigner.address); + }); + it("should handle invalid signature format", async () => { const requirements: PaymentRequirements = { scheme: "exact", diff --git a/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts b/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts index 0aa5406..603007b 100644 --- a/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts @@ -33,6 +33,10 @@ import type { FacilitatorSvmSigner } from "../../signer"; import type { ExactSvmPayloadV2 } from "../../types"; import { decodeTransactionFromPayload, getTokenPayerFromTransaction } from "../../utils"; +function toErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + /** * SVM facilitator implementation for the Exact payment scheme. */ @@ -129,7 +133,8 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator { let transaction; try { transaction = decodeTransactionFromPayload(exactSvmPayload); - } catch { + } catch (error) { + console.warn("Failed to decode SVM transaction payload:", toErrorMessage(error)); return { isValid: false, invalidReason: "invalid_exact_svm_payload_transaction_could_not_be_decoded", @@ -200,7 +205,8 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator { } else { parsedTransfer = parseTransferCheckedInstruction2022(transferIx as never); } - } catch { + } catch (error) { + console.warn("Failed to parse SVM transfer instruction:", toErrorMessage(error)); return { isValid: false, invalidReason: "invalid_exact_svm_payload_no_transfer_instruction", @@ -248,10 +254,13 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator { payer, }; } - } catch { + } catch (error) { + const message = toErrorMessage(error); + console.warn("Failed to derive expected recipient ATA during SVM verification:", message); return { isValid: false, - invalidReason: "invalid_exact_svm_payload_recipient_mismatch", + invalidReason: "invalid_exact_svm_payload_recipient_ata_lookup_failed", + invalidMessage: message, payer, }; }