Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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!;
Expand All @@ -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<typeof parseErc6492Signature>;
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 &&
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -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,
};
}
Expand All @@ -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
Expand All @@ -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 {
Expand Down
173 changes: 173 additions & 0 deletions typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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",
Expand Down
17 changes: 13 additions & 4 deletions typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
};
}
Expand Down