Skip to content

Commit 03b8147

Browse files
holonbot[bot]jolestarclaude
authored
Fix: Facilitator: Missing validation for facilitatorFee <= value causes failed transactions (#201)
* Fix: Facilitator: Missing validation for facilitatorFee <= value causes failed transactions * Apply changes from Holon * Apply changes from Holon * Fix: Complete fee validation test fixes for Issue #200 Fixed failing tests in PR #201 by addressing mock infrastructure issues: **Test Infrastructure Fixes:** - Fixed mock export name: `parseSettlementExtra` instead of `parseSettlementExtraCore` - Added mocks for `@x402x/extensions` functions: - `toCanonicalNetworkKey` for v1→CAIP-2 conversion - `getNetworkAlias` for CAIP-2→v1 conversion - Added direct mock for `network-utils.ts` module functions - Fixed signature format in all test cases (hex string in payload.signature) **Test Data Fixes:** - Fixed `createMockSettlementRouterPaymentRequirements` to deep merge extra object - Updated all fee validation test cases to use correct signature format **Logging Improvements:** - Added debug logging for fee validation checks - Enhanced error logging with structured error messages **Test Results:** - All 360 unit tests passing ✓ - All 7 fee validation tests passing ✓ The fee validation implementation was already correct - tests were failing due to incomplete mocking. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]> --------- Co-authored-by: holonbot[bot] <250454749+holonbot[bot]@users.noreply.github.com> Co-authored-by: jolestar <[email protected]> Co-authored-by: Claude Sonnet 4.5 <[email protected]>
1 parent deaf29c commit 03b8147

File tree

4 files changed

+552
-25
lines changed

4 files changed

+552
-25
lines changed

facilitator/src/settlement.ts

Lines changed: 96 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,69 @@ export async function settleWithRouter(
323323
"Settlement authorization details (before on-chain validation)",
324324
);
325325

326-
// 5.5. Validate payment using @x402x/facilitator-sdk (SECURITY: prevent any invalid payments from wasting gas)
326+
// 5.5. CRITICAL: Validate facilitatorFee <= value BEFORE any validation or arithmetic operations
327+
// This MUST run before:
328+
// - Facilitator SDK verification (line 337) which may call commitment calculation
329+
// - Gas estimation (line 499) which subtracts fee from value
330+
// This validation is UNCONDITIONAL (applies to ALL networks, with or without balanceChecker)
331+
332+
const fee = BigInt(settlementParams.facilitatorFee || "0");
333+
const paymentValue = BigInt(authorization.value || "0");
334+
335+
logger.debug(
336+
{
337+
network,
338+
facilitatorFee: settlementParams.facilitatorFee,
339+
fee: fee.toString(),
340+
value: authorization.value,
341+
paymentValue: paymentValue.toString(),
342+
feeGtValue: fee > paymentValue,
343+
},
344+
"Fee validation check (issue #200)",
345+
);
346+
347+
if (fee > paymentValue) {
348+
// Use BigInt arithmetic for ratio to avoid precision loss
349+
const ratioBps = paymentValue > 0n ? (fee * 10000n) / paymentValue : 0n; // Basis points
350+
const ratioDisplay = Number(ratioBps) / 100; // Convert to percentage for display
351+
352+
logger.error(
353+
{
354+
payer: authorization.from,
355+
network,
356+
facilitatorFee: settlementParams.facilitatorFee,
357+
value: authorization.value,
358+
ratio: ratioDisplay.toFixed(2),
359+
},
360+
"Facilitator fee exceeds payment amount - rejecting transaction (applies to all networks)",
361+
);
362+
363+
return {
364+
success: false,
365+
errorReason: "FACILITATOR_FEE_EXCEEDS_VALUE" as any,
366+
transaction: "",
367+
network: paymentRequirements.network,
368+
payer: authorization.from,
369+
};
370+
}
371+
372+
// 5.6. Warn if fee ratio is suspicious (> 50% for any network)
373+
const feeRatioBps = paymentValue > 0n ? (fee * 10000n) / paymentValue : 0n; // Basis points
374+
const feeRatioPercent = Number(feeRatioBps) / 100; // Convert to percentage
375+
if (feeRatioPercent > 50) {
376+
logger.warn(
377+
{
378+
payer: authorization.from,
379+
network,
380+
facilitatorFee: settlementParams.facilitatorFee,
381+
value: authorization.value,
382+
feeRatio: `${feeRatioPercent.toFixed(1)}%`,
383+
},
384+
"High facilitator fee ratio detected - possible gas price issue (network-agnostic check)",
385+
);
386+
}
387+
388+
// 5.7. Validate payment using @x402x/facilitator-sdk (SECURITY: prevent any invalid payments from wasting gas)
327389
// Import the RouterSettlementFacilitator for verification
328390
const { createRouterSettlementFacilitator } = await import("@x402x/facilitator-sdk");
329391

@@ -602,9 +664,10 @@ export async function settleWithRouter(
602664
}
603665
}
604666

605-
// 8. Defensive balance check (verify stage should have already caught this)
667+
// 8. Defensive balance check and fee validation (applies to ALL networks)
606668
if (balanceChecker) {
607669
try {
670+
// 8.1 Check user balance
608671
const balanceCheck = await balanceChecker.checkBalance(
609672
signer as any, // Signer has readContract method needed for balance checks
610673
authorization.from as `0x${string}`,
@@ -632,29 +695,40 @@ export async function settleWithRouter(
632695
network: paymentRequirements.network,
633696
payer: authorization.from,
634697
};
635-
} else {
636-
logger.debug(
637-
{
638-
payer: authorization.from,
639-
network,
640-
balance: balanceCheck.balance,
641-
required: balanceCheck.required,
642-
cached: balanceCheck.cached,
643-
},
644-
"Balance check passed during settlement (defensive check)",
645-
);
646698
}
699+
700+
// 8.2 Fee validation already done unconditionally before gas estimation (line 487)
701+
// This section only logs the balance check result
702+
logger.debug(
703+
{
704+
payer: authorization.from,
705+
network,
706+
balance: balanceCheck.balance,
707+
required: balanceCheck.required,
708+
facilitatorFee: settlementParams.facilitatorFee,
709+
feeRatio: `${feeRatioPercent.toFixed(2)}%`,
710+
cached: balanceCheck.cached,
711+
},
712+
"Balance check passed (fee validation already completed)",
713+
);
647714
} catch (error) {
715+
// FIXED: Don't proceed with transaction if validation fails (affects ALL networks)
648716
logger.error(
649717
{
650718
error,
651719
payer: authorization.from,
652720
network,
653721
},
654-
"Balance check failed during settlement, proceeding with transaction",
722+
"Balance or fee validation failed - rejecting transaction (network-agnostic)",
655723
);
656-
// If balance check fails, we continue with the transaction
657-
// This ensures settlement can still work even if balance check has issues
724+
725+
return {
726+
success: false,
727+
errorReason: "VALIDATION_FAILED" as any,
728+
transaction: "",
729+
network: paymentRequirements.network,
730+
payer: authorization.from,
731+
};
658732
}
659733
}
660734

@@ -764,11 +838,16 @@ export async function settleWithRouter(
764838
gasMetrics,
765839
};
766840
} catch (error) {
841+
const errorMsg = error instanceof Error ? error.message : String(error);
842+
const errorStack = error instanceof Error ? error.stack : undefined;
843+
767844
logger.error(
768845
{
769-
error,
846+
error: errorMsg,
847+
errorStack,
770848
network: paymentRequirements.network,
771849
router: paymentRequirements.extra?.settlementRouter,
850+
facilitatorFee: paymentRequirements.extra?.facilitatorFee,
772851
},
773852
"Error in settleWithRouter",
774853
);

facilitator/test/mocks/signers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ export function createMockEvmSigner(options?: {
5151
writeContract.mockResolvedValue(options?.writeContractResolve || "0xtxhash");
5252
}
5353

54+
const waitForTransactionReceipt = vi.fn().mockResolvedValue({
55+
status: "success",
56+
transactionHash: options?.writeContractResolve || "0xtxhash",
57+
});
58+
5459
return {
5560
account: {
5661
address,
@@ -61,5 +66,6 @@ export function createMockEvmSigner(options?: {
6166
walletClient: {
6267
writeContract,
6368
},
69+
waitForTransactionReceipt,
6470
} as any;
6571
}

0 commit comments

Comments
 (0)