
XRP Ledger Partial Payments: The Non-Bug That Still Bleeds Balance Sheets
Wootoshi
On XRP Ledger, a transaction can be finalized as "Successful" while delivering less value than its own payload advertises. The amount field says 10,000 XRP. The delivered_amount metadata says 9,987.25. The ledger approves it. Validators reach consensus. The transaction is immutable. This is not an exploit, not a rounding error, not a proof-of-stake bug, not a validator conspiracy. It is a feature. It is called Partial Payments, and a wave of community explainers is currently calling it "not a bug." That label is accurate. That label is also dangerous if the reader translates "not a bug" into "no risk." The risk was never in the protocol. The risk sits inside every exchange, wallet, and merchant integration that trusts the wrong field. Based on my audit experience, that vulnerable population is far larger than the people writing the explainers assume.
Let me make the protocol baseline clear before we argue about risk. XRP Ledger is a Layer-1 consensus network, not a smart-contract platform in the Ethereum sense. It uses a federated consensus model, a native token XRP, and a payment engine optimized for settlement speed and low fees. Transactions are applied by validator nodes, and the resulting state changes are recorded in immutable metadata. A standard Payment transaction moves funds from a source account to a destination account, typically with exact delivery. But the protocol includes a flag, tfPartialPayment, that changes the settlement semantics.
When that flag is set, the transaction is no longer an exact-delivery contract. The Amount field no longer guarantees what the destination will receive. It becomes the requested or maximum target. If available liquidity, a path, a currency conversion, or a transfer fee prevents the exact target from being reached, the transaction does not automatically fail. It can settle with a lower delivered amount. The actual figure is recorded in delivered_amount. The status code can still be tesSUCCESS. From the ledger's perspective, everything worked as designed. The phrase "partial payment" is not slang. It is the formal name.
Why would any payment system design this on purpose? Because XRPL was built for path payments. A sender in one currency can route value through order books and issuer networks to deliver a different currency elsewhere. Those paths carry prices that move, liquidity that shifts, and fees that chip away at a target. If the protocol demanded exact delivery at every hop, millions of legitimate payments would fail under small slippage. Partial Payments acts as a settlement valve: deliver as much as the path can support, confirm the delivery, and let the receiver reconcile the difference. In a world focused on payment finality, this is defensible engineering. It is also, to use the current phrase, "not a bug."
What does "bug" even mean? In protocol code, a bug is a divergence between intended behavior and actual behavior. Partial Payments matches intended behavior, so calling it a bug is inaccurate. In integration code, a bug is anything that causes wrong financial outcomes. Reading Amount instead of delivered_amount is a bug. The phrase "not a bug" is correct in the first frame and dangerously incomplete in the second. Both frames are valid. The headline only uses one.
Let's slow down and inspect the transaction at field level. A Payment transaction on XRPL contains a Type, a Flags bitmask, an Account, a Destination, an Amount, and optionally a SendMax. For a partial payment, the Amount is not final. The node takes the path, estimates the deliverable amount, and records actual delivery. This is different from a standard transfer. If you send 100 XRP to a destination with no partial flag, the destination gets the intended amount. With the partial flag, the destination may receive less because the path consumed fees or liquidity. The delivered_amount field appears in the transaction metadata. That is the canonical record of what the destination actually received.
One nuance is often lost: the shape of delivered_amount is not always identical. In the JSON-RPC response, delivered_amount may appear as a string for XRP and as an object containing currency, value, and issuer for issued assets. This awkward shape is a classic integration trap. A developer under deadline sees Amount as a clean number and delivered_amount as a conditional structure, and chooses Amount. That is not laziness; it is a predictable outcome of where complexity is placed. The protocol solved a settlement problem but pushed the complexity downstream.
The core issue is less exotic than it sounds. Every deposit pipeline has to answer one question: how much value did this transaction actually deliver? The rule should be trivial: never use Amount to credit a user. Use delivered_amount. The fact that this rule needs to be stated in a 2025 explainer is itself the finding.
Why is the rule violated? Because most deposit pipelines are written by developers looking for the path of least resistance. The ledger response is parsed, the "success" result is checked, the first amount-looking field is used, and the account is credited. In a fast-moving bull market, this accidental architecture gets copied across projects. The implementation is not audited for edge-case semantics. The SDK returns are not compared against transaction metadata. The code is deployed because it works on the happy path. Then someone discovers a partial payment where the amount field is 50,000, delivered_amount is 142.17, and the user has already been credited 50,000.
Arbitrage isn't a strategy reserved for quantitative trading desks; it is the math of patience applied to chaos. An attacker who finds a deposit pipeline reading the wrong field will execute that trade as many times as the pipeline allows. The cost is not the single fake credit. The cost is the entire balance sheet line that says "reconciled" when it is not.
Let's model the attack in concrete terms. The attacker opens a deposit address on an exchange. They construct an XRPL Payment transaction to that address, using a path that can deliver a small amount. The transaction's Amount field declares a much larger delivery. The partial payment flag is set. The path executes. The ledger records a delivered_amount that is a small fraction of Amount. The transaction returns tesSUCCESS. If the exchange's deposit handler trusts Amount, it credits the large figure. If it also checks the source account, the attacker is still fine: they are merely exploiting an integration gap, not a consensus flaw. The exchange is left with a deposit that cannot be reversed and a balance sheet hole that has to be written off.
The explainer that prompted this analysis is unsigned. It carries no code references, no link to the official docs, no audit report. That matters. An educational article about a security-sensitive feature should set the example for verification. If a source says "not a bug" without proving it, the reader is being asked to accept a conclusion on authority. The reader is not being given a method. The method is the deliverable.
I have personally reviewed production code where this exact confusion was present. The team had built a bridge middleware that listened to XRPL events. The code checked only the top-level transaction result and the Amount field. When I asked about delivered_amount, the response was blank. The project had been running for eight months. No one had noticed because no attacker had exploited it yet. The code was not malicious. It was simply built on a convenient assumption: ledger says success, amount is amount. The issue took one afternoon to fix and one sentence to explain, but it survived because no one had ever stated the canonical rule.
In another engagement, I found a wallet backend that had no branch for partial payment metadata. It fell into an error handler every time delivered_amount did not match Amount. The feature was not the source of the error; the code was. The wallet team had upgraded their library, saw partial payment events in their logs, and classified them as anomalies. They spent a week chasing a phantom bug. The ledger was acting exactly as specified. The integration was not.
Most partial payments are boring. A merchant receives 99.5 instead of 100 because a path fee was consumed. The customer sees a payment receipt and neither party notices. The problem occurs when the difference is large enough to be weaponized, and the destination side has no reconciliation layer.
Compare XRPL with other rails. Ethereum does not have a native partial-delivery flag for ETH transfers. ERC-20 tokens are either transferred or the call reverts. Stellar has path payment operations with a min_receive parameter, but the integration rules are more explicit. Bitcoin is UTXO-based; the amount in a transaction output is final. XRPL's partial payment is unusual because successful settlement can mean less than the stated request. That unusualness is precisely why it demands a higher engineering standard. A design can be intentional and still be risky. The category "intended behavior" does not make integration errors impossible.
The economic damage is asymmetric. A faked deposit of 500 USD does not hurt a large exchange's capital. But the reconciliation investigation, the support ticket, the compliance review, the engineering hotfix, and the public trust loss can cost thousands. For a smaller wallet or payment gateway, a single exploited partial payment can exceed monthly revenue. The probability is not zero. The probability increases whenever an integration uses Amount instead of delivered_amount. The protocol does not need to add a fee. The feature already taxes the ecosystem through required vigilance.
The official XRPL documentation already warns developers to inspect delivered_amount. The warning exists. Yet a documentation page is not a runtime safeguard. It is a sign on a door that some developers never open.
Here is the uncomfortable part. The "Not a Bug" framing is not really an education campaign. It is a reputation management campaign. The feature is designed; nobody disputes that. But by putting the debate as "bug vs. not-a-bug," the explainer shifts attention away from the real question: is the integration standard around this feature safe? The answer is no. And no article title can fix that.
We don't get to call this case closed because the consensus code is correct. We don't need to tear down XRPL; the protocol is not broken. We need to change the default behavior of every system that connects to it. That is a much harder project. It means updating SDKs, rewriting deposit services, adding regression tests, and forcing vendors to prove that they read delivered_amount before they can claim compliance.
The "not a bug" explainer may even create an aggravating effect. A busy developer who reads the headline may stop worrying about partial payments entirely. The phrase functions as a memory compression error: "not a bug" becomes "not a problem." On a payment rail that explicitly permits partial delivery, "not a problem" is exactly the wrong conclusion.
During a bull market, new endpoints are added quickly. New coin listings, new deposit addresses, new bridge contracts. Pressure to ship is enormous. "Add support for asset X" is prioritized over "verify settlement semantics." Partial payment is the class of bug that lives in that pressure gap. It is not a bug in code; it is a bug in the process around the code.
There is a regulatory layer waiting in the background. Let's be precise, because this matters. A licensed payment processor or virtual asset service provider that loses customer funds because its deposit flow credits Amount instead of delivered_amount will not be able to tell a regulator that the ledger is fine. The regulator will ask a narrower question: did the firm's internal controls detect and prevent a known settlement feature from being misread? If the answer is "we relied on a blog post that said it is not a bug," the conversation will not end well. The future compliance standard for XRPL-connected custodians will likely include, at a minimum, an explicit validation of delivered_amount and a test case for partial payments. That standard will be written not by XRPL's core protocol maintainers, but by the market, after enough integration failures.
This is where the crisis-to-opportunity framework applies. The vulnerability is not an exotic zero-day. It is a well-documented semantic gap, and that makes it addressable. Any project that builds a transaction-verification layer for XRPL can differentiate by treating delivered_amount as the only settlement truth. A wallet that visibly displays "delivered" rather than "requested" in its transaction history is already ahead of the market. An exchange that rejects partial payment deposits unless the sender explicitly confirms the shortfall is doing what a future regulator would demand.
The tooling opportunity is real. A middleware layer that ingests XRPL transaction metadata, checks delivered_amount, compares it against Amount, and raises an alert when the ratio is unusually low would have saved every exchange that has silently absorbed this risk. The same middleware can generate a standard audit log for any compliance review. It is not a complex product. It is simply the missing discipline.
Watch three signals. First, watch whether XRPL.org updates its docs with an explicit integration warning. Second, watch whether major exchanges publish their own validation requirements for partial payments. Third, watch for support threads involving exchange deposits where the quoted amount is in the title and the delivered amount is in the fine print. The first two indicate maturity. The third indicates the lesson has been learned only by attack.
The original explainer says Partial Payments is not a bug. I agree. The next sentence is what matters: a feature that requires integrators to read a different field is a feature that will be integrated incorrectly. The chain will keep settling. The balance sheets will keep reconciling late. The only way to benefit from XRPL's payment efficiency is to build the discipline that the protocol itself does not enforce. In a bull market, speed amplifies every shortcut. The cheapest fix is a one-line validation. The expensive fix is the one you explain to an auditor after the fake deposit has already been credited.