fix(sdk-coin-xrp): partial-payment verify and explain fallthrough (CSHLD-1452) - #9478
Merged
mukeshsp merged 1 commit intoAug 12, 2026
Conversation
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>
Contributor
Contributor
Author
Contributor
|
Claude finished @mukeshsp's task —— View job PR Review: XRP Partial Payment FixesOverall 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 ReviewFinding 1: Partial Payment (
|
mukeshsp
marked this pull request as ready for review
August 12, 2026 08:45
nvjsr
approved these changes
Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.AffectedNodesdeltas, never from theAmountfield — 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):verifyTransaction(F1, F2)explainTransaction(F1, F3)verifyTransactionis 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 theRecoveryInforeturned 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 thanAmountand still succeed; the actual delivered value is inmeta.delivered_amount, not the signed blob. PreviouslyAmountwas read directly and reported as delivered.explainTransaction(coin-level) andexplainPaymentTransaction(lib) now detecttfPartialPayment. When metadata is supplied,outputAmount/outputs[].amountusemeta.delivered_amount; otherwisepartialPayment: trueis surfaced so consumers know the value is requested, not settled.verifyTransactionrejectstfPartialPaymentprebuilds outright (BitGo never builds them — defense in depth).TF_PARTIAL_PAYMENT = 0x00020000constant, with a comment distinguishing it from the numerically-identicalREQUIRE_DESTINATION_TAG_FLAG(same bit, different flag space — Payment tx flag vs AccountRoot ledger flag).metafield onExplainTransactionOptions; newPaymentTransactionExplanationtype in theTransactionExplanationunion.F2 — Cross-currency / ICA payment verification
verifyTransactionskipped the amount comparison entirely wheneveroutput.amountwas an object (every cross-currency/token transfer went unverified).typeof output.amount !== 'object'skip.toBaseUnits()usinggetBaseFactor()(overridden byXrpTokento use the token's decimals, not the base coin's) so display-unitAmount.valueis compared against base-unit recipient intent for both base XRP and tokens.F3 —
explainTransaction()fallthrough (live AccountDelete bug)The coin-level
explainTransaction(xrp.ts:212) had branches only forAccountSet/TrustSet/MPTokenAuthorizeand silently fell through to a Payment-shaped return for any other type — includingAccountDelete(a supported enum member with noAmountfield), whichrecover()invokes. Result:outputAmount: undefined,outputs[0].amount: undefined.AccountDeleteandSignerListSetbranches + adefault: throw, mirroring the safe switch inlib/transaction.ts:196-211.AccountDeletereturns a0-value placeholder (full sweep, exact amount unknown at build time), matchinglib/transaction.ts.F4 — AMM transaction types
XrpTransactionTypeenum has no AMM members;fromRawTransaction()correctly rejects unrecognized types today, but there's no structured handling for future AMM support.Test results
The 27 failures are pre-existing (identical on master): MPT/
xrpldependency 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:delivered_amountfrom metadata (XRP)delivered_amountfrom metadataUpdated existing tests (2 token-transfer verify tests)
Two existing tests in
test/unit/xrp.ts(should verify token transfersandshould verify token transfers with recipient has dt) had their recipientamountchanged from1e82(10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000) to1e13(10000000000000). This was required by the F2 fix, not a change in test intent:verifyTransactionskipped the amount comparison entirely wheneveroutput.amountwas an object (the IOU{value, currency, issuer}shape). So the recipientamountwas never actually compared against the txAmount— the1e82value was nonsense that passed only because the check was skipped. That's the very bug F2 fixes.toBaseUnits()converts the txAmount.valuefrom display units to base units usinggetBaseFactor(). The test's token istxrp:rlusd(15 decimal places), and the txAmount.valueis"0.01", so the correct base-unit recipient amount is0.01 × 10^15 = 1e13 = 10000000000000.should fail to verify a token transfer when the amount does not match (object Amount)) uses2e13with the same0.01tx and confirms it is now rejected — proving the comparison actually runs and a mismatch is caught.Amount.value0.011e131e820.011e131e130.011e132e13Files changed
src/lib/constants.tsTF_PARTIAL_PAYMENTconstantsrc/lib/iface.tsmetaoption onExplainTransactionOptions;PaymentTransactionExplanationtype; enum stability docsrc/lib/transaction.tsexplainPaymentTransactionsurfacespartialPayment;loadInputsAndOutputsnotesrc/lib/utils.tsisPartialPayment(),getDeliveredAmountValue()helperssrc/xrp.tstest/unit/xrp.tsOut of scope
🤖 Generated with Claude Code