Skip to content

fix(sdk-coin-xrp): partial-payment verify and explain fallthrough (CSHLD-1452) - #9478

Open
mukeshsp wants to merge 1 commit into
masterfrom
mukesh/cshld-1452-sdk-xrp-fix-partial-payment-cross-currency-verify
Open

fix(sdk-coin-xrp): partial-payment verify and explain fallthrough (CSHLD-1452)#9478
mukeshsp wants to merge 1 commit into
masterfrom
mukesh/cshld-1452-sdk-xrp-fix-partial-payment-cross-currency-verify

Conversation

@mukeshsp

@mukeshsp mukeshsp commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes 4 SDK-side findings from CR-1406 §5.5 in modules/sdk-coin-xrp. These are the SDK-layer sibling of CSHLD-763 (XRP Transaction Type Enforcement — Whitelist/Blacklist Audit).

Linear: https://linear.app/bitgo/issue/CSHLD-1452/sdk-xrp-fix-partial-payment-cross-currency-verify-explaintransaction

Why these fixes are needed (the indexer is balance-safe, but these guard other paths)

The XRP indexer computes balance entries from meta.AffectedNodes deltas, never from the Amount field — so balance correctness is already handled and no balance fix is needed. However, the SDK fixes below guard concerns the indexer cannot cover (the indexer only sees the confirmed on-chain result, after the fact):

Concern Indexer covers? These fixes?
Balance correctness ✅ Yes (AffectedNodes deltas) ❌ Not needed
Pre-broadcast intent verification ❌ No (runs before confirm) verifyTransaction (F1, F2)
Recovery info / tx display ❌ No (no indexer in path) explainTransaction (F1, F3)

verifyTransaction is a security check that runs before signing/broadcasting to catch a tampered or malicious prebuild; the indexer computing the correct balance after confirmation doesn't undo a wrong tx already signed. explainTransaction/recover() build the RecoveryInfo returned to the user; the indexer is not in that path.

Findings fixed

F1 — Partial payment (tfPartialPayment)

tfPartialPayment (0x00020000) allows an XRPL Payment to deliver less than Amount and still succeed; the actual delivered value is in meta.delivered_amount, not the signed blob. Previously Amount was read directly and reported as delivered.

  • explainTransaction (coin-level) and explainPaymentTransaction (lib) now detect tfPartialPayment. When metadata is supplied, outputAmount/outputs[].amount use meta.delivered_amount; otherwise partialPayment: true is surfaced so consumers know the value is requested, not settled.
  • verifyTransaction rejects tfPartialPayment prebuilds outright (BitGo never builds them — defense in depth).
  • New TF_PARTIAL_PAYMENT = 0x00020000 constant, with a comment distinguishing it from the numerically-identical REQUIRE_DESTINATION_TAG_FLAG (same bit, different flag space — Payment tx flag vs AccountRoot ledger flag).
  • New optional meta field on ExplainTransactionOptions; new PaymentTransactionExplanation type in the TransactionExplanation union.

F2 — Cross-currency / ICA payment verification

verifyTransaction skipped the amount comparison entirely whenever output.amount was an object (every cross-currency/token transfer went unverified).

  • Removed the typeof output.amount !== 'object' skip.
  • Added toBaseUnits() using getBaseFactor() (overridden by XrpToken to use the token's decimals, not the base coin's) so display-unit Amount.value is compared against base-unit recipient intent for both base XRP and tokens.
  • Updated 2 existing token-transfer tests that relied on the skip (see "Updated existing tests" below).

F3 — explainTransaction() fallthrough (live AccountDelete bug)

The coin-level explainTransaction (xrp.ts:212) had branches only for AccountSet/TrustSet/MPTokenAuthorize and silently fell through to a Payment-shaped return for any other type — including AccountDelete (a supported enum member with no Amount field), which recover() invokes. Result: outputAmount: undefined, outputs[0].amount: undefined.

  • Added explicit AccountDelete and SignerListSet branches + a default: throw, mirroring the safe switch in lib/transaction.ts:196-211.
  • AccountDelete returns a 0-value placeholder (full sweep, exact amount unknown at build time), matching lib/transaction.ts.

F4 — AMM transaction types

XrpTransactionType enum has no AMM members; fromRawTransaction() correctly rejects unrecognized types today, but there's no structured handling for future AMM support.

  • Added a stability doc-comment: string values are public API compared by downstream consumers, so AMM members must be added with exact XRPL names alongside switch-statement updates — never change string values.

Test results

127 passing, 27 failing

The 27 failures are pre-existing (identical on master): MPT/xrpl dependency issues (isMPTAmount is not a function, Invalid field TransactionType: MPTokenAuthorize) — none touch the changed code paths. The +8 passing vs master's 119 = my 8 new tests, all green:

  • AccountDelete explain (no fallthrough)
  • SignerListSet explain (no fallthrough)
  • SetRegularKey explain → rejected as unsupported
  • Partial payment flagged without metadata
  • Partial payment uses delivered_amount from metadata (XRP)
  • Partial payment uses issued-currency delivered_amount from metadata
  • Cross-currency mismatch verify → rejected
  • Partial-payment prebuild verify → rejected

Updated existing tests (2 token-transfer verify tests)

Two existing tests in test/unit/xrp.ts (should verify token transfers and should verify token transfers with recipient has dt) had their recipient amount changed from 1e82 (10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000) to 1e13 (10000000000000). This was required by the F2 fix, not a change in test intent:

  • Before the fix, verifyTransaction skipped the amount comparison entirely whenever output.amount was an object (the IOU {value, currency, issuer} shape). So the recipient amount was never actually compared against the tx Amount — the 1e82 value was nonsense that passed only because the check was skipped. That's the very bug F2 fixes.
  • After the fix, the comparison runs. toBaseUnits() converts the tx Amount.value from display units to base units using getBaseFactor(). The test's token is txrp:rlusd (15 decimal places), and the tx Amount.value is "0.01", so the correct base-unit recipient amount is 0.01 × 10^15 = 1e13 = 10000000000000.
  • A new negative test (should fail to verify a token transfer when the amount does not match (object Amount)) uses 2e13 with the same 0.01 tx and confirms it is now rejected — proving the comparison actually runs and a mismatch is caught.
tx Amount.value token decimals base units recipient amount result
Old test (pre-fix) 0.01 15 1e13 1e82 passed — but check was skipped (false green)
Updated test (post-fix) 0.01 15 1e13 1e13 passes — amount genuinely matches
New negative test 0.01 15 1e13 2e13 rejected — comparison runs, mismatch caught

Files changed

File Change
src/lib/constants.ts TF_PARTIAL_PAYMENT constant
src/lib/iface.ts meta option on ExplainTransactionOptions; PaymentTransactionExplanation type; enum stability doc
src/lib/transaction.ts explainPaymentTransaction surfaces partialPayment; loadInputsAndOutputs note
src/lib/utils.ts isPartialPayment(), getDeliveredAmountValue() helpers
src/xrp.ts F1/F2/F3 fixes (explain branches, partial-payment, cross-currency verify)
test/unit/xrp.ts 8 new tests + 2 updated

Out of scope

  • Indexer-layer txType registry/whitelist — that's CSHLD-763 itself, and balance correctness (already handled by the indexer via AffectedNodes).
  • P2 reserve-requirement balance display.
  • Actually adding AMM transaction support (no BitGo usage today).

🤖 Generated with Claude Code

Fix 4 CR-1406 §5.5 findings in modules/sdk-coin-xrp:

F1 partial payment (tfPartialPayment): explainTransaction and
explainPaymentTransaction now detect tfPartialPayment and, when metadata
is supplied, use meta.delivered_amount as the output amount instead of
the requested Amount. Without metadata, partialPayment: true is surfaced
on the explanation. verifyTransaction rejects tfPartialPayment prebuilds
(BitGo never builds them). Adds TF_PARTIAL_PAYMENT constant, distinct
from the numerically-identical REQUIRE_DESTINATION_TAG_FLAG.

F2 cross-currency verify: verifyTransaction no longer skips amount
comparison for object Amount (IssuedCurrencyAmount/MPTAmount). Adds
toBaseUnits using getBaseFactor() so display-unit Amount.value is
compared against base-unit recipient intent for both base XRP and tokens.

F3 explainTransaction fallthrough: adds explicit AccountDelete and
SignerListSet branches plus a default:throw, eliminating the silent
Payment-shaped fallthrough that produced undefined outputAmount/outputs
for AccountDelete recovery. Mirrors the safe switch in lib/transaction.ts.

F4 AMM enum: documents XrpTransactionType string values as public API
compared by downstream consumers, so AMM members can be added later
without breaking string comparisons across the SDK boundary.

Adds 8 unit tests covering all four fixes; updates 2 existing token
transfer tests that relied on the F2 skip.

Ticket: CSHLD-1452

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

CSHLD-1452

@mukeshsp

Copy link
Copy Markdown
Contributor Author

@claude

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @mukeshsp's task —— View job


PR Review: XRP Partial Payment Fixes

Overall Assessment: EXCELLENT

This PR addresses critical security findings with high-quality implementations, comprehensive test coverage, and excellent documentation. The fixes are technically sound and follow security best practices.

Security Findings Review

Finding 1: Partial Payment (tfPartialPayment) ✅

  • Constants: Properly added TF_PARTIAL_PAYMENT = 0x00020000 with excellent documentation distinguishing it from REQUIRE_DESTINATION_TAG_FLAG (constants.ts:30-36)
  • Detection Logic: isPartialPayment() utility correctly uses bitwise AND (utils.ts:241-243)
  • Explain Transaction: Both coin-level (xrp.ts:350-351) and lib-level (transaction.ts:269) properly detect the flag and use meta.delivered_amount when available
  • Verify Transaction: Correctly rejects partial payment prebuilds (xrp.ts:546-548) since BitGo never builds them

Finding 2: Cross-Currency Verification ✅

  • Root Issue: Previously skipped amount comparison for object amounts, leaving all cross-currency/token transfers unverified
  • Fix: Removed the skip and added toBaseUnits() helper using getBaseFactor() for proper unit conversion (xrp.ts:558-566)
  • Token Support: Works correctly with XrpToken.getBaseFactor() override for token-specific decimals
  • Test Updates: Fixed two existing tests from 1e80 to proper 1e13 values

Finding 3: ExplainTransaction Fallthrough ✅

  • Root Issue: Missing branches for AccountDelete/SignerListSet caused dangerous fallthrough with undefined fields
  • Fix: Added explicit branches (xrp.ts:300-343) with proper 0-value placeholders and default: throw case (xrp.ts:387)
  • Prevents Bugs: Eliminates potential crashes in recover() and verifyTransaction()

Finding 4: AMM Transaction Types ✅

  • Documentation: Excellent stability comment explaining string values are public API (iface.ts:22-35)
  • Future-Proofing: Clear guidance for adding AMM support with exact XRPL names
  • Current State: Appropriate rejection of unrecognized types

Code Quality Assessment

Strengths:

  • Security-First: All fixes prevent potential exploits or data corruption
  • Comprehensive Testing: 8 new tests covering all edge cases and scenarios
  • Excellent Documentation: Clear comments explaining security implications
  • Type Safety: Proper TypeScript usage with no any types
  • Error Handling: Proper exception throwing with descriptive messages
  • Backwards Compatibility: Changes don't break existing functionality

Test Coverage:

  • All 8 new tests are well-structured and cover both positive/negative cases
  • Test names clearly describe the security vulnerability being addressed
  • Covers all transaction types and edge cases

Repository Compliance ✅

  • Follows BitGoJS conventional commit format
  • Uses existing patterns and conventions
  • Proper monorepo structure with scoped changes
  • TypeScript guidelines followed (no any types)
  • Security best practices maintained

Minor Suggestions

  1. Consider adding JSDoc comments to the new utility functions for API documentation
  2. The toBaseUnits helper in verifyTransaction could be extracted to utils for reuse

Recommendation: APPROVE

This is an exemplary security fix PR that:

  • Addresses real security vulnerabilities with proper fixes
  • Includes comprehensive test coverage
  • Maintains code quality and follows project conventions
  • Has excellent documentation explaining the security implications
  • Shows deep understanding of the XRP protocol and BitGo's usage patterns

The implementation is production-ready and significantly improves the security posture of the XRP SDK.

@mukeshsp
mukeshsp marked this pull request as ready for review August 12, 2026 08:45
@mukeshsp
mukeshsp requested a review from a team as a code owner August 12, 2026 08:45
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.

1 participant