From 15baae0f90036284238e0c3a543e3d7434bd5e38 Mon Sep 17 00:00:00 2001 From: Dejan Cabrilo Date: Fri, 14 Aug 2026 11:00:38 +0200 Subject: [PATCH 1/2] Normalize line endings to LF --- docs/README.md | 354 +-- docs/amms/README.md | 2674 ++++++++++----------- docs/amms/bidding.md | 882 +++---- docs/amms/withdraw.md | 1598 ++++++------- docs/credentials/README.md | 972 ++++---- docs/glossary.md | 204 +- docs/offers/README.md | 1268 +++++----- docs/overview/motivation.md | 68 +- docs/overview/scope.md | 50 +- docs/path_finding/README.md | 3334 +++++++++++++-------------- docs/payments/README.md | 728 +++--- docs/permissioned_domains/README.md | 516 ++--- docs/transactions/README.md | 1218 +++++----- docs/trust_lines/README.md | 756 +++--- 14 files changed, 7311 insertions(+), 7311 deletions(-) diff --git a/docs/README.md b/docs/README.md index 797eab3..2ecf1f9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,177 +1,177 @@ -> [!NOTE] -> This documentation reflects the XRP Ledger release [3.3.0](https://github.com/XRPLF/rippled/tree/3.3.0). - -> [!WARNING] -> 🚧 This documentation is **work in progress** - -# 1. XRPL Payment System - -> [!IMPORTANT] -> This is a technical specification document intended for developers implementing or verifying XRPL payment system behavior. For user-facing documentation and a high-level overview of XRPL features, please visit [https://xrpl.org/docs](https://xrpl.org/docs). - -The XRP Ledger is a multi-currency network with a built-in decentralized exchange, and its payment system lets value move across all of those asset types. At its heart is the Payment Engine that figures out how value should travel and then carries out those moves so payments can seamlessly draw on trust lines, MPTs, order books, AMMs, and direct XRP. This document, as outlined in the [scope document](overview/scope.md), guides you through that landscape. It explains the ledger objects and transactions and the coordination between discovering viable routes and executing the actual transfer. - -As described in the [motivation section](overview/motivation.md), this specification lays the foundation of the future work on formal verification aspects of XRP Ledger. It should also give new contributors a single place to understand the Payment Engine and the payment system in its entirety. The payment system has lived primarily inside the `xrpld` codebase, so these pages exist to explain the reasoning behind the system as a whole and offer context for its every subsystem. - -## 1.1. XRP Ledger Overview - -The XRP Ledger is a distributed ledger that uses a consensus protocol to validate and record transactions across a decentralized network of validator nodes. - -[Transactions](transactions/README.md) are the mechanism for modifying the XRP Ledger. They are sent by end users to a `xrpld` server using RPC and are propagated to other nodes by a peer-to-peer network. Each transaction passes through three stages to be added to the open ledger by a validator: - -- [Preflight](transactions/README.md#31-preflight): initial check on the transaction's basic format and signatures and the first line of defense. Transactions that fail preflight validation are never added to the ledger -- [Preclaim](transactions/README.md#32-preclaim): a more resource-intensive check that looks at the current ledger to verify things like account balances and sequence numbers. Transactions that fail preclaim with `tec` errors are added to the ledger and claim the fee; other preclaim failures are not added -- [Apply](transactions/README.md#33-doapply): the final stage where the transaction's logic is actually executed, and the ledger state is modified - -When the ledger closes, these transactions form the proposal for the next consensus round. If validators reach consensus on which transactions to accept, agreed upon transactions become part of the validated ledger. - -All interactions with the payment system happen through transactions. For example, a user can send an `OfferCreate` transaction to place an offer. This will, if successful, create an `Offer` *ledger entry* that will store information about this offer on the ledger. Ledger entries can be modified by other transactions. For example, sending a `Payment` transaction may consume an `Offer` ledger entry or modify the `AccountRoot` ledger entry of the sender and the receiver by changing their `Balance` fields. - -# 2. Asset Types - -XRP Ledger supports the following currencies that can be held and traded between accounts: - -- **[XRP](glossary.md#xrp)** -- **[IOU](glossary.md#iou)** -- **[MPT](glossary.md#mpt)** - -Each asset type stores amounts differently on the ledger, which determines what values they can represent: - -| Asset Type | Ledger Entry | Ledger Field | Precision | Can store fractions? | Range | -|-------------|--------------|-----------|-----------|---------------------|-------| -| **XRP** | `AccountRoot` | `Balance` | Exact (1 XRP = 10^6 drops) | No | 0 to 10^17 drops | -| **IOU** | `RippleState` | `Balance` | 15 decimal digits | Yes | ~-10^96 to ~10^96 | -| **MPT** | `MPToken` | `MPTAmount` | Exact | No | 0 to 2^63-1 (0x7FFFFFFFFFFFFFFF) | - -## 2.1. XRP - -XRP is the native currency on the XRP Ledger network. The balance is tracked in `AccountRoot` ledger entry, which is a representation of a user account. - -Aside from being a tradable asset, XRP serves two essential functions in the network. First, all low-level transaction fees (not to be confused with transfer fees) are paid in XRP.[^xrp-fees] When a transaction is processed, the fee is deducted from the sender's XRP balance and destroyed (burned), permanently removing it from circulation. Second, every account must maintain a minimum XRP balance called the reserve.[^xrp-reserve] The reserve requirement increases with each object the account owns on the ledger, such as trust lines, offers, or other entries. - -[^xrp-fees]: Transaction fee deduction: [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/Transactor.cpp#L443) -[^xrp-reserve]: Account reserve calculation: [`Fees.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/Fees.h#L37-L46) - -## 2.2. IOU - -IOU is issued by an account, and the balance is tracked in `RippleState` ledger entry. This is a representation of a bidirectional **[trust line](glossary.md#trust-line)** which keeps the debt balance between two accounts for an IOU. An IOU is identified by the currency code and the account ID of the issuer. - -Trust lines establish that two accounts can trade an IOU between them. For example, Alice can issue currency USD. Bob can establish a trust line with Alice for USD and Alice can send Bob 100 USD. Their trust line will reveal that Alice has a balance of -100 and Bob has a balance of 100 USD. - -Each side of a trust line can configure QualityIn and QualityOut settings[^quality-fields] to specify a custom exchange rate that a gateway or user wants applied when receiving or sending issued tokens across that trust line. These per-trust-line quality settings are distinct from the issuer's account-level TransferRate,[^transfer-rate] which imposes a network-wide percentage fee whenever the tokens are transferred between third-party accounts (not involving the issuer directly as the sender or recipient). - -Issuer accounts can set the RequireAuth flag[^require-auth] to control which trust lines are authorized to receive their IOUs. The DefaultRipple flag[^default-ripple] on an account determines the default rippling behavior for new trust lines: when set the account can serve as an intermediary in cross-currency payments. When not set, trust lines must explicitly enable rippling through the NoRipple flag. - -For an explanation about different transactions used to create and modify trust lines see [Trust Lines](trust_lines/README.md). Their usage in payments will be covered in later reading. - -[^quality-fields]: Quality fields on RippleState: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/ledger_entries.macro#L284-L288) -[^transfer-rate]: TransferRate field on AccountRoot: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/ledger_entries.macro#L142) -[^require-auth]: RequireAuth flag on AccountRoot: [`LedgerFormats.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/LedgerFormats.h#L130) -[^default-ripple]: DefaultRipple flag on AccountRoot: [`LedgerFormats.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/LedgerFormats.h#L135) - -## 2.3. MPT - -Multi-purpose token (MPT) is issued by an account and is identified by its `MPTokenIssuanceID`. The balance is tracked in `MPToken` ledger entry which keeps track of the balance for an account. - -MPTs differ from trust line tokens in several key ways: -- **Per-token configuration**: Each MPT issuance has its own transfer fee, authorization requirements, and capability flags. Trust line tokens share the issuer's account-level settings (e.g., single `TransferRate` for all tokens issued by that account). -- **OwnerCount behavior**: MPTokens always count toward an account's `OwnerCount` once created. Trust lines only count when in a non-default state (non-zero balance, custom quality, flag set, etc.). -- **Issuer-centric control**: The issuer can lock/unlock individual holders and set per-holder authorization. -- **Balance storage format**: MPTokens store balances as unsigned 64-bit integers (`MPTAmount` field, type `UInt64`) with each holder having a separate positive-only balance in their own `MPToken` entry. Trust lines store a single signed balance (`Balance` field, type `STAmount`) in the shared `RippleState` between two accounts -- **Burning vs balance adjustment**: MPTs are burned (destroyed from circulation) when sent to the issuer or clawed back. The holder's `MPTAmount` decreases and the issuance's `OutstandingAmount` decreases. The issuer never holds a balance of their own MPTs. Trust line tokens are never burned. When sent to the issuer or clawed back, the signed balance on the shared `RippleState` entry adjusts (the amount is transferred from holder to issuer), shifting between positive and negative to reflect the debt relationship. - -For details on how MPTs are created and modified please read [MPTs](mpts/README.md). This document also explains the basic mechanics of MPT transfers, but its full integration is explained in later reading. - -# 3. DEX - -The decentralized exchange (DEX) is integrated directly into the XRP Ledger protocol to enable multi-currency payments. When a user wants to send one currency but the recipient wants to receive a different currency, the payment system needs a way to convert between them. -Assets can be converted through offers (limit orders in order books) and AMM pools, which can be consumed during payment execution or offer crossing. - -## 3.1. Liquidity Sources - -Liquidity on the DEX comes from assets that accounts hold and are willing to trade. Accounts can trade from their XRP balance, their IOU balances, and their MPT balances. These assets can be exchanged with each other in any combination - XRP for IOUs, IOUs for MPTs, MPTs for XRP, and so on. - -To make these assets available for trading, accounts create offers or deposit assets into AMM pools. - -Offers represent a limit order. For example, an offer created by Alice with `takerPays` 100 USD and `takerGets` 200 XRP means that Alice is willing to sell 200 XRP for 100 USD, or a better exchange rate. *Order book* refers to valid, unused [resting offers](glossary.md#resting-offer) for a pair of assets. - -To understand how offers are placed and how they can be consumed as limit orders (through crossing), please read [Offers](offers/README.md) documentation. - -AMM pools hold reserves of two assets and provide liquidity based on a conservation function, automatically adjusting the exchange rate as the pool reserves change. - -To understand how AMMs are created, how money is deposited and withdrawn from them, please read [AMMs](amms/README.md). - -Whenever assets are deposited to an AMMs, a mathematical formula is used to determine how many Liquidity Provider Tokens will be awarded to the depositor. AMMs are trying to preserve their ratio of two assets, so a single-asset deposit will be penalized with fewer LP Tokens than a deposit that maintains the ratio. Similarly, withdrawals require depositors to redeem their LP tokens, and the exact amount needed for the withdrawal is calculated. - -[Deposit](amms/deposit.md) and [Withdrawal](amms/withdraw.md) are referenced from the main AMM document, and they provide detailed pseudocode and logic for multi and single-asset deposits and withdrawals. Since these operations often mirror each other, we suggest cross-referencing opposite transactions in two documents to get the full understanding and build intuition behind the inner workings of AMMs. - -When traders are using AMMs they are swapping one asset for another using the AMMs. The more they swap, the worse the exchange rate they get. This discrepancy between the AMM's nominal ratio and the quality that the trader receives is called **slippage** in XRPL terminology. Note that this differs from the standard financial definition of slippage, which refers to the difference between the expected price of a trade and the actual execution price due to market movement or insufficient liquidity - an unintended outcome. In XRPL, the price degradation is intentional and deterministic, resulting from the conservation function that governs AMM behavior as the pool's asset ratio shifts. -Implementations of mathematical functions that calculate the cost of each swap are described in the [Helper Functions](amms/helpers.md) document and this separation mirrors `xrpld` implementation. However, this document still contains information beyond implementation details. It showcases how AMMs retain their ratio during swaps, and contains an important section that showcases AMM's [slippage and quality degradation](amms/helpers.md#313-slippage-and-quality-degradation). - -Helpers document covers another aspect of AMMs: precision and rounding functions. Rounding could cause AMMs to lose value due to losing precision. This document shows how this is circumvented. Helper functions, just like in the code, are referenced from other places in this specification. - -[Bidding](amms/bidding.md) is a standalone document covering the auction slot bidding process, price calculation, refund mechanism, and LP token burning. - -The integration of offers and AMMs into the Payment Engine for automatic liquidity consumption during cross-currency payments is described in the path finding and flow sections. - -## 3.2. Authorizations - -XRPL implements several authorization mechanisms that control access to different features and protect accounts from unwanted interactions. These authorization systems serve different purposes and can work together to provide flexible access control. - -**IOU RequireAuth**: Accounts issuing IOUs can set the RequireAuth flag to control which trust lines are authorized to receive their tokens. When enabled, the issuer must explicitly authorize each trust line before it can receive IOUs. - -**MPT Authorization**: MPT issuances can require authorization through the lsfMPTRequireAuth flag. When enabled, holders must be individually authorized by the issuer (via MPTokenAuthorize transaction) before they can receive MPTs. - -**DepositAuth**: Accounts can enable the DepositAuth flag to require authorization for incoming payments. Authorization can be granted through DepositPreauth. This protects accounts from unwanted payments and enables compliance scenarios where only verified senders should be able to send funds. - -These authorization mechanisms are covered in their respective documentation sections: [Credentials](credentials/README.md) for credential-based authorization and DepositAuth integration, [Trust Lines](trust_lines/README.md) for IOU RequireAuth, and [MPTs](mpts/README.md) for MPT authorization. - -## 3.3. Permissioned DEX - -The Permissioned DEX builds on the credential system to enable access-controlled trading on the XRP Ledger. Using credentials, domain owners can create permissioned trading environments through PermissionedDomains. A domain owner specifies which credentials are required for access, creating segregated order books where only accounts holding those credentials can participate. - -The system supports three types of offers: -- **Open offers**: Regular offers accessible to all accounts, placed in the standard order book -- **Domain offers**: Offers restricted to a specific domain, placed only in that domain's order book, matching only with other domain offers or hybrid offers -- **Hybrid offers**: Offers that exist in both the domain order book and the open order book, providing liquidity bridging between permissioned and open markets - -All asset types supported by XRPL (XRP, IOUs, and MPTs) can be traded in permissioned domains. The domain owner always has access to their own domain regardless of credentials. - -[Permissioned Domains](permissioned_domains/README.md) covers domain creation and management, the PermissionedDomain ledger entry, credential verification logic, and the PermissionedDomainSet and PermissionedDomainDelete transactions. It also explains how domain offers and hybrid offers work within the order book system. - -# 4. Payments - -With three asset types (XRP, IOUs, MPTs), offers in order books, and AMM pools providing liquidity, the challenge is completing payments between accounts - especially when the sender holds one currency and the recipient wants a different one. The payment system solves this through a two-stage process: first discovering viable routes through the network, then executing the payment along those routes. - -The first stage is **path finding**. Accounts are connected through trust lines, offers, AMMs, and MPT holdings, creating a network where value can flow through multiple intermediaries and currency conversions. [Path Finding](path_finding/README.md) explains the pathfinding algorithm, how it discovers and ranks potential routes, and the structure of paths that describe where value can flow. - -Once paths are discovered, they are passed to the **Payment Engine** for execution. The implementation of the Payment Engine is called **Flow** (sometimes called the "Flow Engine" to distinguish it from the payment system in broader terms). Flow converts paths into **strands** - sequences of executable **steps** that move value from source to destination. - -A strand is composed of different step types, each handling a specific operation in the payment route. There are four main step types: -- **DirectStepI**: Transfers IOUs between accounts through trust lines -- **XRPEndpointStep**: Handles XRP transfers at the payment's source or destination -- **MPTEndpointStep**: Handles MPT transfers at the payment's source or destination -- **BookStep**: Converts currencies by consuming liquidity from order books and AMM pools - -For example, a payment from Alice (sending USD) to Bob (receiving EUR) might use a strand composed of: DirectStepI (Alice -> USD Issuer via trust line), BookStep (USD -> EUR conversion via order book), DirectStepI (EUR Issuer -> Bob via trust line). - -Flow ranks strands by quality and iteratively consumes liquidity from the best available strands until the payment is satisfied or no more liquidity is available. It supports exact amount delivery and partial payments, handling all combinations of asset types while respecting quality limits and spending constraints. - -**Quality** represents the effective exchange rate between two assets, expressed as a ratio of output to input (output/input). This includes not just the base exchange rate but also any transfer fees, trust line quality settings, and other costs incurred during the exchange. From the taker's perspective, a lower quality value is better because it means less input is required to obtain a given output. For example, a quality of 0.5 means the taker pays 0.5 units of input for 1 unit of output, while a quality of 2.0 means the taker pays 2 units of input for 1 unit of output. Throughout this documentation, unless otherwise specified, quality comparisons are presented from the taker's perspective where lower quality values indicate better exchange rates. - -[Flow documentation](flow/README.md) provides a high-level overview of how Flow operates. It explains the main algorithm, strand evaluation, and how AMMs and domain payments integrate into the system. The document maintains a high-level perspective to explain the overall flow logic. For detailed implementation of each step type - including the specific mechanics of trust line transfers, endpoint transfers, order book and AMM conversions, quality calculations, and liquidity constraints - see [Steps](flow/steps.md). - -The path finding and Flow processes described above can be triggered in two different ways: through a Payment transaction or through offer crossing. Both use the same underlying Payment Engine (Flow) to execute value transfers, but they serve different purposes and have different transaction semantics. - -## 4.1. Payment Transaction - -A Payment transaction is an explicit instruction to transfer value from a source account to a destination account. The sender specifies the amount to send or the amount the destination should receive, and optionally provides paths to guide the payment. Before the Payment Engine executes the payment, path finding is required to discover viable routes through the network (unless the user explicitly provides paths in the transaction). The Payment Engine then uses these paths to find the best route and executes the transfer. - -[Payments](payments/README.md) covers the Payment transaction, including direct XRP payments and cross-currency payment execution, and all the validation rules and failure conditions for payment processing. - -## 4.2. Offer Crossing - -Offer crossing occurs when an OfferCreate transaction is submitted, and the new offer can be immediately matched with existing offers in the order book. Instead of placing the offer on the ledger as a resting offer, the Payment Engine first attempts to "cross" the new offer with compatible existing offers, effectively executing a trade. For offer crossing, two paths are used: a default direct path and an XRP bridge path (auto-bridging, which uses XRP as an intermediary currency to potentially find better rates). If the new offer is fully satisfied through crossing, no resting offer is created. If only partially satisfied, the remainder becomes a resting offer. - -[Offers](offers/README.md) covers the principle of offer crossing and how it triggers the Flow engine to execute the payment. - +> [!NOTE] +> This documentation reflects the XRP Ledger release [3.3.0](https://github.com/XRPLF/rippled/tree/3.3.0). + +> [!WARNING] +> 🚧 This documentation is **work in progress** + +# 1. XRPL Payment System + +> [!IMPORTANT] +> This is a technical specification document intended for developers implementing or verifying XRPL payment system behavior. For user-facing documentation and a high-level overview of XRPL features, please visit [https://xrpl.org/docs](https://xrpl.org/docs). + +The XRP Ledger is a multi-currency network with a built-in decentralized exchange, and its payment system lets value move across all of those asset types. At its heart is the Payment Engine that figures out how value should travel and then carries out those moves so payments can seamlessly draw on trust lines, MPTs, order books, AMMs, and direct XRP. This document, as outlined in the [scope document](overview/scope.md), guides you through that landscape. It explains the ledger objects and transactions and the coordination between discovering viable routes and executing the actual transfer. + +As described in the [motivation section](overview/motivation.md), this specification lays the foundation of the future work on formal verification aspects of XRP Ledger. It should also give new contributors a single place to understand the Payment Engine and the payment system in its entirety. The payment system has lived primarily inside the `xrpld` codebase, so these pages exist to explain the reasoning behind the system as a whole and offer context for its every subsystem. + +## 1.1. XRP Ledger Overview + +The XRP Ledger is a distributed ledger that uses a consensus protocol to validate and record transactions across a decentralized network of validator nodes. + +[Transactions](transactions/README.md) are the mechanism for modifying the XRP Ledger. They are sent by end users to a `xrpld` server using RPC and are propagated to other nodes by a peer-to-peer network. Each transaction passes through three stages to be added to the open ledger by a validator: + +- [Preflight](transactions/README.md#31-preflight): initial check on the transaction's basic format and signatures and the first line of defense. Transactions that fail preflight validation are never added to the ledger +- [Preclaim](transactions/README.md#32-preclaim): a more resource-intensive check that looks at the current ledger to verify things like account balances and sequence numbers. Transactions that fail preclaim with `tec` errors are added to the ledger and claim the fee; other preclaim failures are not added +- [Apply](transactions/README.md#33-doapply): the final stage where the transaction's logic is actually executed, and the ledger state is modified + +When the ledger closes, these transactions form the proposal for the next consensus round. If validators reach consensus on which transactions to accept, agreed upon transactions become part of the validated ledger. + +All interactions with the payment system happen through transactions. For example, a user can send an `OfferCreate` transaction to place an offer. This will, if successful, create an `Offer` *ledger entry* that will store information about this offer on the ledger. Ledger entries can be modified by other transactions. For example, sending a `Payment` transaction may consume an `Offer` ledger entry or modify the `AccountRoot` ledger entry of the sender and the receiver by changing their `Balance` fields. + +# 2. Asset Types + +XRP Ledger supports the following currencies that can be held and traded between accounts: + +- **[XRP](glossary.md#xrp)** +- **[IOU](glossary.md#iou)** +- **[MPT](glossary.md#mpt)** + +Each asset type stores amounts differently on the ledger, which determines what values they can represent: + +| Asset Type | Ledger Entry | Ledger Field | Precision | Can store fractions? | Range | +|-------------|--------------|-----------|-----------|---------------------|-------| +| **XRP** | `AccountRoot` | `Balance` | Exact (1 XRP = 10^6 drops) | No | 0 to 10^17 drops | +| **IOU** | `RippleState` | `Balance` | 15 decimal digits | Yes | ~-10^96 to ~10^96 | +| **MPT** | `MPToken` | `MPTAmount` | Exact | No | 0 to 2^63-1 (0x7FFFFFFFFFFFFFFF) | + +## 2.1. XRP + +XRP is the native currency on the XRP Ledger network. The balance is tracked in `AccountRoot` ledger entry, which is a representation of a user account. + +Aside from being a tradable asset, XRP serves two essential functions in the network. First, all low-level transaction fees (not to be confused with transfer fees) are paid in XRP.[^xrp-fees] When a transaction is processed, the fee is deducted from the sender's XRP balance and destroyed (burned), permanently removing it from circulation. Second, every account must maintain a minimum XRP balance called the reserve.[^xrp-reserve] The reserve requirement increases with each object the account owns on the ledger, such as trust lines, offers, or other entries. + +[^xrp-fees]: Transaction fee deduction: [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/Transactor.cpp#L443) +[^xrp-reserve]: Account reserve calculation: [`Fees.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/Fees.h#L37-L46) + +## 2.2. IOU + +IOU is issued by an account, and the balance is tracked in `RippleState` ledger entry. This is a representation of a bidirectional **[trust line](glossary.md#trust-line)** which keeps the debt balance between two accounts for an IOU. An IOU is identified by the currency code and the account ID of the issuer. + +Trust lines establish that two accounts can trade an IOU between them. For example, Alice can issue currency USD. Bob can establish a trust line with Alice for USD and Alice can send Bob 100 USD. Their trust line will reveal that Alice has a balance of -100 and Bob has a balance of 100 USD. + +Each side of a trust line can configure QualityIn and QualityOut settings[^quality-fields] to specify a custom exchange rate that a gateway or user wants applied when receiving or sending issued tokens across that trust line. These per-trust-line quality settings are distinct from the issuer's account-level TransferRate,[^transfer-rate] which imposes a network-wide percentage fee whenever the tokens are transferred between third-party accounts (not involving the issuer directly as the sender or recipient). + +Issuer accounts can set the RequireAuth flag[^require-auth] to control which trust lines are authorized to receive their IOUs. The DefaultRipple flag[^default-ripple] on an account determines the default rippling behavior for new trust lines: when set the account can serve as an intermediary in cross-currency payments. When not set, trust lines must explicitly enable rippling through the NoRipple flag. + +For an explanation about different transactions used to create and modify trust lines see [Trust Lines](trust_lines/README.md). Their usage in payments will be covered in later reading. + +[^quality-fields]: Quality fields on RippleState: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/ledger_entries.macro#L284-L288) +[^transfer-rate]: TransferRate field on AccountRoot: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/ledger_entries.macro#L142) +[^require-auth]: RequireAuth flag on AccountRoot: [`LedgerFormats.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/LedgerFormats.h#L130) +[^default-ripple]: DefaultRipple flag on AccountRoot: [`LedgerFormats.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/LedgerFormats.h#L135) + +## 2.3. MPT + +Multi-purpose token (MPT) is issued by an account and is identified by its `MPTokenIssuanceID`. The balance is tracked in `MPToken` ledger entry which keeps track of the balance for an account. + +MPTs differ from trust line tokens in several key ways: +- **Per-token configuration**: Each MPT issuance has its own transfer fee, authorization requirements, and capability flags. Trust line tokens share the issuer's account-level settings (e.g., single `TransferRate` for all tokens issued by that account). +- **OwnerCount behavior**: MPTokens always count toward an account's `OwnerCount` once created. Trust lines only count when in a non-default state (non-zero balance, custom quality, flag set, etc.). +- **Issuer-centric control**: The issuer can lock/unlock individual holders and set per-holder authorization. +- **Balance storage format**: MPTokens store balances as unsigned 64-bit integers (`MPTAmount` field, type `UInt64`) with each holder having a separate positive-only balance in their own `MPToken` entry. Trust lines store a single signed balance (`Balance` field, type `STAmount`) in the shared `RippleState` between two accounts +- **Burning vs balance adjustment**: MPTs are burned (destroyed from circulation) when sent to the issuer or clawed back. The holder's `MPTAmount` decreases and the issuance's `OutstandingAmount` decreases. The issuer never holds a balance of their own MPTs. Trust line tokens are never burned. When sent to the issuer or clawed back, the signed balance on the shared `RippleState` entry adjusts (the amount is transferred from holder to issuer), shifting between positive and negative to reflect the debt relationship. + +For details on how MPTs are created and modified please read [MPTs](mpts/README.md). This document also explains the basic mechanics of MPT transfers, but its full integration is explained in later reading. + +# 3. DEX + +The decentralized exchange (DEX) is integrated directly into the XRP Ledger protocol to enable multi-currency payments. When a user wants to send one currency but the recipient wants to receive a different currency, the payment system needs a way to convert between them. +Assets can be converted through offers (limit orders in order books) and AMM pools, which can be consumed during payment execution or offer crossing. + +## 3.1. Liquidity Sources + +Liquidity on the DEX comes from assets that accounts hold and are willing to trade. Accounts can trade from their XRP balance, their IOU balances, and their MPT balances. These assets can be exchanged with each other in any combination - XRP for IOUs, IOUs for MPTs, MPTs for XRP, and so on. + +To make these assets available for trading, accounts create offers or deposit assets into AMM pools. + +Offers represent a limit order. For example, an offer created by Alice with `takerPays` 100 USD and `takerGets` 200 XRP means that Alice is willing to sell 200 XRP for 100 USD, or a better exchange rate. *Order book* refers to valid, unused [resting offers](glossary.md#resting-offer) for a pair of assets. + +To understand how offers are placed and how they can be consumed as limit orders (through crossing), please read [Offers](offers/README.md) documentation. + +AMM pools hold reserves of two assets and provide liquidity based on a conservation function, automatically adjusting the exchange rate as the pool reserves change. + +To understand how AMMs are created, how money is deposited and withdrawn from them, please read [AMMs](amms/README.md). + +Whenever assets are deposited to an AMMs, a mathematical formula is used to determine how many Liquidity Provider Tokens will be awarded to the depositor. AMMs are trying to preserve their ratio of two assets, so a single-asset deposit will be penalized with fewer LP Tokens than a deposit that maintains the ratio. Similarly, withdrawals require depositors to redeem their LP tokens, and the exact amount needed for the withdrawal is calculated. + +[Deposit](amms/deposit.md) and [Withdrawal](amms/withdraw.md) are referenced from the main AMM document, and they provide detailed pseudocode and logic for multi and single-asset deposits and withdrawals. Since these operations often mirror each other, we suggest cross-referencing opposite transactions in two documents to get the full understanding and build intuition behind the inner workings of AMMs. + +When traders are using AMMs they are swapping one asset for another using the AMMs. The more they swap, the worse the exchange rate they get. This discrepancy between the AMM's nominal ratio and the quality that the trader receives is called **slippage** in XRPL terminology. Note that this differs from the standard financial definition of slippage, which refers to the difference between the expected price of a trade and the actual execution price due to market movement or insufficient liquidity - an unintended outcome. In XRPL, the price degradation is intentional and deterministic, resulting from the conservation function that governs AMM behavior as the pool's asset ratio shifts. +Implementations of mathematical functions that calculate the cost of each swap are described in the [Helper Functions](amms/helpers.md) document and this separation mirrors `xrpld` implementation. However, this document still contains information beyond implementation details. It showcases how AMMs retain their ratio during swaps, and contains an important section that showcases AMM's [slippage and quality degradation](amms/helpers.md#313-slippage-and-quality-degradation). + +Helpers document covers another aspect of AMMs: precision and rounding functions. Rounding could cause AMMs to lose value due to losing precision. This document shows how this is circumvented. Helper functions, just like in the code, are referenced from other places in this specification. + +[Bidding](amms/bidding.md) is a standalone document covering the auction slot bidding process, price calculation, refund mechanism, and LP token burning. + +The integration of offers and AMMs into the Payment Engine for automatic liquidity consumption during cross-currency payments is described in the path finding and flow sections. + +## 3.2. Authorizations + +XRPL implements several authorization mechanisms that control access to different features and protect accounts from unwanted interactions. These authorization systems serve different purposes and can work together to provide flexible access control. + +**IOU RequireAuth**: Accounts issuing IOUs can set the RequireAuth flag to control which trust lines are authorized to receive their tokens. When enabled, the issuer must explicitly authorize each trust line before it can receive IOUs. + +**MPT Authorization**: MPT issuances can require authorization through the lsfMPTRequireAuth flag. When enabled, holders must be individually authorized by the issuer (via MPTokenAuthorize transaction) before they can receive MPTs. + +**DepositAuth**: Accounts can enable the DepositAuth flag to require authorization for incoming payments. Authorization can be granted through DepositPreauth. This protects accounts from unwanted payments and enables compliance scenarios where only verified senders should be able to send funds. + +These authorization mechanisms are covered in their respective documentation sections: [Credentials](credentials/README.md) for credential-based authorization and DepositAuth integration, [Trust Lines](trust_lines/README.md) for IOU RequireAuth, and [MPTs](mpts/README.md) for MPT authorization. + +## 3.3. Permissioned DEX + +The Permissioned DEX builds on the credential system to enable access-controlled trading on the XRP Ledger. Using credentials, domain owners can create permissioned trading environments through PermissionedDomains. A domain owner specifies which credentials are required for access, creating segregated order books where only accounts holding those credentials can participate. + +The system supports three types of offers: +- **Open offers**: Regular offers accessible to all accounts, placed in the standard order book +- **Domain offers**: Offers restricted to a specific domain, placed only in that domain's order book, matching only with other domain offers or hybrid offers +- **Hybrid offers**: Offers that exist in both the domain order book and the open order book, providing liquidity bridging between permissioned and open markets + +All asset types supported by XRPL (XRP, IOUs, and MPTs) can be traded in permissioned domains. The domain owner always has access to their own domain regardless of credentials. + +[Permissioned Domains](permissioned_domains/README.md) covers domain creation and management, the PermissionedDomain ledger entry, credential verification logic, and the PermissionedDomainSet and PermissionedDomainDelete transactions. It also explains how domain offers and hybrid offers work within the order book system. + +# 4. Payments + +With three asset types (XRP, IOUs, MPTs), offers in order books, and AMM pools providing liquidity, the challenge is completing payments between accounts - especially when the sender holds one currency and the recipient wants a different one. The payment system solves this through a two-stage process: first discovering viable routes through the network, then executing the payment along those routes. + +The first stage is **path finding**. Accounts are connected through trust lines, offers, AMMs, and MPT holdings, creating a network where value can flow through multiple intermediaries and currency conversions. [Path Finding](path_finding/README.md) explains the pathfinding algorithm, how it discovers and ranks potential routes, and the structure of paths that describe where value can flow. + +Once paths are discovered, they are passed to the **Payment Engine** for execution. The implementation of the Payment Engine is called **Flow** (sometimes called the "Flow Engine" to distinguish it from the payment system in broader terms). Flow converts paths into **strands** - sequences of executable **steps** that move value from source to destination. + +A strand is composed of different step types, each handling a specific operation in the payment route. There are four main step types: +- **DirectStepI**: Transfers IOUs between accounts through trust lines +- **XRPEndpointStep**: Handles XRP transfers at the payment's source or destination +- **MPTEndpointStep**: Handles MPT transfers at the payment's source or destination +- **BookStep**: Converts currencies by consuming liquidity from order books and AMM pools + +For example, a payment from Alice (sending USD) to Bob (receiving EUR) might use a strand composed of: DirectStepI (Alice -> USD Issuer via trust line), BookStep (USD -> EUR conversion via order book), DirectStepI (EUR Issuer -> Bob via trust line). + +Flow ranks strands by quality and iteratively consumes liquidity from the best available strands until the payment is satisfied or no more liquidity is available. It supports exact amount delivery and partial payments, handling all combinations of asset types while respecting quality limits and spending constraints. + +**Quality** represents the effective exchange rate between two assets, expressed as a ratio of output to input (output/input). This includes not just the base exchange rate but also any transfer fees, trust line quality settings, and other costs incurred during the exchange. From the taker's perspective, a lower quality value is better because it means less input is required to obtain a given output. For example, a quality of 0.5 means the taker pays 0.5 units of input for 1 unit of output, while a quality of 2.0 means the taker pays 2 units of input for 1 unit of output. Throughout this documentation, unless otherwise specified, quality comparisons are presented from the taker's perspective where lower quality values indicate better exchange rates. + +[Flow documentation](flow/README.md) provides a high-level overview of how Flow operates. It explains the main algorithm, strand evaluation, and how AMMs and domain payments integrate into the system. The document maintains a high-level perspective to explain the overall flow logic. For detailed implementation of each step type - including the specific mechanics of trust line transfers, endpoint transfers, order book and AMM conversions, quality calculations, and liquidity constraints - see [Steps](flow/steps.md). + +The path finding and Flow processes described above can be triggered in two different ways: through a Payment transaction or through offer crossing. Both use the same underlying Payment Engine (Flow) to execute value transfers, but they serve different purposes and have different transaction semantics. + +## 4.1. Payment Transaction + +A Payment transaction is an explicit instruction to transfer value from a source account to a destination account. The sender specifies the amount to send or the amount the destination should receive, and optionally provides paths to guide the payment. Before the Payment Engine executes the payment, path finding is required to discover viable routes through the network (unless the user explicitly provides paths in the transaction). The Payment Engine then uses these paths to find the best route and executes the transfer. + +[Payments](payments/README.md) covers the Payment transaction, including direct XRP payments and cross-currency payment execution, and all the validation rules and failure conditions for payment processing. + +## 4.2. Offer Crossing + +Offer crossing occurs when an OfferCreate transaction is submitted, and the new offer can be immediately matched with existing offers in the order book. Instead of placing the offer on the ledger as a resting offer, the Payment Engine first attempts to "cross" the new offer with compatible existing offers, effectively executing a trade. For offer crossing, two paths are used: a default direct path and an XRP bridge path (auto-bridging, which uses XRP as an intermediary currency to potentially find better rates). If the new offer is fully satisfied through crossing, no resting offer is created. If only partially satisfied, the remainder becomes a resting offer. + +[Offers](offers/README.md) covers the principle of offer crossing and how it triggers the Flow engine to execute the payment. + diff --git a/docs/amms/README.md b/docs/amms/README.md index e1e3e41..2eec30d 100644 --- a/docs/amms/README.md +++ b/docs/amms/README.md @@ -1,1337 +1,1337 @@ -# Index - -- [1. Introduction](#1-introduction) - - [1.1. Liquidity Pool Mechanics](#11-liquidity-pool-mechanics) - - [1.1.1. Weighted Geometric Mean](#111-weighted-geometric-mean) - - [1.1.2. Slippage](#112-slippage) - - [1.1.3. LP Tokens](#113-lp-tokens) - - [1.1.4. Effective Price](#114-effective-price) - - [1.2. Trading Fee](#12-trading-fee) - - [1.2.1. Auction Slot](#121-auction-slot) - - [1.2.2. Fee Voting](#122-fee-voting) -- [2. Ledger Entries](#2-ledger-entries) - - [2.1. AMM Ledger Entry](#21-amm-ledger-entry) - - [2.1.1. Object Identifier](#211-object-identifier) - - [2.1.2. Fields](#212-fields) - - [2.1.2.1. VoteSlots](#2121-voteslots) - - [2.1.2.2. AuctionSlot](#2122-auctionslot) - - [2.1.3. Pseudo-accounts](#213-pseudo-accounts) - - [2.1.3.1. Account ID Generation](#2131-account-id-generation) - - [2.1.4. Reserves](#214-reserves) - - [2.2. RippleState Ledger Entry](#22-ripplestate-ledger-entry) - - [2.3. MPToken Ledger Entry](#23-mptoken-ledger-entry) -- [3. Transactions](#3-transactions) - - [3.1. AMMCreate Transaction](#31-ammcreate-transaction) - - [3.1.1. Failure Conditions](#311-failure-conditions) - - [3.1.2. State Changes](#312-state-changes) - - [3.2. AMMDeposit Transaction](#32-ammdeposit-transaction) - - [3.2.1. Deposit Modes](#321-deposit-modes) - - [3.2.2. Failure Conditions](#322-failure-conditions) - - [3.2.3. State Changes](#323-state-changes) - - [3.3. AMMWithdraw Transaction](#33-ammwithdraw-transaction) - - [3.3.1. Withdrawal Modes](#331-withdrawal-modes) - - [3.3.2. Failure Conditions](#332-failure-conditions) - - [3.3.3. State Changes](#333-state-changes) - - [3.4. AMMVote Transaction](#34-ammvote-transaction) - - [3.4.1. Failure Conditions](#341-failure-conditions) - - [3.4.2. State Changes](#342-state-changes) - - [3.5. AMMBid Transaction](#35-ammbid-transaction) - - [3.5.1. Failure Conditions](#351-failure-conditions) - - [3.5.2. State Changes](#352-state-changes) - - [3.6. AMMDelete Transaction](#36-ammdelete-transaction) - - [3.6.1. Failure Conditions](#361-failure-conditions) - - [3.6.2. State Changes](#362-state-changes) - - [3.7. AMMClawback Transaction](#37-ammclawback-transaction) - - [3.7.1. Failure Conditions](#371-failure-conditions) - - [3.7.2. State Changes](#372-state-changes) - -# 1. Introduction - -The XRP Ledger decentralized exchange provides asset exchange liquidity through two mechanisms: [limit order books](../glossary.md#clob) and automated market makers (AMM). AMMs are liquidity pools that use algorithmic pricing to enable asset swaps without relying on discrete offers. - -XRPL implements AMMs with the following characteristics: - -- **Geometric mean market maker (GM3)**: Pools use a weighted geometric mean conservation function to algorithmically determine exchange rates based on pool balances and fees -- **Continuous auction mechanism**: AMM instances auction a 24-hour slot with discounted trading fees -- **Votable trading fee**: LP token holders vote on the trading fee charged by the AMM instance, weighted by their LP token balance -- **LOB integration**: The [Flow payment engine](../flow/README.md) processes AMM liquidity and order book offers together, consuming liquidity from both sources in quality order - -An AMM instance is represented on-ledger by: -- An `AMM` ledger entry storing pool parameters, trading fee, vote slots, and auction slot state -- An `AccountRoot` ledger entry (pseudo-account) holding the pool's XRP balance -- `RippleState` trust lines for IOU balances and LP tokens -- `MPToken` ledger entries for MPT balances (when applicable) - -The AMM manages a liquidity pool containing two assets (any combination of [XRP](../glossary.md#xrp), [IOUs](../glossary.md#iou), or [MPTs](../mpts/README.md)) and issues LP tokens representing proportional ownership of the pool. - -AMMs integrate with the [BookStep](../flow/steps.md#5-bookstep) of the Flow engine. During payment execution or offer crossing, BookStep generates synthetic offers from the AMM based on the current pool state and compares their quality against order book offers. The callback in `revImp` or `fwdImp` consumes whichever source provides better quality, updating either the AMM pool balances or order book entries accordingly. - -## 1.1. Liquidity Pool Mechanics - -### 1.1.1. Weighted Geometric Mean - -The AMM uses a conservation function based on weighted geometric mean: - -``` -C = Γ_A^W_A * Γ_B^W_B -``` - -Where: -- `Γ_A` = current balance of asset A in the AMM instance pool -- `Γ_B` = current balance of asset B in the AMM instance pool -- `W_A` = weight of asset A -- `W_B` = weight of asset B -- `C` = conservation function value - -For XRPL AMMs, `W_A = W_B = 0.5` (equal weights). - -The conservation function C remains constant during swaps (payments). Deposits and withdrawals change C as they add or remove liquidity from the pool. - -When a trader swaps assets, they add one asset to the pool and remove the other, maintaining C (before accounting for trading fees). - -For example, if a trader wants to buy asset A from the pool: -- They deposit asset B into the pool (increasing `Γ_B`) -- They receive asset A from the pool (decreasing `Γ_A`) -- The conservation function C remains constant (approximately, accounting for fees) -- Because `Γ_A` decreases while `Γ_B` increases, the ratio `Γ_B / Γ_A` increases -- This means the next trader will get fewer A assets per B asset (the price of A has increased) - -### 1.1.2. Slippage - -When swapping assets, the actual exchange rate differs from the spot price due to slippage. Note that "slippage" in XRPL terminology differs from the standard financial definition. Standard slippage refers to the difference between expected and execution price due to market movement or insufficient liquidity - an unintended outcome. In XRPL AMMs, slippage is the intentional and deterministic price degradation that results from the conservation function as larger trades shift the pool's asset ratio. - -The **spot price** is the weighted ratio of pool balances representing the exchange rate for an infinitesimally small trade: - -``` -SpotPrice(A) = (Γ_B / W_B) / (Γ_A / W_A) * 1/(1-TFee) -``` - -`TFee` is trading fee as a fraction (fee units / 100,000; see [Trading Fee](#12-trading-fee)). - -For equal weights (W_A = W_B = 0.5), this simplifies to: - -``` -SpotPrice(A) = Γ_B / Γ_A * 1/(1-TFee) -``` - -The **actual exchange rate** of a trade is the ratio of assets actually exchanged: - -``` -ActualExchangeRate(A) = Δ_B / Δ_A -``` - -Where Δ_B is the amount of asset B swapped in and Δ_A is the amount of asset A received. - -**Slippage** is the percentage change in the actual exchange rate relative to the pre-swap spot price. Larger swaps move the pool balances more significantly, resulting in progressively worse exchange rates. - -See [Swap Formulas (helpers.md)](helpers.md#31-swap-formulas) for the detailed formulas that calculate swap amounts, and [Slippage and Quality Degradation (helpers.md)](helpers.md#313-slippage-and-quality-degradation) for more on slippage behavior. - -### 1.1.3. LP Tokens - -**Terminology:** - -- **Liquidity Provider (LP)**: An account that has deposited assets into an AMM pool and holds LP tokens. Also called "LPs" collectively. -- **LP Tokens**: IOUs representing proportional ownership of an AMM pool's assets. LP tokens are issued by the AMM pseudo-account and tracked via trust lines. See [section 2.1.3](#213-pseudo-accounts) for details on LP token currency codes. -- **Issuing**: When an account deposits assets into the pool, the AMM increases the balance on the LP token trust line between the account and the AMM pseudo-account (the issuer). This increases the total LP token supply. -- **Redeeming**: When an LP withdraws assets from the pool, they redeem LP tokens by decreasing the balance on their LP token trust line with the AMM. This reduces the account's holdings and decreases the total LP token supply. -- **Outstanding LP Tokens**: The total number of LP tokens currently in circulation (held by all LPs), tracked in the AMM ledger entry's `LPTokenBalance` field. -- **LP Token Holdings**: The amount of LP tokens that a specific account holds, which determines their proportional share of the pool. - -Liquidity providers deposit assets into the AMM pool and receive LP tokens in return. These LP tokens represent proportional ownership of the pool's assets and can later be redeemed to withdraw assets from the pool. - -**Initial LP Token Calculation:** - -When creating an AMM pool with initial deposits `A` and `B`:[^amm-lp-tokens-calc] - -``` -LPTokens = SQRT(A * B) -``` - -This formula uses the geometric mean of the pool balances to calculate the initial LP token supply. - -**Subsequent Deposits (Issuing LP Tokens):** - -When depositing both assets proportionally: - -``` -LPTokensIssued = (Δ_A / Γ_A) * TotalLPTokens - -where Δ_A and Δ_B must satisfy: Δ_A / Γ_A = Δ_B / Γ_B -``` - -The system increases the LP token balance on the depositor's trust line with the AMM. The total outstanding LP tokens increase. - -**Withdrawals (Redeeming LP Tokens):** - -When withdrawing both assets proportionally: - -``` -Δ_A = (LPTokensRedeemed / TotalLPTokens) * Γ_A -Δ_B = (LPTokensRedeemed / TotalLPTokens) * Γ_B -``` - -The system reduces the balance on the LP's trust line with the AMM by the redeemed amount. The total outstanding LP tokens decrease by the amount redeemed. - -**Example:** - -If Alice creates an AMM with 100 EUR and 1000 USD: -- Initial LP tokens issued = SQRT(100 * 1000) = 316.227766... LP tokens -- Alice receives ~316.23 LP tokens representing 100% ownership -- Total outstanding LP tokens = 316.23 -- If Bob later deposits 10 EUR and 100 USD (same ratio), he receives ~31.62 newly issued LP tokens -- Total outstanding LP tokens = ~347.85 -- Alice holds ~316.23 LP tokens (~90.9% of the pool) -- Bob holds ~31.62 LP tokens (~9.1% of the pool) -- If Alice later redeems 100 LP tokens to withdraw assets, she receives both EUR and USD proportional to her redeemed LP tokens, and the balance on her LP token trust line decreases by 100 -- Total outstanding LP tokens = ~247.85 -- Alice now holds ~216.23 LP tokens (~87.2% of the pool) - -**Single-Asset Deposits and Withdrawals:** - -The formulas above apply to **proportional** deposits and withdrawals, where both pool assets are added or removed in the same ratio as the pool. - -AMMs also support **single-asset** operations, where only one asset is deposited or withdrawn: - -- **Single-Asset Deposits**: When depositing only one asset (e.g., only asset A into an A/B pool), only that asset's pool balance increases. This creates an imbalance in the pool ratio. The depositor receives fewer LP tokens than they would for a proportional deposit of the same value, because the trading fee is applied to account for the imbalance created. - -- **Single-Asset Withdrawals**: When withdrawing only one asset, only that asset's pool balance decreases, creating an imbalance. The withdrawer must redeem more LP tokens than they would for a proportional withdrawal, with the trading fee applied to account for the imbalance. - -The specific formulas for single-asset operations are more complex and involve the trading fee. See [Deposit Formulas](helpers.md#32-deposit-formulas) and [Withdrawal Formulas](helpers.md#33-withdrawal-formulas) for the mathematical details. - -**Example: Proportional Deposit** - -Alice creates an AMM with 100 USD and 100 EUR: -- Initial LP tokens: SQRT(100 * 100) = 100 LP tokens -- Bob later deposits 100 USD and 100 EUR (maintaining the 1:1 ratio) -- LP tokens received: (100 / 100) * 100 = 100 LP tokens -- Total LP tokens: 200 - -**Example: Single-Asset Deposit** - -Alice creates an AMM with 100 USD and 100 EUR (with 0.3% trading fee): -- Initial LP tokens: SQRT(100 * 100) = 100 LP tokens -- Bob later deposits 100 USD only (no EUR) -- Using the single-asset deposit formula, Bob receives ~41.4 LP tokens -- Total LP tokens: ~141.4 - -### 1.1.4. Effective Price - -For single-asset operations, users can specify an **effective price** to protect against unfavorable exchange rates: - -- **Deposit Effective Price** = Asset Deposited / LP Tokens Issued - - Example: Depositing 100 USD to receive 40 LP tokens = 2.5 USD per LP token - - Users set a **maximum** effective price (won't pay more than X asset per LP token) - - Used in [singleDepositEPrice](deposit.md#53-singledepositeprice-tflimitlptoken) mode - -- **Withdrawal Effective Price** = LP Tokens Redeemed / Asset Withdrawn - - Example: Redeeming 40 LP tokens to withdraw 100 USD = 0.4 LP tokens per USD - - Users set a **minimum** effective price (won't pay less than X LP tokens per unit of asset withdrawn) - - Used in [singleWithdrawEPrice](withdraw.md#53-singlewithdraweprice-tflimitlptoken) mode - -## 1.2. Trading Fee - -AMMs charge a trading fee on swaps, which is added to the pool and distributed proportionally to all LP token holders when they withdraw liquidity. The fee is expressed in fee units. - -**Fee Range:** -- Minimum: 0 units (0%) -- Maximum: 1000 units (1% or 100 basis points) -- Fee units: 1 unit = 0.001% (or 1/10 of a basis point) -- Example: A fee of 30 units = 0.03% = 3 basis points - -The trading fee can be set initially when creating the AMM and subsequently adjusted through the voting mechanism. - -### 1.2.1. Auction Slot - -The auction slot mechanism allows any LP token holder to bid for a 24-hour period of discounted trading fees. During this period, the slot holder pays only one-tenth of the regular trading fee when trading through the AMM. The slot holder can also authorize up to four additional accounts to share this discount. - -The auction operates as a continuous bidding system where anyone can take over the slot at any time by outbidding the current holder. The minimum bid price decreases as the current holder uses more of their 24-hour slot time. When someone successfully outbids the current holder, the previous holder receives a refund proportional to their remaining unused time. The difference between the new bid and the refund is burned from the LP token supply, which increases the ownership percentage of all remaining LP token holders. - -See [AMMBid Implementation Details](bidding.md) for comprehensive documentation on the auction mechanics, including price calculations, time-based refunds, and the LP token burning process. - -### 1.2.2. Fee Voting - -Liquidity providers can vote on the trading fee rate. -Each vote is recorded in a **vote slot** - a data structure stored in the AMM ledger entry that tracks who voted, what fee they proposed, and their voting power. -Voting power is determined by the number of LP tokens held: an account holding 30% of all LP tokens has 30% of the voting power. The AMM maintains up to 8 vote slots[^vote-max-slots], and the actual trading fee is calculated as the weighted average of all votes[^vote-weighted-average]. - -**Voting Mechanism:** - -1. LP token holders submit `AMMVote` transactions with their preferred fee (0-1000) -2. The system calculates vote weights: `VoteWeight = (LPTokens / TotalLPTokens) * 100,000` -3. The weighted average determines the actual trading fee: - ``` - TradingFee = SUM(Fee_i * LPTokens_i) / SUM(LPTokens_i) - ``` - -**Vote Slot Management:** - -- Maximum 8 vote slots (defined by `kVoteMaxSlots`)[^vote-max-slots] -- If a slot is available, the new vote is added directly -- If all slots are full, replacement is a two-step process[^vote-min-tokens]: - 1. **Find the eviction candidate:** select the existing slot with the smallest LP token balance, breaking ties by lowest fee, then by lexicographically smallest account ID - 2. **Decide whether to replace:** the new vote replaces the candidate only if the new voter holds more LP tokens, or holds an equal amount and sets a higher fee. If both are equal, the new vote is not added -- Vote weights are automatically recalculated when LP token balances change - -**Example:** - -AMM has 3 voters: -- Alice: 100 LP tokens, votes 500 (0.5% fee) -- Bob: 50 LP tokens, votes 300 (0.3% fee) -- Carol: 50 LP tokens, votes 700 (0.7% fee) - -Actual fee = (100*500 + 50*300 + 50*700) / (100 + 50 + 50) = 100,000 / 200 = 500 (0.5%) - -# 2. Ledger Entries - -The AMM system uses several ledger entry types to track state: - -```mermaid -classDiagram - class AMM { - +AccountID Account - +UInt16 TradingFee - +Array VoteSlots - +Object AuctionSlot - +Amount LPTokenBalance - +Issue Asset - +Issue Asset2 - +UInt64 OwnerNode - } - - class AccountRoot { - +AccountID Account - +Amount Balance - +UInt256 AMMID - +UInt32 Flags - } - - class RippleState { - +Amount Balance - +Amount LowLimit - +Amount HighLimit - +UInt32 Flags - } - - class MPToken { - +AccountID Account - +uint192 MPTokenIssuanceID - +Amount MPTAmount - +UInt32 Flags - } - - class DirectoryNode { - } - - AMM --> AccountRoot : references via sfAccount - AccountRoot -- RippleState : connects to (via LowLimit/HighLimit) - MPToken --> AccountRoot : owned by (via sfAccount) - AMM --> DirectoryNode : linked via sfOwnerNode -``` -*Figure: Key ledger entries that represent an AMM instance* - -## 2.1. AMM Ledger Entry - -The `AMM` ledger entry (type `ltAMM = 0x0079`)[^amm-ledger-entry] tracks the state of an AMM instance. Each AMM is uniquely identified by its asset pair[^amm-keylet]. - -### 2.1.1. Object Identifier - -The key of the `AMM` object is the result of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the `AMM` space key (`0x0041`, uppercase `A`)[^amm-namespace] concatenated with the two assets' identifiers. - -[^amm-namespace]: AMM namespace constant: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Indexes.cpp#L73) - -The two assets are first ordered canonically (lexicographically) to ensure a unique, deterministic key regardless of the order in which assets are specified. - -Each asset contributes its identifier to the hash: -- **XRP**: Issuer AccountID (all zeros) + Currency code (all zeros) -- **IOUs**: Issuer AccountID + Currency code -- **MPTs**: MPTID - -### 2.1.2. Fields - -| Field | Type | Required | Description | -|-------|------|----------------------------|-------------| -| `Account` | AccountID | Yes | The Account ID of the AMM's pseudo-account | -| `TradingFee` | UInt16 | Defaults to 0 if not set | The current trading fee in units of 1/100,000 (0 if not set) | -| `VoteSlots` | Array | Optional | Array of up to 8 `VoteEntry` objects containing fee votes | -| `AuctionSlot` | Object | Optional | Object containing auction slot information | -| `LPTokenBalance` | Amount | Yes | Total outstanding LP tokens for this AMM | -| `Asset` | Issue | Yes | One of the pool's two assets (the lesser by Issue comparison) | -| `Asset2` | Issue | Yes | The other pool asset (the greater by Issue comparison) | -| `OwnerNode` | UInt64 | Yes | Index of the owner directory page for this AMM | -| `PreviousTxnID` | Hash256 | Optional | Transaction hash that most recently modified this entry | -| `PreviousTxnLgrSeq` | UInt32 | Optional | Ledger sequence of the transaction that most recently modified this entry | - -#### 2.1.2.1. VoteSlots - -The `VoteSlots` field contains an array of `VoteEntry` inner objects. Each `VoteEntry` has: - -| Field | Type | Description | -|-------|------|-------------| -| `Account` | AccountID | The account that cast this vote | -| `TradingFee` | UInt16 | The fee this account voted for (0-1000) | -| `VoteWeight` | UInt32 | Weight of this vote = `(LPTokens / TotalLPTokens) * 100,000` | - -#### 2.1.2.2. AuctionSlot - -The `AuctionSlot` field contains an inner object with: - -| Field | Type | Description | -|-------|------|-------------| -| `Account` | AccountID | Current auction slot holder | -| `AuthAccounts` | Array | Optional array of up to 4 authorized accounts | -| `Expiration` | UInt32 | Unix timestamp when the slot expires (current time + 86,400 seconds) | -| `Price` | Amount | Price paid for the auction slot in LP tokens | -| `DiscountedFee` | UInt16 | Discounted fee for slot holder | - - -### 2.1.3. Pseudo-accounts - -The AMM's `Account` field references a pseudo-account[^pseudo-account-creation] created specifically for this AMM. Each AMM instance creates a special pseudo-account to hold the pool's assets. This account: - -- Has a disabled master key, allows default rippling and enables deposit authorization (so nobody can pay into the pseudo-account)[^disabled-master-key] -- Is identified by the `sfAMMID` field[^ammid-field] in its `AccountRoot` entry -- Has an Account ID deterministically generated[^pseudo-account-address] from the AMM ledger entry key -- Holds XRP balance if one of the pool assets is XRP -- Has trust lines for: - - Each IOU in the pool - - Each liquidity provider who holds LP tokens -- Has MPToken entries for MPT assets in the pool (if pool contains MPTs): -- Is automatically deleted when the AMM is deleted - -[^pseudo-account-creation]: Pseudo-account creation for AMM: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L251) -[^disabled-master-key]: Master key disabled with lsfDisableMaster flag: [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L244) -[^ammid-field]: AMMID field set in pseudo-account: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L251) -[^pseudo-account-address]: Pseudo-account address generation: [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L146-L160) -[^zero-credit-limit]: LP token trustline created with zero balance: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L269-L273) - -#### 2.1.3.1. Account ID Generation - -The AMM pseudo-account ID, like any other pseudo-account ID, is generated using a collision-avoidance algorithm[^collision-avoidance-algo] that ensures no existing account has the same address. The generation process uses the `pseudoAccountAddress()` function with the following algorithm: - -[^collision-avoidance-algo]: Collision-avoidance algorithm for pseudo-account address: [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L146-L160) - -**Generation Process:** - -1. **Input**: The AMM ledger entry key (derived from [object identifier](#211-object-identifier)) -2. **Parent Hash**: The hash of the parent ledger (provides uniqueness per ledger) -3. **Iteration Loop**: Try up to 256 attempts (hardcoded as `kMaxAccountAttempts`) - -**Collision avoidance**: Account IDs are 160-bit values derived from cryptographic hashes. While the probability of collision with an existing account is small, multiple attempts provide a safety mechanism to handle this theoretical edge case. - -For each attempt `i` (0 to 255): - -``` -hash = SHA512-Half(i, parentLedgerHash, ammLedgerEntryKey) -accountID = RIPEMD160(SHA256(hash)) -``` - -4. **Collision Check**: Verify that no `AccountRoot` exists with this `accountID` -5. **Success**: If no collision, return the `accountID` -6. **Failure**: If all 256 attempts find collisions, return `beast::kZero` (all zeros account ID) - -**Failure Handling:** - -If `pseudoAccountAddress()` returns `beast::kZero` (indicating all 256 attempts failed): -- `createPseudoAccount()` returns `tecDUPLICATE` -- The AMMCreate transaction fails in `doApply` -- This scenario is extremely unlikely in practice - -**Determinism:** - -For a given asset pair and parent ledger hash, all nodes generate the same sequence of candidate account IDs: -- The iteration counter `i` is hashed along with fixed inputs (parent hash, AMM keylet) -- Each `i` produces a completely different candidate Account ID -- All nodes check the same candidates in the same order against their ledger state -- The first unused candidate found is selected consistently across all nodes -- This ensures reproducibility across nodes in consensus and predictable behavior in transaction replay - -**Example:** - -For an AMM with USD/XRP: -1. AMM keylet = `SHA512-Half(0x0041, XRP_account, XRP_currency, USD_account, USD_currency)`[^amm-keylet-hash] -2. Attempt 0: `hash = SHA512-Half(0, parentHash, ammKeylet)` -> Account ID candidate -3. If Account ID exists, try attempt 1: `hash = SHA512-Half(1, parentHash, ammKeylet)` -> New candidate -4. Continue until unused account ID found or 256 attempts exhausted - -### 2.1.4. Reserves - -The `AMM` ledger entry itself does not require an owner reserve. However: - -- Creating an AMM costs an elevated base fee equal to one owner-reserve increment (`view.fees().increment`), set higher than the normal per-transaction base fee -- The AMM pseudo-account holds reserves if it has XRP -- LP token holders who have trust lines for LP tokens pay reserves according to normal trust line rules - -## 2.2. RippleState Ledger Entry - -AMMs create `RippleState` entries (trust lines) for: -- Each IOU asset in the pool -- The LP token issued by the AMM - -All AMM trust lines: -- Have zero credit limits[^zero-credit-limit] (to prevent unsolicited deposits) -- Do not have quality modifiers (QualityIn/QualityOut)[^ripplestate-no-quality] - -Pool asset trust lines (between the AMM account and the IOU issuer): -- Are additionally marked with the `lsfAMMNode` flag[^ripplestate-amm-flag] - -[^ripplestate-amm-flag]: Trust line marked with lsfAMMNode flag: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L339-L341) -[^ripplestate-no-quality]: Quality modifiers only set if non-zero: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L252-L256) - -See [Trust Lines Documentation](../trust_lines/README.md#21-ripplestate-ledger-entry) for complete details on `RippleState` ledger entries. - -## 2.3. MPToken Ledger Entry - -When an AMM pool contains MPT assets, the AMM pseudo-account holds `MPToken` entries for each MPT in the pool. These MPToken entries: - -- Are marked with the `lsfMPTAMM` flag[^mptoken-amm-flag] (distinguishing them from regular holder MPTokens) -- Are always marked with the `lsfMPTAuthorized` flag[^mptoken-authorized-flag] (the AMM pseudo-account is implicitly authorized to hold the asset, regardless of the issuance's `lsfMPTRequireAuth`) -- Track the AMM's MPT balance via the `MPTAmount` field -- Are created when depositing MPT assets[^mptoken-creation] -- Do not count towards the AMM pseudo-account's `OwnerCount`[^mptoken-no-owner-count] - -[^mptoken-amm-flag]: MPToken created with lsfMPTAMM flag: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L311) -[^mptoken-authorized-flag]: MPToken implicitly authorized (lsfMPTAuthorized set unconditionally): [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L311) -[^mptoken-creation]: MPToken creation for AMM pseudo-account: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L335-L336) -[^mptoken-no-owner-count]: AMM owner count not adjusted for MPToken: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L320-L321) - -See [MPTokens Documentation](../mpts/README.md) for complete details on `MPToken` ledger entries. - -# 3. Transactions - -## Common Error Codes from accountSend() - -Several AMM transactions (`AMMCreate`, `AMMDeposit`, `AMMWithdraw`, `AMMBid`) use the `accountSend()` function to transfer assets between accounts. This function can return various error codes depending on the transfer type and ledger state. These errors may occur during the `doApply` phase of transaction execution: - -**For XRP transfers:** -- `tecFAILED_PROCESSING` or `telFAILED_PROCESSING`: Sender has insufficient XRP balance to complete the transfer (after paying transaction fees and maintaining reserve requirements)[^xrp-insufficient-balance] -- With [fixAMMv1_1](https://xrpl.org/resources/known-amendments#fixammv1_1): `tecINTERNAL` if the transfer amount is negative[^xrp-negative-check] - -[^xrp-insufficient-balance]: Insufficient XRP balance check: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L886-L892) -[^xrp-negative-check]: Negative amount check with fixAMMv1_1: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L825-L830) - -**For IOU transfers:** -- Calls `directSendNoLimitIOU()`[^iou-ripple-send] which then calls `directSendNoFeeIOU()`[^iou-ripple-credit] and may call `issueIOU()`[^iou-issue] or `redeemIOU()`[^iou-redeem] -- These functions can trigger trust line creation, which may fail with: - - `tecDIR_FULL`: Owner directory is full when creating a new trust line[^iou-dir-full] - - `tecNO_LINE_INSUF_RESERVE`: Insufficient XRP reserve to create the trust line[^iou-insuf-reserve] - - `tefINTERNAL`: Trust line doesn't exist after transfer[^iou-no-line] - - `tefINTERNAL`: Receiver account SLE does not exist during trust line creation[^iou-null-account] - - `tecNO_TARGET`: Peer account doesn't exist when creating trust line[^iou-no-target] -- Errors from `directSendNoFeeIOU()` are propagated[^iou-deletable-accounts]. These include: - - `tecDIR_FULL`: Owner directory is full when creating trust line (from `trustCreate()`)[^iou-dir-full] - - `tefINTERNAL`: Receiver account SLE is null (from `trustCreate()`)[^iou-null-account] - - `tecNO_TARGET`: Peer account doesn't exist when creating trust line (from `trustCreate()`)[^iou-no-target] - - `tefBAD_LEDGER`: Directory removal failed when deleting trust line (from `trustDelete()`)[^iou-bad-ledger] - -[^iou-ripple-send]: directSendNoLimitIOU function: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L794-L847) -[^iou-ripple-credit]: directSendNoFeeIOU function: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L643-L789) -[^iou-issue]: issueIOU function: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L397-L489) -[^iou-redeem]: redeemIOU function: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L493-L561) -[^iou-dir-full]: Owner directory full check: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L218-L227) -[^iou-insuf-reserve]: Insufficient reserve to create trust line: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L674-L681) -[^iou-no-line]: Trust line doesn't exist after attempting redeem: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L538-L547) -[^iou-null-account]: Receiver account SLE null check: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L668-L670), [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L233-L234) -[^iou-no-target]: Peer account doesn't exist check: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L239-L241) -[^iou-deletable-accounts]: IOU send error propagation: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L716-L717) -[^iou-bad-ledger]: Directory removal failure in trustDelete: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L308-L318) - -**For MPT transfer:** -- `tecOBJECT_NOT_FOUND`: MPT issuance object doesn't exist[^mpt-object-not-found] -- `tecPATH_DRY`: Transfer would exceed `MaximumAmount` when issuer is sending MPTs[^mpt-path-dry-send][^mpt-path-dry-credit] -- `tecINSUFFICIENT_FUNDS`: Sender's MPToken balance is less than the transfer amount[^mpt-insufficient-funds] -- `tecNO_AUTH`: - - Sender's MPToken ledger entry doesn't exist (not authorized to hold the MPT)[^mpt-sender-no-auth] - - Receiver's MPToken ledger entry doesn't exist (not authorized to hold the MPT)[^mpt-receiver-no-auth] -- `tecINTERNAL`: Outstanding amount is less than the amount being redeemed when receiver is issuer[^mpt-internal] - -[^mpt-object-not-found]: MPT issuance not found: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1269-L1271) -[^mpt-path-dry-send]: MPT transfer exceeds MaximumAmount (directSendNoLimitMPT): [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1178-L1179) -[^mpt-path-dry-credit]: MPT transfer exceeds MaximumAmount (directSendNoFeeMPT): [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1084-L1085) -[^mpt-insufficient-funds]: Sender MPToken balance insufficient: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1095-L1097) -[^mpt-sender-no-auth]: Sender MPToken entry missing: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1102-L1104) -[^mpt-receiver-no-auth]: Receiver MPToken entry missing: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1136-L1138) -[^mpt-internal]: Outstanding amount less than redemption: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1115-L1117) -[^amm-ledger-entry]: AMM ledger entry type definition: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/ledger_entries.macro#L373-L384) -[^amm-keylet]: AMM keylet computation using asset pair: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Indexes.cpp#L425-L456) -[^amm-keylet-hash]: AMM keylet hash with namespace `0x0041` and fields `(account, currency)` per asset: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Indexes.cpp#L432-L437) -[^amm-lp-tokens-calc]: Initial LP token calculation: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L45-L54) -[^vote-max-slots]: Maximum vote slots constant: [`AMMCore.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/AMMCore.h#L24) -[^vote-weighted-average]: Weighted average fee calculation: [`AMMVote.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L205-L207) -[^vote-min-tokens]: Vote slot replacement logic: [`AMMVote.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L135-L144) - -**Note:** Most of these error conditions are checked during the `preclaim` phase (validation against the ledger view), so they are unlikely to occur during `doApply`. However, ledger state can change between validation and application (e.g., due to other transactions in the same ledger), making these errors theoretically possible. - -## 3.1. AMMCreate Transaction - -The `AMMCreate` transaction creates a new AMM instance for a token pair and provides initial liquidity. - -| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | -|------------|:---------:|:-----------:|:---------:|:-------------:|:-------------:|:--------------------------------------------------------------------------| -| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMCreate"` | -| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account creating the AMM instance | -| `Amount` | :heavy_check_mark: | `No` | `String` or `Object` | `Amount` | | Amount of one asset to deposit (XRP as string, tokens as object) | -| `Amount2` | :heavy_check_mark: | `No` | `String` or `Object` | `Amount` | | Amount of the other asset to deposit (XRP as string, tokens as object) | -| `TradingFee` | :heavy_check_mark: | `No` | `Number` | `UInt16` | | Initial trading fee (0-1000, 1 = 0.001%) | -| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags (must be 0 for AMMCreate, only universal flags allowed) | - -The two amounts can be in any order - the AMM will automatically order them as `Asset` and `Asset2` based on Issue comparison. - -### 3.1.1. Failure Conditions - -**Static validation**[^ammcreate-static-validation] - -[^ammcreate-static-validation]: Static validation (preflight): [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L42-L85) - -- `temDISABLED`: - - [AMM](https://xrpl.org/resources/known-amendments#amm) amendment is not enabled - - either `Amount` or `Amount2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled -- `temINVALID_FLAG`: one of the specified flags is not one of common transaction flags -- `temBAD_AMM_TOKENS`: `Amount` and `Amount2` have the same currency and issuer -- `temBAD_CURRENCY`: `Amount` or `Amount2` uses the disallowed 3-letter "XRP" currency code -- `temBAD_ISSUER`: `Amount` or `Amount2` is XRP (currency is all zeros) but has a non-zero issuer account -- `temBAD_MPT`: `Amount` or `Amount2` is an MPT with a zero (empty) issuer -- `temBAD_AMOUNT`: either `Amount` or `Amount2` is zero, negative -- `temBAD_FEE`: `TradingFee` exceeds 1000 - -**Validation against the ledger view**[^ammcreate-preclaim-validation] - -[^ammcreate-preclaim-validation]: Validation against ledger view (preclaim): [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L95-L242) - -- `tecDUPLICATE`: an AMM already exists for this token pair -- `tecNO_LINE`: `Amount` or `Amount2` issuer has `lsfRequireAuth` flag set, but account has no trust line with the issuer -- `tecNO_AUTH`: - - For IOUs: `Amount` or `Amount2` issuer has `lsfRequireAuth` flag set, and the trust line exists but lacks authorization (missing `lsfLowAuth` or `lsfHighAuth` flag) - - For MPTs: Signing account or AMM pseudo-account lacks required authorization for MPT with `lsfMPTRequireAuth` flag -- `tecFROZEN` (IOU/XRP) or `tecLOCKED` (MPT): either asset is globally or individually frozen/locked -- `terNO_RIPPLE`: either asset's issuer does not have DefaultRipple flag set (non-XRP assets only) -- `tecINSUF_RESERVE_LINE`: account has insufficient XRP to cover the LP token trust line reserve -- `tecUNFUNDED_AMM`: account has insufficient balance of either asset or it does not have the trust line -- `tecAMM_INVALID_TOKENS`: either `Amount` or `Amount2` is an LP token from another AMM. The code does not explicitly check for *another* AMM, but at this point, LP token from this AMM should not exist -- With [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault): - - `terADDRESS_COLLISION`: generated AMM account ID already exists - - `tecWRONG_ASSET`: either amount is an MPT issued by a pseudo-account (vault share tokens cannot back an AMM) -- Without [AMMClawback](https://xrpl.org/resources/known-amendments#ammclawback): - - `tecINTERNAL`: `Amount` or `Amount2` issuer account does not exist in the ledger - - `tecNO_PERMISSION`: - - `Amount` or `Amount2` issuer has clawback enabled (`lsfAllowTrustLineClawback` flag is set for IOUs) - - either `Amount` or `Amount2` is an MPT with `lsfMPTCanClawback` flag set -- MPT-specific validations (for either `Amount` or `Amount2` if MPT): Both assets are validated using [`canMPTTradeAndTransfer`](../mpts/README.md#363-canmpttradeandtransfer). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for validation logic and error conditions. - -**Validation during doApply**[^ammcreate-doapply-validation] - -[^ammcreate-doapply-validation]: Validation during doApply: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L268-L359) - -- `tecDUPLICATE`: - - AMM pseudo-account ID generation failed (no valid account ID found after 256 attempts) - - LP Token trust line already exists -- `tecDIR_FULL`: Owner directory is full when linking AMM object -- Propagate errors from `accountSend()` when transferring LP tokens and assets to/from AMM pseudo-account (see [Common Error Codes from accountSend()](#common-error-codes-from-accountsend)) - -### 3.1.2. State Changes[^ammcreate-state-changes] - -[^ammcreate-state-changes]: State changes (doApply): [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L268-L403) - -- `AccountRoot` object is **created** for AMM pseudo-account: - - `Account`: Generated pseudo-account ID (from collision-avoidance algorithm) - - `Balance`: `STAmount{}` (zero XRP initially, then updated to `Amount` if `Amount` is XRP) - - `Sequence`: 0 (with [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault)), otherwise current ledger sequence - - `Flags`: `lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth` - - `sfAMMID`: Set to `ammKeylet.key` (the AMM ledger entry key) - -- `AMM` object is **created**: - - `Account`: AMM pseudo-account ID - - `LPTokenBalance`: `SQRT(Amount * Amount2)` - - `Asset`: Lesser of the two assets by Issue comparison - - `Asset2`: Greater of the two assets - - `TradingFee`: As specified (if non-zero) - - `OwnerNode`: Link to owner directory - - `VoteSlots`: Array field with single `VoteEntry` inner object **created**: - - `Account`: Creator account ID - - `TradingFee`: Initial trading fee (if non-zero) - - `VoteWeight`: 100,000 (= 100%, since creator owns all LP tokens initially) - - `AuctionSlot`: Object field with an inner object **created**: - - `Account`: Creator account ID - - `Expiration`: Current time + 86,400 seconds (24 hours) - - `Price`: 0 LP tokens - - `DiscountedFee`: `TradingFee / 10` (if trading fee is non-zero) - -- `RippleState` objects are **created** (for token assets): - - For each non-XRP token asset: Trust line between AMM account and asset issuer - - Marked with `lsfAMMNode` flag - - For LP tokens: Trust line between AMM account and creator - - All trust lines: - - Have zero credit limits - - Initial balances set to deposited/issued amounts - -- `MPToken` objects are **created** (for MPT assets): - - For each MPT asset: MPToken entry for the AMM pseudo-account - - Flags: - - `lsfMPTAMM`: Marks this as an AMM-owned MPToken entry - - `lsfMPTAuthorized`: Always set (the AMM pseudo-account is implicitly authorized to hold the MPT) - - Initial `MPTAmount` set to deposited amount - - Linked to the AMM pseudo-account's owner directory - -- `DirectoryNode` is **created** for AMM pseudo-account's owner directory: - - Links the AMM ledger entry to the pseudo-account - - The AMM entry's `OwnerNode` field is set to the directory page index - - This directory will later also contain links to trust lines owned by the AMM account - -- Order books are **registered** in [OrderBookDB](../path_finding/README.md#45-orderbookdb) (if not already present): - - Asset->Asset2 trading direction registered - - Asset2->Asset trading direction registered - -## 3.2. AMMDeposit Transaction - -The `AMMDeposit` transaction adds liquidity to an existing AMM pool. There are multiple deposit modes controlled by transaction flags. - -**Fields:** - -| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | -|-------------------|:------------------:|:-----------:|:--------------------:|:-------------:|:-------------:|:---------------------------------------------------------------------------| -| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMDeposit"` | -| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account depositing liquidity | -| `Asset` | :heavy_check_mark: | `No` | `Object` | `Issue` | | One of the pool's assets | -| `Asset2` | :heavy_check_mark: | `No` | `Object` | `Issue` | | The other pool asset | -| `Amount` | | `No` | `String` or `Object` | `Amount` | | Amount of one asset (interpretation depends on flags) | -| `Amount2` | | `No` | `String` or `Object` | `Amount` | | Amount of the other asset (interpretation depends on flags) | -| `LPTokenOut` | | `No` | `String` or `Object` | `Amount` | | Amount of LP tokens to receive (interpretation depends on flags) | -| `EPrice` | | `No` | `String` or `Object` | `Amount` | | Maximum effective price in same currency as `Amount` (tfLimitLPToken only) | -| `TradingFee` | | `No` | `Number` | `UInt16` | | Trading fee for empty pool deposits (tfTwoAssetIfEmpty only) | -| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags specifying deposit mode | - -### 3.2.1. Deposit Modes - -The AMMDeposit transaction supports six different deposit modes. See [AMMDeposit Implementation Details](deposit.md) for detailed documentation. - -All deposit modes require the `Asset` and `Asset2` fields to identify which AMM pool to deposit into. The table below shows the additional fields required for each mode. - -| Function | Flag | Flag Value | Use Case | Assets | User Specifies | System Calculates | -|--------------------------------------------------------------------------------------|---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| -| [equalDepositLimit](deposit.md#41-equaldepositlimit-tftwoasset) | `tfTwoAsset` | `0x00100000` | Depositor specifies maximum amounts of both assets. System deposits both assets maintaining the pool's current ratio, maximizing deposit size within both limits | Both | `Amount` (max), `Amount2` (max), Optional: `LPTokenOut` (min) | Actual `Amount` and `Amount2` to deposit (tries maximizing `Amount` first, then `Amount2` if that fails) | -| [equalDepositTokens](deposit.md#42-equaldeposittokens-tflptoken) | `tfLPToken` | `0x00010000` | Depositor specifies exact LP tokens to receive. System calculates required amounts of both assets maintaining the pool's current ratio | Both | `LPTokenOut` (exact). Optional: both `Amount` (min) and `Amount2` (min), or neither | Required `Amount` and `Amount2` | -| [equalDepositInEmptyState](deposit.md#43-equaldepositinemptystate-tftwoassetifempty) | `tfTwoAssetIfEmpty` | `0x00800000` | Used when pool is empty (zero LP tokens and zero asset balances). Depositor deposits both assets to set new pool ratio and becomes initial LP token holder | Both | `Amount`, `Amount2`, Optional: `TradingFee` | Initial `LPTokenOut` = sqrt(`Amount` * `Amount2`) | -| [singleDeposit](deposit.md#51-singledeposit-tfsingleasset) | `tfSingleAsset` | `0x00080000` | Depositor specifies amount of single asset to deposit. System calculates how many LP tokens depositor receives | One | `Amount`, Optional: `LPTokenOut` (min) | `LPTokenOut` depositor receives | -| [singleDepositTokens](deposit.md#52-singledeposittokens-tfoneassetlptoken) | `tfOneAssetLPToken` | `0x00200000` | Depositor specifies exact LP tokens to receive in exchange for depositing single asset. System calculates required deposit amount | One | `LPTokenOut` (exact), `Amount` (max) | Required `Amount` | -| [singleDepositEPrice](deposit.md#53-singledepositeprice-tflimitlptoken) | `tfLimitLPToken` | `0x00400000` | Depositor sets maximum amount of single asset willing to pay per LP token received. System calculates optimal deposit amount | One | `Amount` (can be 0), `EPrice` (max) | Optimal `Amount` at `EPrice` limit | - -The deposit mode is determined by exactly one of these flags (enforced by checking `popcount(flags & tfDepositSubTx) == 1`). See the table above for flag values and usage details, and [AMMDeposit Implementation Details](deposit.md) for the implementation of each mode. - -### 3.2.2. Failure Conditions - -**Static validation**[^ammdeposit-static-validation] - -[^ammdeposit-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L37-L47), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L51-L54), [`preflight`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L57-L175) - -- `temDISABLED`: - - AMM amendment is not enabled - - either `Asset` or `Asset2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled -- `temINVALID_FLAG`: invalid flags (flags set that are not deposit mode flags) -- `temMALFORMED`: - - Invalid flag combination (must have exactly one deposit mode flag set) - - Required fields missing for chosen deposit mode -- `temBAD_AMM_TOKENS`: - - `Amount` and `Amount2` are the same token (when both specified) - - `LPTokenOut` is zero or negative - - `Asset` and `Asset2` have the same currency and issuer - - `Amount` or `Amount2` currency does not match either pool asset (`Asset` or `Asset2`) - - `EPrice` currency does not match `Amount` currency (checked only when MPTokensV2 is not enabled) -- `temBAD_CURRENCY`: `Asset`, `Asset2`, `Amount`, `Amount2`, or `EPrice` uses the disallowed 3-letter "XRP" currency code (`0x5852500000000000`) -- `temBAD_ISSUER`: `Asset`, `Asset2`, `Amount`, `Amount2`, or `EPrice` is XRP (currency is all zeros) but has a non-zero issuer account -- `temBAD_MPT`: `Asset`, `Asset2`, `Amount`, `Amount2`, or `EPrice` is an MPT with a zero (empty) issuer -- `temBAD_AMOUNT`: `Amount`, `Amount2`, or `EPrice` is zero, negative -- `temBAD_FEE`: `TradingFee` exceeds 1000 - -**Validation against the ledger view**[^ammdeposit-preclaim-validation] - -[^ammdeposit-preclaim-validation]: Validation against ledger view (preclaim): [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L177-L361) - -- `terNO_AMM`: AMM ledger entry does not exist for specified asset pair -- `tecINTERNAL`: - - (tfTwoAssetIfEmpty only) Pool has zero LP tokens but asset balances are not zero (inconsistent empty state) - - pool balances are invalid (zero or negative) -- `tecAMM_NOT_EMPTY`: tfTwoAssetIfEmpty used but AMM is not empty -- `tecAMM_EMPTY`: AMM has zero LP tokens (for non-tfTwoAssetIfEmpty modes) -- Authorization/freeze checks (applied unconditionally to the deposited `Amount`/`Amount2` for non-`tfLPToken` modes, and with [AMMClawback](https://xrpl.org/resources/known-amendments#ammclawback) also to the pool `Asset`/`Asset2`): - - `tecNO_LINE`: the asset's issuer has `lsfRequireAuth` set, but the account has no trust line with the issuer - - `tecNO_AUTH`: the asset's issuer has `lsfRequireAuth` set, and the trust line exists but lacks authorization (missing `lsfLowAuth` or `lsfHighAuth` flag) - - `tecFROZEN` (IOU/XRP) or `tecLOCKED` (MPT): the asset is frozen/locked (AMM account, currency/issuance, or depositor account). Under the `fixCleanup3_3_0` amendment, both pool assets are checked whether or not they are deposited, so a deposit now also fails when the AMM pseudo-account's holding of the non-deposited pool asset is individually frozen (the deposited funds could not later be withdrawn). Without the amendment such a deposit succeeds. The conditions with the amendment: - - the asset is globally frozen or locked - - the AMM pseudo-account's holding of either pool asset is individually frozen - - the depositor's holding of the asset is individually frozen, unless the depositor is that asset's issuer -- `tecUNFUNDED_AMM`: - - account has insufficient token balance to deposit - - account has insufficient XRP to deposit (and LP token trust line already exists) -- `tecINSUF_RESERVE_LINE`: - - account has insufficient XRP to deposit and create LP token trust line (when account is not yet an LP) - - non-LP account has insufficient reserve for LP token trust line -- `temBAD_AMM_TOKENS`: `LPTokenOut` issue (currency code + issuer) does not match the AMM's LP token issue -- MPT-specific validations (for either `Asset` or `Asset2` if MPT): Both assets are validated using [`canMPTTradeAndTransfer`](../mpts/README.md#363-canmpttradeandtransfer). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for validation logic and error conditions. - -**Validation during doApply**[^ammdeposit-doapply-validation] - -[^ammdeposit-doapply-validation]: Validation during doApply: [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L412-L1046) - -- `tecINTERNAL`: AMM ledger entry does not exist (should not happen if preclaim succeeded) -- `temBAD_AMOUNT`: Deposit amount after adjustment/calculation is zero or negative. Deposit amounts are adjusted based on the deposit mode (e.g., proportional calculations for tfLPToken, pool ratio adjustments for tfTwoAsset, or LP token precision adjustments). -- `tecUNFUNDED_AMM`: Insufficient balance to deposit the final calculated amounts. This is re-checked during deposit execution (first check is in preclaim with transaction amounts, but final amounts may differ for certain deposit modes like tfLPToken). -- `tecAMM_FAILED`: Deposit constraints not satisfied. The interpretation of transaction fields as minimums or maximums depends on the deposit mode flag (see [Deposit Modes](#321-deposit-modes)): - - tfLPToken mode: calculated asset deposits are less than `Amount` or `Amount2` (optional minimums) - - tfSingleAsset or tfTwoAsset mode: calculated LP tokens are less than `LPTokenOut` (optional minimum) - - tfTwoAsset mode: neither calculated deposit strategy satisfies both `Amount` and `Amount2` constraints (maximums) - - tfOneAssetLPToken mode: calculated deposit amount exceeds `Amount` (maximum willing to deposit) - - tfLimitLPToken mode: calculated deposit amount is invalid or effective price constraint cannot be satisfied with `EPrice` (maximum effective price) -- `tecAMM_INVALID_TOKENS`: Calculated LP tokens are zero or invalid. This can occur when: - - LP token adjustments for precision result in zero tokens (with [fixAMMv1_3](https://xrpl.org/resources/known-amendments#fixammv1_3)) - - Deposit amount is too small relative to pool size, resulting in zero LP tokens after rounding - - Occurs in any deposit mode where LP tokens are calculated (tfLPToken, tfSingleAsset, tfTwoAsset, tfOneAssetLPToken, tfLimitLPToken) -- Propagate errors from `accountSend()` when transferring assets to AMM account and LP tokens to depositor (see [Common Error Codes from accountSend()](#common-error-codes-from-accountsend)) - -### 3.2.3. State Changes - -- `AMM` object is **modified**: - - `LPTokenBalance`: Increased by deposited LP tokens - - `VoteSlots`: (tfTwoAssetIfEmpty only) Reset with depositor's vote - - `AuctionSlot`: (tfTwoAssetIfEmpty only) Depositor becomes slot holder with `Price` set to 0 and 24-hour expiration - - `TradingFee`: (tfTwoAssetIfEmpty only) Updated if specified - -- AMM pseudo-account balances are **modified**: - - Asset deposits transferred from depositor to AMM pseudo-account - - Balances updated in AMM pseudo-account's `AccountRoot` (for XRP), `RippleState` trust lines (for tokens), or `MPToken` entries (for MPTs) - -- LP tokens are **issued**: - - LP tokens sent from AMM pseudo-account to depositor - - Trust line created if depositor doesn't have one - - `RippleState` balance updated - -- Depositor's `AccountRoot` is **modified**: - - `OwnerCount`: Incremented if new LP token trust line created - - `Balance`: Decreased by XRP deposited (if applicable) - -## 3.3. AMMWithdraw Transaction - -The `AMMWithdraw` transaction removes liquidity from an AMM pool by redeeming LP tokens. - -**Fields:** - -| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | -|-------------------|:------------------:|:-----------:|:--------------------:|:-------------:|:-------------:|:---------------------------------------------------------------------------| -| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMWithdraw"` | -| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account withdrawing liquidity | -| `Asset` | :heavy_check_mark: | `No` | `Object` | `Issue` | | One of the pool's assets | -| `Asset2` | :heavy_check_mark: | `No` | `Object` | `Issue` | | The other pool asset | -| `Amount` | | `No` | `String` or `Object` | `Amount` | | Amount of one asset (interpretation depends on flags) | -| `Amount2` | | `No` | `String` or `Object` | `Amount` | | Amount of the other asset (interpretation depends on flags) | -| `LPTokenIn` | | `No` | `String` or `Object` | `Amount` | | Amount of LP tokens to redeem (interpretation depends on flags) | -| `EPrice` | | `No` | `String` or `Object` | `Amount` | | Minimum effective price in LP token currency (tfLimitLPToken only) | -| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags specifying withdrawal mode | - -### 3.3.1. Withdrawal Modes - -The AMMWithdraw transaction supports seven different withdrawal modes. See [AMMWithdraw Implementation Details](withdraw.md) for detailed documentation. - -All withdrawal modes require the `Asset` and `Asset2` fields to identify which AMM pool to withdraw from. The table below shows the additional fields required for each mode. - -| Function | Flag | Flag Value | Use Case | Assets | User Specifies | System Calculates | -|-----------------------------------------------------------------------------------------------------|-------------------------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| -| [equalWithdrawTokens](withdraw.md#41-equalwithdrawtokens-tflptoken-tfwithdrawall) | `tfLPToken` | `0x00010000` | Withdrawer specifies exact LP tokens to redeem. System withdraws both assets maintaining the pool's current ratio | Both | `LPTokenIn` (exact) | Required `Amount` and `Amount2` to withdraw | -| [equalWithdrawTokens](withdraw.md#41-equalwithdrawtokens-tflptoken-tfwithdrawall) | `tfWithdrawAll` | `0x00020000` | Withdrawer redeems all LP tokens held. System withdraws both assets proportionally based on entire LP token balance | Both | None (redeems all LP tokens) | `Amount` and `Amount2` based on all LP tokens held | -| [equalWithdrawLimit](withdraw.md#42-equalwithdrawlimit-tftwoasset) | `tfTwoAsset` | `0x00100000` | Withdrawer specifies maximum amounts of both assets. System withdraws both assets maintaining the pool's current ratio, maximizing withdrawal size within both limits | Both | `Amount` (max), `Amount2` (max) | Actual `Amount` and `Amount2` to withdraw (tries maximizing `Amount` first, then `Amount2` if that fails), `LPTokenIn` | -| [singleWithdraw](withdraw.md#51-singlewithdraw-tfsingleasset) | `tfSingleAsset` | `0x00080000` | Withdrawer specifies amount of single asset to withdraw. System calculates how many LP tokens withdrawer must redeem | One | `Amount` | `LPTokenIn` withdrawer must redeem | -| [singleWithdrawTokens](withdraw.md#52-singlewithdrawtokens-tfoneassetlptoken-tfoneassetwithdrawall) | `tfOneAssetWithdrawAll` | `0x00040000` | Withdrawer redeems all LP tokens held in exchange for withdrawing single asset. System calculates withdrawal amount based on entire LP token balance | One | `Amount` (required to specify which asset; value is min constraint or 0 for no min) | `Amount` to withdraw based on all LP tokens held | -| [singleWithdrawTokens](withdraw.md#52-singlewithdrawtokens-tfoneassetlptoken-tfoneassetwithdrawall) | `tfOneAssetLPToken` | `0x00200000` | Withdrawer specifies exact LP tokens to redeem in exchange for withdrawing single asset. System calculates withdrawal amount | One | `LPTokenIn` (exact), `Amount` (min or 0 for no min) | Required `Amount` | -| [singleWithdrawEPrice](withdraw.md#53-singlewithdraweprice-tflimitlptoken) | `tfLimitLPToken` | `0x00400000` | Withdrawer sets minimum effective price (asset received per LP token redeemed). System calculates optimal withdrawal amount | One | `Amount` (min or 0 for no min), `EPrice` (min effective price) | Optimal `Amount` and `LPTokenIn` at `EPrice` limit | - -The withdrawal mode is determined by exactly one of these flags (enforced by checking `popcount(flags & tfWithdrawSubTx) == 1`). See the table above for flag values and usage details, and [AMMWithdraw Implementation Details](withdraw.md) for the implementation of each mode. - -### 3.3.2. Failure Conditions - -**Static validation**[^ammwithdraw-static-validation] - -[^ammwithdraw-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L43-L53), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L57-L60), [`preflight`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L63-L168) - -- `temDISABLED`: - - AMM amendment not enabled - - either `Asset` or `Asset2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled -- `temINVALID_FLAG`: invalid flags (flags set that are not withdraw mode flags) -- `temMALFORMED`: - - Invalid flag combination (must have exactly one withdrawal mode flag set) - - Required fields missing for chosen withdrawal mode -- `temBAD_AMM_TOKENS`: - - `Amount` and `Amount2` are the same token (when both specified) - - `LPTokenIn` is zero or negative - - `Asset` and `Asset2` have the same currency and issuer - - `Amount` or `Amount2` currency does not match either pool asset (`Asset` or `Asset2`) -- `temBAD_CURRENCY`: `Asset`, `Asset2`, `Amount`, `Amount2`, or `EPrice` uses the disallowed 3-letter "XRP" currency code (`0x5852500000000000`) -- `temBAD_ISSUER`: `Asset`, `Asset2`, `Amount`, `Amount2`, or `EPrice` is XRP (currency is all zeros) but has a non-zero issuer account -- `temBAD_MPT`: `Asset`, `Asset2`, `Amount`, `Amount2`, or `EPrice` is an MPT with a zero (empty) issuer -- `temBAD_AMOUNT`: `Amount`, `Amount2`, or `EPrice` is zero, negative - -**Note:** AMMWithdraw static validation differs from [AMMDeposit static validation](#322-failure-conditions) in the following ways: - -- Does NOT validate that `EPrice` currency matches `Amount` currency (in deposit, EPrice = asset deposited / LP tokens received so it must match Amount currency; in withdraw, EPrice = LP tokens redeemed / asset received so it must match LP token issue, which is checked in preclaim against the AMM ledger entry, not in preflight) -- Does NOT validate `TradingFee` field (withdraw transactions don't have this field) -- `Amount` validation considers withdrawal mode flags (`tfOneAssetWithdrawAll` | `tfOneAssetLPToken`) in addition to `EPrice` presence - -**Validation against the ledger view**[^ammwithdraw-preclaim-validation] - -[^ammwithdraw-preclaim-validation]: Validation against ledger view (preclaim): [`AMMWithdraw.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L182-L314) - -- `terNO_AMM`: AMM ledger entry does not exist for specified asset pair -- `tecINTERNAL`: - - pool balances are invalid (zero or negative) -- `tecAMM_EMPTY`: AMM has zero LP tokens outstanding -- `tecAMM_BALANCE`: - - Withdrawal amount (`Amount` or `Amount2`) exceeds pool balance - - Account has zero LP tokens -- `tecNO_LINE`: `Asset` or `Asset2` issuer has `lsfRequireAuth` flag set, but account has no trust line with the issuer -- `tecNO_AUTH`: `Asset` or `Asset2` issuer has `lsfRequireAuth` flag set, and the trust line exists but lacks authorization (missing `lsfLowAuth` or `lsfHighAuth` flag) -- `tecFROZEN` (IOU/XRP) or `tecLOCKED` (MPT): `Asset` or `Asset2` is frozen/locked (AMM account, currency/issuance, or withdrawer account). Under the `fixCleanup3_3_0` amendment, the conditions producing these codes change: - - withdrawal is always allowed when the withdrawer is the asset's issuer - - a regular individual freeze on the withdrawer's own holding no longer blocks it, only a deep freeze does - - an issuer withdrawing its own frozen token reads the pool balance ignoring the freeze -- `temBAD_AMM_TOKENS`: - - `LPTokenIn` issue (currency code + issuer) does not match the AMM's LP token issue - - `EPrice` issue does not match the AMM's LP token issue -- `tecAMM_INVALID_TOKENS`: LP token redemption amount (`LPTokenIn`) exceeds account's LP token holdings -- MPT-specific validations (for either `Asset` or `Asset2` if MPT): Both assets are validated using [`canMPTTradeAndTransfer`](../mpts/README.md#363-canmpttradeandtransfer). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for validation logic and error conditions. - -**Validation during doApply**[^ammwithdraw-doapply-validation] - -[^ammwithdraw-doapply-validation]: Validation during doApply: [`AMMWithdraw.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L336-L462) - -- With [fixAMMv1_1](https://xrpl.org/resources/known-amendments#fixammv1_1): `tecAMM_INVALID_TOKENS`: LP token balance adjustment failed. When the withdrawer is the only remaining LP, if their LP token balance differs from the AMM's `LPTokenBalance` by more than 0.1%, the withdrawal fails. If the difference is within 0.1%, the AMM's `LPTokenBalance` is adjusted to match the account's balance to allow full withdrawal despite rounding errors. -- `tecINTERNAL`: AMM ledger entry does not exist (should not happen if preclaim succeeded) -- `tecAMM_BALANCE`: - - Withdrawing one side of the pool (one asset amount equals pool balance but the other doesn't) - - Withdrawing all LP tokens but not all assets - - Withdrawal amount exceeds current pool balance -- `tecAMM_FAILED`: Withdrawal constraints not satisfied (calculated withdrawal amounts don't meet minimum requirements specified in transaction fields). Under `fixCleanup3_3_0`, the `singleWithdrawEPrice` mode also fails with this code when its formula's denominator is exactly zero. Without the amendment that division throws and the transaction fails with `tefEXCEPTION` -- `tecPRECISION_LOSS`: (with both `fixCleanup3_3_0` and [fixAMMv1_3](https://xrpl.org/resources/known-amendments#fixammv1_3)) the pool product invariant fails after computing the new LP token balance. Without `fixCleanup3_3_0` the same situations are rejected by the `ValidAMM` invariant checker with `tecINVARIANT_FAILED` -- `tecAMM_INVALID_TOKENS`: Calculated LP tokens or withdrawal amounts are zero or invalid -- `tecINSUFFICIENT_RESERVE`: (With [fixAMMv1_2](https://xrpl.org/resources/known-amendments#fixammv1_2)) Insufficient XRP reserve to create trust line for withdrawn token that the account doesn't currently hold -- `tecINCOMPLETE`: Withdrawal empties the pool (all LP tokens redeemed) but AMM account deletion is incomplete due to too many trust lines to delete in a single transaction. The withdrawal succeeds, but the AMM account cleanup must be completed with subsequent AMMDelete transactions. Limited to deleting `kMaxDeletableAmmTrustLines` trust lines per transaction. -- Propagate errors from `accountSend()` when transferring assets from AMM account to withdrawer (see [Common Error Codes from accountSend()](#common-error-codes-from-accountsend)) - -### 3.3.3. State Changes - -- `AMM` object is **modified**: - - `LPTokenBalance`: Decreased by redeemed LP tokens - - May be **deleted** if balance becomes zero (see AMMDelete) - -- `AMM` object is **deleted** (if LPTokenBalance becomes zero and all trust lines can be deleted): - - AMM pseudo-account deleted - - All trust lines deleted (up to `kMaxDeletableAmmTrustLines` per transaction) - - Owner directory entries removed - - **Note:** If deletion is incomplete due to too many trust lines (`tecINCOMPLETE` returned), the AMM object and pseudo-account remain in the ledger with zero LP tokens. Subsequent `AMMDelete` transactions are needed to complete cleanup. - -- AMM account balances are **modified**: - - Assets transferred from AMM account to withdrawer - - Balances updated in `AccountRoot` (XRP), `RippleState` (tokens), or `MPToken` entries (MPTs) - -- LP tokens are **redeemed**: - - LP tokens burned (trust line balance decreased) - - Trust line may be deleted if balance becomes zero and all parameters are default - -- Withdrawer's `AccountRoot` is **modified**: - - `Balance`: Increased by XRP withdrawn (if applicable) - - `OwnerCount`: Decremented if LP token trust line deleted - - `OwnerCount`: Incremented if new trust line created for withdrawn token (with fixAMMv1_2) - -## 3.4. AMMVote Transaction - -The `AMMVote` transaction allows LP token holders to vote on the AMM's trading fee. - -**Fields:** - -| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | -|------------|:---------:|:-----------:|:---------:|:-------------:|:-------------:|:------------| -| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMVote"` | -| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account casting the vote | -| `Asset` | :heavy_check_mark: | `No` | `Object` | `Issue` | | One of the pool's assets | -| `Asset2` | :heavy_check_mark: | `No` | `Object` | `Issue` | | The other pool asset | -| `TradingFee` | :heavy_check_mark: | `No` | `Number` | `UInt16` | | Proposed trading fee (0-1000, 1 = 0.001%) | -| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags (must be 0 for AMMVote, only universal flags allowed) | - -### 3.4.1. Failure Conditions - -**Static validation**[^ammvote-static-validation] - -[^ammvote-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L32-L39), [`preflight`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L42-L57) - -- `temDISABLED`: - - AMM amendment not enabled - - either `Asset` or `Asset2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled -- `temINVALID_FLAG`: Invalid transaction flags (any flags set other than universal flags) -- `temBAD_AMM_TOKENS`: `Asset` and `Asset2` have the same currency and issuer -- `temBAD_CURRENCY`: `Asset` or `Asset2` uses the disallowed 3-letter "XRP" currency code -- `temBAD_ISSUER`: `Asset` or `Asset2` is XRP (currency is all zeros) but has a non-zero issuer account -- `temBAD_FEE`: `TradingFee` exceeds 1000 - -**Validation against the ledger view**[^ammvote-preclaim-validation] - -[^ammvote-preclaim-validation]: Validation against ledger view (preclaim): [`AMMVote.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L60-L80) - -- `terNO_AMM`: AMM ledger entry does not exist for specified asset pair -- `tecAMM_EMPTY`: AMM has zero LP tokens outstanding -- `tecAMM_INVALID_TOKENS`: Account holds zero LP tokens (not an LP) - -**Validation during doApply**[^ammvote-doapply-validation] - -[^ammvote-doapply-validation]: Validation during doApply: [`AMMVote.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L81-L232) - -- `tecINTERNAL`: AMM ledger entry does not exist (should not happen if preclaim succeeded) - -### 3.4.2. State Changes - -- `AMM` object is **modified**: - - `VoteSlots`: Updated with new/modified vote entry - - Vote slots for accounts with zero LP tokens are **removed** - - If account already has a vote: Update fee and recalculate weight - - If account doesn't have a vote: - - If fewer than 8 votes: Add new vote - - If 8 votes exist: Replace vote with smallest LP balance (if new vote has more) - - `TradingFee`: Recalculated as weighted average of all votes: - ``` - TradingFee = SUM(VoteFee_i * LPTokens_i) / SUM(LPTokens_i) - ``` - - If the calculated fee is non-zero, the `TradingFee` field is set - - If the calculated fee is zero, the `TradingFee` field is removed (made absent) - - `AuctionSlot.DiscountedFee`: Updated based on the new trading fee (if `AuctionSlot` exists) - - If `TradingFee` is non-zero and `TradingFee / 10` is non-zero, set to `TradingFee / 10` - - Otherwise, the `DiscountedFee` field is removed (made absent) - -## 3.5. AMMBid Transaction - -The `AMMBid` transaction allows LP token holders to bid for the AMM's 24-hour auction slot. - -**Fields:** - -| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | -|------------|:---------:|:-----------:|:---------:|:-------------:|:-------------:|:------------| -| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMBid"` | -| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account bidding for the auction slot | -| `Asset` | :heavy_check_mark: | `No` | `Object` | `Issue` | | One of the pool's assets | -| `Asset2` | :heavy_check_mark: | `No` | `Object` | `Issue` | | The other pool asset | -| `BidMin` | | `No` | `String` or `Object` | `Amount` | | Minimum slot price willing to pay (in LP tokens) | -| `BidMax` | | `No` | `String` or `Object` | `Amount` | | Maximum slot price willing to pay (in LP tokens) | -| `AuthAccounts` | | `No` | `Array` | `Array` | | Array of up to 4 accounts to authorize for discounted fee | -| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags (must be 0 for AMMBid, only universal flags allowed) | - - -See [Bidding documentation](bidding.md) for more details. - -### 3.5.1. Failure Conditions - -**Static validation**[^ammbid-static-validation] - -[^ammbid-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMBid.cpp#L38-L48), [`preflight`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMBid.cpp#L51-L103) - -- `temDISABLED`: - - AMM amendment not enabled - - either `Asset` or `Asset2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled -- `temINVALID_FLAG`: Invalid transaction flags (any flags set other than universal flags) -- `temBAD_AMM_TOKENS`: `Asset` and `Asset2` have the same currency and issuer -- `temBAD_CURRENCY`: `Asset`, `Asset2`, `BidMin`, or `BidMax` uses the disallowed 3-letter "XRP" currency code (`0x5852500000000000`) -- `temBAD_ISSUER`: `Asset`, `Asset2`, `BidMin`, or `BidMax` is XRP (currency is all zeros) but has a non-zero issuer account -- `temBAD_AMOUNT`: `BidMin` or `BidMax` is negative or zero -- `temMALFORMED`: - - More than 4 accounts in `AuthAccounts` - - (With [fixAMMv1_3](https://xrpl.org/resources/known-amendments#fixammv1_3)) `AuthAccounts` contains the bidder account or duplicate accounts - -**Validation against the ledger view**[^ammbid-preclaim-validation] - -[^ammbid-preclaim-validation]: Validation against ledger view (preclaim): [`AMMBid.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMBid.cpp#L106-L177) - -- `terNO_AMM`: AMM ledger entry does not exist for specified asset pair -- `tecAMM_EMPTY`: AMM has zero LP tokens outstanding -- `terNO_ACCOUNT`: Any account in `AuthAccounts` does not exist -- `temBAD_AMM_TOKENS`: `BidMin` or `BidMax` issue (currency code + issuer) does not match the AMM's LP token issue -- `tecAMM_INVALID_TOKENS`: - - Account holds zero LP tokens (not an LP) - - `BidMin` or `BidMax` is greater than the account's LP token holdings, or greater than or equal to the AMM's total LP token balance - - `BidMin` > `BidMax` - -**Validation during doApply**[^ammbid-doapply-validation] - -[^ammbid-doapply-validation]: Validation during doApply: [`AMMBid.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMBid.cpp#L179-L355) - -- `tecAMM_FAILED`: Computed price exceeds `BidMax` -- `tecAMM_INVALID_TOKENS`: Pay price exceeds LP token holdings -- Propagate errors from `accountSend()` when transferring LP tokens between bidder, previous holder, and AMM account (see [Common Error Codes from accountSend()](#common-error-codes-from-accountsend)) - -### 3.5.2. State Changes - -The AMMBid transaction executes through the `applyBid()` function, which determines the slot price based on whether someone currently owns the auction slot and how much time has elapsed. For an unowned or expired slot, the bidder pays only the minimum price. For an owned slot, the price includes a 5% markup with a decay function over the 24-hour period. The system refunds the previous slot holder proportionally to their remaining time and burns the difference (bid price minus refund). State changes only occur when the bid execution succeeds. If validation fails (e.g., computed price exceeds `BidMax`, insufficient LP tokens), no ledger modifications are made. See [Bidding documentation](bidding.md) for the complete bidding logic including price calculation, refund mechanism, and LP token burning. - -- `AMM` object is **modified**: - - `AuctionSlot`: - - `Account`: Set to bidder - - `Expiration`: Set to current time + 86,400 seconds - - `Price`: Set to amount paid - - `DiscountedFee`: Set to `TradingFee / 10` when that quotient is non-zero; otherwise the field is removed (made absent) - - `AuthAccounts`: Set to specified accounts (or cleared if not specified) - - `LPTokenBalance`: Decreased by burned amount - -- LP tokens are **burned**: - - Bid amount (minus refund) burned from bidder's LP token balance - - Reduces total LP token supply - -- Previous slot holder receives **refund** (if slot not expired): - - Refund = `(1 - fractionUsed) * PricePaid` - - Sent as LP tokens from bidder to previous holder - -## 3.6. AMMDelete Transaction - -The `AMMDelete` transaction is used to clean up AMM instances that have been emptied (all LP tokens withdrawn). While the AMM can be automatically deleted when the last LP token is withdrawn, this transaction provides an explicit way to delete empty AMMs, especially useful when automatic deletion is incomplete. - -**Fields:** - -| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | -|------------|:---------:|:-----------:|:---------:|:-------------:|:-------------:|:------------| -| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMDelete"` | -| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account deleting the AMM instance | -| `Asset` | :heavy_check_mark: | `No` | `Object` | `Issue` | | One of the pool's assets | -| `Asset2` | :heavy_check_mark: | `No` | `Object` | `Issue` | | The other pool asset | -| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags (must be 0 for AMMDelete, only universal flags allowed) | - -### 3.6.1. Failure Conditions - -**Static validation**[^ammdelete-static-validation] - -[^ammdelete-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMDelete.cpp#L23-L30), [`preflight`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMDelete.cpp#L33-L36) - -- `temDISABLED`: - - AMM amendment not enabled - - either `Asset` or `Asset2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled -- `temINVALID_FLAG`: Invalid transaction flags (any flags set other than universal flags) - -**Validation against the ledger view**[^ammdelete-preclaim-validation] - -[^ammdelete-preclaim-validation]: Validation against ledger view (preclaim): [`AMMDelete.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMDelete.cpp#L39-L53) - -- `terNO_AMM`: AMM ledger entry does not exist for specified asset pair -- `tecAMM_NOT_EMPTY`: AMM has non-zero LP tokens outstanding (AMM must be empty to delete) - -**Validation during doApply**[^ammdelete-doapply-validation] - -[^ammdelete-doapply-validation]: Validation during doApply: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L713-L766) - -- `tecINTERNAL`: - - AMM ledger entry does not exist (should not happen if preclaim succeeded) - - AMM pseudo-account does not exist (should not happen if AMM entry exists) - - Directory node has invalid index during trustline deletion - - Non-trustline/non-MPToken ledger entry found in AMM owner directory (should only contain trust lines, MPTokens, and AMM entry) - - Trustline has non-zero balance during deletion (all trust lines should have zero balance if AMM is empty) - - Failed to remove AMM entry from owner directory - - Cannot delete root directory node -- `tecINCOMPLETE`: Too many trust lines to delete in a single transaction (limited by `kMaxDeletableAmmTrustLines`). The transaction should be called again to continue deletion. This is not an error - it indicates partial success. -- Propagate errors from `deleteAMMTrustLine()` when deleting individual trust lines: - - `tecINTERNAL`: Trust line SLE is null or has wrong type - - `tefBAD_LEDGER`: Failed to remove directory link during trust line deletion - -### 3.6.2. State Changes - -The AMMDelete transaction cleans up an empty AMM instance. The deletion process may complete in a single transaction or require multiple transactions if there are many trust lines. - -**Complete deletion (tesSUCCESS):** - -- `RippleState` objects (trust lines) are **deleted**: - - All trust lines associated with the AMM pseudo-account are removed - - This includes LP token trust lines and IOU asset trust lines - - Each trust line must have zero balance - - The counterparty (non-AMM) side of the trust line has its `OwnerCount` decremented - - Directory entries for each trust line are removed from both accounts' owner directories - - Limited to `kMaxDeletableAmmTrustLines` trust lines per transaction - -- `MPToken` objects are **deleted** (if AMM uses MPT assets): - - All MPToken entries associated with the AMM pseudo-account are removed - - Each MPToken must have zero `MPTAmount` and zero `LockedAmount` - - At most two MPToken objects (one per asset) - - Each MPToken is removed from the AMM pseudo-account's owner directory and erased; no `OwnerCount` is adjusted - - MPTokens are only deleted after all trust lines are deleted - -- `AMM` object is **deleted**: - - The AMM ledger entry is removed from the ledger - - The entry is removed from the AMM pseudo-account's owner directory - -- `AccountRoot` object (AMM pseudo-account) is **deleted**: - - The AMM pseudo-account is removed from the ledger - - Any remaining XRP balance should be zero (or minimal dust) - - The account's owner directory is removed - -- `DirectoryNode` objects are **deleted**: - - The AMM pseudo-account's owner directory is removed - - All directory links are cleaned up - -**Partial deletion (tecINCOMPLETE):** - -When there are too many trust lines to delete in a single transaction: - -- `RippleState` objects are **partially deleted**: - - Up to `kMaxDeletableAmmTrustLines` trust lines are deleted - - Remaining trust lines stay in the ledger - - Each deleted trust line decrements the counterparty account's `OwnerCount` - -- `MPToken` objects **remain** in the ledger: - - MPToken entries are not deleted during partial deletion - - MPTokens are only deleted after all trust lines are deleted - - This ensures AMM can be re-created with AMMDeposit if needed - -- `AMM` object is **deleted unless** there are remaining trust lines or MPTokens: - - When deletion is incomplete, the AMM object remains in the ledger - - Still has `LPTokenBalance` of zero - - Still references the pseudo-account - -- `AccountRoot` object (AMM pseudo-account) is **deleted unless** there are remaining trust lines or MPTokens: - - When deletion is incomplete, the pseudo-account remains in the ledger - - Owner directory still contains remaining trust lines and MPTokens (if present) - -- **Subsequent AMMDelete transactions** must be submitted: - - Each transaction deletes up to `kMaxDeletableAmmTrustLines` more trust lines - - Process continues until all trust lines are deleted - - Final transaction completes the full deletion (returns tesSUCCESS) - -**Note:** The `kMaxDeletableAmmTrustLines` limit exists to prevent transactions from consuming excessive resources. AMMs with many LPs (and therefore many LP token trust lines) will require multiple AMMDelete transactions to fully clean up. - -The deletion process: -1. Verifies the AMM exists and is empty (zero LP tokens) -2. Deletes all trust lines associated with the AMM account -3. Removes the AMM from owner directories -4. Deletes the AMM pseudo-account -5. Deletes the AMM ledger entry - -If there are too many trust lines to delete in a single transaction (limited by `kMaxDeletableAmmTrustLines`), the transaction returns `tecINCOMPLETE` and must be called again. - -## 3.7. AMMClawback Transaction - -The `AMMClawback` transaction allows asset issuers to claw back their issued assets from AMM liquidity pools by withdrawing them from a specific LP token holder's position. This transaction is only available when the [AMMClawback](https://xrpl.org/resources/known-amendments#ammclawback) amendment is enabled. - -Unlike the regular [Clawback transaction](../trust_lines/README.md#312-clawback-transaction) which claws back trust line tokens and [MPTs](../mpts/README.md#35-clawback-transaction-with-mpts) from individual holder balances, `AMMClawback` targets assets held in AMM liquidity pools. The issuer specifies an LP token holder, and the transaction withdraws the issuer's assets from the pool proportionally to that holder's LP token position, burning the corresponding LP tokens. - -**How it works:** - -The issuer identifies a holder who possesses LP tokens for the AMM pool. The transaction executes a proportional withdrawal from the pool, with the mechanics varying based on whether an explicit amount is specified. - -When no `Amount` is provided, the transaction burns all of the holder's LP tokens and withdraws both pool assets proportionally using the AMM's equal-withdrawal formula. The issuer's asset (`Asset`) is immediately clawed back - transferred from the pool to the issuer where it is effectively removed from circulation. The second asset (`Asset2`), if not issued by the same issuer or if the `tfClawTwoAssets` flag is not set, is transferred to the holder rather than being clawed back. - -When an `Amount` is specified, the transaction calculates the fraction of the pool that corresponds to the requested amount of the issuer's asset. It determines the number of LP tokens required to withdraw that precise amount, accounting for the current pool ratio. If the calculated LP tokens exceed the holder's balance, the transaction instead burns all available LP tokens and withdraws proportionally. Otherwise, it burns only the calculated LP tokens and withdraws both assets proportionally from the pool. - -If the `tfClawTwoAssets` flag is set - which requires the issuer to issue both pool assets - the second asset is also clawed back. Without this flag, the second asset remains with the holder, leaving them with that asset while the issuer's asset is removed from circulation. The withdrawal from the AMM pool ignores freeze and authorization restrictions (`FreezeHandling::IgnoreFreeze` and `AuthHandling::IgnoreAuth`), ensuring clawback operations succeed even when assets are frozen or the holder lacks authorization. The subsequent transfer from holder to issuer uses standard clawback mechanics, which also bypasses authorization and freeze checks. - -**Fields:** - -| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | -|-------------------|:------------------:|:-----------:|:--------------------------:|:-------------:|:-------------:|:------------------------------------------------------------------------------------------| -| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMClawback"` | -| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Issuer account (must be issuer of `Asset`) | -| `Asset` | :heavy_check_mark: | `No` | `Object` | `Issue` | | Asset to claw back (issuer must match `Account`) | -| `Asset2` | :heavy_check_mark: | `No` | `Object` | `Issue` | | The other asset in the AMM pool | -| `Holder` | :heavy_check_mark: | `No` | `String` | `AccountID` | | LP token holder whose position is being clawed back | -| `Amount` | | `No` | `String - Currency Amount` | `Amount` | | Specific amount of `Asset` to claw back (if omitted, claws back holder's entire position) | -| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags (see below) | - -**Transaction Flags:** - -| Flag Name | Hex Value | Description | -|-------------------|--------------|---------------------------------------------------------------------------------| -| `tfClawTwoAssets` | `0x00000001` | Claw back both assets (only valid when issuer issues both `Asset` and `Asset2`) | - -**Clawback mechanics:** - -The transaction uses AMM withdrawal logic internally: -- Calculates proportional amounts using the preservation function -- Burns LP tokens from the holder -- Transfers withdrawn assets from AMM pool to issuer -- For trust line tokens: Assets are burned (balance adjusts on shared RippleState) -- For MPTs: Assets are burned (holder's `MPTAmount` decreases, issuance's `OutstandingAmount` decreases) - -### 3.7.1. Failure Conditions - -**Static validation**[^ammclawback-static-validation] - -[^ammclawback-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L43-L53), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L37-L40), [`preflight`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L56-L99) - -- `temDISABLED`: - - [AMMClawback](https://xrpl.org/resources/known-amendments#ammclawback) amendment not enabled - - Either `Asset` or `Asset2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment not enabled -- `temMALFORMED`: - - `Account` equals `Holder` (cannot claw back from self) - - `Asset` is XRP (XRP cannot be clawed back) - - `Asset.issuer` does not match `Account` (issuer must match transaction sender) -- `temBAD_AMOUNT`: - - `Amount` is specified but `Amount.asset` does not match `Asset` - - `Amount` is zero or negative -- `temINVALID_FLAG`: - - `tfClawTwoAssets` is set but `Asset.issuer` differs from `Asset2.issuer` (can only claw both assets if issuer issues both) - - Invalid flags specified - -**Validation against the ledger view**[^ammclawback-preclaim-validation] - -[^ammclawback-preclaim-validation]: Validation against ledger view (preclaim): [`AMMClawback.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L101-L153) - -- `terNO_ACCOUNT`: Issuer account or holder account does not exist -- `terNO_AMM`: AMM pool does not exist for the specified asset pair -- `tecNO_PERMISSION`: - - For trust line tokens (`Asset` is `Issue`): - - Issuer does not have `lsfAllowTrustLineClawback` flag set - - Issuer has `lsfNoFreeze` flag set - - For MPTs (`Asset` is `MPTIssue`): - - MPT issuance does not have `lsfMPTCanClawback` flag set - - `Asset.issuer` does not match the MPT issuance's issuer - - With `tfClawTwoAssets`: `Asset2` does not meet the clawback requirements above - -**Validation during doApply**[^ammclawback-doapply-validation] - -[^ammclawback-doapply-validation]: Validation during doApply: [`AMMClawback.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L168-L300) - -- `tecINTERNAL`: - - AMM ledger entry does not exist - - AMM pseudo-account does not exist - - With [fixAMMClawbackRounding](https://xrpl.org/resources/known-amendments#fixammclawbackrounding): LP token balance verification encountered internal error when checking if holder is the only LP -- `tecAMM_BALANCE`: Holder has zero LP tokens (nothing to claw back) -- `tecAMM_INVALID_TOKENS`: - - With [fixAMMClawbackRounding](https://xrpl.org/resources/known-amendments#fixammclawbackrounding): Holder is the only remaining LP and their LP token balance differs from the AMM's `LPTokenBalance` by more than 0.1% - - Calculated LP token amount during withdrawal is zero or invalid - - LP token balance adjustment failed during withdrawal -- `tecPRECISION_LOSS`: (with both `fixCleanup3_3_0` and [fixAMMv1_3](https://xrpl.org/resources/known-amendments#fixammv1_3)) the pool product invariant fails after computing the new LP token balance, the same check as in AMMWithdraw -- Propagate errors from withdrawal logic (uses `AMMWithdraw::equalWithdrawTokens` or `equalWithdrawMatchingOneAmount`): - - `tecAMM_FAILED`: Withdrawal constraints not satisfied - - Other withdrawal-related errors (see [AMMWithdraw Failure Conditions](#332-failure-conditions)) - -### 3.7.2. State Changes - -The `AMMClawback` transaction withdraws assets from an AMM pool by burning LP tokens from the holder's balance. - -**LP Token Changes:** - -- Holder's LP token balance is **decreased**: - - LP tokens are burned (destroyed from circulation) - - The amount burned equals either: - - All of holder's LP tokens (if `Amount` not specified) - - Proportional LP tokens to withdraw the specified `Amount` - - If holder's LP token balance reaches zero and the trust line has no other non-default fields, the trust line may be deleted - - Holder's `OwnerCount` may decrement if trust line is deleted - -**AMM Ledger Entry Changes:** - -- `AMM` object is **modified**: - - `LPTokenBalance`: Decreased by the burned LP tokens - - If `LPTokenBalance` reaches zero, the AMM may be automatically deleted (see [AMMDelete](#36-ammdelete-transaction)) - -**Pool Asset Changes:** - -- AMM pseudo-account's asset balances are **decreased**: - - For trust line tokens (`RippleState` balance adjusted) - - For MPTs (`MPToken.MPTAmount` decreased) - - For XRP (`AccountRoot.Balance` decreased) - - Amounts withdrawn are proportional based on burned LP tokens and current pool balances - -**Asset Distribution:** - -- **`Asset` (always clawed back)**: - - For trust line tokens: Transferred from holder to issuer via `directSendNoFee`, adjusting the shared `RippleState` balance - - For MPTs: Burned from holder's `MPToken` (decreases holder's `MPTAmount` and issuance's `OutstandingAmount`) - -- **`Asset2` (conditionally clawed back)**: - - **With `tfClawTwoAssets`**: Same treatment as `Asset` (transferred to issuer and burned) - - **Without `tfClawTwoAssets`**: Remains with the holder (transferred from AMM pool to holder's balance) \ No newline at end of file +# Index + +- [1. Introduction](#1-introduction) + - [1.1. Liquidity Pool Mechanics](#11-liquidity-pool-mechanics) + - [1.1.1. Weighted Geometric Mean](#111-weighted-geometric-mean) + - [1.1.2. Slippage](#112-slippage) + - [1.1.3. LP Tokens](#113-lp-tokens) + - [1.1.4. Effective Price](#114-effective-price) + - [1.2. Trading Fee](#12-trading-fee) + - [1.2.1. Auction Slot](#121-auction-slot) + - [1.2.2. Fee Voting](#122-fee-voting) +- [2. Ledger Entries](#2-ledger-entries) + - [2.1. AMM Ledger Entry](#21-amm-ledger-entry) + - [2.1.1. Object Identifier](#211-object-identifier) + - [2.1.2. Fields](#212-fields) + - [2.1.2.1. VoteSlots](#2121-voteslots) + - [2.1.2.2. AuctionSlot](#2122-auctionslot) + - [2.1.3. Pseudo-accounts](#213-pseudo-accounts) + - [2.1.3.1. Account ID Generation](#2131-account-id-generation) + - [2.1.4. Reserves](#214-reserves) + - [2.2. RippleState Ledger Entry](#22-ripplestate-ledger-entry) + - [2.3. MPToken Ledger Entry](#23-mptoken-ledger-entry) +- [3. Transactions](#3-transactions) + - [3.1. AMMCreate Transaction](#31-ammcreate-transaction) + - [3.1.1. Failure Conditions](#311-failure-conditions) + - [3.1.2. State Changes](#312-state-changes) + - [3.2. AMMDeposit Transaction](#32-ammdeposit-transaction) + - [3.2.1. Deposit Modes](#321-deposit-modes) + - [3.2.2. Failure Conditions](#322-failure-conditions) + - [3.2.3. State Changes](#323-state-changes) + - [3.3. AMMWithdraw Transaction](#33-ammwithdraw-transaction) + - [3.3.1. Withdrawal Modes](#331-withdrawal-modes) + - [3.3.2. Failure Conditions](#332-failure-conditions) + - [3.3.3. State Changes](#333-state-changes) + - [3.4. AMMVote Transaction](#34-ammvote-transaction) + - [3.4.1. Failure Conditions](#341-failure-conditions) + - [3.4.2. State Changes](#342-state-changes) + - [3.5. AMMBid Transaction](#35-ammbid-transaction) + - [3.5.1. Failure Conditions](#351-failure-conditions) + - [3.5.2. State Changes](#352-state-changes) + - [3.6. AMMDelete Transaction](#36-ammdelete-transaction) + - [3.6.1. Failure Conditions](#361-failure-conditions) + - [3.6.2. State Changes](#362-state-changes) + - [3.7. AMMClawback Transaction](#37-ammclawback-transaction) + - [3.7.1. Failure Conditions](#371-failure-conditions) + - [3.7.2. State Changes](#372-state-changes) + +# 1. Introduction + +The XRP Ledger decentralized exchange provides asset exchange liquidity through two mechanisms: [limit order books](../glossary.md#clob) and automated market makers (AMM). AMMs are liquidity pools that use algorithmic pricing to enable asset swaps without relying on discrete offers. + +XRPL implements AMMs with the following characteristics: + +- **Geometric mean market maker (GM3)**: Pools use a weighted geometric mean conservation function to algorithmically determine exchange rates based on pool balances and fees +- **Continuous auction mechanism**: AMM instances auction a 24-hour slot with discounted trading fees +- **Votable trading fee**: LP token holders vote on the trading fee charged by the AMM instance, weighted by their LP token balance +- **LOB integration**: The [Flow payment engine](../flow/README.md) processes AMM liquidity and order book offers together, consuming liquidity from both sources in quality order + +An AMM instance is represented on-ledger by: +- An `AMM` ledger entry storing pool parameters, trading fee, vote slots, and auction slot state +- An `AccountRoot` ledger entry (pseudo-account) holding the pool's XRP balance +- `RippleState` trust lines for IOU balances and LP tokens +- `MPToken` ledger entries for MPT balances (when applicable) + +The AMM manages a liquidity pool containing two assets (any combination of [XRP](../glossary.md#xrp), [IOUs](../glossary.md#iou), or [MPTs](../mpts/README.md)) and issues LP tokens representing proportional ownership of the pool. + +AMMs integrate with the [BookStep](../flow/steps.md#5-bookstep) of the Flow engine. During payment execution or offer crossing, BookStep generates synthetic offers from the AMM based on the current pool state and compares their quality against order book offers. The callback in `revImp` or `fwdImp` consumes whichever source provides better quality, updating either the AMM pool balances or order book entries accordingly. + +## 1.1. Liquidity Pool Mechanics + +### 1.1.1. Weighted Geometric Mean + +The AMM uses a conservation function based on weighted geometric mean: + +``` +C = Γ_A^W_A * Γ_B^W_B +``` + +Where: +- `Γ_A` = current balance of asset A in the AMM instance pool +- `Γ_B` = current balance of asset B in the AMM instance pool +- `W_A` = weight of asset A +- `W_B` = weight of asset B +- `C` = conservation function value + +For XRPL AMMs, `W_A = W_B = 0.5` (equal weights). + +The conservation function C remains constant during swaps (payments). Deposits and withdrawals change C as they add or remove liquidity from the pool. + +When a trader swaps assets, they add one asset to the pool and remove the other, maintaining C (before accounting for trading fees). + +For example, if a trader wants to buy asset A from the pool: +- They deposit asset B into the pool (increasing `Γ_B`) +- They receive asset A from the pool (decreasing `Γ_A`) +- The conservation function C remains constant (approximately, accounting for fees) +- Because `Γ_A` decreases while `Γ_B` increases, the ratio `Γ_B / Γ_A` increases +- This means the next trader will get fewer A assets per B asset (the price of A has increased) + +### 1.1.2. Slippage + +When swapping assets, the actual exchange rate differs from the spot price due to slippage. Note that "slippage" in XRPL terminology differs from the standard financial definition. Standard slippage refers to the difference between expected and execution price due to market movement or insufficient liquidity - an unintended outcome. In XRPL AMMs, slippage is the intentional and deterministic price degradation that results from the conservation function as larger trades shift the pool's asset ratio. + +The **spot price** is the weighted ratio of pool balances representing the exchange rate for an infinitesimally small trade: + +``` +SpotPrice(A) = (Γ_B / W_B) / (Γ_A / W_A) * 1/(1-TFee) +``` + +`TFee` is trading fee as a fraction (fee units / 100,000; see [Trading Fee](#12-trading-fee)). + +For equal weights (W_A = W_B = 0.5), this simplifies to: + +``` +SpotPrice(A) = Γ_B / Γ_A * 1/(1-TFee) +``` + +The **actual exchange rate** of a trade is the ratio of assets actually exchanged: + +``` +ActualExchangeRate(A) = Δ_B / Δ_A +``` + +Where Δ_B is the amount of asset B swapped in and Δ_A is the amount of asset A received. + +**Slippage** is the percentage change in the actual exchange rate relative to the pre-swap spot price. Larger swaps move the pool balances more significantly, resulting in progressively worse exchange rates. + +See [Swap Formulas (helpers.md)](helpers.md#31-swap-formulas) for the detailed formulas that calculate swap amounts, and [Slippage and Quality Degradation (helpers.md)](helpers.md#313-slippage-and-quality-degradation) for more on slippage behavior. + +### 1.1.3. LP Tokens + +**Terminology:** + +- **Liquidity Provider (LP)**: An account that has deposited assets into an AMM pool and holds LP tokens. Also called "LPs" collectively. +- **LP Tokens**: IOUs representing proportional ownership of an AMM pool's assets. LP tokens are issued by the AMM pseudo-account and tracked via trust lines. See [section 2.1.3](#213-pseudo-accounts) for details on LP token currency codes. +- **Issuing**: When an account deposits assets into the pool, the AMM increases the balance on the LP token trust line between the account and the AMM pseudo-account (the issuer). This increases the total LP token supply. +- **Redeeming**: When an LP withdraws assets from the pool, they redeem LP tokens by decreasing the balance on their LP token trust line with the AMM. This reduces the account's holdings and decreases the total LP token supply. +- **Outstanding LP Tokens**: The total number of LP tokens currently in circulation (held by all LPs), tracked in the AMM ledger entry's `LPTokenBalance` field. +- **LP Token Holdings**: The amount of LP tokens that a specific account holds, which determines their proportional share of the pool. + +Liquidity providers deposit assets into the AMM pool and receive LP tokens in return. These LP tokens represent proportional ownership of the pool's assets and can later be redeemed to withdraw assets from the pool. + +**Initial LP Token Calculation:** + +When creating an AMM pool with initial deposits `A` and `B`:[^amm-lp-tokens-calc] + +``` +LPTokens = SQRT(A * B) +``` + +This formula uses the geometric mean of the pool balances to calculate the initial LP token supply. + +**Subsequent Deposits (Issuing LP Tokens):** + +When depositing both assets proportionally: + +``` +LPTokensIssued = (Δ_A / Γ_A) * TotalLPTokens + +where Δ_A and Δ_B must satisfy: Δ_A / Γ_A = Δ_B / Γ_B +``` + +The system increases the LP token balance on the depositor's trust line with the AMM. The total outstanding LP tokens increase. + +**Withdrawals (Redeeming LP Tokens):** + +When withdrawing both assets proportionally: + +``` +Δ_A = (LPTokensRedeemed / TotalLPTokens) * Γ_A +Δ_B = (LPTokensRedeemed / TotalLPTokens) * Γ_B +``` + +The system reduces the balance on the LP's trust line with the AMM by the redeemed amount. The total outstanding LP tokens decrease by the amount redeemed. + +**Example:** + +If Alice creates an AMM with 100 EUR and 1000 USD: +- Initial LP tokens issued = SQRT(100 * 1000) = 316.227766... LP tokens +- Alice receives ~316.23 LP tokens representing 100% ownership +- Total outstanding LP tokens = 316.23 +- If Bob later deposits 10 EUR and 100 USD (same ratio), he receives ~31.62 newly issued LP tokens +- Total outstanding LP tokens = ~347.85 +- Alice holds ~316.23 LP tokens (~90.9% of the pool) +- Bob holds ~31.62 LP tokens (~9.1% of the pool) +- If Alice later redeems 100 LP tokens to withdraw assets, she receives both EUR and USD proportional to her redeemed LP tokens, and the balance on her LP token trust line decreases by 100 +- Total outstanding LP tokens = ~247.85 +- Alice now holds ~216.23 LP tokens (~87.2% of the pool) + +**Single-Asset Deposits and Withdrawals:** + +The formulas above apply to **proportional** deposits and withdrawals, where both pool assets are added or removed in the same ratio as the pool. + +AMMs also support **single-asset** operations, where only one asset is deposited or withdrawn: + +- **Single-Asset Deposits**: When depositing only one asset (e.g., only asset A into an A/B pool), only that asset's pool balance increases. This creates an imbalance in the pool ratio. The depositor receives fewer LP tokens than they would for a proportional deposit of the same value, because the trading fee is applied to account for the imbalance created. + +- **Single-Asset Withdrawals**: When withdrawing only one asset, only that asset's pool balance decreases, creating an imbalance. The withdrawer must redeem more LP tokens than they would for a proportional withdrawal, with the trading fee applied to account for the imbalance. + +The specific formulas for single-asset operations are more complex and involve the trading fee. See [Deposit Formulas](helpers.md#32-deposit-formulas) and [Withdrawal Formulas](helpers.md#33-withdrawal-formulas) for the mathematical details. + +**Example: Proportional Deposit** + +Alice creates an AMM with 100 USD and 100 EUR: +- Initial LP tokens: SQRT(100 * 100) = 100 LP tokens +- Bob later deposits 100 USD and 100 EUR (maintaining the 1:1 ratio) +- LP tokens received: (100 / 100) * 100 = 100 LP tokens +- Total LP tokens: 200 + +**Example: Single-Asset Deposit** + +Alice creates an AMM with 100 USD and 100 EUR (with 0.3% trading fee): +- Initial LP tokens: SQRT(100 * 100) = 100 LP tokens +- Bob later deposits 100 USD only (no EUR) +- Using the single-asset deposit formula, Bob receives ~41.4 LP tokens +- Total LP tokens: ~141.4 + +### 1.1.4. Effective Price + +For single-asset operations, users can specify an **effective price** to protect against unfavorable exchange rates: + +- **Deposit Effective Price** = Asset Deposited / LP Tokens Issued + - Example: Depositing 100 USD to receive 40 LP tokens = 2.5 USD per LP token + - Users set a **maximum** effective price (won't pay more than X asset per LP token) + - Used in [singleDepositEPrice](deposit.md#53-singledepositeprice-tflimitlptoken) mode + +- **Withdrawal Effective Price** = LP Tokens Redeemed / Asset Withdrawn + - Example: Redeeming 40 LP tokens to withdraw 100 USD = 0.4 LP tokens per USD + - Users set a **minimum** effective price (won't pay less than X LP tokens per unit of asset withdrawn) + - Used in [singleWithdrawEPrice](withdraw.md#53-singlewithdraweprice-tflimitlptoken) mode + +## 1.2. Trading Fee + +AMMs charge a trading fee on swaps, which is added to the pool and distributed proportionally to all LP token holders when they withdraw liquidity. The fee is expressed in fee units. + +**Fee Range:** +- Minimum: 0 units (0%) +- Maximum: 1000 units (1% or 100 basis points) +- Fee units: 1 unit = 0.001% (or 1/10 of a basis point) +- Example: A fee of 30 units = 0.03% = 3 basis points + +The trading fee can be set initially when creating the AMM and subsequently adjusted through the voting mechanism. + +### 1.2.1. Auction Slot + +The auction slot mechanism allows any LP token holder to bid for a 24-hour period of discounted trading fees. During this period, the slot holder pays only one-tenth of the regular trading fee when trading through the AMM. The slot holder can also authorize up to four additional accounts to share this discount. + +The auction operates as a continuous bidding system where anyone can take over the slot at any time by outbidding the current holder. The minimum bid price decreases as the current holder uses more of their 24-hour slot time. When someone successfully outbids the current holder, the previous holder receives a refund proportional to their remaining unused time. The difference between the new bid and the refund is burned from the LP token supply, which increases the ownership percentage of all remaining LP token holders. + +See [AMMBid Implementation Details](bidding.md) for comprehensive documentation on the auction mechanics, including price calculations, time-based refunds, and the LP token burning process. + +### 1.2.2. Fee Voting + +Liquidity providers can vote on the trading fee rate. +Each vote is recorded in a **vote slot** - a data structure stored in the AMM ledger entry that tracks who voted, what fee they proposed, and their voting power. +Voting power is determined by the number of LP tokens held: an account holding 30% of all LP tokens has 30% of the voting power. The AMM maintains up to 8 vote slots[^vote-max-slots], and the actual trading fee is calculated as the weighted average of all votes[^vote-weighted-average]. + +**Voting Mechanism:** + +1. LP token holders submit `AMMVote` transactions with their preferred fee (0-1000) +2. The system calculates vote weights: `VoteWeight = (LPTokens / TotalLPTokens) * 100,000` +3. The weighted average determines the actual trading fee: + ``` + TradingFee = SUM(Fee_i * LPTokens_i) / SUM(LPTokens_i) + ``` + +**Vote Slot Management:** + +- Maximum 8 vote slots (defined by `kVoteMaxSlots`)[^vote-max-slots] +- If a slot is available, the new vote is added directly +- If all slots are full, replacement is a two-step process[^vote-min-tokens]: + 1. **Find the eviction candidate:** select the existing slot with the smallest LP token balance, breaking ties by lowest fee, then by lexicographically smallest account ID + 2. **Decide whether to replace:** the new vote replaces the candidate only if the new voter holds more LP tokens, or holds an equal amount and sets a higher fee. If both are equal, the new vote is not added +- Vote weights are automatically recalculated when LP token balances change + +**Example:** + +AMM has 3 voters: +- Alice: 100 LP tokens, votes 500 (0.5% fee) +- Bob: 50 LP tokens, votes 300 (0.3% fee) +- Carol: 50 LP tokens, votes 700 (0.7% fee) + +Actual fee = (100*500 + 50*300 + 50*700) / (100 + 50 + 50) = 100,000 / 200 = 500 (0.5%) + +# 2. Ledger Entries + +The AMM system uses several ledger entry types to track state: + +```mermaid +classDiagram + class AMM { + +AccountID Account + +UInt16 TradingFee + +Array VoteSlots + +Object AuctionSlot + +Amount LPTokenBalance + +Issue Asset + +Issue Asset2 + +UInt64 OwnerNode + } + + class AccountRoot { + +AccountID Account + +Amount Balance + +UInt256 AMMID + +UInt32 Flags + } + + class RippleState { + +Amount Balance + +Amount LowLimit + +Amount HighLimit + +UInt32 Flags + } + + class MPToken { + +AccountID Account + +uint192 MPTokenIssuanceID + +Amount MPTAmount + +UInt32 Flags + } + + class DirectoryNode { + } + + AMM --> AccountRoot : references via sfAccount + AccountRoot -- RippleState : connects to (via LowLimit/HighLimit) + MPToken --> AccountRoot : owned by (via sfAccount) + AMM --> DirectoryNode : linked via sfOwnerNode +``` +*Figure: Key ledger entries that represent an AMM instance* + +## 2.1. AMM Ledger Entry + +The `AMM` ledger entry (type `ltAMM = 0x0079`)[^amm-ledger-entry] tracks the state of an AMM instance. Each AMM is uniquely identified by its asset pair[^amm-keylet]. + +### 2.1.1. Object Identifier + +The key of the `AMM` object is the result of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the `AMM` space key (`0x0041`, uppercase `A`)[^amm-namespace] concatenated with the two assets' identifiers. + +[^amm-namespace]: AMM namespace constant: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Indexes.cpp#L73) + +The two assets are first ordered canonically (lexicographically) to ensure a unique, deterministic key regardless of the order in which assets are specified. + +Each asset contributes its identifier to the hash: +- **XRP**: Issuer AccountID (all zeros) + Currency code (all zeros) +- **IOUs**: Issuer AccountID + Currency code +- **MPTs**: MPTID + +### 2.1.2. Fields + +| Field | Type | Required | Description | +|-------|------|----------------------------|-------------| +| `Account` | AccountID | Yes | The Account ID of the AMM's pseudo-account | +| `TradingFee` | UInt16 | Defaults to 0 if not set | The current trading fee in units of 1/100,000 (0 if not set) | +| `VoteSlots` | Array | Optional | Array of up to 8 `VoteEntry` objects containing fee votes | +| `AuctionSlot` | Object | Optional | Object containing auction slot information | +| `LPTokenBalance` | Amount | Yes | Total outstanding LP tokens for this AMM | +| `Asset` | Issue | Yes | One of the pool's two assets (the lesser by Issue comparison) | +| `Asset2` | Issue | Yes | The other pool asset (the greater by Issue comparison) | +| `OwnerNode` | UInt64 | Yes | Index of the owner directory page for this AMM | +| `PreviousTxnID` | Hash256 | Optional | Transaction hash that most recently modified this entry | +| `PreviousTxnLgrSeq` | UInt32 | Optional | Ledger sequence of the transaction that most recently modified this entry | + +#### 2.1.2.1. VoteSlots + +The `VoteSlots` field contains an array of `VoteEntry` inner objects. Each `VoteEntry` has: + +| Field | Type | Description | +|-------|------|-------------| +| `Account` | AccountID | The account that cast this vote | +| `TradingFee` | UInt16 | The fee this account voted for (0-1000) | +| `VoteWeight` | UInt32 | Weight of this vote = `(LPTokens / TotalLPTokens) * 100,000` | + +#### 2.1.2.2. AuctionSlot + +The `AuctionSlot` field contains an inner object with: + +| Field | Type | Description | +|-------|------|-------------| +| `Account` | AccountID | Current auction slot holder | +| `AuthAccounts` | Array | Optional array of up to 4 authorized accounts | +| `Expiration` | UInt32 | Unix timestamp when the slot expires (current time + 86,400 seconds) | +| `Price` | Amount | Price paid for the auction slot in LP tokens | +| `DiscountedFee` | UInt16 | Discounted fee for slot holder | + + +### 2.1.3. Pseudo-accounts + +The AMM's `Account` field references a pseudo-account[^pseudo-account-creation] created specifically for this AMM. Each AMM instance creates a special pseudo-account to hold the pool's assets. This account: + +- Has a disabled master key, allows default rippling and enables deposit authorization (so nobody can pay into the pseudo-account)[^disabled-master-key] +- Is identified by the `sfAMMID` field[^ammid-field] in its `AccountRoot` entry +- Has an Account ID deterministically generated[^pseudo-account-address] from the AMM ledger entry key +- Holds XRP balance if one of the pool assets is XRP +- Has trust lines for: + - Each IOU in the pool + - Each liquidity provider who holds LP tokens +- Has MPToken entries for MPT assets in the pool (if pool contains MPTs): +- Is automatically deleted when the AMM is deleted + +[^pseudo-account-creation]: Pseudo-account creation for AMM: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L251) +[^disabled-master-key]: Master key disabled with lsfDisableMaster flag: [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L244) +[^ammid-field]: AMMID field set in pseudo-account: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L251) +[^pseudo-account-address]: Pseudo-account address generation: [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L146-L160) +[^zero-credit-limit]: LP token trustline created with zero balance: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L269-L273) + +#### 2.1.3.1. Account ID Generation + +The AMM pseudo-account ID, like any other pseudo-account ID, is generated using a collision-avoidance algorithm[^collision-avoidance-algo] that ensures no existing account has the same address. The generation process uses the `pseudoAccountAddress()` function with the following algorithm: + +[^collision-avoidance-algo]: Collision-avoidance algorithm for pseudo-account address: [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L146-L160) + +**Generation Process:** + +1. **Input**: The AMM ledger entry key (derived from [object identifier](#211-object-identifier)) +2. **Parent Hash**: The hash of the parent ledger (provides uniqueness per ledger) +3. **Iteration Loop**: Try up to 256 attempts (hardcoded as `kMaxAccountAttempts`) + +**Collision avoidance**: Account IDs are 160-bit values derived from cryptographic hashes. While the probability of collision with an existing account is small, multiple attempts provide a safety mechanism to handle this theoretical edge case. + +For each attempt `i` (0 to 255): + +``` +hash = SHA512-Half(i, parentLedgerHash, ammLedgerEntryKey) +accountID = RIPEMD160(SHA256(hash)) +``` + +4. **Collision Check**: Verify that no `AccountRoot` exists with this `accountID` +5. **Success**: If no collision, return the `accountID` +6. **Failure**: If all 256 attempts find collisions, return `beast::kZero` (all zeros account ID) + +**Failure Handling:** + +If `pseudoAccountAddress()` returns `beast::kZero` (indicating all 256 attempts failed): +- `createPseudoAccount()` returns `tecDUPLICATE` +- The AMMCreate transaction fails in `doApply` +- This scenario is extremely unlikely in practice + +**Determinism:** + +For a given asset pair and parent ledger hash, all nodes generate the same sequence of candidate account IDs: +- The iteration counter `i` is hashed along with fixed inputs (parent hash, AMM keylet) +- Each `i` produces a completely different candidate Account ID +- All nodes check the same candidates in the same order against their ledger state +- The first unused candidate found is selected consistently across all nodes +- This ensures reproducibility across nodes in consensus and predictable behavior in transaction replay + +**Example:** + +For an AMM with USD/XRP: +1. AMM keylet = `SHA512-Half(0x0041, XRP_account, XRP_currency, USD_account, USD_currency)`[^amm-keylet-hash] +2. Attempt 0: `hash = SHA512-Half(0, parentHash, ammKeylet)` -> Account ID candidate +3. If Account ID exists, try attempt 1: `hash = SHA512-Half(1, parentHash, ammKeylet)` -> New candidate +4. Continue until unused account ID found or 256 attempts exhausted + +### 2.1.4. Reserves + +The `AMM` ledger entry itself does not require an owner reserve. However: + +- Creating an AMM costs an elevated base fee equal to one owner-reserve increment (`view.fees().increment`), set higher than the normal per-transaction base fee +- The AMM pseudo-account holds reserves if it has XRP +- LP token holders who have trust lines for LP tokens pay reserves according to normal trust line rules + +## 2.2. RippleState Ledger Entry + +AMMs create `RippleState` entries (trust lines) for: +- Each IOU asset in the pool +- The LP token issued by the AMM + +All AMM trust lines: +- Have zero credit limits[^zero-credit-limit] (to prevent unsolicited deposits) +- Do not have quality modifiers (QualityIn/QualityOut)[^ripplestate-no-quality] + +Pool asset trust lines (between the AMM account and the IOU issuer): +- Are additionally marked with the `lsfAMMNode` flag[^ripplestate-amm-flag] + +[^ripplestate-amm-flag]: Trust line marked with lsfAMMNode flag: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L339-L341) +[^ripplestate-no-quality]: Quality modifiers only set if non-zero: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L252-L256) + +See [Trust Lines Documentation](../trust_lines/README.md#21-ripplestate-ledger-entry) for complete details on `RippleState` ledger entries. + +## 2.3. MPToken Ledger Entry + +When an AMM pool contains MPT assets, the AMM pseudo-account holds `MPToken` entries for each MPT in the pool. These MPToken entries: + +- Are marked with the `lsfMPTAMM` flag[^mptoken-amm-flag] (distinguishing them from regular holder MPTokens) +- Are always marked with the `lsfMPTAuthorized` flag[^mptoken-authorized-flag] (the AMM pseudo-account is implicitly authorized to hold the asset, regardless of the issuance's `lsfMPTRequireAuth`) +- Track the AMM's MPT balance via the `MPTAmount` field +- Are created when depositing MPT assets[^mptoken-creation] +- Do not count towards the AMM pseudo-account's `OwnerCount`[^mptoken-no-owner-count] + +[^mptoken-amm-flag]: MPToken created with lsfMPTAMM flag: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L311) +[^mptoken-authorized-flag]: MPToken implicitly authorized (lsfMPTAuthorized set unconditionally): [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L311) +[^mptoken-creation]: MPToken creation for AMM pseudo-account: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L335-L336) +[^mptoken-no-owner-count]: AMM owner count not adjusted for MPToken: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L320-L321) + +See [MPTokens Documentation](../mpts/README.md) for complete details on `MPToken` ledger entries. + +# 3. Transactions + +## Common Error Codes from accountSend() + +Several AMM transactions (`AMMCreate`, `AMMDeposit`, `AMMWithdraw`, `AMMBid`) use the `accountSend()` function to transfer assets between accounts. This function can return various error codes depending on the transfer type and ledger state. These errors may occur during the `doApply` phase of transaction execution: + +**For XRP transfers:** +- `tecFAILED_PROCESSING` or `telFAILED_PROCESSING`: Sender has insufficient XRP balance to complete the transfer (after paying transaction fees and maintaining reserve requirements)[^xrp-insufficient-balance] +- With [fixAMMv1_1](https://xrpl.org/resources/known-amendments#fixammv1_1): `tecINTERNAL` if the transfer amount is negative[^xrp-negative-check] + +[^xrp-insufficient-balance]: Insufficient XRP balance check: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L886-L892) +[^xrp-negative-check]: Negative amount check with fixAMMv1_1: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L825-L830) + +**For IOU transfers:** +- Calls `directSendNoLimitIOU()`[^iou-ripple-send] which then calls `directSendNoFeeIOU()`[^iou-ripple-credit] and may call `issueIOU()`[^iou-issue] or `redeemIOU()`[^iou-redeem] +- These functions can trigger trust line creation, which may fail with: + - `tecDIR_FULL`: Owner directory is full when creating a new trust line[^iou-dir-full] + - `tecNO_LINE_INSUF_RESERVE`: Insufficient XRP reserve to create the trust line[^iou-insuf-reserve] + - `tefINTERNAL`: Trust line doesn't exist after transfer[^iou-no-line] + - `tefINTERNAL`: Receiver account SLE does not exist during trust line creation[^iou-null-account] + - `tecNO_TARGET`: Peer account doesn't exist when creating trust line[^iou-no-target] +- Errors from `directSendNoFeeIOU()` are propagated[^iou-deletable-accounts]. These include: + - `tecDIR_FULL`: Owner directory is full when creating trust line (from `trustCreate()`)[^iou-dir-full] + - `tefINTERNAL`: Receiver account SLE is null (from `trustCreate()`)[^iou-null-account] + - `tecNO_TARGET`: Peer account doesn't exist when creating trust line (from `trustCreate()`)[^iou-no-target] + - `tefBAD_LEDGER`: Directory removal failed when deleting trust line (from `trustDelete()`)[^iou-bad-ledger] + +[^iou-ripple-send]: directSendNoLimitIOU function: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L794-L847) +[^iou-ripple-credit]: directSendNoFeeIOU function: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L643-L789) +[^iou-issue]: issueIOU function: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L397-L489) +[^iou-redeem]: redeemIOU function: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L493-L561) +[^iou-dir-full]: Owner directory full check: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L218-L227) +[^iou-insuf-reserve]: Insufficient reserve to create trust line: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L674-L681) +[^iou-no-line]: Trust line doesn't exist after attempting redeem: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L538-L547) +[^iou-null-account]: Receiver account SLE null check: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L668-L670), [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L233-L234) +[^iou-no-target]: Peer account doesn't exist check: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L239-L241) +[^iou-deletable-accounts]: IOU send error propagation: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L716-L717) +[^iou-bad-ledger]: Directory removal failure in trustDelete: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L308-L318) + +**For MPT transfer:** +- `tecOBJECT_NOT_FOUND`: MPT issuance object doesn't exist[^mpt-object-not-found] +- `tecPATH_DRY`: Transfer would exceed `MaximumAmount` when issuer is sending MPTs[^mpt-path-dry-send][^mpt-path-dry-credit] +- `tecINSUFFICIENT_FUNDS`: Sender's MPToken balance is less than the transfer amount[^mpt-insufficient-funds] +- `tecNO_AUTH`: + - Sender's MPToken ledger entry doesn't exist (not authorized to hold the MPT)[^mpt-sender-no-auth] + - Receiver's MPToken ledger entry doesn't exist (not authorized to hold the MPT)[^mpt-receiver-no-auth] +- `tecINTERNAL`: Outstanding amount is less than the amount being redeemed when receiver is issuer[^mpt-internal] + +[^mpt-object-not-found]: MPT issuance not found: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1269-L1271) +[^mpt-path-dry-send]: MPT transfer exceeds MaximumAmount (directSendNoLimitMPT): [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1178-L1179) +[^mpt-path-dry-credit]: MPT transfer exceeds MaximumAmount (directSendNoFeeMPT): [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1084-L1085) +[^mpt-insufficient-funds]: Sender MPToken balance insufficient: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1095-L1097) +[^mpt-sender-no-auth]: Sender MPToken entry missing: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1102-L1104) +[^mpt-receiver-no-auth]: Receiver MPToken entry missing: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1136-L1138) +[^mpt-internal]: Outstanding amount less than redemption: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1115-L1117) +[^amm-ledger-entry]: AMM ledger entry type definition: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/ledger_entries.macro#L373-L384) +[^amm-keylet]: AMM keylet computation using asset pair: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Indexes.cpp#L425-L456) +[^amm-keylet-hash]: AMM keylet hash with namespace `0x0041` and fields `(account, currency)` per asset: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Indexes.cpp#L432-L437) +[^amm-lp-tokens-calc]: Initial LP token calculation: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L45-L54) +[^vote-max-slots]: Maximum vote slots constant: [`AMMCore.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/AMMCore.h#L24) +[^vote-weighted-average]: Weighted average fee calculation: [`AMMVote.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L205-L207) +[^vote-min-tokens]: Vote slot replacement logic: [`AMMVote.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L135-L144) + +**Note:** Most of these error conditions are checked during the `preclaim` phase (validation against the ledger view), so they are unlikely to occur during `doApply`. However, ledger state can change between validation and application (e.g., due to other transactions in the same ledger), making these errors theoretically possible. + +## 3.1. AMMCreate Transaction + +The `AMMCreate` transaction creates a new AMM instance for a token pair and provides initial liquidity. + +| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | +|------------|:---------:|:-----------:|:---------:|:-------------:|:-------------:|:--------------------------------------------------------------------------| +| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMCreate"` | +| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account creating the AMM instance | +| `Amount` | :heavy_check_mark: | `No` | `String` or `Object` | `Amount` | | Amount of one asset to deposit (XRP as string, tokens as object) | +| `Amount2` | :heavy_check_mark: | `No` | `String` or `Object` | `Amount` | | Amount of the other asset to deposit (XRP as string, tokens as object) | +| `TradingFee` | :heavy_check_mark: | `No` | `Number` | `UInt16` | | Initial trading fee (0-1000, 1 = 0.001%) | +| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags (must be 0 for AMMCreate, only universal flags allowed) | + +The two amounts can be in any order - the AMM will automatically order them as `Asset` and `Asset2` based on Issue comparison. + +### 3.1.1. Failure Conditions + +**Static validation**[^ammcreate-static-validation] + +[^ammcreate-static-validation]: Static validation (preflight): [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L42-L85) + +- `temDISABLED`: + - [AMM](https://xrpl.org/resources/known-amendments#amm) amendment is not enabled + - either `Amount` or `Amount2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled +- `temINVALID_FLAG`: one of the specified flags is not one of common transaction flags +- `temBAD_AMM_TOKENS`: `Amount` and `Amount2` have the same currency and issuer +- `temBAD_CURRENCY`: `Amount` or `Amount2` uses the disallowed 3-letter "XRP" currency code +- `temBAD_ISSUER`: `Amount` or `Amount2` is XRP (currency is all zeros) but has a non-zero issuer account +- `temBAD_MPT`: `Amount` or `Amount2` is an MPT with a zero (empty) issuer +- `temBAD_AMOUNT`: either `Amount` or `Amount2` is zero, negative +- `temBAD_FEE`: `TradingFee` exceeds 1000 + +**Validation against the ledger view**[^ammcreate-preclaim-validation] + +[^ammcreate-preclaim-validation]: Validation against ledger view (preclaim): [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L95-L242) + +- `tecDUPLICATE`: an AMM already exists for this token pair +- `tecNO_LINE`: `Amount` or `Amount2` issuer has `lsfRequireAuth` flag set, but account has no trust line with the issuer +- `tecNO_AUTH`: + - For IOUs: `Amount` or `Amount2` issuer has `lsfRequireAuth` flag set, and the trust line exists but lacks authorization (missing `lsfLowAuth` or `lsfHighAuth` flag) + - For MPTs: Signing account or AMM pseudo-account lacks required authorization for MPT with `lsfMPTRequireAuth` flag +- `tecFROZEN` (IOU/XRP) or `tecLOCKED` (MPT): either asset is globally or individually frozen/locked +- `terNO_RIPPLE`: either asset's issuer does not have DefaultRipple flag set (non-XRP assets only) +- `tecINSUF_RESERVE_LINE`: account has insufficient XRP to cover the LP token trust line reserve +- `tecUNFUNDED_AMM`: account has insufficient balance of either asset or it does not have the trust line +- `tecAMM_INVALID_TOKENS`: either `Amount` or `Amount2` is an LP token from another AMM. The code does not explicitly check for *another* AMM, but at this point, LP token from this AMM should not exist +- With [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault): + - `terADDRESS_COLLISION`: generated AMM account ID already exists + - `tecWRONG_ASSET`: either amount is an MPT issued by a pseudo-account (vault share tokens cannot back an AMM) +- Without [AMMClawback](https://xrpl.org/resources/known-amendments#ammclawback): + - `tecINTERNAL`: `Amount` or `Amount2` issuer account does not exist in the ledger + - `tecNO_PERMISSION`: + - `Amount` or `Amount2` issuer has clawback enabled (`lsfAllowTrustLineClawback` flag is set for IOUs) + - either `Amount` or `Amount2` is an MPT with `lsfMPTCanClawback` flag set +- MPT-specific validations (for either `Amount` or `Amount2` if MPT): Both assets are validated using [`canMPTTradeAndTransfer`](../mpts/README.md#363-canmpttradeandtransfer). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for validation logic and error conditions. + +**Validation during doApply**[^ammcreate-doapply-validation] + +[^ammcreate-doapply-validation]: Validation during doApply: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L268-L359) + +- `tecDUPLICATE`: + - AMM pseudo-account ID generation failed (no valid account ID found after 256 attempts) + - LP Token trust line already exists +- `tecDIR_FULL`: Owner directory is full when linking AMM object +- Propagate errors from `accountSend()` when transferring LP tokens and assets to/from AMM pseudo-account (see [Common Error Codes from accountSend()](#common-error-codes-from-accountsend)) + +### 3.1.2. State Changes[^ammcreate-state-changes] + +[^ammcreate-state-changes]: State changes (doApply): [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L268-L403) + +- `AccountRoot` object is **created** for AMM pseudo-account: + - `Account`: Generated pseudo-account ID (from collision-avoidance algorithm) + - `Balance`: `STAmount{}` (zero XRP initially, then updated to `Amount` if `Amount` is XRP) + - `Sequence`: 0 (with [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault)), otherwise current ledger sequence + - `Flags`: `lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth` + - `sfAMMID`: Set to `ammKeylet.key` (the AMM ledger entry key) + +- `AMM` object is **created**: + - `Account`: AMM pseudo-account ID + - `LPTokenBalance`: `SQRT(Amount * Amount2)` + - `Asset`: Lesser of the two assets by Issue comparison + - `Asset2`: Greater of the two assets + - `TradingFee`: As specified (if non-zero) + - `OwnerNode`: Link to owner directory + - `VoteSlots`: Array field with single `VoteEntry` inner object **created**: + - `Account`: Creator account ID + - `TradingFee`: Initial trading fee (if non-zero) + - `VoteWeight`: 100,000 (= 100%, since creator owns all LP tokens initially) + - `AuctionSlot`: Object field with an inner object **created**: + - `Account`: Creator account ID + - `Expiration`: Current time + 86,400 seconds (24 hours) + - `Price`: 0 LP tokens + - `DiscountedFee`: `TradingFee / 10` (if trading fee is non-zero) + +- `RippleState` objects are **created** (for token assets): + - For each non-XRP token asset: Trust line between AMM account and asset issuer + - Marked with `lsfAMMNode` flag + - For LP tokens: Trust line between AMM account and creator + - All trust lines: + - Have zero credit limits + - Initial balances set to deposited/issued amounts + +- `MPToken` objects are **created** (for MPT assets): + - For each MPT asset: MPToken entry for the AMM pseudo-account + - Flags: + - `lsfMPTAMM`: Marks this as an AMM-owned MPToken entry + - `lsfMPTAuthorized`: Always set (the AMM pseudo-account is implicitly authorized to hold the MPT) + - Initial `MPTAmount` set to deposited amount + - Linked to the AMM pseudo-account's owner directory + +- `DirectoryNode` is **created** for AMM pseudo-account's owner directory: + - Links the AMM ledger entry to the pseudo-account + - The AMM entry's `OwnerNode` field is set to the directory page index + - This directory will later also contain links to trust lines owned by the AMM account + +- Order books are **registered** in [OrderBookDB](../path_finding/README.md#45-orderbookdb) (if not already present): + - Asset->Asset2 trading direction registered + - Asset2->Asset trading direction registered + +## 3.2. AMMDeposit Transaction + +The `AMMDeposit` transaction adds liquidity to an existing AMM pool. There are multiple deposit modes controlled by transaction flags. + +**Fields:** + +| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | +|-------------------|:------------------:|:-----------:|:--------------------:|:-------------:|:-------------:|:---------------------------------------------------------------------------| +| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMDeposit"` | +| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account depositing liquidity | +| `Asset` | :heavy_check_mark: | `No` | `Object` | `Issue` | | One of the pool's assets | +| `Asset2` | :heavy_check_mark: | `No` | `Object` | `Issue` | | The other pool asset | +| `Amount` | | `No` | `String` or `Object` | `Amount` | | Amount of one asset (interpretation depends on flags) | +| `Amount2` | | `No` | `String` or `Object` | `Amount` | | Amount of the other asset (interpretation depends on flags) | +| `LPTokenOut` | | `No` | `String` or `Object` | `Amount` | | Amount of LP tokens to receive (interpretation depends on flags) | +| `EPrice` | | `No` | `String` or `Object` | `Amount` | | Maximum effective price in same currency as `Amount` (tfLimitLPToken only) | +| `TradingFee` | | `No` | `Number` | `UInt16` | | Trading fee for empty pool deposits (tfTwoAssetIfEmpty only) | +| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags specifying deposit mode | + +### 3.2.1. Deposit Modes + +The AMMDeposit transaction supports six different deposit modes. See [AMMDeposit Implementation Details](deposit.md) for detailed documentation. + +All deposit modes require the `Asset` and `Asset2` fields to identify which AMM pool to deposit into. The table below shows the additional fields required for each mode. + +| Function | Flag | Flag Value | Use Case | Assets | User Specifies | System Calculates | +|--------------------------------------------------------------------------------------|---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| +| [equalDepositLimit](deposit.md#41-equaldepositlimit-tftwoasset) | `tfTwoAsset` | `0x00100000` | Depositor specifies maximum amounts of both assets. System deposits both assets maintaining the pool's current ratio, maximizing deposit size within both limits | Both | `Amount` (max), `Amount2` (max), Optional: `LPTokenOut` (min) | Actual `Amount` and `Amount2` to deposit (tries maximizing `Amount` first, then `Amount2` if that fails) | +| [equalDepositTokens](deposit.md#42-equaldeposittokens-tflptoken) | `tfLPToken` | `0x00010000` | Depositor specifies exact LP tokens to receive. System calculates required amounts of both assets maintaining the pool's current ratio | Both | `LPTokenOut` (exact). Optional: both `Amount` (min) and `Amount2` (min), or neither | Required `Amount` and `Amount2` | +| [equalDepositInEmptyState](deposit.md#43-equaldepositinemptystate-tftwoassetifempty) | `tfTwoAssetIfEmpty` | `0x00800000` | Used when pool is empty (zero LP tokens and zero asset balances). Depositor deposits both assets to set new pool ratio and becomes initial LP token holder | Both | `Amount`, `Amount2`, Optional: `TradingFee` | Initial `LPTokenOut` = sqrt(`Amount` * `Amount2`) | +| [singleDeposit](deposit.md#51-singledeposit-tfsingleasset) | `tfSingleAsset` | `0x00080000` | Depositor specifies amount of single asset to deposit. System calculates how many LP tokens depositor receives | One | `Amount`, Optional: `LPTokenOut` (min) | `LPTokenOut` depositor receives | +| [singleDepositTokens](deposit.md#52-singledeposittokens-tfoneassetlptoken) | `tfOneAssetLPToken` | `0x00200000` | Depositor specifies exact LP tokens to receive in exchange for depositing single asset. System calculates required deposit amount | One | `LPTokenOut` (exact), `Amount` (max) | Required `Amount` | +| [singleDepositEPrice](deposit.md#53-singledepositeprice-tflimitlptoken) | `tfLimitLPToken` | `0x00400000` | Depositor sets maximum amount of single asset willing to pay per LP token received. System calculates optimal deposit amount | One | `Amount` (can be 0), `EPrice` (max) | Optimal `Amount` at `EPrice` limit | + +The deposit mode is determined by exactly one of these flags (enforced by checking `popcount(flags & tfDepositSubTx) == 1`). See the table above for flag values and usage details, and [AMMDeposit Implementation Details](deposit.md) for the implementation of each mode. + +### 3.2.2. Failure Conditions + +**Static validation**[^ammdeposit-static-validation] + +[^ammdeposit-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L37-L47), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L51-L54), [`preflight`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L57-L175) + +- `temDISABLED`: + - AMM amendment is not enabled + - either `Asset` or `Asset2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled +- `temINVALID_FLAG`: invalid flags (flags set that are not deposit mode flags) +- `temMALFORMED`: + - Invalid flag combination (must have exactly one deposit mode flag set) + - Required fields missing for chosen deposit mode +- `temBAD_AMM_TOKENS`: + - `Amount` and `Amount2` are the same token (when both specified) + - `LPTokenOut` is zero or negative + - `Asset` and `Asset2` have the same currency and issuer + - `Amount` or `Amount2` currency does not match either pool asset (`Asset` or `Asset2`) + - `EPrice` currency does not match `Amount` currency (checked only when MPTokensV2 is not enabled) +- `temBAD_CURRENCY`: `Asset`, `Asset2`, `Amount`, `Amount2`, or `EPrice` uses the disallowed 3-letter "XRP" currency code (`0x5852500000000000`) +- `temBAD_ISSUER`: `Asset`, `Asset2`, `Amount`, `Amount2`, or `EPrice` is XRP (currency is all zeros) but has a non-zero issuer account +- `temBAD_MPT`: `Asset`, `Asset2`, `Amount`, `Amount2`, or `EPrice` is an MPT with a zero (empty) issuer +- `temBAD_AMOUNT`: `Amount`, `Amount2`, or `EPrice` is zero, negative +- `temBAD_FEE`: `TradingFee` exceeds 1000 + +**Validation against the ledger view**[^ammdeposit-preclaim-validation] + +[^ammdeposit-preclaim-validation]: Validation against ledger view (preclaim): [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L177-L361) + +- `terNO_AMM`: AMM ledger entry does not exist for specified asset pair +- `tecINTERNAL`: + - (tfTwoAssetIfEmpty only) Pool has zero LP tokens but asset balances are not zero (inconsistent empty state) + - pool balances are invalid (zero or negative) +- `tecAMM_NOT_EMPTY`: tfTwoAssetIfEmpty used but AMM is not empty +- `tecAMM_EMPTY`: AMM has zero LP tokens (for non-tfTwoAssetIfEmpty modes) +- Authorization/freeze checks (applied unconditionally to the deposited `Amount`/`Amount2` for non-`tfLPToken` modes, and with [AMMClawback](https://xrpl.org/resources/known-amendments#ammclawback) also to the pool `Asset`/`Asset2`): + - `tecNO_LINE`: the asset's issuer has `lsfRequireAuth` set, but the account has no trust line with the issuer + - `tecNO_AUTH`: the asset's issuer has `lsfRequireAuth` set, and the trust line exists but lacks authorization (missing `lsfLowAuth` or `lsfHighAuth` flag) + - `tecFROZEN` (IOU/XRP) or `tecLOCKED` (MPT): the asset is frozen/locked (AMM account, currency/issuance, or depositor account). Under the `fixCleanup3_3_0` amendment, both pool assets are checked whether or not they are deposited, so a deposit now also fails when the AMM pseudo-account's holding of the non-deposited pool asset is individually frozen (the deposited funds could not later be withdrawn). Without the amendment such a deposit succeeds. The conditions with the amendment: + - the asset is globally frozen or locked + - the AMM pseudo-account's holding of either pool asset is individually frozen + - the depositor's holding of the asset is individually frozen, unless the depositor is that asset's issuer +- `tecUNFUNDED_AMM`: + - account has insufficient token balance to deposit + - account has insufficient XRP to deposit (and LP token trust line already exists) +- `tecINSUF_RESERVE_LINE`: + - account has insufficient XRP to deposit and create LP token trust line (when account is not yet an LP) + - non-LP account has insufficient reserve for LP token trust line +- `temBAD_AMM_TOKENS`: `LPTokenOut` issue (currency code + issuer) does not match the AMM's LP token issue +- MPT-specific validations (for either `Asset` or `Asset2` if MPT): Both assets are validated using [`canMPTTradeAndTransfer`](../mpts/README.md#363-canmpttradeandtransfer). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for validation logic and error conditions. + +**Validation during doApply**[^ammdeposit-doapply-validation] + +[^ammdeposit-doapply-validation]: Validation during doApply: [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L412-L1046) + +- `tecINTERNAL`: AMM ledger entry does not exist (should not happen if preclaim succeeded) +- `temBAD_AMOUNT`: Deposit amount after adjustment/calculation is zero or negative. Deposit amounts are adjusted based on the deposit mode (e.g., proportional calculations for tfLPToken, pool ratio adjustments for tfTwoAsset, or LP token precision adjustments). +- `tecUNFUNDED_AMM`: Insufficient balance to deposit the final calculated amounts. This is re-checked during deposit execution (first check is in preclaim with transaction amounts, but final amounts may differ for certain deposit modes like tfLPToken). +- `tecAMM_FAILED`: Deposit constraints not satisfied. The interpretation of transaction fields as minimums or maximums depends on the deposit mode flag (see [Deposit Modes](#321-deposit-modes)): + - tfLPToken mode: calculated asset deposits are less than `Amount` or `Amount2` (optional minimums) + - tfSingleAsset or tfTwoAsset mode: calculated LP tokens are less than `LPTokenOut` (optional minimum) + - tfTwoAsset mode: neither calculated deposit strategy satisfies both `Amount` and `Amount2` constraints (maximums) + - tfOneAssetLPToken mode: calculated deposit amount exceeds `Amount` (maximum willing to deposit) + - tfLimitLPToken mode: calculated deposit amount is invalid or effective price constraint cannot be satisfied with `EPrice` (maximum effective price) +- `tecAMM_INVALID_TOKENS`: Calculated LP tokens are zero or invalid. This can occur when: + - LP token adjustments for precision result in zero tokens (with [fixAMMv1_3](https://xrpl.org/resources/known-amendments#fixammv1_3)) + - Deposit amount is too small relative to pool size, resulting in zero LP tokens after rounding + - Occurs in any deposit mode where LP tokens are calculated (tfLPToken, tfSingleAsset, tfTwoAsset, tfOneAssetLPToken, tfLimitLPToken) +- Propagate errors from `accountSend()` when transferring assets to AMM account and LP tokens to depositor (see [Common Error Codes from accountSend()](#common-error-codes-from-accountsend)) + +### 3.2.3. State Changes + +- `AMM` object is **modified**: + - `LPTokenBalance`: Increased by deposited LP tokens + - `VoteSlots`: (tfTwoAssetIfEmpty only) Reset with depositor's vote + - `AuctionSlot`: (tfTwoAssetIfEmpty only) Depositor becomes slot holder with `Price` set to 0 and 24-hour expiration + - `TradingFee`: (tfTwoAssetIfEmpty only) Updated if specified + +- AMM pseudo-account balances are **modified**: + - Asset deposits transferred from depositor to AMM pseudo-account + - Balances updated in AMM pseudo-account's `AccountRoot` (for XRP), `RippleState` trust lines (for tokens), or `MPToken` entries (for MPTs) + +- LP tokens are **issued**: + - LP tokens sent from AMM pseudo-account to depositor + - Trust line created if depositor doesn't have one + - `RippleState` balance updated + +- Depositor's `AccountRoot` is **modified**: + - `OwnerCount`: Incremented if new LP token trust line created + - `Balance`: Decreased by XRP deposited (if applicable) + +## 3.3. AMMWithdraw Transaction + +The `AMMWithdraw` transaction removes liquidity from an AMM pool by redeeming LP tokens. + +**Fields:** + +| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | +|-------------------|:------------------:|:-----------:|:--------------------:|:-------------:|:-------------:|:---------------------------------------------------------------------------| +| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMWithdraw"` | +| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account withdrawing liquidity | +| `Asset` | :heavy_check_mark: | `No` | `Object` | `Issue` | | One of the pool's assets | +| `Asset2` | :heavy_check_mark: | `No` | `Object` | `Issue` | | The other pool asset | +| `Amount` | | `No` | `String` or `Object` | `Amount` | | Amount of one asset (interpretation depends on flags) | +| `Amount2` | | `No` | `String` or `Object` | `Amount` | | Amount of the other asset (interpretation depends on flags) | +| `LPTokenIn` | | `No` | `String` or `Object` | `Amount` | | Amount of LP tokens to redeem (interpretation depends on flags) | +| `EPrice` | | `No` | `String` or `Object` | `Amount` | | Minimum effective price in LP token currency (tfLimitLPToken only) | +| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags specifying withdrawal mode | + +### 3.3.1. Withdrawal Modes + +The AMMWithdraw transaction supports seven different withdrawal modes. See [AMMWithdraw Implementation Details](withdraw.md) for detailed documentation. + +All withdrawal modes require the `Asset` and `Asset2` fields to identify which AMM pool to withdraw from. The table below shows the additional fields required for each mode. + +| Function | Flag | Flag Value | Use Case | Assets | User Specifies | System Calculates | +|-----------------------------------------------------------------------------------------------------|-------------------------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| +| [equalWithdrawTokens](withdraw.md#41-equalwithdrawtokens-tflptoken-tfwithdrawall) | `tfLPToken` | `0x00010000` | Withdrawer specifies exact LP tokens to redeem. System withdraws both assets maintaining the pool's current ratio | Both | `LPTokenIn` (exact) | Required `Amount` and `Amount2` to withdraw | +| [equalWithdrawTokens](withdraw.md#41-equalwithdrawtokens-tflptoken-tfwithdrawall) | `tfWithdrawAll` | `0x00020000` | Withdrawer redeems all LP tokens held. System withdraws both assets proportionally based on entire LP token balance | Both | None (redeems all LP tokens) | `Amount` and `Amount2` based on all LP tokens held | +| [equalWithdrawLimit](withdraw.md#42-equalwithdrawlimit-tftwoasset) | `tfTwoAsset` | `0x00100000` | Withdrawer specifies maximum amounts of both assets. System withdraws both assets maintaining the pool's current ratio, maximizing withdrawal size within both limits | Both | `Amount` (max), `Amount2` (max) | Actual `Amount` and `Amount2` to withdraw (tries maximizing `Amount` first, then `Amount2` if that fails), `LPTokenIn` | +| [singleWithdraw](withdraw.md#51-singlewithdraw-tfsingleasset) | `tfSingleAsset` | `0x00080000` | Withdrawer specifies amount of single asset to withdraw. System calculates how many LP tokens withdrawer must redeem | One | `Amount` | `LPTokenIn` withdrawer must redeem | +| [singleWithdrawTokens](withdraw.md#52-singlewithdrawtokens-tfoneassetlptoken-tfoneassetwithdrawall) | `tfOneAssetWithdrawAll` | `0x00040000` | Withdrawer redeems all LP tokens held in exchange for withdrawing single asset. System calculates withdrawal amount based on entire LP token balance | One | `Amount` (required to specify which asset; value is min constraint or 0 for no min) | `Amount` to withdraw based on all LP tokens held | +| [singleWithdrawTokens](withdraw.md#52-singlewithdrawtokens-tfoneassetlptoken-tfoneassetwithdrawall) | `tfOneAssetLPToken` | `0x00200000` | Withdrawer specifies exact LP tokens to redeem in exchange for withdrawing single asset. System calculates withdrawal amount | One | `LPTokenIn` (exact), `Amount` (min or 0 for no min) | Required `Amount` | +| [singleWithdrawEPrice](withdraw.md#53-singlewithdraweprice-tflimitlptoken) | `tfLimitLPToken` | `0x00400000` | Withdrawer sets minimum effective price (asset received per LP token redeemed). System calculates optimal withdrawal amount | One | `Amount` (min or 0 for no min), `EPrice` (min effective price) | Optimal `Amount` and `LPTokenIn` at `EPrice` limit | + +The withdrawal mode is determined by exactly one of these flags (enforced by checking `popcount(flags & tfWithdrawSubTx) == 1`). See the table above for flag values and usage details, and [AMMWithdraw Implementation Details](withdraw.md) for the implementation of each mode. + +### 3.3.2. Failure Conditions + +**Static validation**[^ammwithdraw-static-validation] + +[^ammwithdraw-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L43-L53), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L57-L60), [`preflight`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L63-L168) + +- `temDISABLED`: + - AMM amendment not enabled + - either `Asset` or `Asset2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled +- `temINVALID_FLAG`: invalid flags (flags set that are not withdraw mode flags) +- `temMALFORMED`: + - Invalid flag combination (must have exactly one withdrawal mode flag set) + - Required fields missing for chosen withdrawal mode +- `temBAD_AMM_TOKENS`: + - `Amount` and `Amount2` are the same token (when both specified) + - `LPTokenIn` is zero or negative + - `Asset` and `Asset2` have the same currency and issuer + - `Amount` or `Amount2` currency does not match either pool asset (`Asset` or `Asset2`) +- `temBAD_CURRENCY`: `Asset`, `Asset2`, `Amount`, `Amount2`, or `EPrice` uses the disallowed 3-letter "XRP" currency code (`0x5852500000000000`) +- `temBAD_ISSUER`: `Asset`, `Asset2`, `Amount`, `Amount2`, or `EPrice` is XRP (currency is all zeros) but has a non-zero issuer account +- `temBAD_MPT`: `Asset`, `Asset2`, `Amount`, `Amount2`, or `EPrice` is an MPT with a zero (empty) issuer +- `temBAD_AMOUNT`: `Amount`, `Amount2`, or `EPrice` is zero, negative + +**Note:** AMMWithdraw static validation differs from [AMMDeposit static validation](#322-failure-conditions) in the following ways: + +- Does NOT validate that `EPrice` currency matches `Amount` currency (in deposit, EPrice = asset deposited / LP tokens received so it must match Amount currency; in withdraw, EPrice = LP tokens redeemed / asset received so it must match LP token issue, which is checked in preclaim against the AMM ledger entry, not in preflight) +- Does NOT validate `TradingFee` field (withdraw transactions don't have this field) +- `Amount` validation considers withdrawal mode flags (`tfOneAssetWithdrawAll` | `tfOneAssetLPToken`) in addition to `EPrice` presence + +**Validation against the ledger view**[^ammwithdraw-preclaim-validation] + +[^ammwithdraw-preclaim-validation]: Validation against ledger view (preclaim): [`AMMWithdraw.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L182-L314) + +- `terNO_AMM`: AMM ledger entry does not exist for specified asset pair +- `tecINTERNAL`: + - pool balances are invalid (zero or negative) +- `tecAMM_EMPTY`: AMM has zero LP tokens outstanding +- `tecAMM_BALANCE`: + - Withdrawal amount (`Amount` or `Amount2`) exceeds pool balance + - Account has zero LP tokens +- `tecNO_LINE`: `Asset` or `Asset2` issuer has `lsfRequireAuth` flag set, but account has no trust line with the issuer +- `tecNO_AUTH`: `Asset` or `Asset2` issuer has `lsfRequireAuth` flag set, and the trust line exists but lacks authorization (missing `lsfLowAuth` or `lsfHighAuth` flag) +- `tecFROZEN` (IOU/XRP) or `tecLOCKED` (MPT): `Asset` or `Asset2` is frozen/locked (AMM account, currency/issuance, or withdrawer account). Under the `fixCleanup3_3_0` amendment, the conditions producing these codes change: + - withdrawal is always allowed when the withdrawer is the asset's issuer + - a regular individual freeze on the withdrawer's own holding no longer blocks it, only a deep freeze does + - an issuer withdrawing its own frozen token reads the pool balance ignoring the freeze +- `temBAD_AMM_TOKENS`: + - `LPTokenIn` issue (currency code + issuer) does not match the AMM's LP token issue + - `EPrice` issue does not match the AMM's LP token issue +- `tecAMM_INVALID_TOKENS`: LP token redemption amount (`LPTokenIn`) exceeds account's LP token holdings +- MPT-specific validations (for either `Asset` or `Asset2` if MPT): Both assets are validated using [`canMPTTradeAndTransfer`](../mpts/README.md#363-canmpttradeandtransfer). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for validation logic and error conditions. + +**Validation during doApply**[^ammwithdraw-doapply-validation] + +[^ammwithdraw-doapply-validation]: Validation during doApply: [`AMMWithdraw.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L336-L462) + +- With [fixAMMv1_1](https://xrpl.org/resources/known-amendments#fixammv1_1): `tecAMM_INVALID_TOKENS`: LP token balance adjustment failed. When the withdrawer is the only remaining LP, if their LP token balance differs from the AMM's `LPTokenBalance` by more than 0.1%, the withdrawal fails. If the difference is within 0.1%, the AMM's `LPTokenBalance` is adjusted to match the account's balance to allow full withdrawal despite rounding errors. +- `tecINTERNAL`: AMM ledger entry does not exist (should not happen if preclaim succeeded) +- `tecAMM_BALANCE`: + - Withdrawing one side of the pool (one asset amount equals pool balance but the other doesn't) + - Withdrawing all LP tokens but not all assets + - Withdrawal amount exceeds current pool balance +- `tecAMM_FAILED`: Withdrawal constraints not satisfied (calculated withdrawal amounts don't meet minimum requirements specified in transaction fields). Under `fixCleanup3_3_0`, the `singleWithdrawEPrice` mode also fails with this code when its formula's denominator is exactly zero. Without the amendment that division throws and the transaction fails with `tefEXCEPTION` +- `tecPRECISION_LOSS`: (with both `fixCleanup3_3_0` and [fixAMMv1_3](https://xrpl.org/resources/known-amendments#fixammv1_3)) the pool product invariant fails after computing the new LP token balance. Without `fixCleanup3_3_0` the same situations are rejected by the `ValidAMM` invariant checker with `tecINVARIANT_FAILED` +- `tecAMM_INVALID_TOKENS`: Calculated LP tokens or withdrawal amounts are zero or invalid +- `tecINSUFFICIENT_RESERVE`: (With [fixAMMv1_2](https://xrpl.org/resources/known-amendments#fixammv1_2)) Insufficient XRP reserve to create trust line for withdrawn token that the account doesn't currently hold +- `tecINCOMPLETE`: Withdrawal empties the pool (all LP tokens redeemed) but AMM account deletion is incomplete due to too many trust lines to delete in a single transaction. The withdrawal succeeds, but the AMM account cleanup must be completed with subsequent AMMDelete transactions. Limited to deleting `kMaxDeletableAmmTrustLines` trust lines per transaction. +- Propagate errors from `accountSend()` when transferring assets from AMM account to withdrawer (see [Common Error Codes from accountSend()](#common-error-codes-from-accountsend)) + +### 3.3.3. State Changes + +- `AMM` object is **modified**: + - `LPTokenBalance`: Decreased by redeemed LP tokens + - May be **deleted** if balance becomes zero (see AMMDelete) + +- `AMM` object is **deleted** (if LPTokenBalance becomes zero and all trust lines can be deleted): + - AMM pseudo-account deleted + - All trust lines deleted (up to `kMaxDeletableAmmTrustLines` per transaction) + - Owner directory entries removed + - **Note:** If deletion is incomplete due to too many trust lines (`tecINCOMPLETE` returned), the AMM object and pseudo-account remain in the ledger with zero LP tokens. Subsequent `AMMDelete` transactions are needed to complete cleanup. + +- AMM account balances are **modified**: + - Assets transferred from AMM account to withdrawer + - Balances updated in `AccountRoot` (XRP), `RippleState` (tokens), or `MPToken` entries (MPTs) + +- LP tokens are **redeemed**: + - LP tokens burned (trust line balance decreased) + - Trust line may be deleted if balance becomes zero and all parameters are default + +- Withdrawer's `AccountRoot` is **modified**: + - `Balance`: Increased by XRP withdrawn (if applicable) + - `OwnerCount`: Decremented if LP token trust line deleted + - `OwnerCount`: Incremented if new trust line created for withdrawn token (with fixAMMv1_2) + +## 3.4. AMMVote Transaction + +The `AMMVote` transaction allows LP token holders to vote on the AMM's trading fee. + +**Fields:** + +| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | +|------------|:---------:|:-----------:|:---------:|:-------------:|:-------------:|:------------| +| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMVote"` | +| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account casting the vote | +| `Asset` | :heavy_check_mark: | `No` | `Object` | `Issue` | | One of the pool's assets | +| `Asset2` | :heavy_check_mark: | `No` | `Object` | `Issue` | | The other pool asset | +| `TradingFee` | :heavy_check_mark: | `No` | `Number` | `UInt16` | | Proposed trading fee (0-1000, 1 = 0.001%) | +| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags (must be 0 for AMMVote, only universal flags allowed) | + +### 3.4.1. Failure Conditions + +**Static validation**[^ammvote-static-validation] + +[^ammvote-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L32-L39), [`preflight`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L42-L57) + +- `temDISABLED`: + - AMM amendment not enabled + - either `Asset` or `Asset2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled +- `temINVALID_FLAG`: Invalid transaction flags (any flags set other than universal flags) +- `temBAD_AMM_TOKENS`: `Asset` and `Asset2` have the same currency and issuer +- `temBAD_CURRENCY`: `Asset` or `Asset2` uses the disallowed 3-letter "XRP" currency code +- `temBAD_ISSUER`: `Asset` or `Asset2` is XRP (currency is all zeros) but has a non-zero issuer account +- `temBAD_FEE`: `TradingFee` exceeds 1000 + +**Validation against the ledger view**[^ammvote-preclaim-validation] + +[^ammvote-preclaim-validation]: Validation against ledger view (preclaim): [`AMMVote.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L60-L80) + +- `terNO_AMM`: AMM ledger entry does not exist for specified asset pair +- `tecAMM_EMPTY`: AMM has zero LP tokens outstanding +- `tecAMM_INVALID_TOKENS`: Account holds zero LP tokens (not an LP) + +**Validation during doApply**[^ammvote-doapply-validation] + +[^ammvote-doapply-validation]: Validation during doApply: [`AMMVote.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L81-L232) + +- `tecINTERNAL`: AMM ledger entry does not exist (should not happen if preclaim succeeded) + +### 3.4.2. State Changes + +- `AMM` object is **modified**: + - `VoteSlots`: Updated with new/modified vote entry + - Vote slots for accounts with zero LP tokens are **removed** + - If account already has a vote: Update fee and recalculate weight + - If account doesn't have a vote: + - If fewer than 8 votes: Add new vote + - If 8 votes exist: Replace vote with smallest LP balance (if new vote has more) + - `TradingFee`: Recalculated as weighted average of all votes: + ``` + TradingFee = SUM(VoteFee_i * LPTokens_i) / SUM(LPTokens_i) + ``` + - If the calculated fee is non-zero, the `TradingFee` field is set + - If the calculated fee is zero, the `TradingFee` field is removed (made absent) + - `AuctionSlot.DiscountedFee`: Updated based on the new trading fee (if `AuctionSlot` exists) + - If `TradingFee` is non-zero and `TradingFee / 10` is non-zero, set to `TradingFee / 10` + - Otherwise, the `DiscountedFee` field is removed (made absent) + +## 3.5. AMMBid Transaction + +The `AMMBid` transaction allows LP token holders to bid for the AMM's 24-hour auction slot. + +**Fields:** + +| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | +|------------|:---------:|:-----------:|:---------:|:-------------:|:-------------:|:------------| +| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMBid"` | +| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account bidding for the auction slot | +| `Asset` | :heavy_check_mark: | `No` | `Object` | `Issue` | | One of the pool's assets | +| `Asset2` | :heavy_check_mark: | `No` | `Object` | `Issue` | | The other pool asset | +| `BidMin` | | `No` | `String` or `Object` | `Amount` | | Minimum slot price willing to pay (in LP tokens) | +| `BidMax` | | `No` | `String` or `Object` | `Amount` | | Maximum slot price willing to pay (in LP tokens) | +| `AuthAccounts` | | `No` | `Array` | `Array` | | Array of up to 4 accounts to authorize for discounted fee | +| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags (must be 0 for AMMBid, only universal flags allowed) | + + +See [Bidding documentation](bidding.md) for more details. + +### 3.5.1. Failure Conditions + +**Static validation**[^ammbid-static-validation] + +[^ammbid-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMBid.cpp#L38-L48), [`preflight`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMBid.cpp#L51-L103) + +- `temDISABLED`: + - AMM amendment not enabled + - either `Asset` or `Asset2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled +- `temINVALID_FLAG`: Invalid transaction flags (any flags set other than universal flags) +- `temBAD_AMM_TOKENS`: `Asset` and `Asset2` have the same currency and issuer +- `temBAD_CURRENCY`: `Asset`, `Asset2`, `BidMin`, or `BidMax` uses the disallowed 3-letter "XRP" currency code (`0x5852500000000000`) +- `temBAD_ISSUER`: `Asset`, `Asset2`, `BidMin`, or `BidMax` is XRP (currency is all zeros) but has a non-zero issuer account +- `temBAD_AMOUNT`: `BidMin` or `BidMax` is negative or zero +- `temMALFORMED`: + - More than 4 accounts in `AuthAccounts` + - (With [fixAMMv1_3](https://xrpl.org/resources/known-amendments#fixammv1_3)) `AuthAccounts` contains the bidder account or duplicate accounts + +**Validation against the ledger view**[^ammbid-preclaim-validation] + +[^ammbid-preclaim-validation]: Validation against ledger view (preclaim): [`AMMBid.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMBid.cpp#L106-L177) + +- `terNO_AMM`: AMM ledger entry does not exist for specified asset pair +- `tecAMM_EMPTY`: AMM has zero LP tokens outstanding +- `terNO_ACCOUNT`: Any account in `AuthAccounts` does not exist +- `temBAD_AMM_TOKENS`: `BidMin` or `BidMax` issue (currency code + issuer) does not match the AMM's LP token issue +- `tecAMM_INVALID_TOKENS`: + - Account holds zero LP tokens (not an LP) + - `BidMin` or `BidMax` is greater than the account's LP token holdings, or greater than or equal to the AMM's total LP token balance + - `BidMin` > `BidMax` + +**Validation during doApply**[^ammbid-doapply-validation] + +[^ammbid-doapply-validation]: Validation during doApply: [`AMMBid.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMBid.cpp#L179-L355) + +- `tecAMM_FAILED`: Computed price exceeds `BidMax` +- `tecAMM_INVALID_TOKENS`: Pay price exceeds LP token holdings +- Propagate errors from `accountSend()` when transferring LP tokens between bidder, previous holder, and AMM account (see [Common Error Codes from accountSend()](#common-error-codes-from-accountsend)) + +### 3.5.2. State Changes + +The AMMBid transaction executes through the `applyBid()` function, which determines the slot price based on whether someone currently owns the auction slot and how much time has elapsed. For an unowned or expired slot, the bidder pays only the minimum price. For an owned slot, the price includes a 5% markup with a decay function over the 24-hour period. The system refunds the previous slot holder proportionally to their remaining time and burns the difference (bid price minus refund). State changes only occur when the bid execution succeeds. If validation fails (e.g., computed price exceeds `BidMax`, insufficient LP tokens), no ledger modifications are made. See [Bidding documentation](bidding.md) for the complete bidding logic including price calculation, refund mechanism, and LP token burning. + +- `AMM` object is **modified**: + - `AuctionSlot`: + - `Account`: Set to bidder + - `Expiration`: Set to current time + 86,400 seconds + - `Price`: Set to amount paid + - `DiscountedFee`: Set to `TradingFee / 10` when that quotient is non-zero; otherwise the field is removed (made absent) + - `AuthAccounts`: Set to specified accounts (or cleared if not specified) + - `LPTokenBalance`: Decreased by burned amount + +- LP tokens are **burned**: + - Bid amount (minus refund) burned from bidder's LP token balance + - Reduces total LP token supply + +- Previous slot holder receives **refund** (if slot not expired): + - Refund = `(1 - fractionUsed) * PricePaid` + - Sent as LP tokens from bidder to previous holder + +## 3.6. AMMDelete Transaction + +The `AMMDelete` transaction is used to clean up AMM instances that have been emptied (all LP tokens withdrawn). While the AMM can be automatically deleted when the last LP token is withdrawn, this transaction provides an explicit way to delete empty AMMs, especially useful when automatic deletion is incomplete. + +**Fields:** + +| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | +|------------|:---------:|:-----------:|:---------:|:-------------:|:-------------:|:------------| +| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMDelete"` | +| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account deleting the AMM instance | +| `Asset` | :heavy_check_mark: | `No` | `Object` | `Issue` | | One of the pool's assets | +| `Asset2` | :heavy_check_mark: | `No` | `Object` | `Issue` | | The other pool asset | +| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags (must be 0 for AMMDelete, only universal flags allowed) | + +### 3.6.1. Failure Conditions + +**Static validation**[^ammdelete-static-validation] + +[^ammdelete-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMDelete.cpp#L23-L30), [`preflight`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMDelete.cpp#L33-L36) + +- `temDISABLED`: + - AMM amendment not enabled + - either `Asset` or `Asset2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled +- `temINVALID_FLAG`: Invalid transaction flags (any flags set other than universal flags) + +**Validation against the ledger view**[^ammdelete-preclaim-validation] + +[^ammdelete-preclaim-validation]: Validation against ledger view (preclaim): [`AMMDelete.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMDelete.cpp#L39-L53) + +- `terNO_AMM`: AMM ledger entry does not exist for specified asset pair +- `tecAMM_NOT_EMPTY`: AMM has non-zero LP tokens outstanding (AMM must be empty to delete) + +**Validation during doApply**[^ammdelete-doapply-validation] + +[^ammdelete-doapply-validation]: Validation during doApply: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L713-L766) + +- `tecINTERNAL`: + - AMM ledger entry does not exist (should not happen if preclaim succeeded) + - AMM pseudo-account does not exist (should not happen if AMM entry exists) + - Directory node has invalid index during trustline deletion + - Non-trustline/non-MPToken ledger entry found in AMM owner directory (should only contain trust lines, MPTokens, and AMM entry) + - Trustline has non-zero balance during deletion (all trust lines should have zero balance if AMM is empty) + - Failed to remove AMM entry from owner directory + - Cannot delete root directory node +- `tecINCOMPLETE`: Too many trust lines to delete in a single transaction (limited by `kMaxDeletableAmmTrustLines`). The transaction should be called again to continue deletion. This is not an error - it indicates partial success. +- Propagate errors from `deleteAMMTrustLine()` when deleting individual trust lines: + - `tecINTERNAL`: Trust line SLE is null or has wrong type + - `tefBAD_LEDGER`: Failed to remove directory link during trust line deletion + +### 3.6.2. State Changes + +The AMMDelete transaction cleans up an empty AMM instance. The deletion process may complete in a single transaction or require multiple transactions if there are many trust lines. + +**Complete deletion (tesSUCCESS):** + +- `RippleState` objects (trust lines) are **deleted**: + - All trust lines associated with the AMM pseudo-account are removed + - This includes LP token trust lines and IOU asset trust lines + - Each trust line must have zero balance + - The counterparty (non-AMM) side of the trust line has its `OwnerCount` decremented + - Directory entries for each trust line are removed from both accounts' owner directories + - Limited to `kMaxDeletableAmmTrustLines` trust lines per transaction + +- `MPToken` objects are **deleted** (if AMM uses MPT assets): + - All MPToken entries associated with the AMM pseudo-account are removed + - Each MPToken must have zero `MPTAmount` and zero `LockedAmount` + - At most two MPToken objects (one per asset) + - Each MPToken is removed from the AMM pseudo-account's owner directory and erased; no `OwnerCount` is adjusted + - MPTokens are only deleted after all trust lines are deleted + +- `AMM` object is **deleted**: + - The AMM ledger entry is removed from the ledger + - The entry is removed from the AMM pseudo-account's owner directory + +- `AccountRoot` object (AMM pseudo-account) is **deleted**: + - The AMM pseudo-account is removed from the ledger + - Any remaining XRP balance should be zero (or minimal dust) + - The account's owner directory is removed + +- `DirectoryNode` objects are **deleted**: + - The AMM pseudo-account's owner directory is removed + - All directory links are cleaned up + +**Partial deletion (tecINCOMPLETE):** + +When there are too many trust lines to delete in a single transaction: + +- `RippleState` objects are **partially deleted**: + - Up to `kMaxDeletableAmmTrustLines` trust lines are deleted + - Remaining trust lines stay in the ledger + - Each deleted trust line decrements the counterparty account's `OwnerCount` + +- `MPToken` objects **remain** in the ledger: + - MPToken entries are not deleted during partial deletion + - MPTokens are only deleted after all trust lines are deleted + - This ensures AMM can be re-created with AMMDeposit if needed + +- `AMM` object is **deleted unless** there are remaining trust lines or MPTokens: + - When deletion is incomplete, the AMM object remains in the ledger + - Still has `LPTokenBalance` of zero + - Still references the pseudo-account + +- `AccountRoot` object (AMM pseudo-account) is **deleted unless** there are remaining trust lines or MPTokens: + - When deletion is incomplete, the pseudo-account remains in the ledger + - Owner directory still contains remaining trust lines and MPTokens (if present) + +- **Subsequent AMMDelete transactions** must be submitted: + - Each transaction deletes up to `kMaxDeletableAmmTrustLines` more trust lines + - Process continues until all trust lines are deleted + - Final transaction completes the full deletion (returns tesSUCCESS) + +**Note:** The `kMaxDeletableAmmTrustLines` limit exists to prevent transactions from consuming excessive resources. AMMs with many LPs (and therefore many LP token trust lines) will require multiple AMMDelete transactions to fully clean up. + +The deletion process: +1. Verifies the AMM exists and is empty (zero LP tokens) +2. Deletes all trust lines associated with the AMM account +3. Removes the AMM from owner directories +4. Deletes the AMM pseudo-account +5. Deletes the AMM ledger entry + +If there are too many trust lines to delete in a single transaction (limited by `kMaxDeletableAmmTrustLines`), the transaction returns `tecINCOMPLETE` and must be called again. + +## 3.7. AMMClawback Transaction + +The `AMMClawback` transaction allows asset issuers to claw back their issued assets from AMM liquidity pools by withdrawing them from a specific LP token holder's position. This transaction is only available when the [AMMClawback](https://xrpl.org/resources/known-amendments#ammclawback) amendment is enabled. + +Unlike the regular [Clawback transaction](../trust_lines/README.md#312-clawback-transaction) which claws back trust line tokens and [MPTs](../mpts/README.md#35-clawback-transaction-with-mpts) from individual holder balances, `AMMClawback` targets assets held in AMM liquidity pools. The issuer specifies an LP token holder, and the transaction withdraws the issuer's assets from the pool proportionally to that holder's LP token position, burning the corresponding LP tokens. + +**How it works:** + +The issuer identifies a holder who possesses LP tokens for the AMM pool. The transaction executes a proportional withdrawal from the pool, with the mechanics varying based on whether an explicit amount is specified. + +When no `Amount` is provided, the transaction burns all of the holder's LP tokens and withdraws both pool assets proportionally using the AMM's equal-withdrawal formula. The issuer's asset (`Asset`) is immediately clawed back - transferred from the pool to the issuer where it is effectively removed from circulation. The second asset (`Asset2`), if not issued by the same issuer or if the `tfClawTwoAssets` flag is not set, is transferred to the holder rather than being clawed back. + +When an `Amount` is specified, the transaction calculates the fraction of the pool that corresponds to the requested amount of the issuer's asset. It determines the number of LP tokens required to withdraw that precise amount, accounting for the current pool ratio. If the calculated LP tokens exceed the holder's balance, the transaction instead burns all available LP tokens and withdraws proportionally. Otherwise, it burns only the calculated LP tokens and withdraws both assets proportionally from the pool. + +If the `tfClawTwoAssets` flag is set - which requires the issuer to issue both pool assets - the second asset is also clawed back. Without this flag, the second asset remains with the holder, leaving them with that asset while the issuer's asset is removed from circulation. The withdrawal from the AMM pool ignores freeze and authorization restrictions (`FreezeHandling::IgnoreFreeze` and `AuthHandling::IgnoreAuth`), ensuring clawback operations succeed even when assets are frozen or the holder lacks authorization. The subsequent transfer from holder to issuer uses standard clawback mechanics, which also bypasses authorization and freeze checks. + +**Fields:** + +| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | +|-------------------|:------------------:|:-----------:|:--------------------------:|:-------------:|:-------------:|:------------------------------------------------------------------------------------------| +| `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"AMMClawback"` | +| `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Issuer account (must be issuer of `Asset`) | +| `Asset` | :heavy_check_mark: | `No` | `Object` | `Issue` | | Asset to claw back (issuer must match `Account`) | +| `Asset2` | :heavy_check_mark: | `No` | `Object` | `Issue` | | The other asset in the AMM pool | +| `Holder` | :heavy_check_mark: | `No` | `String` | `AccountID` | | LP token holder whose position is being clawed back | +| `Amount` | | `No` | `String - Currency Amount` | `Amount` | | Specific amount of `Asset` to claw back (if omitted, claws back holder's entire position) | +| `Flags` | | `No` | `Number` | `UInt32` | `0` | Transaction flags (see below) | + +**Transaction Flags:** + +| Flag Name | Hex Value | Description | +|-------------------|--------------|---------------------------------------------------------------------------------| +| `tfClawTwoAssets` | `0x00000001` | Claw back both assets (only valid when issuer issues both `Asset` and `Asset2`) | + +**Clawback mechanics:** + +The transaction uses AMM withdrawal logic internally: +- Calculates proportional amounts using the preservation function +- Burns LP tokens from the holder +- Transfers withdrawn assets from AMM pool to issuer +- For trust line tokens: Assets are burned (balance adjusts on shared RippleState) +- For MPTs: Assets are burned (holder's `MPTAmount` decreases, issuance's `OutstandingAmount` decreases) + +### 3.7.1. Failure Conditions + +**Static validation**[^ammclawback-static-validation] + +[^ammclawback-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L43-L53), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L37-L40), [`preflight`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L56-L99) + +- `temDISABLED`: + - [AMMClawback](https://xrpl.org/resources/known-amendments#ammclawback) amendment not enabled + - Either `Asset` or `Asset2` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment not enabled +- `temMALFORMED`: + - `Account` equals `Holder` (cannot claw back from self) + - `Asset` is XRP (XRP cannot be clawed back) + - `Asset.issuer` does not match `Account` (issuer must match transaction sender) +- `temBAD_AMOUNT`: + - `Amount` is specified but `Amount.asset` does not match `Asset` + - `Amount` is zero or negative +- `temINVALID_FLAG`: + - `tfClawTwoAssets` is set but `Asset.issuer` differs from `Asset2.issuer` (can only claw both assets if issuer issues both) + - Invalid flags specified + +**Validation against the ledger view**[^ammclawback-preclaim-validation] + +[^ammclawback-preclaim-validation]: Validation against ledger view (preclaim): [`AMMClawback.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L101-L153) + +- `terNO_ACCOUNT`: Issuer account or holder account does not exist +- `terNO_AMM`: AMM pool does not exist for the specified asset pair +- `tecNO_PERMISSION`: + - For trust line tokens (`Asset` is `Issue`): + - Issuer does not have `lsfAllowTrustLineClawback` flag set + - Issuer has `lsfNoFreeze` flag set + - For MPTs (`Asset` is `MPTIssue`): + - MPT issuance does not have `lsfMPTCanClawback` flag set + - `Asset.issuer` does not match the MPT issuance's issuer + - With `tfClawTwoAssets`: `Asset2` does not meet the clawback requirements above + +**Validation during doApply**[^ammclawback-doapply-validation] + +[^ammclawback-doapply-validation]: Validation during doApply: [`AMMClawback.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L168-L300) + +- `tecINTERNAL`: + - AMM ledger entry does not exist + - AMM pseudo-account does not exist + - With [fixAMMClawbackRounding](https://xrpl.org/resources/known-amendments#fixammclawbackrounding): LP token balance verification encountered internal error when checking if holder is the only LP +- `tecAMM_BALANCE`: Holder has zero LP tokens (nothing to claw back) +- `tecAMM_INVALID_TOKENS`: + - With [fixAMMClawbackRounding](https://xrpl.org/resources/known-amendments#fixammclawbackrounding): Holder is the only remaining LP and their LP token balance differs from the AMM's `LPTokenBalance` by more than 0.1% + - Calculated LP token amount during withdrawal is zero or invalid + - LP token balance adjustment failed during withdrawal +- `tecPRECISION_LOSS`: (with both `fixCleanup3_3_0` and [fixAMMv1_3](https://xrpl.org/resources/known-amendments#fixammv1_3)) the pool product invariant fails after computing the new LP token balance, the same check as in AMMWithdraw +- Propagate errors from withdrawal logic (uses `AMMWithdraw::equalWithdrawTokens` or `equalWithdrawMatchingOneAmount`): + - `tecAMM_FAILED`: Withdrawal constraints not satisfied + - Other withdrawal-related errors (see [AMMWithdraw Failure Conditions](#332-failure-conditions)) + +### 3.7.2. State Changes + +The `AMMClawback` transaction withdraws assets from an AMM pool by burning LP tokens from the holder's balance. + +**LP Token Changes:** + +- Holder's LP token balance is **decreased**: + - LP tokens are burned (destroyed from circulation) + - The amount burned equals either: + - All of holder's LP tokens (if `Amount` not specified) + - Proportional LP tokens to withdraw the specified `Amount` + - If holder's LP token balance reaches zero and the trust line has no other non-default fields, the trust line may be deleted + - Holder's `OwnerCount` may decrement if trust line is deleted + +**AMM Ledger Entry Changes:** + +- `AMM` object is **modified**: + - `LPTokenBalance`: Decreased by the burned LP tokens + - If `LPTokenBalance` reaches zero, the AMM may be automatically deleted (see [AMMDelete](#36-ammdelete-transaction)) + +**Pool Asset Changes:** + +- AMM pseudo-account's asset balances are **decreased**: + - For trust line tokens (`RippleState` balance adjusted) + - For MPTs (`MPToken.MPTAmount` decreased) + - For XRP (`AccountRoot.Balance` decreased) + - Amounts withdrawn are proportional based on burned LP tokens and current pool balances + +**Asset Distribution:** + +- **`Asset` (always clawed back)**: + - For trust line tokens: Transferred from holder to issuer via `directSendNoFee`, adjusting the shared `RippleState` balance + - For MPTs: Burned from holder's `MPToken` (decreases holder's `MPTAmount` and issuance's `OutstandingAmount`) + +- **`Asset2` (conditionally clawed back)**: + - **With `tfClawTwoAssets`**: Same treatment as `Asset` (transferred to issuer and burned) + - **Without `tfClawTwoAssets`**: Remains with the holder (transferred from AMM pool to holder's balance) diff --git a/docs/amms/bidding.md b/docs/amms/bidding.md index 279b10f..18ee823 100644 --- a/docs/amms/bidding.md +++ b/docs/amms/bidding.md @@ -1,441 +1,441 @@ -# Index - -- [1. Introduction](#1-introduction) - - [1.1. Bidding Process](#11-bidding-process) - - [1.2. Implementation](#12-implementation) -- [2. applyBid](#2-applybid) - - [2.1. applyBid Pseudo-Code](#21-applybid-pseudo-code) -- [3. updateSlot](#3-updateslot) - - [3.1. updateSlot Pseudo-Code](#31-updateslot-pseudo-code) -- [4. validOwner](#4-validowner) - - [4.1. validOwner Pseudo-Code](#41-validowner-pseudo-code) -- [5. getPayPrice](#5-getpayprice) - - [5.1. getPayPrice Pseudo-Code](#51-getpayprice-pseudo-code) -- [6. Price Calculation](#6-price-calculation) - - [6.1. Minimum Slot Price](#61-minimum-slot-price) - - [6.2. Computed Price for Owned Slots](#62-computed-price-for-owned-slots) - - [6.3. Time Slot Calculation](#63-time-slot-calculation) -- [7. Refund Mechanism](#7-refund-mechanism) -- [8. LP Token Burning](#8-lp-token-burning) - -# 1. Introduction - -The auction slot is a 24-hour privilege that allows an account to trade through the AMM at a discounted fee (1/10th of the regular trading fee). When an AMM is created, the creator automatically receives the auction slot for free (with price set to 0). Subsequently, accounts can bid LP tokens to win the slot from the current holder, and the winning bid amount (minus any refund to the previous owner) is burned from circulation. - -AMM pools cannot directly observe external market prices, so when asset prices diverge between the pool and external markets, arbitrageurs must intervene to rebalance the pool. However, traditional arbitrage faces two problems: (1) arbitrageurs must wait until their profit exceeds the trading fee, creating a window where the pool offers suboptimal prices and experiences reduced trading volume, and (2) multiple arbitrageurs compete in a race, reducing their individual success probability. - -The auction slot solves this by offering discounted trading fees to the slot owner, enabling them to execute arbitrage immediately without waiting for profits to exceed fees. This eliminates the race and narrows the window of price inefficiency. The slot owner pays for this advantage by bidding LP tokens, which are burned upon winning - this burn reduces total LP token supply while pool assets remain unchanged, effectively distributing the auction proceeds to all LP token holders through increased ownership percentage. - -**LP tokens** are used to bid. The winning bid amount (minus any refund) is burned from the total LP token supply, effectively distributing value to all remaining LP token holders. - -The auction slot lasts 24 hours (86,400 seconds). Since the slot can be taken over by a new bidder at any time during this period, the slot owner may not use the full 24 hours. - -If a new bidder takes over the slot before it expires, the previous owner receives a **refund** proportional to the remaining time: `refund = (1 - fractionUsed) * pricePurchased`. For example, if the previous owner paid 1,000 LP tokens and used 6 hours (time slot 5, so `(5+1)/20 = 30%` is treated as used), they receive a refund of 700 LP tokens (70% of what they paid). This compensates them for the unused portion of their slot time. The slot's lifecycle is tracked using 20 time intervals of ~1.2 hours each (4,320 seconds) to calculate how much time has been used. - -**Price Structure**: -- When no one owns the slot or it has expired, bidders pay the minimum slot price: `TotalLPTokens * TradingFee / 25`. Note that if the `TradingFee` is 0, the minimum slot price is also 0, making the slot free to claim. -- When the slot is owned, new bidders must pay a computed price that starts with a 5% markup over the previous purchase price. This markup decays exponentially as time progresses - early in the slot period, the price is close to `pricePurchased * 1.05 + minSlotPrice`, but as time elapses, the markup portion diminishes and the price approaches just `minSlotPrice` - -The slot owner pays only **1/10th** of the regular trading fee when they trade through the AMM. The slot owner can designate up to 4 additional accounts to share the discounted fee benefit. - -## 1.1. Bidding Process - -When an account submits an AMMBid transaction, they can optionally specify `BidMin` and `BidMax` constraints to control how much they're willing to pay. The system first calculates a computed price based on whether the slot is currently owned and how much time has elapsed since it was purchased. If no one owns the slot or it has expired, the computed price is the minimum slot price. If the slot is owned, the computed price starts with a 5% markup over what the previous owner paid, with this markup decaying exponentially as the slot ages. - -The actual pay price is then determined by reconciling the computed price with the bidder's constraints. If the slot is currently owned and the bid succeeds, the previous owner receives a time-based refund proportional to the remaining slot time - this refund comes from the new bidder's payment. The remaining amount (pay price minus any refund) is burned from the LP token supply, reducing total supply while keeping pool assets unchanged. Finally, the auction slot is updated with the new owner, a fresh 24-hour expiration time, and the discounted trading fee. - -## 1.2. Implementation - -In the `xrpld` C++ implementation (`AMMBid.cpp`), the main transaction handler calls the [`applyBid`](#2-applybid) function with the transaction context, sandbox view, and bidder account. This function retrieves the AMM ledger entry and the bidder's LP token holdings, then calculates the [minimum slot price](#61-minimum-slot-price) and discounted fee based on the AMM's trading fee and total LP token supply. - -The function calls [`ammAuctionTimeSlot`](#63-time-slot-calculation) to determine the current time interval (0-19) based on elapsed time since the slot was won. It then defines three lambda functions inline: [`validOwner`](#4-validowner) (checks if the current slot owner is valid and not in the expiring interval), [`updateSlot`](#3-updateslot) (updates auction slot fields and burns LP tokens), and [`getPayPrice`](#5-getpayprice) (determines the actual price to pay given the computed price and bid constraints). - -The logic branches into two cases: If no one owns the slot or it has expired (determined by checking the slot's account field and calling [`validOwner`](#4-validowner)), the bidder pays the [minimum slot price](#61-minimum-slot-price) and the entire amount is [burned](#8-lp-token-burning). If the slot is currently owned, the function calculates the [time-based pricing](#62-computed-price-for-owned-slots) using the decay formula `pricePurchased * 1.05 * (1 - fractionUsed^60) + minSlotPrice`, determines the pay price via [`getPayPrice`](#5-getpayprice), transfers a proportional [refund](#7-refund-mechanism) to the previous owner using `accountSend`, and [burns](#8-lp-token-burning) the remaining amount. In both cases, [`updateSlot`](#3-updateslot) is called to finalize the auction slot state. - -# 2. applyBid - -The `applyBid` function is the main entry point that orchestrates the entire auction slot bidding flow. It handles slot ownership validation, price calculation, refund processing, and LP token burning. See [Implementation](#12-implementation) for a detailed walkthrough of how this function executes. - -## 2.1. applyBid Pseudo-Code - -```python -def applyBid(ctx: ApplyContext, sb: &Sandbox, account: AccountID): - """ - Main bidding logic for auction slot. - Returns (TER, bool) where bool indicates whether to apply the sandbox. - """ - - # Get AMM ledger entry - ammSle = sb.getAMM(ctx.tx[sfAsset], ctx.tx[sfAsset2]) - if not ammSle: - return (tecINTERNAL, False) - - lptAMMBalance = ammSle[sfLPTokenBalance] - lpTokens = ammLPHolds(sb, ammSle, account) - - # Ensure auction slot exists - # Without fixInnerObjTemplate: Create slot if missing - # With fixInnerObjTemplate: Slot must already exist - if not rules.enabled(fixInnerObjTemplate): - if not ammSle.isFieldPresent(sfAuctionSlot): - ammSle.makeFieldPresent(sfAuctionSlot) - else: - if not ammSle.isFieldPresent(sfAuctionSlot): - return (tecINTERNAL, False) - - auctionSlot = ammSle.peekFieldObject(sfAuctionSlot) - current = ctx.view().header().parentCloseTime # in seconds - - # Calculate fees and prices - discountedFee = ammSle[sfTradingFee] / kAuctionSlotDiscountedFeeFraction # Divide by 10 - tradingFee = getFee(ammSle[sfTradingFee]) - minSlotPrice = lptAMMBalance * tradingFee / kAuctionSlotMinFeeFraction # Divide by 25 - - # Determine current time slot (0-19) - # Returns None if slot is not owned or expired - timeSlot = ammAuctionTimeSlot(current, auctionSlot) - - # Get bid constraints from transaction - bidMin = ctx.tx[~sfBidMin] - bidMax = ctx.tx[~sfBidMax] - - # CASE 1: No one owns slot or slot is expired - currentOwner = auctionSlot[~sfAccount] - if not currentOwner or not validOwner(currentOwner, timeSlot, sb): - # Pay minimum price, no refund - payPrice = getPayPrice(minSlotPrice, bidMin, bidMax, lpTokens) - if payPrice is error: - return (payPrice.error(), False) - - # Update slot with new owner - result = updateSlot( - sb, - ammSle, - auctionSlot, - account, - current, - discountedFee, - payPrice, - payPrice, # burn entire amount (no refund) - lpTokens.asset(), - lptAMMBalance, - ctx.tx, - ) - return (result, result == tesSUCCESS) - - # CASE 2: Slot is currently owned - pricePurchased = auctionSlot[sfPrice] - fractionUsed = (timeSlot + 1) / kAuctionSlotTimeIntervals # timeSlot is 0-19, so (timeSlot+1)/20 - fractionRemaining = 1 - fractionUsed - - # Calculate computed price based on time slot - if timeSlot == 0: - # First interval: simple 5% markup - computedPrice = pricePurchased * 1.05 + minSlotPrice - else: - # Other intervals: decay function - computedPrice = pricePurchased * 1.05 * (1 - fractionUsed^60) + minSlotPrice - - payPrice = getPayPrice(computedPrice, bidMin, bidMax, lpTokens) - if payPrice is error: - return (payPrice.error(), False) - - # Calculate refund to previous owner - refund = fractionRemaining * pricePurchased - if refund > payPrice: - # This should never happen - log "AMM Bid: refund exceeds payPrice" - return (tecINTERNAL, False) - - # Send refund to previous owner - result = accountSend( - sb, - from = account, # New bidder pays - to = auctionSlot[sfAccount], # Previous owner receives - amount = toSTAmount(lpTokens.asset(), refund), - ) - if result != tesSUCCESS: - log "AMM Bid: failed to refund" - return (result, False) - - # Update slot with new owner - burn = payPrice - refund - result = updateSlot( - sb, - ammSle, - auctionSlot, - account, - current, - discountedFee, - payPrice, - burn, - lpTokens.asset(), - lptAMMBalance, - ctx.tx, - ) - - return (result, result == tesSUCCESS) -``` - -# 3. updateSlot - -Updates the auction slot with the new bidder and burns LP tokens. - -## 3.1. updateSlot Pseudo-Code - -```python -def updateSlot( - sb: &Sandbox, - ammSle, # AMM ledger entry - auctionSlot, # Auction slot object reference - account: AccountID, # New bidder account - current: int, # Current time in seconds - fee: int, # Discounted fee - price: Number, # Price paid for the slot - burn: Number, # Amount to burn - lpTokenIssue, # LP token issue - lptAMMBalance, # Total outstanding LP tokens - tx: Transaction, # Transaction - ) -> TER: - """ - Update auction slot fields and burn LP tokens. - """ - - # Update auction slot fields - auctionSlot.setAccountID(sfAccount, account) - auctionSlot.setFieldU32(sfExpiration, current + kTotalTimeSlotSecs) # +86,400 seconds - - if fee != 0: - auctionSlot.setFieldU16(sfDiscountedFee, fee) - elif auctionSlot.isFieldPresent(sfDiscountedFee): - auctionSlot.makeFieldAbsent(sfDiscountedFee) - - auctionSlot.setFieldAmount(sfPrice, toSTAmount(lpTokenIssue, price)) - - if tx.isFieldPresent(sfAuthAccounts): - auctionSlot.setFieldArray(sfAuthAccounts, tx[sfAuthAccounts]) - else: - auctionSlot.makeFieldAbsent(sfAuthAccounts) - - # Burn LP tokens - saBurn = adjustLPTokens(lptAMMBalance, toSTAmount(lpTokenIssue, burn), IsDeposit=False) # helpers.md#22-adjustlptokens - - if saBurn >= lptAMMBalance: - # This should never happen - log "AMM Bid: LP Token burn exceeds AMM balance" - return tecINTERNAL - - result = redeemIOU(sb, account, saBurn, lpTokenIssue, journal) - if result != tesSUCCESS: - log "AMM Bid: failed to redeem" - return result - - ammSle.setFieldAmount(sfLPTokenBalance, lptAMMBalance - saBurn) - sb.update(ammSle) - - return tesSUCCESS -``` - -# 4. validOwner - -Checks if the current slot owner is valid and the slot is not expired. - -## 4.1. validOwner Pseudo-Code - -```python -def validOwner(account: AccountID, timeSlot: Optional[int], sb: &Sandbox) -> bool: - """ - Check if account is a valid auction slot owner. - Valid range is 0-19 but tailing slot (19) pays MinSlotPrice and doesn't refund - so check is < 19 instead of <= 19 to optimize. - """ - - # Valid range is 0-19 but the tailing slot pays MinSlotPrice - # and doesn't refund so the check is < instead of <= to optimize. - # timeSlot is an optional: test presence separately, since slot 0 is a valid owner. - return timeSlot is not None and timeSlot < 19 and sb.read(keylet.account(account)) -``` - -# 5. getPayPrice - -The `getPayPrice` function reconciles the system-computed price with the bidder's optional `BidMin` and `BidMax` constraints. Bidders use these constraints to protect themselves from price volatility: `BidMin` ensures they pay at least a certain amount (useful when they want to guarantee winning the slot even if the computed price is lower), while `BidMax` sets an upper limit they're willing to pay (preventing overpayment if the computed price is unexpectedly high). The function validates that the computed price falls within the bidder's acceptable range and returns an error if it doesn't, or returns the reconciled pay price if it does. - -## 5.1. getPayPrice Pseudo-Code - -```python -def getPayPrice( - computedPrice: Number, - bidMin: Optional[STAmount], - bidMax: Optional[STAmount], - lpTokens: STAmount) -> Expected[Number, TER]: - """ - Determine pay price from computed price and bid constraints. - - Returns either the price to pay or an error code. - """ - - # Both min/max bid price are defined - if bidMin and bidMax: - if computedPrice <= bidMax: - payPrice = max(computedPrice, bidMin) - else: - log "AMM Bid: not in range" - return tecAMM_FAILED - - # Only bidMin defined - elif bidMin: - payPrice = max(computedPrice, bidMin) - - # Only bidMax defined - elif bidMax: - if computedPrice <= bidMax: - payPrice = computedPrice - else: - log "AMM Bid: not in range" - return tecAMM_FAILED - - # Neither defined - else: - payPrice = computedPrice - - # Final validation: check if payPrice exceeds LP token holdings - if payPrice > lpTokens: - return tecAMM_INVALID_TOKENS - - return payPrice -``` - -# 6. Price Calculation - -## 6.1. Minimum Slot Price - -The minimum slot price is always: - -``` -minSlotPrice = (TotalLPTokens * TradingFee) / 25 -``` - -This is the price paid when: -- No one owns the slot -- The slot has expired -- The slot is in the tailing period (slot 19) - -## 6.2. Computed Price for Owned Slots - -When the slot is owned and not expired, the computed price depends on the time slot: - -**For the first interval (timeSlot = 0):** - -``` -computedPrice = pricePurchased * 1.05 + minSlotPrice -``` - -This applies a simple 5% markup to the price the current owner paid. - -**For other intervals (timeSlot = 1-18):** - -``` -fractionUsed = (timeSlot + 1) / 20 -computedPrice = pricePurchased * 1.05 * (1 - fractionUsed^60) + minSlotPrice -``` - -The `fractionUsed^60` creates a decay function that makes the price increase more slowly as time progresses. - -**Example:** - -If the current owner paid 1,000 LP tokens and we're in slot 5: -``` -fractionUsed = (5 + 1) / 20 = 0.3 (30% of time elapsed) -computedPrice = 1,000 * 1.05 * (1 - 0.3^60) + minSlotPrice -computedPrice =~ 1,050 + minSlotPrice -``` - -The `0.3^60` is essentially 0, so the price is close to the full 105% markup early in the slot period. - -## 6.3. Time Slot Calculation - -The auction slot is divided into 20 time intervals over 24 hours: - -``` -kTotalTimeSlotSecs = 86,400 seconds (24 hours) -kAuctionSlotTimeIntervals = 20 -Each interval = 86,400 / 20 = 4,320 seconds (~1.2 hours) -``` - -The time slot is calculated as: - -``` -timeSlot = (currentTime - slotExpiration + 86,400) / 4,320 -``` - -Valid time slots are 0-19, where: -- 0 = first interval (just won the slot) -- 19 = last interval (tailing slot) - -The `ammAuctionTimeSlot()` function returns: -- `None` if the slot has expired (or has an invalid expiration) -- A value 0-19 indicating the current time interval - -# 7. Refund Mechanism - -When a new bidder wins the slot from a current owner: - -1. Calculate the fraction of time remaining: - ``` - fractionRemaining = 1 - (timeSlot + 1) / 20 - ``` - -2. Calculate refund to previous owner: - ``` - refund = fractionRemaining * pricePurchased - ``` - -3. Transfer refund (in LP tokens) from new bidder to previous owner via `accountSend()` - -4. Burn remaining amount: - ``` - burn = payPrice - refund - ``` - -**Example:** - -Current owner paid 1,000 LP tokens and we're in slot 8: -``` -fractionUsed = (8 + 1) / 20 = 0.45 (45% of time used) -fractionRemaining = 1 - 0.45 = 0.55 (55% of time remaining) -refund = 0.55 * 1,000 = 550 LP tokens -``` - -If new bidder pays 1,200 LP tokens: -``` -burn = 1,200 - 550 = 650 LP tokens -``` - -The refund compensates the previous owner for the unused portion of their slot time. - -# 8. LP Token Burning - -The burn amount (bid price minus refund) is removed from the LP token supply: - -1. **Adjust burn amount** for LP token precision: - ``` - saBurn = adjustLPTokens(lptAMMBalance, burn, IsDeposit=False) - ``` - -2. **Validate burn amount** doesn't exceed AMM balance: - ``` - if saBurn >= lptAMMBalance: - return tecINTERNAL - ``` - -3. **Redeem (burn) LP tokens** from bidder's balance: - ``` - redeemIOU(sb, account, saBurn, lpTokens.get()) - ``` - -4. **Decrease AMM's LPTokenBalance**: - ``` - ammSle.setFieldAmount(sfLPTokenBalance, lptAMMBalance - saBurn) - ``` - -This effectively distributes value to all remaining LP token holders by reducing the total supply while keeping the pool's assets unchanged. \ No newline at end of file +# Index + +- [1. Introduction](#1-introduction) + - [1.1. Bidding Process](#11-bidding-process) + - [1.2. Implementation](#12-implementation) +- [2. applyBid](#2-applybid) + - [2.1. applyBid Pseudo-Code](#21-applybid-pseudo-code) +- [3. updateSlot](#3-updateslot) + - [3.1. updateSlot Pseudo-Code](#31-updateslot-pseudo-code) +- [4. validOwner](#4-validowner) + - [4.1. validOwner Pseudo-Code](#41-validowner-pseudo-code) +- [5. getPayPrice](#5-getpayprice) + - [5.1. getPayPrice Pseudo-Code](#51-getpayprice-pseudo-code) +- [6. Price Calculation](#6-price-calculation) + - [6.1. Minimum Slot Price](#61-minimum-slot-price) + - [6.2. Computed Price for Owned Slots](#62-computed-price-for-owned-slots) + - [6.3. Time Slot Calculation](#63-time-slot-calculation) +- [7. Refund Mechanism](#7-refund-mechanism) +- [8. LP Token Burning](#8-lp-token-burning) + +# 1. Introduction + +The auction slot is a 24-hour privilege that allows an account to trade through the AMM at a discounted fee (1/10th of the regular trading fee). When an AMM is created, the creator automatically receives the auction slot for free (with price set to 0). Subsequently, accounts can bid LP tokens to win the slot from the current holder, and the winning bid amount (minus any refund to the previous owner) is burned from circulation. + +AMM pools cannot directly observe external market prices, so when asset prices diverge between the pool and external markets, arbitrageurs must intervene to rebalance the pool. However, traditional arbitrage faces two problems: (1) arbitrageurs must wait until their profit exceeds the trading fee, creating a window where the pool offers suboptimal prices and experiences reduced trading volume, and (2) multiple arbitrageurs compete in a race, reducing their individual success probability. + +The auction slot solves this by offering discounted trading fees to the slot owner, enabling them to execute arbitrage immediately without waiting for profits to exceed fees. This eliminates the race and narrows the window of price inefficiency. The slot owner pays for this advantage by bidding LP tokens, which are burned upon winning - this burn reduces total LP token supply while pool assets remain unchanged, effectively distributing the auction proceeds to all LP token holders through increased ownership percentage. + +**LP tokens** are used to bid. The winning bid amount (minus any refund) is burned from the total LP token supply, effectively distributing value to all remaining LP token holders. + +The auction slot lasts 24 hours (86,400 seconds). Since the slot can be taken over by a new bidder at any time during this period, the slot owner may not use the full 24 hours. + +If a new bidder takes over the slot before it expires, the previous owner receives a **refund** proportional to the remaining time: `refund = (1 - fractionUsed) * pricePurchased`. For example, if the previous owner paid 1,000 LP tokens and used 6 hours (time slot 5, so `(5+1)/20 = 30%` is treated as used), they receive a refund of 700 LP tokens (70% of what they paid). This compensates them for the unused portion of their slot time. The slot's lifecycle is tracked using 20 time intervals of ~1.2 hours each (4,320 seconds) to calculate how much time has been used. + +**Price Structure**: +- When no one owns the slot or it has expired, bidders pay the minimum slot price: `TotalLPTokens * TradingFee / 25`. Note that if the `TradingFee` is 0, the minimum slot price is also 0, making the slot free to claim. +- When the slot is owned, new bidders must pay a computed price that starts with a 5% markup over the previous purchase price. This markup decays exponentially as time progresses - early in the slot period, the price is close to `pricePurchased * 1.05 + minSlotPrice`, but as time elapses, the markup portion diminishes and the price approaches just `minSlotPrice` + +The slot owner pays only **1/10th** of the regular trading fee when they trade through the AMM. The slot owner can designate up to 4 additional accounts to share the discounted fee benefit. + +## 1.1. Bidding Process + +When an account submits an AMMBid transaction, they can optionally specify `BidMin` and `BidMax` constraints to control how much they're willing to pay. The system first calculates a computed price based on whether the slot is currently owned and how much time has elapsed since it was purchased. If no one owns the slot or it has expired, the computed price is the minimum slot price. If the slot is owned, the computed price starts with a 5% markup over what the previous owner paid, with this markup decaying exponentially as the slot ages. + +The actual pay price is then determined by reconciling the computed price with the bidder's constraints. If the slot is currently owned and the bid succeeds, the previous owner receives a time-based refund proportional to the remaining slot time - this refund comes from the new bidder's payment. The remaining amount (pay price minus any refund) is burned from the LP token supply, reducing total supply while keeping pool assets unchanged. Finally, the auction slot is updated with the new owner, a fresh 24-hour expiration time, and the discounted trading fee. + +## 1.2. Implementation + +In the `xrpld` C++ implementation (`AMMBid.cpp`), the main transaction handler calls the [`applyBid`](#2-applybid) function with the transaction context, sandbox view, and bidder account. This function retrieves the AMM ledger entry and the bidder's LP token holdings, then calculates the [minimum slot price](#61-minimum-slot-price) and discounted fee based on the AMM's trading fee and total LP token supply. + +The function calls [`ammAuctionTimeSlot`](#63-time-slot-calculation) to determine the current time interval (0-19) based on elapsed time since the slot was won. It then defines three lambda functions inline: [`validOwner`](#4-validowner) (checks if the current slot owner is valid and not in the expiring interval), [`updateSlot`](#3-updateslot) (updates auction slot fields and burns LP tokens), and [`getPayPrice`](#5-getpayprice) (determines the actual price to pay given the computed price and bid constraints). + +The logic branches into two cases: If no one owns the slot or it has expired (determined by checking the slot's account field and calling [`validOwner`](#4-validowner)), the bidder pays the [minimum slot price](#61-minimum-slot-price) and the entire amount is [burned](#8-lp-token-burning). If the slot is currently owned, the function calculates the [time-based pricing](#62-computed-price-for-owned-slots) using the decay formula `pricePurchased * 1.05 * (1 - fractionUsed^60) + minSlotPrice`, determines the pay price via [`getPayPrice`](#5-getpayprice), transfers a proportional [refund](#7-refund-mechanism) to the previous owner using `accountSend`, and [burns](#8-lp-token-burning) the remaining amount. In both cases, [`updateSlot`](#3-updateslot) is called to finalize the auction slot state. + +# 2. applyBid + +The `applyBid` function is the main entry point that orchestrates the entire auction slot bidding flow. It handles slot ownership validation, price calculation, refund processing, and LP token burning. See [Implementation](#12-implementation) for a detailed walkthrough of how this function executes. + +## 2.1. applyBid Pseudo-Code + +```python +def applyBid(ctx: ApplyContext, sb: &Sandbox, account: AccountID): + """ + Main bidding logic for auction slot. + Returns (TER, bool) where bool indicates whether to apply the sandbox. + """ + + # Get AMM ledger entry + ammSle = sb.getAMM(ctx.tx[sfAsset], ctx.tx[sfAsset2]) + if not ammSle: + return (tecINTERNAL, False) + + lptAMMBalance = ammSle[sfLPTokenBalance] + lpTokens = ammLPHolds(sb, ammSle, account) + + # Ensure auction slot exists + # Without fixInnerObjTemplate: Create slot if missing + # With fixInnerObjTemplate: Slot must already exist + if not rules.enabled(fixInnerObjTemplate): + if not ammSle.isFieldPresent(sfAuctionSlot): + ammSle.makeFieldPresent(sfAuctionSlot) + else: + if not ammSle.isFieldPresent(sfAuctionSlot): + return (tecINTERNAL, False) + + auctionSlot = ammSle.peekFieldObject(sfAuctionSlot) + current = ctx.view().header().parentCloseTime # in seconds + + # Calculate fees and prices + discountedFee = ammSle[sfTradingFee] / kAuctionSlotDiscountedFeeFraction # Divide by 10 + tradingFee = getFee(ammSle[sfTradingFee]) + minSlotPrice = lptAMMBalance * tradingFee / kAuctionSlotMinFeeFraction # Divide by 25 + + # Determine current time slot (0-19) + # Returns None if slot is not owned or expired + timeSlot = ammAuctionTimeSlot(current, auctionSlot) + + # Get bid constraints from transaction + bidMin = ctx.tx[~sfBidMin] + bidMax = ctx.tx[~sfBidMax] + + # CASE 1: No one owns slot or slot is expired + currentOwner = auctionSlot[~sfAccount] + if not currentOwner or not validOwner(currentOwner, timeSlot, sb): + # Pay minimum price, no refund + payPrice = getPayPrice(minSlotPrice, bidMin, bidMax, lpTokens) + if payPrice is error: + return (payPrice.error(), False) + + # Update slot with new owner + result = updateSlot( + sb, + ammSle, + auctionSlot, + account, + current, + discountedFee, + payPrice, + payPrice, # burn entire amount (no refund) + lpTokens.asset(), + lptAMMBalance, + ctx.tx, + ) + return (result, result == tesSUCCESS) + + # CASE 2: Slot is currently owned + pricePurchased = auctionSlot[sfPrice] + fractionUsed = (timeSlot + 1) / kAuctionSlotTimeIntervals # timeSlot is 0-19, so (timeSlot+1)/20 + fractionRemaining = 1 - fractionUsed + + # Calculate computed price based on time slot + if timeSlot == 0: + # First interval: simple 5% markup + computedPrice = pricePurchased * 1.05 + minSlotPrice + else: + # Other intervals: decay function + computedPrice = pricePurchased * 1.05 * (1 - fractionUsed^60) + minSlotPrice + + payPrice = getPayPrice(computedPrice, bidMin, bidMax, lpTokens) + if payPrice is error: + return (payPrice.error(), False) + + # Calculate refund to previous owner + refund = fractionRemaining * pricePurchased + if refund > payPrice: + # This should never happen + log "AMM Bid: refund exceeds payPrice" + return (tecINTERNAL, False) + + # Send refund to previous owner + result = accountSend( + sb, + from = account, # New bidder pays + to = auctionSlot[sfAccount], # Previous owner receives + amount = toSTAmount(lpTokens.asset(), refund), + ) + if result != tesSUCCESS: + log "AMM Bid: failed to refund" + return (result, False) + + # Update slot with new owner + burn = payPrice - refund + result = updateSlot( + sb, + ammSle, + auctionSlot, + account, + current, + discountedFee, + payPrice, + burn, + lpTokens.asset(), + lptAMMBalance, + ctx.tx, + ) + + return (result, result == tesSUCCESS) +``` + +# 3. updateSlot + +Updates the auction slot with the new bidder and burns LP tokens. + +## 3.1. updateSlot Pseudo-Code + +```python +def updateSlot( + sb: &Sandbox, + ammSle, # AMM ledger entry + auctionSlot, # Auction slot object reference + account: AccountID, # New bidder account + current: int, # Current time in seconds + fee: int, # Discounted fee + price: Number, # Price paid for the slot + burn: Number, # Amount to burn + lpTokenIssue, # LP token issue + lptAMMBalance, # Total outstanding LP tokens + tx: Transaction, # Transaction + ) -> TER: + """ + Update auction slot fields and burn LP tokens. + """ + + # Update auction slot fields + auctionSlot.setAccountID(sfAccount, account) + auctionSlot.setFieldU32(sfExpiration, current + kTotalTimeSlotSecs) # +86,400 seconds + + if fee != 0: + auctionSlot.setFieldU16(sfDiscountedFee, fee) + elif auctionSlot.isFieldPresent(sfDiscountedFee): + auctionSlot.makeFieldAbsent(sfDiscountedFee) + + auctionSlot.setFieldAmount(sfPrice, toSTAmount(lpTokenIssue, price)) + + if tx.isFieldPresent(sfAuthAccounts): + auctionSlot.setFieldArray(sfAuthAccounts, tx[sfAuthAccounts]) + else: + auctionSlot.makeFieldAbsent(sfAuthAccounts) + + # Burn LP tokens + saBurn = adjustLPTokens(lptAMMBalance, toSTAmount(lpTokenIssue, burn), IsDeposit=False) # helpers.md#22-adjustlptokens + + if saBurn >= lptAMMBalance: + # This should never happen + log "AMM Bid: LP Token burn exceeds AMM balance" + return tecINTERNAL + + result = redeemIOU(sb, account, saBurn, lpTokenIssue, journal) + if result != tesSUCCESS: + log "AMM Bid: failed to redeem" + return result + + ammSle.setFieldAmount(sfLPTokenBalance, lptAMMBalance - saBurn) + sb.update(ammSle) + + return tesSUCCESS +``` + +# 4. validOwner + +Checks if the current slot owner is valid and the slot is not expired. + +## 4.1. validOwner Pseudo-Code + +```python +def validOwner(account: AccountID, timeSlot: Optional[int], sb: &Sandbox) -> bool: + """ + Check if account is a valid auction slot owner. + Valid range is 0-19 but tailing slot (19) pays MinSlotPrice and doesn't refund + so check is < 19 instead of <= 19 to optimize. + """ + + # Valid range is 0-19 but the tailing slot pays MinSlotPrice + # and doesn't refund so the check is < instead of <= to optimize. + # timeSlot is an optional: test presence separately, since slot 0 is a valid owner. + return timeSlot is not None and timeSlot < 19 and sb.read(keylet.account(account)) +``` + +# 5. getPayPrice + +The `getPayPrice` function reconciles the system-computed price with the bidder's optional `BidMin` and `BidMax` constraints. Bidders use these constraints to protect themselves from price volatility: `BidMin` ensures they pay at least a certain amount (useful when they want to guarantee winning the slot even if the computed price is lower), while `BidMax` sets an upper limit they're willing to pay (preventing overpayment if the computed price is unexpectedly high). The function validates that the computed price falls within the bidder's acceptable range and returns an error if it doesn't, or returns the reconciled pay price if it does. + +## 5.1. getPayPrice Pseudo-Code + +```python +def getPayPrice( + computedPrice: Number, + bidMin: Optional[STAmount], + bidMax: Optional[STAmount], + lpTokens: STAmount) -> Expected[Number, TER]: + """ + Determine pay price from computed price and bid constraints. + + Returns either the price to pay or an error code. + """ + + # Both min/max bid price are defined + if bidMin and bidMax: + if computedPrice <= bidMax: + payPrice = max(computedPrice, bidMin) + else: + log "AMM Bid: not in range" + return tecAMM_FAILED + + # Only bidMin defined + elif bidMin: + payPrice = max(computedPrice, bidMin) + + # Only bidMax defined + elif bidMax: + if computedPrice <= bidMax: + payPrice = computedPrice + else: + log "AMM Bid: not in range" + return tecAMM_FAILED + + # Neither defined + else: + payPrice = computedPrice + + # Final validation: check if payPrice exceeds LP token holdings + if payPrice > lpTokens: + return tecAMM_INVALID_TOKENS + + return payPrice +``` + +# 6. Price Calculation + +## 6.1. Minimum Slot Price + +The minimum slot price is always: + +``` +minSlotPrice = (TotalLPTokens * TradingFee) / 25 +``` + +This is the price paid when: +- No one owns the slot +- The slot has expired +- The slot is in the tailing period (slot 19) + +## 6.2. Computed Price for Owned Slots + +When the slot is owned and not expired, the computed price depends on the time slot: + +**For the first interval (timeSlot = 0):** + +``` +computedPrice = pricePurchased * 1.05 + minSlotPrice +``` + +This applies a simple 5% markup to the price the current owner paid. + +**For other intervals (timeSlot = 1-18):** + +``` +fractionUsed = (timeSlot + 1) / 20 +computedPrice = pricePurchased * 1.05 * (1 - fractionUsed^60) + minSlotPrice +``` + +The `fractionUsed^60` creates a decay function that makes the price increase more slowly as time progresses. + +**Example:** + +If the current owner paid 1,000 LP tokens and we're in slot 5: +``` +fractionUsed = (5 + 1) / 20 = 0.3 (30% of time elapsed) +computedPrice = 1,000 * 1.05 * (1 - 0.3^60) + minSlotPrice +computedPrice =~ 1,050 + minSlotPrice +``` + +The `0.3^60` is essentially 0, so the price is close to the full 105% markup early in the slot period. + +## 6.3. Time Slot Calculation + +The auction slot is divided into 20 time intervals over 24 hours: + +``` +kTotalTimeSlotSecs = 86,400 seconds (24 hours) +kAuctionSlotTimeIntervals = 20 +Each interval = 86,400 / 20 = 4,320 seconds (~1.2 hours) +``` + +The time slot is calculated as: + +``` +timeSlot = (currentTime - slotExpiration + 86,400) / 4,320 +``` + +Valid time slots are 0-19, where: +- 0 = first interval (just won the slot) +- 19 = last interval (tailing slot) + +The `ammAuctionTimeSlot()` function returns: +- `None` if the slot has expired (or has an invalid expiration) +- A value 0-19 indicating the current time interval + +# 7. Refund Mechanism + +When a new bidder wins the slot from a current owner: + +1. Calculate the fraction of time remaining: + ``` + fractionRemaining = 1 - (timeSlot + 1) / 20 + ``` + +2. Calculate refund to previous owner: + ``` + refund = fractionRemaining * pricePurchased + ``` + +3. Transfer refund (in LP tokens) from new bidder to previous owner via `accountSend()` + +4. Burn remaining amount: + ``` + burn = payPrice - refund + ``` + +**Example:** + +Current owner paid 1,000 LP tokens and we're in slot 8: +``` +fractionUsed = (8 + 1) / 20 = 0.45 (45% of time used) +fractionRemaining = 1 - 0.45 = 0.55 (55% of time remaining) +refund = 0.55 * 1,000 = 550 LP tokens +``` + +If new bidder pays 1,200 LP tokens: +``` +burn = 1,200 - 550 = 650 LP tokens +``` + +The refund compensates the previous owner for the unused portion of their slot time. + +# 8. LP Token Burning + +The burn amount (bid price minus refund) is removed from the LP token supply: + +1. **Adjust burn amount** for LP token precision: + ``` + saBurn = adjustLPTokens(lptAMMBalance, burn, IsDeposit=False) + ``` + +2. **Validate burn amount** doesn't exceed AMM balance: + ``` + if saBurn >= lptAMMBalance: + return tecINTERNAL + ``` + +3. **Redeem (burn) LP tokens** from bidder's balance: + ``` + redeemIOU(sb, account, saBurn, lpTokens.get()) + ``` + +4. **Decrease AMM's LPTokenBalance**: + ``` + ammSle.setFieldAmount(sfLPTokenBalance, lptAMMBalance - saBurn) + ``` + +This effectively distributes value to all remaining LP token holders by reducing the total supply while keeping the pool's assets unchanged. diff --git a/docs/amms/withdraw.md b/docs/amms/withdraw.md index d4bb874..4b43fb0 100644 --- a/docs/amms/withdraw.md +++ b/docs/amms/withdraw.md @@ -1,799 +1,799 @@ -# Index - -- [1. Introduction](#1-introduction) -- [2. applyGuts](#2-applyguts) - - [2.1. applyGuts Pseudo-Code](#21-applyguts-pseudo-code) -- [3. getTradingFee](#3-gettradingfee) -- [4. Multi-Asset Withdrawal Modes](#4-multi-asset-withdrawal-modes) - - [4.1. equalWithdrawTokens (tfLPToken, tfWithdrawAll)](#41-equalwithdrawtokens-tflptoken-tfwithdrawall) - - [4.1.1 equalWithdrawTokens Pseudo-Code](#411-equalwithdrawtokens-pseudo-code) - - [4.2. equalWithdrawLimit (tfTwoAsset)](#42-equalwithdrawlimit-tftwoasset) - - [4.2.1 equalWithdrawLimit Pseudo-Code](#421-equalwithdrawlimit-pseudo-code) -- [5. Single-Asset Withdrawal Modes](#5-single-asset-withdrawal-modes) - - [5.1. singleWithdraw (tfSingleAsset)](#51-singlewithdraw-tfsingleasset) - - [5.1.1. singleWithdraw Pseudo-Code](#511-singlewithdraw-pseudo-code) - - [5.2. singleWithdrawTokens (tfOneAssetLPToken, tfOneAssetWithdrawAll)](#52-singlewithdrawtokens-tfoneassetlptoken-tfoneassetwithdrawall) - - [5.2.1. singleWithdrawTokens Pseudo-Code](#521-singlewithdrawtokens-pseudo-code) - - [5.3. singleWithdrawEPrice (tfLimitLPToken)](#53-singlewithdraweprice-tflimitlptoken) - - [5.3.1. singleWithdrawEPrice Pseudo-Code](#531-singlewithdraweprice-pseudo-code) -- [6. Common Withdraw Function](#6-common-withdraw-function) - - [6.1. withdraw Pseudo-Code](#61-withdraw-pseudo-code) - -# 1. Introduction - -The AMMWithdraw transaction allows liquidity providers to redeem their LP tokens for underlying pool assets. This document provides technical implementation details for the withdrawal logic in `xrpld`. - -The [AMM documentation](README.md#33-ammwithdraw-transaction) describes the high-level business logic of withdrawals, including different [withdrawal modes](README.md#331-withdrawal-modes), their purposes, and user-facing behavior. This document focuses on the implementation: how the code validates constraints, calculates withdrawal amounts, and executes asset transfers. - -**Terminology Notes**: - -Throughout the document we refer to equations by their number in the [XLS-30 specification](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0030-automated-market-maker). -AMM helpers use **token** as a subject in many function names. This refers to any supported currency in the system, not only a particular token implementation, like trust lines or MPTs. - -# 2. applyGuts - -The `applyGuts` function[^applyGuts] is the main entry point for processing AMMWithdraw transactions. It retrieves the AMM ledger entry and the withdrawer's LP token balance, determines how many LP tokens to redeem (all tokens for `tfWithdrawAll`/`tfOneAssetWithdrawAll`, or the specified amount from `LPTokenIn`), then adjusts the LP token balance for precision if needed. The function gets the current pool balances and determines which trading fee applies to the withdrawer (regular or discounted for [auction slot holders](#3-gettradingfee)). Based on the transaction flags and provided fields, it dispatches to one of five withdrawal mode handlers (implementing seven total modes): two [multi-asset modes](#4-multi-asset-withdrawal-modes) that maintain proportional withdrawals, and three [single-asset modes](#5-single-asset-withdrawal-modes) that perform single-sided withdrawals. Each mode handler calculates the withdrawal amounts and LP tokens to burn, then calls the [common withdraw function](#6-common-withdraw-function) to execute the actual asset transfers and update the pool state. After the withdrawal, if the pool is empty (zero LP tokens), the function attempts to delete the AMM account - if successful, the AMM is fully removed; if incomplete due to remaining trust lines, the AMM remains in an empty state with the LP token balance set to zero. - -Under the `fixCleanup3_3_0` amendment, the freeze rules relax as described in the [failure conditions](README.md#332-failure-conditions). In this path, an issuer withdrawing its own frozen token reads the pool balances with `IgnoreFreeze` instead of the `ZeroIfFrozen` shown in the pseudo-code below. - -[^applyGuts]: AMMWithdraw::applyGuts: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L336-L462) - -## 2.1. applyGuts Pseudo-Code - -```python -def applyGuts(sb: &Sandbox, tx: Transaction): - amount = tx[~sfAmount] - amount2 = tx[~sfAmount2] - ePrice = tx[~sfEPrice] - ammSle = sb.getAMM(tx[sfAsset], tx[sfAsset2]) - if not ammSle: - return (tecINTERNAL, False) - - ammAccountID = ammSle[sfAccount] - - # Get withdrawer's LP token balance - lpTokens = ammLPHolds(view, ammSle, accountID_) - - # Determine LP tokens to withdraw - # For tfWithdrawAll and tfOneAssetWithdrawAll: use all LP tokens - # Otherwise: use tx[sfLPTokenIn] - lpTokensWithdraw = tokensWithdraw(lpTokens, tx[~sfLPTokenIn], tx.getFlags()) - - # Adjust LP token balance for precision (with fixAMMv1_1) - # This handles rounding issues for the last LP - if rules.enabled(fixAMMv1_1): - if not verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, accountID_): - return (tecAMM_INVALID_TOKENS, False) - - # Get current trading fee (with potential discount) - tfee = getTradingFee(view, ammSle, accountID_) - - # Get current pool balances - # FreezeHandling.ZeroIfFrozen: treat frozen assets as having zero balance - # AuthHandling.ZeroIfUnauthorized: treat unauthorized MPT holders as having zero balance - currentBalances = ammHolds(sb, ammSle, amount.asset(), amount2.asset(), FreezeHandling.ZeroIfFrozen, AuthHandling.ZeroIfUnauthorized) - if not currentBalances: - return (currentBalances.error(), False) - - amountBalance, amount2Balance, lptAMMBalance = currentBalances - - subTxType = tx.getFlags() & tfWithdrawSubTx - - # Dispatch to appropriate withdrawal mode handler - # Returns (result_code, new_lp_token_balance) - if subTxType & tfTwoAsset: - # Proportional withdrawal with max constraints on both assets - result, newLPTokenBalance = equalWithdrawLimit( - sb, - ammSle, - ammAccountID, - amountBalance, - amount2Balance, - lptAMMBalance, - amount, # max amount1 to withdraw - amount2, # max amount2 to withdraw - tfee - ) - - elif subTxType & (tfOneAssetLPToken | tfOneAssetWithdrawAll): - # Single asset withdrawal for specified LP tokens - result, newLPTokenBalance = singleWithdrawTokens( - sb, - ammSle, - ammAccountID, - amountBalance, - lptAMMBalance, - amount, # min amount or asset specifier - lpTokensWithdraw, # LP tokens to redeem - tfee - ) - - elif subTxType & tfLimitLPToken: - # Single asset withdrawal with effective price constraint - result, newLPTokenBalance = singleWithdrawEPrice( - sb, - ammSle, - ammAccountID, - amountBalance, - lptAMMBalance, - amount, # min amount - ePrice, # min effective price - tfee - ) - - elif subTxType & tfSingleAsset: - # Single asset withdrawal for specified amount - result, newLPTokenBalance = singleWithdraw( - sb, - ammSle, - ammAccountID, - amountBalance, - lptAMMBalance, - amount, # amount to withdraw - tfee - ) - - elif subTxType & (tfLPToken | tfWithdrawAll): - # Proportional withdrawal for LP tokens - result, newLPTokenBalance = equalWithdrawTokens( - sb, - ammSle, - ammAccountID, - amountBalance, - amount2Balance, - lptAMMBalance, - lpTokens, - lpTokensWithdraw, # LP tokens to redeem - tfee - ) - - else: - # Should not happen (validated in preflight) - return (tecINTERNAL, False) - - if result != tesSUCCESS: - return (result, False) - - # Delete AMM if empty, or update LP token balance - res = deleteAMMAccountIfEmpty( - sb, - ammSle, - newLPTokenBalance, - tx[sfAsset], - tx[sfAsset2], - journal - ) - - if not res.second: - return (res.first, False) - - return (tesSUCCESS, True) -``` - -# 3. getTradingFee - -Determines the trading fee for the withdrawer, accounting for auction slot discounts. Same implementation as AMMDeposit, see [deposit.md](deposit.md#3-gettradingfee). - -# 4. Multi-Asset Withdrawal Modes - -Multi-asset withdrawal modes maintain proportional withdrawals by removing both pool assets simultaneously. This preserves the pool's price (asset ratio) while decreasing liquidity. Since these withdrawals maintain proportional ratios, they incur no trading fees. - -The modes are: -- **[equalWithdrawTokens](#41-equalwithdrawtokens-tflptoken-tfwithdrawall) (tfLPToken, tfWithdrawAll)** - Redeem exact LP tokens or all LP tokens, receive proportional amounts of both assets -- **[equalWithdrawLimit](#42-equalwithdrawlimit-tftwoasset) (tfTwoAsset)** - Specify maximum amounts for both assets, system calculates LP tokens to burn - -## 4.1. equalWithdrawTokens (tfLPToken, tfWithdrawAll) - -> "I want to redeem exactly X LP tokens, how much of both assets do I get?" (tfLPToken) -> "Redeem all my LP tokens for both assets." (tfWithdrawAll) - -Proportional withdrawal of pool assets for the amount of LP tokens.[^equalWithdrawTokens] - -[^equalWithdrawTokens]: AMMWithdraw::equalWithdrawTokens: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L790-L871) - -This function handles two related modes. With `tfLPToken`, the user specifies the exact number of LP tokens to redeem using `LPTokenIn`, and the function calculates the proportional amounts of both assets to withdraw. With `tfWithdrawAll`, the user redeems their entire LP token balance without specifying an amount. The function handles a special case when withdrawing all LP tokens from the pool (`lpTokensWithdraw == lptAMMBalance`), which empties the pool completely. For partial withdrawals, it calculates the pool fraction (`frac = tokensAdj / lptAMMBalance`), then multiplies each asset balance by this fraction, rounding down with [`getRoundedAsset`](helpers.md#23-getroundedasset) to ensure the pool retains sufficient assets. - -**Example:** - -A pool has 150 USD, 150 EUR, and 150 LP tokens outstanding. Bob holds 30 LP tokens (20% of the pool). - -**Case 1: Bob redeems exactly 15 LP tokens (tfLPToken)** -- LP tokens to redeem: 15 -- Fraction of pool: 15 / 150 = 0.1 (10%) -- USD withdrawn: 150 * 0.1 = 15 USD -- EUR withdrawn: 150 * 0.1 = 15 EUR -- Result: Bob redeems 15 LP tokens, receives 15 USD + 15 EUR -- Bob now holds: 15 LP tokens (10% of remaining pool) - -**Case 2: Bob redeems all his LP tokens (tfWithdrawAll)** -- LP tokens to redeem: 30 (all Bob's holdings) -- Fraction of pool: 30 / 150 = 0.2 (20%) -- USD withdrawn: 150 * 0.2 = 30 USD -- EUR withdrawn: 150 * 0.2 = 30 EUR -- Result: Bob redeems 30 LP tokens, receives 30 USD + 30 EUR -- Bob now holds: 0 LP tokens - -### 4.1.1 equalWithdrawTokens Pseudo-Code - -```python -def equalWithdrawTokens( - view, # Ledger view (sandbox) - ammSle, # AMM ledger entry - account, # Withdrawer account ID - ammAccount, # AMM pseudo-account ID - amountBalance, # Current pool balance of asset1 - amount2Balance, # Current pool balance of asset2 - lptAMMBalance, # Total outstanding LP tokens - lpTokens, # Withdrawer's LP token balance - lpTokensWithdraw, # LP tokens to redeem - tfee, # Trading fee (not used for proportional) - freezeHandling, # How to handle frozen assets - withdrawAll, # Whether this is tfWithdrawAll - priorBalance, # Withdrawer's prior XRP balance - journal): # Debug journal - # CASE 1: Withdrawing all LP tokens in the pool - if lpTokensWithdraw == lptAMMBalance: - # Withdraw all assets, empty the pool - return withdraw( - view, - ammSle, - ammAccount, - account, - amountBalance, - amountBalance, # withdraw all of asset1 - amount2Balance, # withdraw all of asset2 - lptAMMBalance, - lpTokensWithdraw, - tfee, - freezeHandling, - WithdrawAll=True, # special handling for complete withdrawal - priorBalance, - journal - ) - - # CASE 2: Partial withdrawal - # Adjust LP tokens for precision (with fixAMMv1_3) - tokensAdj = adjustLPTokensIn(rules, lptAMMBalance, lpTokensWithdraw, withdrawAll) # helpers.md#25-adjustlptokensin-withdrawals - - if rules.enabled(fixAMMv1_3) and tokensAdj == 0: - return (tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, None) - - # Calculate the fraction of the pool being withdrawn - # Example: 5,000 LP tokens / 50,000 total = 0.1 (10% of pool) - frac = tokensAdj / lptAMMBalance - - # Calculate withdrawal amounts for both assets - # With fixAMMv1_3: Round DOWN (conservative, ensures pool keeps enough) - amountWithdraw = getRoundedAsset(rules, amountBalance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code - amount2Withdraw = getRoundedAsset(rules, amount2Balance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code - - # Prevent one-sided pool withdrawal due to rounding - # If either amount rounds to zero, fail so user withdraws more tokens - if amountWithdraw == 0 or amount2Withdraw == 0: - return (tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}) - - return withdraw( - view, - ammSle, - ammAccount, - account, - amountBalance, - amountWithdraw, - amount2Withdraw, - lptAMMBalance, - tokensAdj, - tfee, - freezeHandling, - withdrawAll, - priorBalance, - journal - ) -``` - -## 4.2. equalWithdrawLimit (tfTwoAsset) - -> "I want to withdraw up to X and Y, how many LP tokens do I burn?" - -The user specifies maximum amounts they want to withdraw for both assets (`Amount` and `Amount2`).[^equalWithdrawLimit] Since the withdrawal must maintain the pool's ratio, the function cannot simply use both maximum amounts - one will typically be limiting while the other has excess. - -[^equalWithdrawLimit]: AMMWithdraw::equalWithdrawLimit: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L899-L961) - -The function tries two strategies to maximize the withdrawal within the user's constraints. First, it attempts to withdraw all of `Amount` by calculating the pool fraction this represents (`frac = Amount / amountBalance`), converting this to LP tokens with proper rounding, then recalculating the fraction from the rounded LP tokens (`frac = adjustFracByTokens(...)`) to ensure precision consistency. Using this adjusted fraction, it calculates the proportional amount2 needed. If this amount2 fits within `Amount2`, the withdrawal proceeds immediately. - -Only if the first strategy fails does it try the second strategy: starting with all of `Amount2`, going through the same fraction -> LP tokens -> adjusted fraction → amount1 calculation. If the calculated amount1 exceeds `Amount`, the entire withdrawal fails. - -**Example:** - -There is an AMM with 100 USD and 100 EUR (1:1 ratio), 100 LP tokens outstanding. Bob holds 50 LP tokens and wants to make a proportional withdrawal. - -**Case 1: Bob tries to withdraw up to 30 USD + 20 EUR** -- Strategy 1: Use all 30 USD and needs 30 EUR to maintain 1:1 ratio - FAILS (only wants 20 EUR) -- Strategy 2: Use all 20 EUR and needs 20 USD to maintain 1:1 ratio - SUCCESS (wants up to 30 USD) -- Result: Withdraws 20 USD + 20 EUR, redeems 20 LP tokens - -**Case 2: Bob tries to withdraw up to 20 USD + 30 EUR** -- Strategy 1: Use all 20 USD and needs 20 EUR to maintain 1:1 ratio - SUCCESS (wants up to 30 EUR) -- Result: Withdraws 20 USD + 20 EUR, redeems 20 LP tokens - -### 4.2.1 equalWithdrawLimit Pseudo-Code - -```python -def equalWithdrawLimit( - view, # Ledger view (sandbox) - ammSle, # AMM ledger entry - ammAccount, # AMM pseudo-account ID - amountBalance, # Current pool balance of asset1 - amount2Balance, # Current pool balance of asset2 - lptAMMBalance, # Total outstanding LP tokens - amount, # User's max amount1 to withdraw - amount2, # User's max amount2 to withdraw - tfee): # Trading fee (not used for proportional) - # STRATEGY 1: Try using all of amount (asset1) - # Calculate what fraction of the pool this represents - frac = amount / amountBalance - - # Calculate LP tokens for this fraction - # Using simple version of getRoundedLPTokens (direct fraction) - tokensAdj = getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit=False) # helpers.md#211-getroundedlptokens-simple-pseudo-code - - if rules.enabled(fixAMMv1_3) and tokensAdj == 0: - return (tecAMM_INVALID_TOKENS, STAmount{}) - - # Adjust fraction based on rounded tokens (for precision consistency) - frac = adjustFracByTokens(rules, lptAMMBalance, tokensAdj, frac) - - # Calculate how much asset2 would be withdrawn for this fraction - # Using simple version of getRoundedAsset (direct fraction) - amount2Withdraw = getRoundedAsset(rules, amount2Balance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code - - # Check if calculated amount2 fits within user's max constraint - if amount2Withdraw <= amount2: - # Success! Use all of amount, calculated amount2Withdraw - return withdraw( - view, - ammSle, - ammAccount, - amountBalance, - amount, # withdraw all of asset1 - amount2Withdraw, # calculated asset2 - lptAMMBalance, - tokensAdj, - tfee - ) - - # STRATEGY 2: Strategy 1 failed, try using all of amount2 - # Calculate what fraction of the pool amount2 represents - frac = amount2 / amount2Balance - - # Calculate how much asset1 would be withdrawn for this fraction (preliminary) - # Using simple version of getRoundedAsset (direct fraction) - amountWithdraw = getRoundedAsset(rules, amountBalance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code - - # Calculate LP tokens for this fraction - # Using simple version of getRoundedLPTokens (direct fraction) - tokensAdj = getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit=False) # helpers.md#211-getroundedlptokens-simple-pseudo-code - - if rules.enabled(fixAMMv1_3) and tokensAdj == 0: - return (tecAMM_INVALID_TOKENS, STAmount{}) - - # Adjust fraction based on rounded tokens (for precision consistency) - frac = adjustFracByTokens(rules, lptAMMBalance, tokensAdj, frac) - - # Recalculate asset1 amount with adjusted fraction - # Using simple version of getRoundedAsset (direct fraction) - amountWithdraw = getRoundedAsset(rules, amountBalance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code - - # Check if calculated amount fits within user's max constraint - if rules.enabled(fixAMMv1_3): - if amountWithdraw > amount: - return (tecAMM_FAILED, STAmount{}) - - # Success! Use calculated amountWithdraw, all of amount2 - return withdraw( - view, - ammSle, - ammAccount, - amountBalance, - amountWithdraw, # calculated asset1 - amount2, # withdraw all of asset2 - lptAMMBalance, - tokensAdj, - tfee - ) -``` - -# 5. Single-Asset Withdrawal Modes - -Single-asset withdrawal modes allow users to withdraw only one asset instead of both assets proportionally. Unlike [multi-asset withdrawals](#4-multi-asset-withdrawal-modes) that maintain the pool ratio, single-asset withdrawals change the pool composition. Because they alter the pool ratio, [trading fees](#3-gettradingfee) apply to single-asset withdrawals. There are three modes: - -- **[singleWithdraw](#51-singlewithdraw-tfsingleasset) (tfSingleAsset)** - User specifies withdrawal amount, system calculates LP tokens to redeem -- **[singleWithdrawTokens](#52-singlewithdrawtokens-tfoneassetlptoken-tfoneassetwithdrawall) (tfOneAssetLPToken, tfOneAssetWithdrawAll)** - User specifies exact LP tokens to redeem or all LP tokens, withdraws calculated amount of single asset -- **[singleWithdrawEPrice](#53-singlewithdraweprice-tflimitlptoken) (tfLimitLPToken)** - User specifies minimum effective price limit - -## 5.1. singleWithdraw (tfSingleAsset) - -> "I want to withdraw X amount, how many LP tokens must I redeem?" - -The user specifies `Amount` (the asset amount to withdraw) and the function calculates how many LP tokens must be redeemed.[^singleWithdraw] Since this is a single-asset withdrawal that changes the pool ratio, a [trading fee](#3-gettradingfee) applies. The function uses [`lpTokensIn`](helpers.md#331-lptokensin-equation-7) (Equation 7) to calculate the LP tokens based on the withdrawal amount and trading fee, then adjusts the result for precision with [`adjustLPTokensIn`](helpers.md#25-adjustlptokensin-withdrawals). The adjusted tokens are passed to `adjustAssetOutByTokens` to recalculate the withdrawal amount, ensuring the reverse calculation produces consistent results and doesn't underpay the user due to rounding. - -[^singleWithdraw]: AMMWithdraw::singleWithdraw: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L969-L1007) - -### 5.1.1. singleWithdraw Pseudo-Code - -```python -def singleWithdraw( - view, # Ledger view (sandbox) - ammSle, # AMM ledger entry - ammAccount, # AMM pseudo-account ID - amountBalance, # Current pool balance of the asset - lptAMMBalance, # Total outstanding LP tokens - amount, # Amount to withdraw - tfee): # Trading fee (for single-asset withdrawal) - # Calculate LP tokens using the single-asset withdrawal formula - # lpTokensIn solves: "How many LP tokens to redeem to get `amount` assets?" - tokens = lpTokensIn(amountBalance, amount, lptAMMBalance, tfee) # helpers.md#331-lptokensin-equation-7 - - # Adjust LP tokens for precision (with fixAMMv1_3) - tokensAdj = adjustLPTokensIn(rules, lptAMMBalance, tokens, isWithdrawAll(tx)) # helpers.md#25-adjustlptokensin-withdrawals - - if tokensAdj == 0: - if not rules.enabled(fixAMMv1_3): - return (tecAMM_FAILED, STAmount{}) - else: - return (tecAMM_INVALID_TOKENS, STAmount{}) - - # Adjust withdrawal amount based on adjusted tokens - # This ensures the reverse calculation produces consistent results - tokensAdj, amountWithdrawAdj = adjustAssetOutByTokens( - rules, amountBalance, amount, lptAMMBalance, tokensAdj, tfee) - - if rules.enabled(fixAMMv1_3) and tokensAdj == 0: - return (tecAMM_INVALID_TOKENS, STAmount{}) - - return withdraw( - view, - ammSle, - ammAccount, - amountBalance, - amountWithdrawAdj, # adjusted: actual amount to withdraw - None, # single-asset withdrawal (no asset2) - lptAMMBalance, - tokensAdj, # calculated: LP tokens to redeem - tfee - ) -``` - -## 5.2. singleWithdrawTokens (tfOneAssetLPToken, tfOneAssetWithdrawAll) - -> "I'll redeem exactly X LP tokens, how much asset (single asset only) do I get?" (tfOneAssetLPToken) -> "Redeem all my LP tokens for a single asset." (tfOneAssetWithdrawAll) - -Withdraw a single asset by redeeming specified LP tokens.[^singleWithdrawTokens] - -[^singleWithdrawTokens]: AMMWithdraw::singleWithdrawTokens: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L1020-L1051) - -This function handles the reverse calculation from [`singleWithdraw`](#51-singlewithdraw-tfsingleasset): the user specifies the exact number of LP tokens to redeem (using `LPTokenIn` for tfOneAssetLPToken, or all LP tokens for tfOneAssetWithdrawAll), and the function calculates the withdrawal amount of a single asset. The user can provide `Amount` as a minimum constraint on how much they expect to receive. - -The function first adjusts the LP tokens for precision using [`adjustLPTokensIn`](helpers.md#25-adjustlptokensin-withdrawals), then uses [`ammAssetOut`](helpers.md#332-ammassetout-equation-8) (Equation 8) to calculate the withdrawal amount by solving the inverse single-asset withdrawal problem. If the calculated amount is less than the user's `Amount` constraint (when non-zero), the transaction fails with `tecAMM_FAILED`. - -### 5.2.1. singleWithdrawTokens Pseudo-Code - -```python -def singleWithdrawTokens( - view, # Ledger view (sandbox) - ammSle, # AMM ledger entry - ammAccount, # AMM pseudo-account ID - amountBalance, # Current pool balance of the asset - lptAMMBalance, # Total outstanding LP tokens - amount, # Min asset to receive (or 0 for no min, or asset specifier) - lpTokensWithdraw, # LP tokens to redeem - tfee): # Trading fee (for single-asset withdrawal) - # Adjust LP tokens for precision (with fixAMMv1_3) - tokensAdj = adjustLPTokensIn(rules, lptAMMBalance, lpTokensWithdraw, isWithdrawAll(tx)) # helpers.md#25-adjustlptokensin-withdrawals - - if rules.enabled(fixAMMv1_3) and tokensAdj == 0: - return (tecAMM_INVALID_TOKENS, STAmount{}) - - # Calculate withdrawal amount using ammAssetOut formula - amountWithdraw = ammAssetOut(amountBalance, lptAMMBalance, tokensAdj, tfee) # helpers.md#332-ammassetout-equation-8 - - # Check if calculated amount meets user's minimum (if specified) - if amount == 0 or amountWithdraw >= amount: - # Either no minimum specified, or calculated amount meets minimum - return withdraw( - view, - ammSle, - ammAccount, - amountBalance, - amountWithdraw, # calculated: amount to withdraw - None, # single-asset withdrawal - lptAMMBalance, - tokensAdj, # exact: LP tokens to redeem - tfee - ) - - # Calculated amount is less than user's minimum - return (tecAMM_FAILED, STAmount{}) -``` - -## 5.3. singleWithdrawEPrice (tfLimitLPToken) - -> "I'll withdraw (single asset), but only if the effective price per LP token is reasonable." - -Withdraw a single asset with an effective price constraint.[^singleWithdrawEPrice] - -This mode allows users to control the effective price when redeeming LP tokens, where effective price is defined as the ratio of LP tokens redeemed to asset withdrawn. The user provides `EPrice` (maximum effective price) and optionally `Amount` (minimum withdrawal amount). As with deposits, `EPrice` is an upper bound: the trade is sized so the effective price does not exceed `EPrice`. A lower effective price means a better deal for the withdrawer (fewer LP tokens per asset withdrawn). - -The function solves a derived formula from Equation 8 to calculate the LP tokens that achieve exactly the specified effective price. It then calculates the withdrawal amount as `tokensAdj / ePrice`. If the calculated amount is less than the user's optional `Amount` constraint, the transaction fails with `tecAMM_FAILED`. Under the `fixCleanup3_3_0` amendment, a denominator (`T*f - B*E`) of exactly zero also fails with `tecAMM_FAILED`. Without the amendment that division throws and the transaction fails with `tefEXCEPTION`. - -[^singleWithdrawEPrice]: AMMWithdraw::singleWithdrawEPrice: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L1119-L1179) - -### 5.3.1. singleWithdrawEPrice Pseudo-Code - -```python -def singleWithdrawEPrice( - view, # Ledger view (sandbox) - ammSle, # AMM ledger entry - ammAccount, # AMM pseudo-account ID - amountBalance, # Current pool balance of the asset - lptAMMBalance, # Total outstanding LP tokens - amount, # Min asset to receive (or 0 for no min) - ePrice, # Min effective price (LPTokenIn / AssetOut) - tfee): # Trading fee (for single-asset withdrawal) - # Calculate intermediate value: B * E (balance * effective price) - ae = amountBalance * ePrice - - # Get fee multiplier - f = getFee(tfee) # fee in units of 1/100,000 (e.g., 30 -> 0.0003) - - # Calculate LP tokens using derived formula - # t = T*(T + B*E*(f-2)) / (T*f - B*E) - tokNoRoundCb = lambda: ( - lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae) - ) - tokProdCb = lambda: ( - (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae) - ) - - tokensAdj = getRoundedLPTokens( - rules, tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit=False) # helpers.md#212-getroundedlptokens-callback-pseudo-code - - if tokensAdj <= 0: - if not rules.enabled(fixAMMv1_3): - return (tecAMM_FAILED, STAmount{}) - else: - return (tecAMM_INVALID_TOKENS, STAmount{}) - - # Calculate withdrawal amount from tokens and effective price - # amountWithdraw = tokensAdj / ePrice - amtNoRoundCb = lambda: tokensAdj / ePrice - amtProdCb = lambda: tokensAdj / ePrice - - amountWithdraw = getRoundedAsset( - rules, amtNoRoundCb, amount, amtProdCb, IsDeposit=False) # helpers.md#232-getroundedasset-callback-pseudo-code - - # Check if calculated amount meets user's minimum (if specified) - if amount == 0 or amountWithdraw >= amount: - return withdraw( - view, - ammSle, - ammAccount, - amountBalance, - amountWithdraw, # calculated: amount to withdraw - None, # single-asset withdrawal - lptAMMBalance, - tokensAdj, # calculated: LP tokens to redeem - tfee - ) - - # Calculated amount is less than user's minimum - return (tecAMM_FAILED, STAmount{}) -``` - -# 6. Common Withdraw Function - -The `withdraw()` function[^withdraw] serves as the final common pathway for all withdrawal modes, executing the actual asset transfers after mode-specific handlers determine the withdrawal amounts. - -Under `fixCleanup3_3_0` together with `fixAMMv1_3`, the common path also runs the pool product check described in [Precision and Rounding](helpers.md#2-precision-and-rounding). See the [failure conditions](README.md#332-failure-conditions) for the resulting `tecPRECISION_LOSS`. - -[^withdraw]: AMMWithdraw::withdraw: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L479-L749) - -This function orchestrates a sequenced validation and execution flow. It begins by verifying the withdrawer holds sufficient LP tokens to redeem, then enforces pool integrity constraints that prevent malformed states. - -The function prohibits one-sided pool withdrawals - situations where all of one asset would be withdrawn while the other remains. When a withdrawal would redeem all outstanding LP tokens, the function mandates that all pool assets must also be withdrawn, preventing orphaned assets in an empty pool. - -Before executing transfers, the function checks whether the withdrawer has adequate XRP reserves if new trust lines or MPTokens need creation. For MPTs, this includes verifying proper authorization from the issuer. The function then transfers each withdrawn asset from the AMM account to the withdrawer, waiving transfer fees as AMM operations are privileged. Finally, it burns the redeemed LP tokens by calling `redeemIOU`, which reduces both the withdrawer's LP token balance and the total outstanding token supply. - -## 6.1. withdraw Pseudo-Code - -```python -def withdraw( - view, # Ledger view (sandbox) - ammSle, # AMM ledger entry - ammAccount, # AMM pseudo-account ID - account, # Withdrawer account ID - amountBalance, # Current pool balance of asset1 - amountWithdraw, # Amount1 to withdraw - amount2Withdraw, # Optional: amount2 to withdraw - lpTokensAMMBalance, # Total outstanding LP tokens - lpTokensWithdraw, # LP tokens to redeem - tfee, # Trading fee - freezeHandling, # How to handle frozen assets - authHandling, # How to handle unauthorized MPT holders - withdrawAll, # Whether this is a complete withdrawal - priorBalance: # Withdrawer's prior XRP balance - # Get withdrawer's current LP token balance - lpTokens = ammLPHolds(view, ammSle, account, journal) - - # Get current pool balances (accounting for freezes and authorization) - currentBalances = ammHolds(view, ammSle, amountWithdraw.issue, None, freezeHandling, authHandling) - if not currentBalances: - return (currentBalances.error(), STAmount{}, STAmount{}, STAmount{}) - - curBalance, curBalance2, _ = currentBalances - - # Adjust amounts for precision (with fixAMMv1_3, this shouldn't be needed as we have already adjusted and rounded all numbers properly) - # When withdrawing all, skip adjustment and use exact values - if withdrawAll == No: - amountWithdrawActual, amount2WithdrawActual, lpTokensWithdrawActual = \ - adjustAmountsByLPTokens( - amountBalance, - amountWithdraw, - amount2Withdraw, - lpTokensAMMBalance, - lpTokensWithdraw, - tfee, - IsDeposit=False - ) - else: - amountWithdrawActual = amountWithdraw - amount2WithdrawActual = amount2Withdraw - lpTokensWithdrawActual = lpTokensWithdraw - - # Validate LP tokens - if lpTokensWithdrawActual <= 0 or lpTokensWithdrawActual > lpTokens: - return (tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, STAmount{}) - - # With fixAMMv1_1: Additional validation - if rules.enabled(fixAMMv1_1) and lpTokensWithdrawActual > lpTokensAMMBalance: - return (tecINTERNAL, STAmount{}, STAmount{}, STAmount{}) - - # Prevent one-sided pool withdrawal - # If withdrawing all of one asset but not the other, fail - # This ensures pools are always balanced (or completely empty) - if (amountWithdrawActual == curBalance and amount2WithdrawActual != curBalance2) or \ - (amount2WithdrawActual == curBalance2 and amountWithdrawActual != curBalance): - return (tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}) - - # If redeeming all LP tokens, must withdraw all assets - # This prevents situations where LP tokens are zero but assets remain - if lpTokensWithdrawActual == lpTokensAMMBalance and \ - (amountWithdrawActual != curBalance or amount2WithdrawActual != curBalance2): - return (tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}) - - # Check withdrawal doesn't exceed pool balance - if amountWithdrawActual > curBalance or amount2WithdrawActual > curBalance2: - return (tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}) - - # With featureMPTokensV2: the post-withdrawal pool state must be consistent - # (all balances zero or all non-zero, agreeing with the LP token total) - if rules.enabled(featureMPTokensV2): - newBalanceZero = (curBalance - amountWithdrawActual) == 0 - newBalance2Zero = (curBalance2 - amount2WithdrawActual) == 0 - newLPTokensZero = (lpTokensAMMBalance - lpTokensWithdrawActual) == 0 - if amount2WithdrawActual is None: - valid = (newBalanceZero == newLPTokensZero) - else: - valid = (newBalanceZero == newBalance2Zero and newBalance2Zero == newLPTokensZero) - if not valid: - return (tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}) - - # Helper function to check reserve requirements (with fixAMMv1_2) - # Checks if withdrawer has sufficient XRP reserve for trust line or MPToken creation - def sufficientReserve(asset): - if not rules.enabled(fixAMMv1_2) or isXRP(asset): - return (tesSUCCESS, None) - - # Check if trust line (for IOUs) or MPToken (for MPTs) exists - if isIOU(asset): - assetExists = view.exists(keylet.line(account, asset.issue)) - mptokenKey = None - else: # MPT - issuanceKey = keylet.mptIssuance(asset.mptID) - mptokenKey = keylet.mptoken(issuanceKey, account) - assetExists = view.exists(mptokenKey) - if assetExists: - mptokenKey = None # Already exists, no need to create - - if not assetExists: - sleAccount = view.peek(keylet.account(account)) - if not sleAccount: - return (tecINTERNAL, None) - - balance = sleAccount[sfBalance].xrp - ownerCount = sleAccount[sfOwnerCount] - - reserve = view.fees().accountReserve(ownerCount + 1) if ownerCount >= 2 else 0 - - # For IOUs: use max of prior and current balance - # For MPTs: use prior balance only - balanceToCheck = max(priorBalance, balance) if isIOU(asset) else priorBalance - - if balanceToCheck < reserve: - return (tecINSUFFICIENT_RESERVE, None) - - return (tesSUCCESS, mptokenKey) - - # Helper function to create MPToken if needed - def createMPToken(asset, mptokenKey): - if mptokenKey and account != asset.getIssuer(): - # Must authorize MPToken - if requireAuth(view, asset.mptIssue, account, WeakAuth) != tesSUCCESS: - return err - - if checkCreateMPT(view, asset.mptIssue, account, journal) != tesSUCCESS: - return err - - return tesSUCCESS - - # Check reserve and create MPToken for asset1 - result, mptokenKey = sufficientReserve(amountWithdrawActual.asset) - if result != tesSUCCESS: - return (result, STAmount{}, STAmount{}, STAmount{}) - - result = createMPToken(amountWithdrawActual.asset, mptokenKey) - if result != tesSUCCESS: - return (result, STAmount{}, STAmount{}, STAmount{}) - - # Transfer asset1 from AMM to withdrawer - result = accountSend( - view, - ammAccount, # from: AMM pseudo-account - account, # to: withdrawer - amountWithdrawActual, # amount - WaiveTransferFee=Yes # AMM withdrawals don't pay transfer fees - ) - if result != tesSUCCESS: - return (result, STAmount{}, STAmount{}, STAmount{}) - - # If two-asset withdrawal, check reserve, create MPToken, and transfer asset2 - if amount2WithdrawActual: - # Check reserve and create MPToken for asset2 - result, mptokenKey = sufficientReserve(amount2WithdrawActual.asset) - if result != tesSUCCESS: - return (result, STAmount{}, STAmount{}, STAmount{}) - - result = createMPToken(amount2WithdrawActual.asset, mptokenKey) - if result != tesSUCCESS: - return (result, STAmount{}, STAmount{}, STAmount{}) - - result = accountSend( - view, - ammAccount, - account, - amount2WithdrawActual, - WaiveTransferFee=Yes - ) - if result != tesSUCCESS: - return (result, STAmount{}, STAmount{}, STAmount{}) - - # Redeem (burn) LP tokens - # This decreases the trust line balance and may delete the trust line - result = redeemIOU( - view, - account, - lpTokensWithdrawActual, - lpTokensWithdrawActual.issue, - journal - ) - if result != tesSUCCESS: - return (result, STAmount{}, STAmount{}, STAmount{}) - - # Return success with new LP token balance and actual withdrawal amounts - return ( - tesSUCCESS, - lpTokensAMMBalance - lpTokensWithdrawActual, - amountWithdrawActual, - amount2WithdrawActual - ) -``` +# Index + +- [1. Introduction](#1-introduction) +- [2. applyGuts](#2-applyguts) + - [2.1. applyGuts Pseudo-Code](#21-applyguts-pseudo-code) +- [3. getTradingFee](#3-gettradingfee) +- [4. Multi-Asset Withdrawal Modes](#4-multi-asset-withdrawal-modes) + - [4.1. equalWithdrawTokens (tfLPToken, tfWithdrawAll)](#41-equalwithdrawtokens-tflptoken-tfwithdrawall) + - [4.1.1 equalWithdrawTokens Pseudo-Code](#411-equalwithdrawtokens-pseudo-code) + - [4.2. equalWithdrawLimit (tfTwoAsset)](#42-equalwithdrawlimit-tftwoasset) + - [4.2.1 equalWithdrawLimit Pseudo-Code](#421-equalwithdrawlimit-pseudo-code) +- [5. Single-Asset Withdrawal Modes](#5-single-asset-withdrawal-modes) + - [5.1. singleWithdraw (tfSingleAsset)](#51-singlewithdraw-tfsingleasset) + - [5.1.1. singleWithdraw Pseudo-Code](#511-singlewithdraw-pseudo-code) + - [5.2. singleWithdrawTokens (tfOneAssetLPToken, tfOneAssetWithdrawAll)](#52-singlewithdrawtokens-tfoneassetlptoken-tfoneassetwithdrawall) + - [5.2.1. singleWithdrawTokens Pseudo-Code](#521-singlewithdrawtokens-pseudo-code) + - [5.3. singleWithdrawEPrice (tfLimitLPToken)](#53-singlewithdraweprice-tflimitlptoken) + - [5.3.1. singleWithdrawEPrice Pseudo-Code](#531-singlewithdraweprice-pseudo-code) +- [6. Common Withdraw Function](#6-common-withdraw-function) + - [6.1. withdraw Pseudo-Code](#61-withdraw-pseudo-code) + +# 1. Introduction + +The AMMWithdraw transaction allows liquidity providers to redeem their LP tokens for underlying pool assets. This document provides technical implementation details for the withdrawal logic in `xrpld`. + +The [AMM documentation](README.md#33-ammwithdraw-transaction) describes the high-level business logic of withdrawals, including different [withdrawal modes](README.md#331-withdrawal-modes), their purposes, and user-facing behavior. This document focuses on the implementation: how the code validates constraints, calculates withdrawal amounts, and executes asset transfers. + +**Terminology Notes**: + +Throughout the document we refer to equations by their number in the [XLS-30 specification](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0030-automated-market-maker). +AMM helpers use **token** as a subject in many function names. This refers to any supported currency in the system, not only a particular token implementation, like trust lines or MPTs. + +# 2. applyGuts + +The `applyGuts` function[^applyGuts] is the main entry point for processing AMMWithdraw transactions. It retrieves the AMM ledger entry and the withdrawer's LP token balance, determines how many LP tokens to redeem (all tokens for `tfWithdrawAll`/`tfOneAssetWithdrawAll`, or the specified amount from `LPTokenIn`), then adjusts the LP token balance for precision if needed. The function gets the current pool balances and determines which trading fee applies to the withdrawer (regular or discounted for [auction slot holders](#3-gettradingfee)). Based on the transaction flags and provided fields, it dispatches to one of five withdrawal mode handlers (implementing seven total modes): two [multi-asset modes](#4-multi-asset-withdrawal-modes) that maintain proportional withdrawals, and three [single-asset modes](#5-single-asset-withdrawal-modes) that perform single-sided withdrawals. Each mode handler calculates the withdrawal amounts and LP tokens to burn, then calls the [common withdraw function](#6-common-withdraw-function) to execute the actual asset transfers and update the pool state. After the withdrawal, if the pool is empty (zero LP tokens), the function attempts to delete the AMM account - if successful, the AMM is fully removed; if incomplete due to remaining trust lines, the AMM remains in an empty state with the LP token balance set to zero. + +Under the `fixCleanup3_3_0` amendment, the freeze rules relax as described in the [failure conditions](README.md#332-failure-conditions). In this path, an issuer withdrawing its own frozen token reads the pool balances with `IgnoreFreeze` instead of the `ZeroIfFrozen` shown in the pseudo-code below. + +[^applyGuts]: AMMWithdraw::applyGuts: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L336-L462) + +## 2.1. applyGuts Pseudo-Code + +```python +def applyGuts(sb: &Sandbox, tx: Transaction): + amount = tx[~sfAmount] + amount2 = tx[~sfAmount2] + ePrice = tx[~sfEPrice] + ammSle = sb.getAMM(tx[sfAsset], tx[sfAsset2]) + if not ammSle: + return (tecINTERNAL, False) + + ammAccountID = ammSle[sfAccount] + + # Get withdrawer's LP token balance + lpTokens = ammLPHolds(view, ammSle, accountID_) + + # Determine LP tokens to withdraw + # For tfWithdrawAll and tfOneAssetWithdrawAll: use all LP tokens + # Otherwise: use tx[sfLPTokenIn] + lpTokensWithdraw = tokensWithdraw(lpTokens, tx[~sfLPTokenIn], tx.getFlags()) + + # Adjust LP token balance for precision (with fixAMMv1_1) + # This handles rounding issues for the last LP + if rules.enabled(fixAMMv1_1): + if not verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, accountID_): + return (tecAMM_INVALID_TOKENS, False) + + # Get current trading fee (with potential discount) + tfee = getTradingFee(view, ammSle, accountID_) + + # Get current pool balances + # FreezeHandling.ZeroIfFrozen: treat frozen assets as having zero balance + # AuthHandling.ZeroIfUnauthorized: treat unauthorized MPT holders as having zero balance + currentBalances = ammHolds(sb, ammSle, amount.asset(), amount2.asset(), FreezeHandling.ZeroIfFrozen, AuthHandling.ZeroIfUnauthorized) + if not currentBalances: + return (currentBalances.error(), False) + + amountBalance, amount2Balance, lptAMMBalance = currentBalances + + subTxType = tx.getFlags() & tfWithdrawSubTx + + # Dispatch to appropriate withdrawal mode handler + # Returns (result_code, new_lp_token_balance) + if subTxType & tfTwoAsset: + # Proportional withdrawal with max constraints on both assets + result, newLPTokenBalance = equalWithdrawLimit( + sb, + ammSle, + ammAccountID, + amountBalance, + amount2Balance, + lptAMMBalance, + amount, # max amount1 to withdraw + amount2, # max amount2 to withdraw + tfee + ) + + elif subTxType & (tfOneAssetLPToken | tfOneAssetWithdrawAll): + # Single asset withdrawal for specified LP tokens + result, newLPTokenBalance = singleWithdrawTokens( + sb, + ammSle, + ammAccountID, + amountBalance, + lptAMMBalance, + amount, # min amount or asset specifier + lpTokensWithdraw, # LP tokens to redeem + tfee + ) + + elif subTxType & tfLimitLPToken: + # Single asset withdrawal with effective price constraint + result, newLPTokenBalance = singleWithdrawEPrice( + sb, + ammSle, + ammAccountID, + amountBalance, + lptAMMBalance, + amount, # min amount + ePrice, # min effective price + tfee + ) + + elif subTxType & tfSingleAsset: + # Single asset withdrawal for specified amount + result, newLPTokenBalance = singleWithdraw( + sb, + ammSle, + ammAccountID, + amountBalance, + lptAMMBalance, + amount, # amount to withdraw + tfee + ) + + elif subTxType & (tfLPToken | tfWithdrawAll): + # Proportional withdrawal for LP tokens + result, newLPTokenBalance = equalWithdrawTokens( + sb, + ammSle, + ammAccountID, + amountBalance, + amount2Balance, + lptAMMBalance, + lpTokens, + lpTokensWithdraw, # LP tokens to redeem + tfee + ) + + else: + # Should not happen (validated in preflight) + return (tecINTERNAL, False) + + if result != tesSUCCESS: + return (result, False) + + # Delete AMM if empty, or update LP token balance + res = deleteAMMAccountIfEmpty( + sb, + ammSle, + newLPTokenBalance, + tx[sfAsset], + tx[sfAsset2], + journal + ) + + if not res.second: + return (res.first, False) + + return (tesSUCCESS, True) +``` + +# 3. getTradingFee + +Determines the trading fee for the withdrawer, accounting for auction slot discounts. Same implementation as AMMDeposit, see [deposit.md](deposit.md#3-gettradingfee). + +# 4. Multi-Asset Withdrawal Modes + +Multi-asset withdrawal modes maintain proportional withdrawals by removing both pool assets simultaneously. This preserves the pool's price (asset ratio) while decreasing liquidity. Since these withdrawals maintain proportional ratios, they incur no trading fees. + +The modes are: +- **[equalWithdrawTokens](#41-equalwithdrawtokens-tflptoken-tfwithdrawall) (tfLPToken, tfWithdrawAll)** - Redeem exact LP tokens or all LP tokens, receive proportional amounts of both assets +- **[equalWithdrawLimit](#42-equalwithdrawlimit-tftwoasset) (tfTwoAsset)** - Specify maximum amounts for both assets, system calculates LP tokens to burn + +## 4.1. equalWithdrawTokens (tfLPToken, tfWithdrawAll) + +> "I want to redeem exactly X LP tokens, how much of both assets do I get?" (tfLPToken) +> "Redeem all my LP tokens for both assets." (tfWithdrawAll) + +Proportional withdrawal of pool assets for the amount of LP tokens.[^equalWithdrawTokens] + +[^equalWithdrawTokens]: AMMWithdraw::equalWithdrawTokens: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L790-L871) + +This function handles two related modes. With `tfLPToken`, the user specifies the exact number of LP tokens to redeem using `LPTokenIn`, and the function calculates the proportional amounts of both assets to withdraw. With `tfWithdrawAll`, the user redeems their entire LP token balance without specifying an amount. The function handles a special case when withdrawing all LP tokens from the pool (`lpTokensWithdraw == lptAMMBalance`), which empties the pool completely. For partial withdrawals, it calculates the pool fraction (`frac = tokensAdj / lptAMMBalance`), then multiplies each asset balance by this fraction, rounding down with [`getRoundedAsset`](helpers.md#23-getroundedasset) to ensure the pool retains sufficient assets. + +**Example:** + +A pool has 150 USD, 150 EUR, and 150 LP tokens outstanding. Bob holds 30 LP tokens (20% of the pool). + +**Case 1: Bob redeems exactly 15 LP tokens (tfLPToken)** +- LP tokens to redeem: 15 +- Fraction of pool: 15 / 150 = 0.1 (10%) +- USD withdrawn: 150 * 0.1 = 15 USD +- EUR withdrawn: 150 * 0.1 = 15 EUR +- Result: Bob redeems 15 LP tokens, receives 15 USD + 15 EUR +- Bob now holds: 15 LP tokens (10% of remaining pool) + +**Case 2: Bob redeems all his LP tokens (tfWithdrawAll)** +- LP tokens to redeem: 30 (all Bob's holdings) +- Fraction of pool: 30 / 150 = 0.2 (20%) +- USD withdrawn: 150 * 0.2 = 30 USD +- EUR withdrawn: 150 * 0.2 = 30 EUR +- Result: Bob redeems 30 LP tokens, receives 30 USD + 30 EUR +- Bob now holds: 0 LP tokens + +### 4.1.1 equalWithdrawTokens Pseudo-Code + +```python +def equalWithdrawTokens( + view, # Ledger view (sandbox) + ammSle, # AMM ledger entry + account, # Withdrawer account ID + ammAccount, # AMM pseudo-account ID + amountBalance, # Current pool balance of asset1 + amount2Balance, # Current pool balance of asset2 + lptAMMBalance, # Total outstanding LP tokens + lpTokens, # Withdrawer's LP token balance + lpTokensWithdraw, # LP tokens to redeem + tfee, # Trading fee (not used for proportional) + freezeHandling, # How to handle frozen assets + withdrawAll, # Whether this is tfWithdrawAll + priorBalance, # Withdrawer's prior XRP balance + journal): # Debug journal + # CASE 1: Withdrawing all LP tokens in the pool + if lpTokensWithdraw == lptAMMBalance: + # Withdraw all assets, empty the pool + return withdraw( + view, + ammSle, + ammAccount, + account, + amountBalance, + amountBalance, # withdraw all of asset1 + amount2Balance, # withdraw all of asset2 + lptAMMBalance, + lpTokensWithdraw, + tfee, + freezeHandling, + WithdrawAll=True, # special handling for complete withdrawal + priorBalance, + journal + ) + + # CASE 2: Partial withdrawal + # Adjust LP tokens for precision (with fixAMMv1_3) + tokensAdj = adjustLPTokensIn(rules, lptAMMBalance, lpTokensWithdraw, withdrawAll) # helpers.md#25-adjustlptokensin-withdrawals + + if rules.enabled(fixAMMv1_3) and tokensAdj == 0: + return (tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, None) + + # Calculate the fraction of the pool being withdrawn + # Example: 5,000 LP tokens / 50,000 total = 0.1 (10% of pool) + frac = tokensAdj / lptAMMBalance + + # Calculate withdrawal amounts for both assets + # With fixAMMv1_3: Round DOWN (conservative, ensures pool keeps enough) + amountWithdraw = getRoundedAsset(rules, amountBalance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code + amount2Withdraw = getRoundedAsset(rules, amount2Balance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code + + # Prevent one-sided pool withdrawal due to rounding + # If either amount rounds to zero, fail so user withdraws more tokens + if amountWithdraw == 0 or amount2Withdraw == 0: + return (tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}) + + return withdraw( + view, + ammSle, + ammAccount, + account, + amountBalance, + amountWithdraw, + amount2Withdraw, + lptAMMBalance, + tokensAdj, + tfee, + freezeHandling, + withdrawAll, + priorBalance, + journal + ) +``` + +## 4.2. equalWithdrawLimit (tfTwoAsset) + +> "I want to withdraw up to X and Y, how many LP tokens do I burn?" + +The user specifies maximum amounts they want to withdraw for both assets (`Amount` and `Amount2`).[^equalWithdrawLimit] Since the withdrawal must maintain the pool's ratio, the function cannot simply use both maximum amounts - one will typically be limiting while the other has excess. + +[^equalWithdrawLimit]: AMMWithdraw::equalWithdrawLimit: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L899-L961) + +The function tries two strategies to maximize the withdrawal within the user's constraints. First, it attempts to withdraw all of `Amount` by calculating the pool fraction this represents (`frac = Amount / amountBalance`), converting this to LP tokens with proper rounding, then recalculating the fraction from the rounded LP tokens (`frac = adjustFracByTokens(...)`) to ensure precision consistency. Using this adjusted fraction, it calculates the proportional amount2 needed. If this amount2 fits within `Amount2`, the withdrawal proceeds immediately. + +Only if the first strategy fails does it try the second strategy: starting with all of `Amount2`, going through the same fraction -> LP tokens -> adjusted fraction → amount1 calculation. If the calculated amount1 exceeds `Amount`, the entire withdrawal fails. + +**Example:** + +There is an AMM with 100 USD and 100 EUR (1:1 ratio), 100 LP tokens outstanding. Bob holds 50 LP tokens and wants to make a proportional withdrawal. + +**Case 1: Bob tries to withdraw up to 30 USD + 20 EUR** +- Strategy 1: Use all 30 USD and needs 30 EUR to maintain 1:1 ratio - FAILS (only wants 20 EUR) +- Strategy 2: Use all 20 EUR and needs 20 USD to maintain 1:1 ratio - SUCCESS (wants up to 30 USD) +- Result: Withdraws 20 USD + 20 EUR, redeems 20 LP tokens + +**Case 2: Bob tries to withdraw up to 20 USD + 30 EUR** +- Strategy 1: Use all 20 USD and needs 20 EUR to maintain 1:1 ratio - SUCCESS (wants up to 30 EUR) +- Result: Withdraws 20 USD + 20 EUR, redeems 20 LP tokens + +### 4.2.1 equalWithdrawLimit Pseudo-Code + +```python +def equalWithdrawLimit( + view, # Ledger view (sandbox) + ammSle, # AMM ledger entry + ammAccount, # AMM pseudo-account ID + amountBalance, # Current pool balance of asset1 + amount2Balance, # Current pool balance of asset2 + lptAMMBalance, # Total outstanding LP tokens + amount, # User's max amount1 to withdraw + amount2, # User's max amount2 to withdraw + tfee): # Trading fee (not used for proportional) + # STRATEGY 1: Try using all of amount (asset1) + # Calculate what fraction of the pool this represents + frac = amount / amountBalance + + # Calculate LP tokens for this fraction + # Using simple version of getRoundedLPTokens (direct fraction) + tokensAdj = getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit=False) # helpers.md#211-getroundedlptokens-simple-pseudo-code + + if rules.enabled(fixAMMv1_3) and tokensAdj == 0: + return (tecAMM_INVALID_TOKENS, STAmount{}) + + # Adjust fraction based on rounded tokens (for precision consistency) + frac = adjustFracByTokens(rules, lptAMMBalance, tokensAdj, frac) + + # Calculate how much asset2 would be withdrawn for this fraction + # Using simple version of getRoundedAsset (direct fraction) + amount2Withdraw = getRoundedAsset(rules, amount2Balance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code + + # Check if calculated amount2 fits within user's max constraint + if amount2Withdraw <= amount2: + # Success! Use all of amount, calculated amount2Withdraw + return withdraw( + view, + ammSle, + ammAccount, + amountBalance, + amount, # withdraw all of asset1 + amount2Withdraw, # calculated asset2 + lptAMMBalance, + tokensAdj, + tfee + ) + + # STRATEGY 2: Strategy 1 failed, try using all of amount2 + # Calculate what fraction of the pool amount2 represents + frac = amount2 / amount2Balance + + # Calculate how much asset1 would be withdrawn for this fraction (preliminary) + # Using simple version of getRoundedAsset (direct fraction) + amountWithdraw = getRoundedAsset(rules, amountBalance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code + + # Calculate LP tokens for this fraction + # Using simple version of getRoundedLPTokens (direct fraction) + tokensAdj = getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit=False) # helpers.md#211-getroundedlptokens-simple-pseudo-code + + if rules.enabled(fixAMMv1_3) and tokensAdj == 0: + return (tecAMM_INVALID_TOKENS, STAmount{}) + + # Adjust fraction based on rounded tokens (for precision consistency) + frac = adjustFracByTokens(rules, lptAMMBalance, tokensAdj, frac) + + # Recalculate asset1 amount with adjusted fraction + # Using simple version of getRoundedAsset (direct fraction) + amountWithdraw = getRoundedAsset(rules, amountBalance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code + + # Check if calculated amount fits within user's max constraint + if rules.enabled(fixAMMv1_3): + if amountWithdraw > amount: + return (tecAMM_FAILED, STAmount{}) + + # Success! Use calculated amountWithdraw, all of amount2 + return withdraw( + view, + ammSle, + ammAccount, + amountBalance, + amountWithdraw, # calculated asset1 + amount2, # withdraw all of asset2 + lptAMMBalance, + tokensAdj, + tfee + ) +``` + +# 5. Single-Asset Withdrawal Modes + +Single-asset withdrawal modes allow users to withdraw only one asset instead of both assets proportionally. Unlike [multi-asset withdrawals](#4-multi-asset-withdrawal-modes) that maintain the pool ratio, single-asset withdrawals change the pool composition. Because they alter the pool ratio, [trading fees](#3-gettradingfee) apply to single-asset withdrawals. There are three modes: + +- **[singleWithdraw](#51-singlewithdraw-tfsingleasset) (tfSingleAsset)** - User specifies withdrawal amount, system calculates LP tokens to redeem +- **[singleWithdrawTokens](#52-singlewithdrawtokens-tfoneassetlptoken-tfoneassetwithdrawall) (tfOneAssetLPToken, tfOneAssetWithdrawAll)** - User specifies exact LP tokens to redeem or all LP tokens, withdraws calculated amount of single asset +- **[singleWithdrawEPrice](#53-singlewithdraweprice-tflimitlptoken) (tfLimitLPToken)** - User specifies minimum effective price limit + +## 5.1. singleWithdraw (tfSingleAsset) + +> "I want to withdraw X amount, how many LP tokens must I redeem?" + +The user specifies `Amount` (the asset amount to withdraw) and the function calculates how many LP tokens must be redeemed.[^singleWithdraw] Since this is a single-asset withdrawal that changes the pool ratio, a [trading fee](#3-gettradingfee) applies. The function uses [`lpTokensIn`](helpers.md#331-lptokensin-equation-7) (Equation 7) to calculate the LP tokens based on the withdrawal amount and trading fee, then adjusts the result for precision with [`adjustLPTokensIn`](helpers.md#25-adjustlptokensin-withdrawals). The adjusted tokens are passed to `adjustAssetOutByTokens` to recalculate the withdrawal amount, ensuring the reverse calculation produces consistent results and doesn't underpay the user due to rounding. + +[^singleWithdraw]: AMMWithdraw::singleWithdraw: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L969-L1007) + +### 5.1.1. singleWithdraw Pseudo-Code + +```python +def singleWithdraw( + view, # Ledger view (sandbox) + ammSle, # AMM ledger entry + ammAccount, # AMM pseudo-account ID + amountBalance, # Current pool balance of the asset + lptAMMBalance, # Total outstanding LP tokens + amount, # Amount to withdraw + tfee): # Trading fee (for single-asset withdrawal) + # Calculate LP tokens using the single-asset withdrawal formula + # lpTokensIn solves: "How many LP tokens to redeem to get `amount` assets?" + tokens = lpTokensIn(amountBalance, amount, lptAMMBalance, tfee) # helpers.md#331-lptokensin-equation-7 + + # Adjust LP tokens for precision (with fixAMMv1_3) + tokensAdj = adjustLPTokensIn(rules, lptAMMBalance, tokens, isWithdrawAll(tx)) # helpers.md#25-adjustlptokensin-withdrawals + + if tokensAdj == 0: + if not rules.enabled(fixAMMv1_3): + return (tecAMM_FAILED, STAmount{}) + else: + return (tecAMM_INVALID_TOKENS, STAmount{}) + + # Adjust withdrawal amount based on adjusted tokens + # This ensures the reverse calculation produces consistent results + tokensAdj, amountWithdrawAdj = adjustAssetOutByTokens( + rules, amountBalance, amount, lptAMMBalance, tokensAdj, tfee) + + if rules.enabled(fixAMMv1_3) and tokensAdj == 0: + return (tecAMM_INVALID_TOKENS, STAmount{}) + + return withdraw( + view, + ammSle, + ammAccount, + amountBalance, + amountWithdrawAdj, # adjusted: actual amount to withdraw + None, # single-asset withdrawal (no asset2) + lptAMMBalance, + tokensAdj, # calculated: LP tokens to redeem + tfee + ) +``` + +## 5.2. singleWithdrawTokens (tfOneAssetLPToken, tfOneAssetWithdrawAll) + +> "I'll redeem exactly X LP tokens, how much asset (single asset only) do I get?" (tfOneAssetLPToken) +> "Redeem all my LP tokens for a single asset." (tfOneAssetWithdrawAll) + +Withdraw a single asset by redeeming specified LP tokens.[^singleWithdrawTokens] + +[^singleWithdrawTokens]: AMMWithdraw::singleWithdrawTokens: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L1020-L1051) + +This function handles the reverse calculation from [`singleWithdraw`](#51-singlewithdraw-tfsingleasset): the user specifies the exact number of LP tokens to redeem (using `LPTokenIn` for tfOneAssetLPToken, or all LP tokens for tfOneAssetWithdrawAll), and the function calculates the withdrawal amount of a single asset. The user can provide `Amount` as a minimum constraint on how much they expect to receive. + +The function first adjusts the LP tokens for precision using [`adjustLPTokensIn`](helpers.md#25-adjustlptokensin-withdrawals), then uses [`ammAssetOut`](helpers.md#332-ammassetout-equation-8) (Equation 8) to calculate the withdrawal amount by solving the inverse single-asset withdrawal problem. If the calculated amount is less than the user's `Amount` constraint (when non-zero), the transaction fails with `tecAMM_FAILED`. + +### 5.2.1. singleWithdrawTokens Pseudo-Code + +```python +def singleWithdrawTokens( + view, # Ledger view (sandbox) + ammSle, # AMM ledger entry + ammAccount, # AMM pseudo-account ID + amountBalance, # Current pool balance of the asset + lptAMMBalance, # Total outstanding LP tokens + amount, # Min asset to receive (or 0 for no min, or asset specifier) + lpTokensWithdraw, # LP tokens to redeem + tfee): # Trading fee (for single-asset withdrawal) + # Adjust LP tokens for precision (with fixAMMv1_3) + tokensAdj = adjustLPTokensIn(rules, lptAMMBalance, lpTokensWithdraw, isWithdrawAll(tx)) # helpers.md#25-adjustlptokensin-withdrawals + + if rules.enabled(fixAMMv1_3) and tokensAdj == 0: + return (tecAMM_INVALID_TOKENS, STAmount{}) + + # Calculate withdrawal amount using ammAssetOut formula + amountWithdraw = ammAssetOut(amountBalance, lptAMMBalance, tokensAdj, tfee) # helpers.md#332-ammassetout-equation-8 + + # Check if calculated amount meets user's minimum (if specified) + if amount == 0 or amountWithdraw >= amount: + # Either no minimum specified, or calculated amount meets minimum + return withdraw( + view, + ammSle, + ammAccount, + amountBalance, + amountWithdraw, # calculated: amount to withdraw + None, # single-asset withdrawal + lptAMMBalance, + tokensAdj, # exact: LP tokens to redeem + tfee + ) + + # Calculated amount is less than user's minimum + return (tecAMM_FAILED, STAmount{}) +``` + +## 5.3. singleWithdrawEPrice (tfLimitLPToken) + +> "I'll withdraw (single asset), but only if the effective price per LP token is reasonable." + +Withdraw a single asset with an effective price constraint.[^singleWithdrawEPrice] + +This mode allows users to control the effective price when redeeming LP tokens, where effective price is defined as the ratio of LP tokens redeemed to asset withdrawn. The user provides `EPrice` (maximum effective price) and optionally `Amount` (minimum withdrawal amount). As with deposits, `EPrice` is an upper bound: the trade is sized so the effective price does not exceed `EPrice`. A lower effective price means a better deal for the withdrawer (fewer LP tokens per asset withdrawn). + +The function solves a derived formula from Equation 8 to calculate the LP tokens that achieve exactly the specified effective price. It then calculates the withdrawal amount as `tokensAdj / ePrice`. If the calculated amount is less than the user's optional `Amount` constraint, the transaction fails with `tecAMM_FAILED`. Under the `fixCleanup3_3_0` amendment, a denominator (`T*f - B*E`) of exactly zero also fails with `tecAMM_FAILED`. Without the amendment that division throws and the transaction fails with `tefEXCEPTION`. + +[^singleWithdrawEPrice]: AMMWithdraw::singleWithdrawEPrice: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L1119-L1179) + +### 5.3.1. singleWithdrawEPrice Pseudo-Code + +```python +def singleWithdrawEPrice( + view, # Ledger view (sandbox) + ammSle, # AMM ledger entry + ammAccount, # AMM pseudo-account ID + amountBalance, # Current pool balance of the asset + lptAMMBalance, # Total outstanding LP tokens + amount, # Min asset to receive (or 0 for no min) + ePrice, # Min effective price (LPTokenIn / AssetOut) + tfee): # Trading fee (for single-asset withdrawal) + # Calculate intermediate value: B * E (balance * effective price) + ae = amountBalance * ePrice + + # Get fee multiplier + f = getFee(tfee) # fee in units of 1/100,000 (e.g., 30 -> 0.0003) + + # Calculate LP tokens using derived formula + # t = T*(T + B*E*(f-2)) / (T*f - B*E) + tokNoRoundCb = lambda: ( + lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae) + ) + tokProdCb = lambda: ( + (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae) + ) + + tokensAdj = getRoundedLPTokens( + rules, tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit=False) # helpers.md#212-getroundedlptokens-callback-pseudo-code + + if tokensAdj <= 0: + if not rules.enabled(fixAMMv1_3): + return (tecAMM_FAILED, STAmount{}) + else: + return (tecAMM_INVALID_TOKENS, STAmount{}) + + # Calculate withdrawal amount from tokens and effective price + # amountWithdraw = tokensAdj / ePrice + amtNoRoundCb = lambda: tokensAdj / ePrice + amtProdCb = lambda: tokensAdj / ePrice + + amountWithdraw = getRoundedAsset( + rules, amtNoRoundCb, amount, amtProdCb, IsDeposit=False) # helpers.md#232-getroundedasset-callback-pseudo-code + + # Check if calculated amount meets user's minimum (if specified) + if amount == 0 or amountWithdraw >= amount: + return withdraw( + view, + ammSle, + ammAccount, + amountBalance, + amountWithdraw, # calculated: amount to withdraw + None, # single-asset withdrawal + lptAMMBalance, + tokensAdj, # calculated: LP tokens to redeem + tfee + ) + + # Calculated amount is less than user's minimum + return (tecAMM_FAILED, STAmount{}) +``` + +# 6. Common Withdraw Function + +The `withdraw()` function[^withdraw] serves as the final common pathway for all withdrawal modes, executing the actual asset transfers after mode-specific handlers determine the withdrawal amounts. + +Under `fixCleanup3_3_0` together with `fixAMMv1_3`, the common path also runs the pool product check described in [Precision and Rounding](helpers.md#2-precision-and-rounding). See the [failure conditions](README.md#332-failure-conditions) for the resulting `tecPRECISION_LOSS`. + +[^withdraw]: AMMWithdraw::withdraw: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L479-L749) + +This function orchestrates a sequenced validation and execution flow. It begins by verifying the withdrawer holds sufficient LP tokens to redeem, then enforces pool integrity constraints that prevent malformed states. + +The function prohibits one-sided pool withdrawals - situations where all of one asset would be withdrawn while the other remains. When a withdrawal would redeem all outstanding LP tokens, the function mandates that all pool assets must also be withdrawn, preventing orphaned assets in an empty pool. + +Before executing transfers, the function checks whether the withdrawer has adequate XRP reserves if new trust lines or MPTokens need creation. For MPTs, this includes verifying proper authorization from the issuer. The function then transfers each withdrawn asset from the AMM account to the withdrawer, waiving transfer fees as AMM operations are privileged. Finally, it burns the redeemed LP tokens by calling `redeemIOU`, which reduces both the withdrawer's LP token balance and the total outstanding token supply. + +## 6.1. withdraw Pseudo-Code + +```python +def withdraw( + view, # Ledger view (sandbox) + ammSle, # AMM ledger entry + ammAccount, # AMM pseudo-account ID + account, # Withdrawer account ID + amountBalance, # Current pool balance of asset1 + amountWithdraw, # Amount1 to withdraw + amount2Withdraw, # Optional: amount2 to withdraw + lpTokensAMMBalance, # Total outstanding LP tokens + lpTokensWithdraw, # LP tokens to redeem + tfee, # Trading fee + freezeHandling, # How to handle frozen assets + authHandling, # How to handle unauthorized MPT holders + withdrawAll, # Whether this is a complete withdrawal + priorBalance: # Withdrawer's prior XRP balance + # Get withdrawer's current LP token balance + lpTokens = ammLPHolds(view, ammSle, account, journal) + + # Get current pool balances (accounting for freezes and authorization) + currentBalances = ammHolds(view, ammSle, amountWithdraw.issue, None, freezeHandling, authHandling) + if not currentBalances: + return (currentBalances.error(), STAmount{}, STAmount{}, STAmount{}) + + curBalance, curBalance2, _ = currentBalances + + # Adjust amounts for precision (with fixAMMv1_3, this shouldn't be needed as we have already adjusted and rounded all numbers properly) + # When withdrawing all, skip adjustment and use exact values + if withdrawAll == No: + amountWithdrawActual, amount2WithdrawActual, lpTokensWithdrawActual = \ + adjustAmountsByLPTokens( + amountBalance, + amountWithdraw, + amount2Withdraw, + lpTokensAMMBalance, + lpTokensWithdraw, + tfee, + IsDeposit=False + ) + else: + amountWithdrawActual = amountWithdraw + amount2WithdrawActual = amount2Withdraw + lpTokensWithdrawActual = lpTokensWithdraw + + # Validate LP tokens + if lpTokensWithdrawActual <= 0 or lpTokensWithdrawActual > lpTokens: + return (tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, STAmount{}) + + # With fixAMMv1_1: Additional validation + if rules.enabled(fixAMMv1_1) and lpTokensWithdrawActual > lpTokensAMMBalance: + return (tecINTERNAL, STAmount{}, STAmount{}, STAmount{}) + + # Prevent one-sided pool withdrawal + # If withdrawing all of one asset but not the other, fail + # This ensures pools are always balanced (or completely empty) + if (amountWithdrawActual == curBalance and amount2WithdrawActual != curBalance2) or \ + (amount2WithdrawActual == curBalance2 and amountWithdrawActual != curBalance): + return (tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}) + + # If redeeming all LP tokens, must withdraw all assets + # This prevents situations where LP tokens are zero but assets remain + if lpTokensWithdrawActual == lpTokensAMMBalance and \ + (amountWithdrawActual != curBalance or amount2WithdrawActual != curBalance2): + return (tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}) + + # Check withdrawal doesn't exceed pool balance + if amountWithdrawActual > curBalance or amount2WithdrawActual > curBalance2: + return (tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}) + + # With featureMPTokensV2: the post-withdrawal pool state must be consistent + # (all balances zero or all non-zero, agreeing with the LP token total) + if rules.enabled(featureMPTokensV2): + newBalanceZero = (curBalance - amountWithdrawActual) == 0 + newBalance2Zero = (curBalance2 - amount2WithdrawActual) == 0 + newLPTokensZero = (lpTokensAMMBalance - lpTokensWithdrawActual) == 0 + if amount2WithdrawActual is None: + valid = (newBalanceZero == newLPTokensZero) + else: + valid = (newBalanceZero == newBalance2Zero and newBalance2Zero == newLPTokensZero) + if not valid: + return (tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}) + + # Helper function to check reserve requirements (with fixAMMv1_2) + # Checks if withdrawer has sufficient XRP reserve for trust line or MPToken creation + def sufficientReserve(asset): + if not rules.enabled(fixAMMv1_2) or isXRP(asset): + return (tesSUCCESS, None) + + # Check if trust line (for IOUs) or MPToken (for MPTs) exists + if isIOU(asset): + assetExists = view.exists(keylet.line(account, asset.issue)) + mptokenKey = None + else: # MPT + issuanceKey = keylet.mptIssuance(asset.mptID) + mptokenKey = keylet.mptoken(issuanceKey, account) + assetExists = view.exists(mptokenKey) + if assetExists: + mptokenKey = None # Already exists, no need to create + + if not assetExists: + sleAccount = view.peek(keylet.account(account)) + if not sleAccount: + return (tecINTERNAL, None) + + balance = sleAccount[sfBalance].xrp + ownerCount = sleAccount[sfOwnerCount] + + reserve = view.fees().accountReserve(ownerCount + 1) if ownerCount >= 2 else 0 + + # For IOUs: use max of prior and current balance + # For MPTs: use prior balance only + balanceToCheck = max(priorBalance, balance) if isIOU(asset) else priorBalance + + if balanceToCheck < reserve: + return (tecINSUFFICIENT_RESERVE, None) + + return (tesSUCCESS, mptokenKey) + + # Helper function to create MPToken if needed + def createMPToken(asset, mptokenKey): + if mptokenKey and account != asset.getIssuer(): + # Must authorize MPToken + if requireAuth(view, asset.mptIssue, account, WeakAuth) != tesSUCCESS: + return err + + if checkCreateMPT(view, asset.mptIssue, account, journal) != tesSUCCESS: + return err + + return tesSUCCESS + + # Check reserve and create MPToken for asset1 + result, mptokenKey = sufficientReserve(amountWithdrawActual.asset) + if result != tesSUCCESS: + return (result, STAmount{}, STAmount{}, STAmount{}) + + result = createMPToken(amountWithdrawActual.asset, mptokenKey) + if result != tesSUCCESS: + return (result, STAmount{}, STAmount{}, STAmount{}) + + # Transfer asset1 from AMM to withdrawer + result = accountSend( + view, + ammAccount, # from: AMM pseudo-account + account, # to: withdrawer + amountWithdrawActual, # amount + WaiveTransferFee=Yes # AMM withdrawals don't pay transfer fees + ) + if result != tesSUCCESS: + return (result, STAmount{}, STAmount{}, STAmount{}) + + # If two-asset withdrawal, check reserve, create MPToken, and transfer asset2 + if amount2WithdrawActual: + # Check reserve and create MPToken for asset2 + result, mptokenKey = sufficientReserve(amount2WithdrawActual.asset) + if result != tesSUCCESS: + return (result, STAmount{}, STAmount{}, STAmount{}) + + result = createMPToken(amount2WithdrawActual.asset, mptokenKey) + if result != tesSUCCESS: + return (result, STAmount{}, STAmount{}, STAmount{}) + + result = accountSend( + view, + ammAccount, + account, + amount2WithdrawActual, + WaiveTransferFee=Yes + ) + if result != tesSUCCESS: + return (result, STAmount{}, STAmount{}, STAmount{}) + + # Redeem (burn) LP tokens + # This decreases the trust line balance and may delete the trust line + result = redeemIOU( + view, + account, + lpTokensWithdrawActual, + lpTokensWithdrawActual.issue, + journal + ) + if result != tesSUCCESS: + return (result, STAmount{}, STAmount{}, STAmount{}) + + # Return success with new LP token balance and actual withdrawal amounts + return ( + tesSUCCESS, + lpTokensAMMBalance - lpTokensWithdrawActual, + amountWithdrawActual, + amount2WithdrawActual + ) +``` diff --git a/docs/credentials/README.md b/docs/credentials/README.md index 0f9071a..68d2bf5 100644 --- a/docs/credentials/README.md +++ b/docs/credentials/README.md @@ -1,486 +1,486 @@ -# Index - -- [1. Introduction](#1-introduction) - - [1.1. Key Concepts](#11-key-concepts) - - [1.2. Credential Lifecycle](#12-credential-lifecycle) - - [1.3. Use Cases](#13-use-cases) -- [2. Ledger Entries](#2-ledger-entries) - - [2.1. Credential Ledger Entry](#21-credential-ledger-entry) - - [2.1.1. Object Identifier](#211-object-identifier) - - [2.1.2. Fields](#212-fields) - - [2.1.2.1. Flags](#2121-flags) - - [2.1.3. Pseudo-accounts](#213-pseudo-accounts) - - [2.1.4. Ownership](#214-ownership) - - [2.1.5. Reserves](#215-reserves) -- [3. Transactions](#3-transactions) - - [3.1. CredentialCreate Transaction](#31-credentialcreate-transaction) - - [3.1.1. Failure Conditions](#311-failure-conditions) - - [3.1.2. State Changes](#312-state-changes) - - [3.2. CredentialAccept Transaction](#32-credentialaccept-transaction) - - [3.2.1. Failure Conditions](#321-failure-conditions) - - [3.2.2. State Changes](#322-state-changes) - - [3.3. CredentialDelete Transaction](#33-credentialdelete-transaction) - - [3.3.1. Failure Conditions](#331-failure-conditions) - - [3.3.2. State Changes](#332-state-changes) -- [4. Authorization Integration](#4-authorization-integration) - - [4.1. DepositAuth Integration](#41-depositauth-integration) - - [4.2. PermissionedDomain Integration](#42-permissioneddomain-integration) - -# 1. Introduction - -Credentials are a decentralized authorization mechanism on the XRP Ledger, borrowing concepts from the W3C Verifiable Credentials Data Model[^1], that allows accounts to create credentials for subjects (individuals, organizations, or devices) and use those credentials for access control. Once a credential is accepted and stored on the ledger, it can be autonomously verified by checking the ledger state without requiring interaction with the issuer. - -For example, a trading venue creates a [PermissionedDomain](../permissioned_domains/README.md) requiring an "accredited_investor" credential from a regulatory authority. When Alice wants to trade on this venue: -1. The Regulator issues the credential: RegulatorAccountID sends a CredentialCreate transaction with Subject=AliceAccountID and CredentialType="accredited_investor" -2. Alice accepts it: AliceAccountID sends a CredentialAccept transaction -3. The credential ledger entry now exists with `lsfAccepted` flag set -4. When Alice submits an OfferCreate transaction on the permissioned domain, the ledger checks: Does a Credential exist where `Subject=AliceAccountID, Issuer=RegulatorAccountID, CredentialType="accredited_investor", lsfAccepted=true`, and not expired? -5. If yes, the transaction is authorized - -Credentials can also be used for: - -- **[`DepositAuth`](#41-depositauth-integration)**: An account with the `lsfDepositAuth` flag accepts incoming payments, for XRP, IOUs, and MPTs alike, only from authorized senders. Instead of pre-authorizing each sender individually, credentials can be used to authorize an `(issuer, credentialType)` pair. Any holder of a matching accepted credential can make a payment to an account with the `lsfDepositAuth` flag by listing it in the payment's `CredentialIDs` field. -- **Self-attestation (issuer == subject)**: an account can issue a credential to itself. This is a verifiable on-ledger claim about itself, accepted automatically. -- **Tiered access control**: different credential types representing different authorization levels from the same issuer. - -[^1]: W3C Verifiable Credentials Data Model: https://www.w3.org/TR/vc-data-model-2.0/ -[^2]: For self-issued credentials (issuer == subject), the credential appears in only one directory, so SubjectNode is not set. sfSubjectNode defined as soeOPTIONAL: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/ledger_entries.macro#L445). SubjectNode only set in the issuer != subject branch: [`Credentials.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp#L163-L174) - -## 1.1. Terminology and Concepts - -**Issuer**: The account that creates and signs the credential. The issuer attests to some property or status of the subject. The issuer's identity determines the trust level of the credential. - -**Subject**: The account that holds the credential. This is the entity the credential makes claims about. The subject must accept the credential before it becomes active (unless the subject is also the issuer). - -**Credential Type**: A string identifier (max 64 bytes) that categorizes the credential. This allows different credential types from the same issuer (e.g., "kyc_basic", "kyc_advanced", "membership_gold"). Applications use the credential type to determine what authorization the credential provides. - -**Acceptance**: Before a credential can be used for authorization, the subject must explicitly accept it via `CredentialAccept` (self-issued credentials, where issuer == subject, are accepted automatically). An unaccepted credential still appears in the subject's directory but is inactive, and the *issuer* pays its reserve until acceptance. This ensures a credential can't be used on the subject's behalf, or charged against the subject's reserve, without their consent. - -**Expiration**: Credentials can optionally have an expiration time. After expiration, the credential can no longer be used for authorization and can be deleted by anyone to recover ledger space. - -## 1.2. Credential Lifecycle - -```mermaid -stateDiagram-v2 - [*] --> Created: CredentialCreate - Created --> Accepted: CredentialAccept - Created --> Expired: Expiration time passes - Accepted --> Expired: Expiration time passes - Expired --> [*]: CredentialDelete (by anyone) - Accepted --> [*]: CredentialDelete - Created --> [*]: CredentialDelete -``` -Figure: Credential state machine - - -**1. Creation Phase (CredentialCreate)**: -- The issuer creates a credential for a subject -- The credential is stored in the issuer's owner directory -- Issuer's owner count increases by 1 (issuer pays the reserve) -- If issuer == subject: - - Credential is immediately marked as accepted - - Credential appears in only one directory (since issuer and subject are the same account) -- If issuer != subject: - - Credential is also added to the subject's owner directory - -**2. Acceptance Phase (CredentialAccept)**: -- The subject explicitly accepts the credential -- The `lsfAccepted` flag is set -- The credential ownership (who pays the reserve) transfers from the issuer to the subject: - - Issuer's owner count decreases by 1 (freeing the issuer's reserve) - - Subject's owner count increases by 1 (subject now pays the reserve) - - The credential remains in both the issuer's and subject's owner directories -- The credential becomes active and can be used for authorization - -**3. Usage Phase**: -- The credential can be referenced in transactions via the `CredentialIDs` field -- Used for DepositAuth authorization or PermissionedDomain access -- Remains valid until deleted or expired - -**4. Expiration/Deletion Phase**: -- Credentials with an `Expiration` field become unusable after that time -- Expired credentials can be deleted by anyone -- Active credentials can be deleted by the issuer or subject at any time -- Deletion removes the credential from both owner directories - -# 2. Ledger Entries - - -## 2.1. Credential Ledger Entry - -The `Credential` ledger entry (type `ltCREDENTIAL = 0x0081`) represents a verifiable credential issued by one account to another. Each credential is uniquely identified by the combination of subject, issuer, and credential type. - -### 2.1.1. Object Identifier - -The key of the `Credential` object is the result of SHA512-Half of the following values concatenated in order: - -- The `CREDENTIAL` space key `0x0044` (character 'D') -- The subject account ID (160 bits) -- The issuer account ID (160 bits) -- The credential type string (variable length, max 64 bytes) - -This ensures each credential is uniquely identified by its (subject, issuer, type) triple. Multiple credentials can exist between the same subject and issuer as long as they have different credential types. - - -### 2.1.2. Fields - -| Field | Type | Required | Description | -|---------------------|-----------|----------|-----------------------------------------------------------| -| `Subject` | AccountID | Yes | The account holding this credential | -| `Issuer` | AccountID | Yes | The account that issued this credential | -| `CredentialType` | Blob | Yes | Type identifier string (max 64 bytes) | -| `Expiration` | UInt32 | Optional | Unix timestamp when credential expires | -| `URI` | Blob | Optional | Reference URI for credential metadata (max 256 bytes) | -| `IssuerNode` | UInt64 | Yes | Index of the issuer's owner directory page | -| `SubjectNode` | UInt64 | Optional | Index of the subject's owner directory page (only present when issuer != subject)[^2] | -| `Sponsor` | AccountID | Optional | Account currently covering this credential's owner reserve. Present only while the reserve is sponsored (`Sponsor` amendment)[^6] | -| `Flags` | UInt32 | Yes | Credential flags (see below); always present, 0 until `lsfAccepted` is set | -| `PreviousTxnID` | Hash256 | Yes | Hash of the previous transaction that modified this entry | -| `PreviousTxnLgrSeq` | UInt32 | Yes | Ledger sequence of the previous transaction | - -#### 2.1.2.1. Flags - -The `Flags` field can contain the following values: - -| Flag Name | Hex Value | Description | -|-----------|-----------|-------------| -| `lsfAccepted` | `0x00010000` | The subject has accepted this credential and it is now active | - -**Flag Behavior**: -- When `lsfAccepted` is not set: The credential exists but has not been accepted by the subject. It cannot be used for authorization. It appears in both the issuer's and subject's owner directories, but only the issuer's owner count is incremented (the issuer pays the reserve).[^3] -- When `lsfAccepted` is set: The credential has been accepted and is active. It appears in both the issuer's and subject's owner directories and can be used for authorization. - -[^3]: Credential added to both directories during creation: [`CredentialCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp#L154-L183) -[^4]: Deletion authorization: [`Credentials.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp#L89-L94) -[^6]: [`LedgerFormats.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/protocol/LedgerFormats.cpp#L13-L21), [`SponsorHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/SponsorHelpers.cpp#L267-L285) -- Self-issued credentials (issuer == subject) automatically have `lsfAccepted` set during creation. - -### 2.1.3. Pseudo-accounts - -Credential transactions (creating, accepting, or deleting credentials) do not create pseudo-accounts. - -### 2.1.4. Ownership - -Credentials can appear in up to two owner directories: - -**Before Acceptance** (when issuer != subject): -- Appears in both the issuer's and subject's owner directories -- Issuer's owner count increases by 1 - -**After Acceptance**: -- Still appears in both the issuer's and subject's owner directories -- Ownership transfers: Issuer's owner count decreases by 1, subject's owner count increases by 1 - -**Self-Issued Credentials** (issuer == subject): -- Immediately accepted during creation -- Appears in a single owner directory (since issuer and subject are the same) -- Account's owner count increases by 1 - -### 2.1.5. Reserves - -Credentials follow the standard XRP Ledger reserve requirements: - -- **Owner Reserve**: Each credential requires one owner reserve from the account that owns it -- **Before Acceptance**: Issuer pays the reserve (since credential is in issuer's directory) -- **After Acceptance**: Subject pays the reserve (reserve responsibility transfers to the subject) -- **Self-Issued**: Account pays one reserve (not two, since there's only one directory entry) - -The owner reserve is calculated as `incrementalReserve` (the per-object owner reserve increment set by the network). When a credential is deleted, the reserve is freed and the owner count decreases. - -Under the `Sponsor` amendment (XLS-68), a credential's reserve can be covered by a reserve sponsor recorded in the credential's `Sponsor` field. The sponsor then bears the reserve in place of the issuer or subject. Sponsorship does not carry over automatically when the subject accepts. Deletion releases the reserve against the recorded sponsor. The sponsorship mechanism is described in the [transactions documentation](../transactions/README.md).[^7] - -[^7]: [`SponsorHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/SponsorHelpers.cpp#L28-L61), [`CredentialCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp#L141-L170), [`CredentialAccept.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp#L96-L134), [`CredentialHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/CredentialHelpers.cpp#L97-L101) - -# 3. Transactions - -## 3.1. CredentialCreate Transaction - -The `CredentialCreate` transaction creates a new credential from an issuer to a subject. Under the `fixCleanup3_3_0` amendment, the subject cannot be a pseudo-account (an AMM, Vault, or LoanBroker account). - -| Field Name | Required? | JSON Type | Internal Type | Description | -|-------------------|:------------------:|:---------:|:-------------:|:------------------------------------------------------------| -| `TransactionType` | :heavy_check_mark: | String | UInt16 | Must be `"CredentialCreate"` | -| `Account` | :heavy_check_mark: | String | AccountID | The issuer account creating the credential | -| `Subject` | :heavy_check_mark: | String | AccountID | The account that will hold the credential | -| `CredentialType` | :heavy_check_mark: | String | Blob | Type identifier (max 64 bytes) | -| `Expiration` | | Number | UInt32 | Unix timestamp when credential expires | -| `URI` | | String | Blob | Reference URI (max 256 bytes) | -| `Flags` | | Number | UInt32 | Transaction flags (must be 0, only universal flags allowed) | - -### 3.1.1. Failure Conditions - -**Static validation:** - -- `temDISABLED`: featureCredentials not enabled -- `temMALFORMED`: - - `Subject` is a zero AccountID - - `CredentialType` is empty or exceeds 64 bytes - - `URI` is empty or exceeds 256 bytes - -**Validation against the ledger view:** - -- `tecNO_TARGET`: Subject account does not exist -- `tecDUPLICATE`: A credential with this (subject, issuer, credentialType) triple already exists -- `tecPSEUDO_ACCOUNT`: `Subject` is a pseudo-account, such as an AMM, Vault, or LoanBroker account (requires the `fixCleanup3_3_0` amendment)[^8] - -[^8]: [`CredentialCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp#L101-L105) - -**Validation during doApply** - -- `tefINTERNAL`: Failed to create credential ledger entry or issuer account not found -- `tecEXPIRED`: `Expiration` field is set to a time in the past (before ledger close time) -- `tecINSUFFICIENT_RESERVE`: Issuer has insufficient XRP to pay the owner reserve. For a sponsored reserve, the sponsor has insufficient XRP or the pre-funded sponsorship has no remaining owner-count allowance -- `tecDIR_FULL`: Owner directory is full and cannot add new entry - -### 3.1.2. State Changes - -- `Credential` object is **created**: - - `Subject`: Set to subject account ID - - `Issuer`: Set to issuer account ID (transaction sender) - - `CredentialType`: Set to specified type string - - `Expiration`: Set to specified timestamp (if provided) - - `URI`: Set to specified URI (if provided) - - `SubjectNode`: Index in subject's owner directory - - `IssuerNode`: Index in issuer's owner directory - - `Sponsor`: Set to the reserve sponsor (only when the transaction's reserve is sponsored) - - `Flags`: - - If issuer == subject: `lsfAccepted` is set immediately - - If issuer != subject: No flags set (credential awaits acceptance) - -- Issuer's `AccountRoot` is **modified**: - - `OwnerCount`: Incremented by 1 - -- For a reserve-sponsored transaction, the sponsorship accounting fields on the issuer, the sponsor, and any pre-funded `Sponsorship` entry are also updated, as described in the [transactions documentation](../transactions/README.md). - -- `DirectoryNode` entries are **created/modified**: - - Credential added to issuer's owner directory (always) - - If issuer == subject: Same directory entry (counted once) - - If issuer != subject: Credential added to both issuer's and subject's owner directories - -## 3.2. CredentialAccept Transaction - -The `CredentialAccept` transaction allows a subject to accept a credential that has been issued to them. - -| Field Name | Required? | JSON Type | Internal Type | Description | -|-------------------|:------------------:|:---------:|:-------------:|:------------------------------------------------------------| -| `TransactionType` | :heavy_check_mark: | String | UInt16 | Must be `"CredentialAccept"` | -| `Account` | :heavy_check_mark: | String | AccountID | The subject account accepting the credential | -| `Issuer` | :heavy_check_mark: | String | AccountID | The issuer of the credential | -| `CredentialType` | :heavy_check_mark: | String | Blob | Type identifier (max 64 bytes) | -| `Flags` | | Number | UInt32 | Transaction flags (must be 0, only universal flags allowed) | - -### 3.2.1. Failure Conditions - -**Static validation:** - -- `temDISABLED`: featureCredentials not enabled -- `temINVALID_ACCOUNT_ID`: `Issuer` is a zero AccountID -- `temMALFORMED`: `CredentialType` is empty or exceeds 64 bytes - -**Validation against the ledger view:** - -- `tecNO_ISSUER`: Issuer account does not exist -- `tecNO_ENTRY`: Credential does not exist for this (subject=Account, issuer, credentialType) triple -- `tecDUPLICATE`: Credential already has `lsfAccepted` flag set - -**Validation during doApply** - -- `tefINTERNAL`: Subject or issuer account not found -- `tecINSUFFICIENT_RESERVE`: Subject has insufficient XRP to pay the owner reserve. For a sponsored reserve, the sponsor has insufficient XRP or the pre-funded sponsorship has no remaining owner-count allowance -- `tecEXPIRED`: Credential has expired (current ledger time > credential's `Expiration`) - -### 3.2.2. State Changes - -**If credential is not expired:** - -- `Credential` object is **modified**: - - `Flags`: `lsfAccepted` flag is set - - `Sponsor`: The pre-acceptance sponsor, if any, is removed. The accept transaction's reserve sponsor, if any, is recorded - -- Issuer's `AccountRoot` is **modified**: - - `OwnerCount`: Decremented by 1 - -- Subject's `AccountRoot` is **modified**: - - `OwnerCount`: Incremented by 1 - -- For sponsored reserves, the issuer-side release is applied against the pre-acceptance sponsor and the subject's new reserve is accounted against the accept transaction's sponsor, as described in the [transactions documentation](../transactions/README.md). - -**If credential is expired:** - -- `Credential` object is **deleted** (removed from ledger) -- State changes follow deletion rules (see [section 3.3.2](#332-state-changes)) - -## 3.3. CredentialDelete Transaction - -The `CredentialDelete` transaction removes a credential from the ledger. - -| Field Name | Required? | JSON Type | Internal Type | Description | -|-------------------|:------------------:|:---------:|:-------------:|:-----------------------------------------------------------------------| -| `TransactionType` | :heavy_check_mark: | String | UInt16 | Must be `"CredentialDelete"` | -| `Account` | :heavy_check_mark: | String | AccountID | The account deleting the credential | -| `Subject` | | String | AccountID | The subject of the credential (defaults to `Account` if not specified) | -| `Issuer` | | String | AccountID | The issuer of the credential (defaults to `Account` if not specified) | -| `CredentialType` | :heavy_check_mark: | String | Blob | Type identifier (max 64 bytes) | -| `Flags` | | Number | UInt32 | Transaction flags (must be 0, only universal flags allowed) | - -**Deletion Authorization**: -- The issuer can always delete any credential they issued -- The subject can always delete any credential they hold[^4] -- Anyone can delete an expired credential (when current time > `Expiration`) -- Others cannot delete active, non-expired credentials - -**Field Defaults**: -- If only `Subject` is provided: `Issuer` defaults to `Account` (deletes credential where Account is issuer and specified account is subject) -- If only `Issuer` is provided: `Subject` defaults to `Account` (deletes credential where specified account is issuer and Account is subject) -- If both are provided: Uses those exact issuer and subject values -- At least one of `Subject` or `Issuer` must be provided (otherwise `temMALFORMED`) - -### 3.3.1. Failure Conditions - -**Static validation:** - -- `temDISABLED`: featureCredentials not enabled -- `temMALFORMED`: - - Neither `Subject` nor `Issuer` field is present - - `CredentialType` is empty or exceeds 64 bytes -- `temINVALID_ACCOUNT_ID`: `Subject` or `Issuer` field is zero - -**Validation against the ledger view:** - -- `tecNO_ENTRY`: Credential does not exist for the specified (subject, issuer, credentialType) triple - -**Validation during doApply** - -- `tefINTERNAL`: Credential no longer exists -- `tecNO_PERMISSION`: `Account` is neither the issuer nor the subject, and the credential is not expired -- `tecNO_ENTRY`: Credential is null during deletion (from `deleteSLE`) -- `tecINTERNAL`: Account not found during directory removal (from `deleteSLE`) -- `tefBAD_LEDGER`: Failed to remove credential from owner directory (from `deleteSLE`) - -### 3.3.2. State Changes - -- `Credential` object is **deleted**: - - Removed from ledger entirely - -- Issuer's `AccountRoot` is **modified** (if credential was not yet accepted): - - `OwnerCount`: Decremented by 1 - -- Subject's `AccountRoot` is **modified** (if credential was accepted): - - `OwnerCount`: Decremented by 1 - -- If the credential carries a `Sponsor` field, the reserve release is accounted against that sponsor, as described in the [transactions documentation](../transactions/README.md). - -- `DirectoryNode` entries are **updated**: - - Credential entry removed from issuer's owner directory (always) - - Credential entry removed from subject's owner directory (if subject != issuer) - -# 4. Authorization Integration - -Credentials integrate with the XRP Ledger's authorization systems to enable credential-based access control. - -## 4.1. DepositAuth Integration - -Accounts with the `lsfDepositAuth` flag set require incoming payments to be authorized. Authorization can be granted in two ways: by pre-authorizing specific accounts, or by accepting credentials from trusted issuers. Credentials provide a scalable alternative to pre-authorizing individual accounts. - -**The authorization mechanism works as follows**: - -1. Destination account enables `lsfDepositAuth` flag -2. When a payment arrives, the ledger checks: - - If sender == destination, allow (self-payment) - - If sender is individually pre-authorized (`DepositPreauth` entry exists for sender's account), allow - - Otherwise, if transaction includes `CredentialIDs` field, verify credentials against destination's accepted credential specifications -3. The destination creates a `DepositPreauth` ledger entry specifying trusted (issuer, credentialType) pairs -4. Senders who hold matching credentials include the credential hashes in the `CredentialIDs` field - -**CredentialIDs Field**: - -Transactions that move value (Payment, EscrowFinish, etc.) include an optional `CredentialIDs` field: - -| Field Name | Required? | JSON Type | Internal Type | Description | -|------------|:---------:|:---------:|:-------------:|:------------| -| `CredentialIDs` | | Array | VECTOR256 | Array of credential object hashes (max 8) | - -The sender includes the hashes of credentials they hold. During transaction processing, the ledger: -1. Checks if the destination has `lsfDepositAuth` enabled -2. If yes, verifies the sender is preauthorized OR holds a valid credential -3. Looks up each credential hash in `CredentialIDs` -4. Checks that the `(issuer, credentialType)` pairs of all supplied credentials together form a set that exactly matches one of the destination's credential `DepositPreauth` entries -5. Each supplied credential must exist, belong to the sender, and have `lsfAccepted` set (else `tecBAD_CREDENTIALS`); and must not be expired (else `tecEXPIRED`) - -Supplying `CredentialIDs` is itself constrained: if any listed credential is expired, the transaction fails with `tecEXPIRED` before the deposit-authorization checks run, and this applies to any transaction that carries `CredentialIDs` (Payment, EscrowFinish, etc.), even when the destination does not require deposit authorization. The expired credential is also deleted as part of this (recovering its reserve), even though the transaction fails. Under the `fixCleanup3_1_3` amendment, if that deletion itself fails, the transaction halts and returns the deletion's error (e.g. `tecINTERNAL`) instead of `tecEXPIRED`.[^5] - -[^5]: [`CredentialHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/CredentialHelpers.cpp#L61-L64) - -**Example Flow**: - -Setup: -- Bob has lsfDepositAuth enabled -- Bob creates DepositPreauth entry trusting Carol (KYC provider) for "verified_user" credential type -- Carol issues "verified_user" credential to Alice -- Alice accepts the credential - -Payment: -1. Alice sends Payment to Bob - - CredentialIDs: [hash of Alice's credential] -2. Ledger checks Bob has lsfDepositAuth = true -3. Ledger verifies Alice has no individual deposit preauth from Bob -4. Ledger checks CredentialIDs field -5. Ledger looks up credential, confirms: - - Issuer = Carol - - Type = "verified_user" - - Subject = Alice - - lsfAccepted = true - - Not expired -6. Payment succeeds - - -## 4.2. PermissionedDomain Integration - -Credentials can gate access to permissioned domains. Domain owners specify which credential types from which issuers are required to interact with the domain. - -**PermissionedDomain Ledger Entry**: - -Contains an `AcceptedCredentials` field - an array of credential specifications: - -``` -AcceptedCredentials: [ - { - Issuer: , - CredentialType: - }, - ... -] -``` - -**The domain access control mechanism works as follows**: - -1. Domain owner creates a PermissionedDomain entry -2. Sets `AcceptedCredentials` to specify required credentials -3. Users attempting to interact with the domain must provide valid credentials -4. The ledger checks the user holds a credential matching any entry in `AcceptedCredentials` - -**Example**: - -Setup: -- Bob creates a PermissionedDomain for his trading venue -- Bob sets AcceptedCredentials: - -```json - [ - { Issuer: Carol, CredentialType: "accredited_investor" } - ] -``` -- Carol (regulatory authority) issues "accredited_investor" credential to Alice -- Alice accepts the credential - -Offer Creation: -1. Alice submits an OfferCreate transaction with the `DomainID` field set to Bob's PermissionedDomain ID -2. Ledger checks Bob's PermissionedDomain has credential requirements -3. Ledger verifies Alice holds credential where: - - Issuer = Carol - - Type = "accredited_investor" - - lsfAccepted = true - - Not expired -4. OfferCreate transaction succeeds, placing Alice's offer in the domain's order book - -This enables fine-grained access control where different domains can require different credentials, and issuers can manage authorization by issuing or revoking credentials without the domain owner's involvement. +# Index + +- [1. Introduction](#1-introduction) + - [1.1. Key Concepts](#11-key-concepts) + - [1.2. Credential Lifecycle](#12-credential-lifecycle) + - [1.3. Use Cases](#13-use-cases) +- [2. Ledger Entries](#2-ledger-entries) + - [2.1. Credential Ledger Entry](#21-credential-ledger-entry) + - [2.1.1. Object Identifier](#211-object-identifier) + - [2.1.2. Fields](#212-fields) + - [2.1.2.1. Flags](#2121-flags) + - [2.1.3. Pseudo-accounts](#213-pseudo-accounts) + - [2.1.4. Ownership](#214-ownership) + - [2.1.5. Reserves](#215-reserves) +- [3. Transactions](#3-transactions) + - [3.1. CredentialCreate Transaction](#31-credentialcreate-transaction) + - [3.1.1. Failure Conditions](#311-failure-conditions) + - [3.1.2. State Changes](#312-state-changes) + - [3.2. CredentialAccept Transaction](#32-credentialaccept-transaction) + - [3.2.1. Failure Conditions](#321-failure-conditions) + - [3.2.2. State Changes](#322-state-changes) + - [3.3. CredentialDelete Transaction](#33-credentialdelete-transaction) + - [3.3.1. Failure Conditions](#331-failure-conditions) + - [3.3.2. State Changes](#332-state-changes) +- [4. Authorization Integration](#4-authorization-integration) + - [4.1. DepositAuth Integration](#41-depositauth-integration) + - [4.2. PermissionedDomain Integration](#42-permissioneddomain-integration) + +# 1. Introduction + +Credentials are a decentralized authorization mechanism on the XRP Ledger, borrowing concepts from the W3C Verifiable Credentials Data Model[^1], that allows accounts to create credentials for subjects (individuals, organizations, or devices) and use those credentials for access control. Once a credential is accepted and stored on the ledger, it can be autonomously verified by checking the ledger state without requiring interaction with the issuer. + +For example, a trading venue creates a [PermissionedDomain](../permissioned_domains/README.md) requiring an "accredited_investor" credential from a regulatory authority. When Alice wants to trade on this venue: +1. The Regulator issues the credential: RegulatorAccountID sends a CredentialCreate transaction with Subject=AliceAccountID and CredentialType="accredited_investor" +2. Alice accepts it: AliceAccountID sends a CredentialAccept transaction +3. The credential ledger entry now exists with `lsfAccepted` flag set +4. When Alice submits an OfferCreate transaction on the permissioned domain, the ledger checks: Does a Credential exist where `Subject=AliceAccountID, Issuer=RegulatorAccountID, CredentialType="accredited_investor", lsfAccepted=true`, and not expired? +5. If yes, the transaction is authorized + +Credentials can also be used for: + +- **[`DepositAuth`](#41-depositauth-integration)**: An account with the `lsfDepositAuth` flag accepts incoming payments, for XRP, IOUs, and MPTs alike, only from authorized senders. Instead of pre-authorizing each sender individually, credentials can be used to authorize an `(issuer, credentialType)` pair. Any holder of a matching accepted credential can make a payment to an account with the `lsfDepositAuth` flag by listing it in the payment's `CredentialIDs` field. +- **Self-attestation (issuer == subject)**: an account can issue a credential to itself. This is a verifiable on-ledger claim about itself, accepted automatically. +- **Tiered access control**: different credential types representing different authorization levels from the same issuer. + +[^1]: W3C Verifiable Credentials Data Model: https://www.w3.org/TR/vc-data-model-2.0/ +[^2]: For self-issued credentials (issuer == subject), the credential appears in only one directory, so SubjectNode is not set. sfSubjectNode defined as soeOPTIONAL: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/ledger_entries.macro#L445). SubjectNode only set in the issuer != subject branch: [`Credentials.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp#L163-L174) + +## 1.1. Terminology and Concepts + +**Issuer**: The account that creates and signs the credential. The issuer attests to some property or status of the subject. The issuer's identity determines the trust level of the credential. + +**Subject**: The account that holds the credential. This is the entity the credential makes claims about. The subject must accept the credential before it becomes active (unless the subject is also the issuer). + +**Credential Type**: A string identifier (max 64 bytes) that categorizes the credential. This allows different credential types from the same issuer (e.g., "kyc_basic", "kyc_advanced", "membership_gold"). Applications use the credential type to determine what authorization the credential provides. + +**Acceptance**: Before a credential can be used for authorization, the subject must explicitly accept it via `CredentialAccept` (self-issued credentials, where issuer == subject, are accepted automatically). An unaccepted credential still appears in the subject's directory but is inactive, and the *issuer* pays its reserve until acceptance. This ensures a credential can't be used on the subject's behalf, or charged against the subject's reserve, without their consent. + +**Expiration**: Credentials can optionally have an expiration time. After expiration, the credential can no longer be used for authorization and can be deleted by anyone to recover ledger space. + +## 1.2. Credential Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Created: CredentialCreate + Created --> Accepted: CredentialAccept + Created --> Expired: Expiration time passes + Accepted --> Expired: Expiration time passes + Expired --> [*]: CredentialDelete (by anyone) + Accepted --> [*]: CredentialDelete + Created --> [*]: CredentialDelete +``` +Figure: Credential state machine + + +**1. Creation Phase (CredentialCreate)**: +- The issuer creates a credential for a subject +- The credential is stored in the issuer's owner directory +- Issuer's owner count increases by 1 (issuer pays the reserve) +- If issuer == subject: + - Credential is immediately marked as accepted + - Credential appears in only one directory (since issuer and subject are the same account) +- If issuer != subject: + - Credential is also added to the subject's owner directory + +**2. Acceptance Phase (CredentialAccept)**: +- The subject explicitly accepts the credential +- The `lsfAccepted` flag is set +- The credential ownership (who pays the reserve) transfers from the issuer to the subject: + - Issuer's owner count decreases by 1 (freeing the issuer's reserve) + - Subject's owner count increases by 1 (subject now pays the reserve) + - The credential remains in both the issuer's and subject's owner directories +- The credential becomes active and can be used for authorization + +**3. Usage Phase**: +- The credential can be referenced in transactions via the `CredentialIDs` field +- Used for DepositAuth authorization or PermissionedDomain access +- Remains valid until deleted or expired + +**4. Expiration/Deletion Phase**: +- Credentials with an `Expiration` field become unusable after that time +- Expired credentials can be deleted by anyone +- Active credentials can be deleted by the issuer or subject at any time +- Deletion removes the credential from both owner directories + +# 2. Ledger Entries + + +## 2.1. Credential Ledger Entry + +The `Credential` ledger entry (type `ltCREDENTIAL = 0x0081`) represents a verifiable credential issued by one account to another. Each credential is uniquely identified by the combination of subject, issuer, and credential type. + +### 2.1.1. Object Identifier + +The key of the `Credential` object is the result of SHA512-Half of the following values concatenated in order: + +- The `CREDENTIAL` space key `0x0044` (character 'D') +- The subject account ID (160 bits) +- The issuer account ID (160 bits) +- The credential type string (variable length, max 64 bytes) + +This ensures each credential is uniquely identified by its (subject, issuer, type) triple. Multiple credentials can exist between the same subject and issuer as long as they have different credential types. + + +### 2.1.2. Fields + +| Field | Type | Required | Description | +|---------------------|-----------|----------|-----------------------------------------------------------| +| `Subject` | AccountID | Yes | The account holding this credential | +| `Issuer` | AccountID | Yes | The account that issued this credential | +| `CredentialType` | Blob | Yes | Type identifier string (max 64 bytes) | +| `Expiration` | UInt32 | Optional | Unix timestamp when credential expires | +| `URI` | Blob | Optional | Reference URI for credential metadata (max 256 bytes) | +| `IssuerNode` | UInt64 | Yes | Index of the issuer's owner directory page | +| `SubjectNode` | UInt64 | Optional | Index of the subject's owner directory page (only present when issuer != subject)[^2] | +| `Sponsor` | AccountID | Optional | Account currently covering this credential's owner reserve. Present only while the reserve is sponsored (`Sponsor` amendment)[^6] | +| `Flags` | UInt32 | Yes | Credential flags (see below); always present, 0 until `lsfAccepted` is set | +| `PreviousTxnID` | Hash256 | Yes | Hash of the previous transaction that modified this entry | +| `PreviousTxnLgrSeq` | UInt32 | Yes | Ledger sequence of the previous transaction | + +#### 2.1.2.1. Flags + +The `Flags` field can contain the following values: + +| Flag Name | Hex Value | Description | +|-----------|-----------|-------------| +| `lsfAccepted` | `0x00010000` | The subject has accepted this credential and it is now active | + +**Flag Behavior**: +- When `lsfAccepted` is not set: The credential exists but has not been accepted by the subject. It cannot be used for authorization. It appears in both the issuer's and subject's owner directories, but only the issuer's owner count is incremented (the issuer pays the reserve).[^3] +- When `lsfAccepted` is set: The credential has been accepted and is active. It appears in both the issuer's and subject's owner directories and can be used for authorization. + +[^3]: Credential added to both directories during creation: [`CredentialCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp#L154-L183) +[^4]: Deletion authorization: [`Credentials.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp#L89-L94) +[^6]: [`LedgerFormats.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/protocol/LedgerFormats.cpp#L13-L21), [`SponsorHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/SponsorHelpers.cpp#L267-L285) +- Self-issued credentials (issuer == subject) automatically have `lsfAccepted` set during creation. + +### 2.1.3. Pseudo-accounts + +Credential transactions (creating, accepting, or deleting credentials) do not create pseudo-accounts. + +### 2.1.4. Ownership + +Credentials can appear in up to two owner directories: + +**Before Acceptance** (when issuer != subject): +- Appears in both the issuer's and subject's owner directories +- Issuer's owner count increases by 1 + +**After Acceptance**: +- Still appears in both the issuer's and subject's owner directories +- Ownership transfers: Issuer's owner count decreases by 1, subject's owner count increases by 1 + +**Self-Issued Credentials** (issuer == subject): +- Immediately accepted during creation +- Appears in a single owner directory (since issuer and subject are the same) +- Account's owner count increases by 1 + +### 2.1.5. Reserves + +Credentials follow the standard XRP Ledger reserve requirements: + +- **Owner Reserve**: Each credential requires one owner reserve from the account that owns it +- **Before Acceptance**: Issuer pays the reserve (since credential is in issuer's directory) +- **After Acceptance**: Subject pays the reserve (reserve responsibility transfers to the subject) +- **Self-Issued**: Account pays one reserve (not two, since there's only one directory entry) + +The owner reserve is calculated as `incrementalReserve` (the per-object owner reserve increment set by the network). When a credential is deleted, the reserve is freed and the owner count decreases. + +Under the `Sponsor` amendment (XLS-68), a credential's reserve can be covered by a reserve sponsor recorded in the credential's `Sponsor` field. The sponsor then bears the reserve in place of the issuer or subject. Sponsorship does not carry over automatically when the subject accepts. Deletion releases the reserve against the recorded sponsor. The sponsorship mechanism is described in the [transactions documentation](../transactions/README.md).[^7] + +[^7]: [`SponsorHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/SponsorHelpers.cpp#L28-L61), [`CredentialCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp#L141-L170), [`CredentialAccept.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp#L96-L134), [`CredentialHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/CredentialHelpers.cpp#L97-L101) + +# 3. Transactions + +## 3.1. CredentialCreate Transaction + +The `CredentialCreate` transaction creates a new credential from an issuer to a subject. Under the `fixCleanup3_3_0` amendment, the subject cannot be a pseudo-account (an AMM, Vault, or LoanBroker account). + +| Field Name | Required? | JSON Type | Internal Type | Description | +|-------------------|:------------------:|:---------:|:-------------:|:------------------------------------------------------------| +| `TransactionType` | :heavy_check_mark: | String | UInt16 | Must be `"CredentialCreate"` | +| `Account` | :heavy_check_mark: | String | AccountID | The issuer account creating the credential | +| `Subject` | :heavy_check_mark: | String | AccountID | The account that will hold the credential | +| `CredentialType` | :heavy_check_mark: | String | Blob | Type identifier (max 64 bytes) | +| `Expiration` | | Number | UInt32 | Unix timestamp when credential expires | +| `URI` | | String | Blob | Reference URI (max 256 bytes) | +| `Flags` | | Number | UInt32 | Transaction flags (must be 0, only universal flags allowed) | + +### 3.1.1. Failure Conditions + +**Static validation:** + +- `temDISABLED`: featureCredentials not enabled +- `temMALFORMED`: + - `Subject` is a zero AccountID + - `CredentialType` is empty or exceeds 64 bytes + - `URI` is empty or exceeds 256 bytes + +**Validation against the ledger view:** + +- `tecNO_TARGET`: Subject account does not exist +- `tecDUPLICATE`: A credential with this (subject, issuer, credentialType) triple already exists +- `tecPSEUDO_ACCOUNT`: `Subject` is a pseudo-account, such as an AMM, Vault, or LoanBroker account (requires the `fixCleanup3_3_0` amendment)[^8] + +[^8]: [`CredentialCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp#L101-L105) + +**Validation during doApply** + +- `tefINTERNAL`: Failed to create credential ledger entry or issuer account not found +- `tecEXPIRED`: `Expiration` field is set to a time in the past (before ledger close time) +- `tecINSUFFICIENT_RESERVE`: Issuer has insufficient XRP to pay the owner reserve. For a sponsored reserve, the sponsor has insufficient XRP or the pre-funded sponsorship has no remaining owner-count allowance +- `tecDIR_FULL`: Owner directory is full and cannot add new entry + +### 3.1.2. State Changes + +- `Credential` object is **created**: + - `Subject`: Set to subject account ID + - `Issuer`: Set to issuer account ID (transaction sender) + - `CredentialType`: Set to specified type string + - `Expiration`: Set to specified timestamp (if provided) + - `URI`: Set to specified URI (if provided) + - `SubjectNode`: Index in subject's owner directory + - `IssuerNode`: Index in issuer's owner directory + - `Sponsor`: Set to the reserve sponsor (only when the transaction's reserve is sponsored) + - `Flags`: + - If issuer == subject: `lsfAccepted` is set immediately + - If issuer != subject: No flags set (credential awaits acceptance) + +- Issuer's `AccountRoot` is **modified**: + - `OwnerCount`: Incremented by 1 + +- For a reserve-sponsored transaction, the sponsorship accounting fields on the issuer, the sponsor, and any pre-funded `Sponsorship` entry are also updated, as described in the [transactions documentation](../transactions/README.md). + +- `DirectoryNode` entries are **created/modified**: + - Credential added to issuer's owner directory (always) + - If issuer == subject: Same directory entry (counted once) + - If issuer != subject: Credential added to both issuer's and subject's owner directories + +## 3.2. CredentialAccept Transaction + +The `CredentialAccept` transaction allows a subject to accept a credential that has been issued to them. + +| Field Name | Required? | JSON Type | Internal Type | Description | +|-------------------|:------------------:|:---------:|:-------------:|:------------------------------------------------------------| +| `TransactionType` | :heavy_check_mark: | String | UInt16 | Must be `"CredentialAccept"` | +| `Account` | :heavy_check_mark: | String | AccountID | The subject account accepting the credential | +| `Issuer` | :heavy_check_mark: | String | AccountID | The issuer of the credential | +| `CredentialType` | :heavy_check_mark: | String | Blob | Type identifier (max 64 bytes) | +| `Flags` | | Number | UInt32 | Transaction flags (must be 0, only universal flags allowed) | + +### 3.2.1. Failure Conditions + +**Static validation:** + +- `temDISABLED`: featureCredentials not enabled +- `temINVALID_ACCOUNT_ID`: `Issuer` is a zero AccountID +- `temMALFORMED`: `CredentialType` is empty or exceeds 64 bytes + +**Validation against the ledger view:** + +- `tecNO_ISSUER`: Issuer account does not exist +- `tecNO_ENTRY`: Credential does not exist for this (subject=Account, issuer, credentialType) triple +- `tecDUPLICATE`: Credential already has `lsfAccepted` flag set + +**Validation during doApply** + +- `tefINTERNAL`: Subject or issuer account not found +- `tecINSUFFICIENT_RESERVE`: Subject has insufficient XRP to pay the owner reserve. For a sponsored reserve, the sponsor has insufficient XRP or the pre-funded sponsorship has no remaining owner-count allowance +- `tecEXPIRED`: Credential has expired (current ledger time > credential's `Expiration`) + +### 3.2.2. State Changes + +**If credential is not expired:** + +- `Credential` object is **modified**: + - `Flags`: `lsfAccepted` flag is set + - `Sponsor`: The pre-acceptance sponsor, if any, is removed. The accept transaction's reserve sponsor, if any, is recorded + +- Issuer's `AccountRoot` is **modified**: + - `OwnerCount`: Decremented by 1 + +- Subject's `AccountRoot` is **modified**: + - `OwnerCount`: Incremented by 1 + +- For sponsored reserves, the issuer-side release is applied against the pre-acceptance sponsor and the subject's new reserve is accounted against the accept transaction's sponsor, as described in the [transactions documentation](../transactions/README.md). + +**If credential is expired:** + +- `Credential` object is **deleted** (removed from ledger) +- State changes follow deletion rules (see [section 3.3.2](#332-state-changes)) + +## 3.3. CredentialDelete Transaction + +The `CredentialDelete` transaction removes a credential from the ledger. + +| Field Name | Required? | JSON Type | Internal Type | Description | +|-------------------|:------------------:|:---------:|:-------------:|:-----------------------------------------------------------------------| +| `TransactionType` | :heavy_check_mark: | String | UInt16 | Must be `"CredentialDelete"` | +| `Account` | :heavy_check_mark: | String | AccountID | The account deleting the credential | +| `Subject` | | String | AccountID | The subject of the credential (defaults to `Account` if not specified) | +| `Issuer` | | String | AccountID | The issuer of the credential (defaults to `Account` if not specified) | +| `CredentialType` | :heavy_check_mark: | String | Blob | Type identifier (max 64 bytes) | +| `Flags` | | Number | UInt32 | Transaction flags (must be 0, only universal flags allowed) | + +**Deletion Authorization**: +- The issuer can always delete any credential they issued +- The subject can always delete any credential they hold[^4] +- Anyone can delete an expired credential (when current time > `Expiration`) +- Others cannot delete active, non-expired credentials + +**Field Defaults**: +- If only `Subject` is provided: `Issuer` defaults to `Account` (deletes credential where Account is issuer and specified account is subject) +- If only `Issuer` is provided: `Subject` defaults to `Account` (deletes credential where specified account is issuer and Account is subject) +- If both are provided: Uses those exact issuer and subject values +- At least one of `Subject` or `Issuer` must be provided (otherwise `temMALFORMED`) + +### 3.3.1. Failure Conditions + +**Static validation:** + +- `temDISABLED`: featureCredentials not enabled +- `temMALFORMED`: + - Neither `Subject` nor `Issuer` field is present + - `CredentialType` is empty or exceeds 64 bytes +- `temINVALID_ACCOUNT_ID`: `Subject` or `Issuer` field is zero + +**Validation against the ledger view:** + +- `tecNO_ENTRY`: Credential does not exist for the specified (subject, issuer, credentialType) triple + +**Validation during doApply** + +- `tefINTERNAL`: Credential no longer exists +- `tecNO_PERMISSION`: `Account` is neither the issuer nor the subject, and the credential is not expired +- `tecNO_ENTRY`: Credential is null during deletion (from `deleteSLE`) +- `tecINTERNAL`: Account not found during directory removal (from `deleteSLE`) +- `tefBAD_LEDGER`: Failed to remove credential from owner directory (from `deleteSLE`) + +### 3.3.2. State Changes + +- `Credential` object is **deleted**: + - Removed from ledger entirely + +- Issuer's `AccountRoot` is **modified** (if credential was not yet accepted): + - `OwnerCount`: Decremented by 1 + +- Subject's `AccountRoot` is **modified** (if credential was accepted): + - `OwnerCount`: Decremented by 1 + +- If the credential carries a `Sponsor` field, the reserve release is accounted against that sponsor, as described in the [transactions documentation](../transactions/README.md). + +- `DirectoryNode` entries are **updated**: + - Credential entry removed from issuer's owner directory (always) + - Credential entry removed from subject's owner directory (if subject != issuer) + +# 4. Authorization Integration + +Credentials integrate with the XRP Ledger's authorization systems to enable credential-based access control. + +## 4.1. DepositAuth Integration + +Accounts with the `lsfDepositAuth` flag set require incoming payments to be authorized. Authorization can be granted in two ways: by pre-authorizing specific accounts, or by accepting credentials from trusted issuers. Credentials provide a scalable alternative to pre-authorizing individual accounts. + +**The authorization mechanism works as follows**: + +1. Destination account enables `lsfDepositAuth` flag +2. When a payment arrives, the ledger checks: + - If sender == destination, allow (self-payment) + - If sender is individually pre-authorized (`DepositPreauth` entry exists for sender's account), allow + - Otherwise, if transaction includes `CredentialIDs` field, verify credentials against destination's accepted credential specifications +3. The destination creates a `DepositPreauth` ledger entry specifying trusted (issuer, credentialType) pairs +4. Senders who hold matching credentials include the credential hashes in the `CredentialIDs` field + +**CredentialIDs Field**: + +Transactions that move value (Payment, EscrowFinish, etc.) include an optional `CredentialIDs` field: + +| Field Name | Required? | JSON Type | Internal Type | Description | +|------------|:---------:|:---------:|:-------------:|:------------| +| `CredentialIDs` | | Array | VECTOR256 | Array of credential object hashes (max 8) | + +The sender includes the hashes of credentials they hold. During transaction processing, the ledger: +1. Checks if the destination has `lsfDepositAuth` enabled +2. If yes, verifies the sender is preauthorized OR holds a valid credential +3. Looks up each credential hash in `CredentialIDs` +4. Checks that the `(issuer, credentialType)` pairs of all supplied credentials together form a set that exactly matches one of the destination's credential `DepositPreauth` entries +5. Each supplied credential must exist, belong to the sender, and have `lsfAccepted` set (else `tecBAD_CREDENTIALS`); and must not be expired (else `tecEXPIRED`) + +Supplying `CredentialIDs` is itself constrained: if any listed credential is expired, the transaction fails with `tecEXPIRED` before the deposit-authorization checks run, and this applies to any transaction that carries `CredentialIDs` (Payment, EscrowFinish, etc.), even when the destination does not require deposit authorization. The expired credential is also deleted as part of this (recovering its reserve), even though the transaction fails. Under the `fixCleanup3_1_3` amendment, if that deletion itself fails, the transaction halts and returns the deletion's error (e.g. `tecINTERNAL`) instead of `tecEXPIRED`.[^5] + +[^5]: [`CredentialHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/CredentialHelpers.cpp#L61-L64) + +**Example Flow**: + +Setup: +- Bob has lsfDepositAuth enabled +- Bob creates DepositPreauth entry trusting Carol (KYC provider) for "verified_user" credential type +- Carol issues "verified_user" credential to Alice +- Alice accepts the credential + +Payment: +1. Alice sends Payment to Bob + - CredentialIDs: [hash of Alice's credential] +2. Ledger checks Bob has lsfDepositAuth = true +3. Ledger verifies Alice has no individual deposit preauth from Bob +4. Ledger checks CredentialIDs field +5. Ledger looks up credential, confirms: + - Issuer = Carol + - Type = "verified_user" + - Subject = Alice + - lsfAccepted = true + - Not expired +6. Payment succeeds + + +## 4.2. PermissionedDomain Integration + +Credentials can gate access to permissioned domains. Domain owners specify which credential types from which issuers are required to interact with the domain. + +**PermissionedDomain Ledger Entry**: + +Contains an `AcceptedCredentials` field - an array of credential specifications: + +``` +AcceptedCredentials: [ + { + Issuer: , + CredentialType: + }, + ... +] +``` + +**The domain access control mechanism works as follows**: + +1. Domain owner creates a PermissionedDomain entry +2. Sets `AcceptedCredentials` to specify required credentials +3. Users attempting to interact with the domain must provide valid credentials +4. The ledger checks the user holds a credential matching any entry in `AcceptedCredentials` + +**Example**: + +Setup: +- Bob creates a PermissionedDomain for his trading venue +- Bob sets AcceptedCredentials: + +```json + [ + { Issuer: Carol, CredentialType: "accredited_investor" } + ] +``` +- Carol (regulatory authority) issues "accredited_investor" credential to Alice +- Alice accepts the credential + +Offer Creation: +1. Alice submits an OfferCreate transaction with the `DomainID` field set to Bob's PermissionedDomain ID +2. Ledger checks Bob's PermissionedDomain has credential requirements +3. Ledger verifies Alice holds credential where: + - Issuer = Carol + - Type = "accredited_investor" + - lsfAccepted = true + - Not expired +4. OfferCreate transaction succeeds, placing Alice's offer in the domain's order book + +This enables fine-grained access control where different domains can require different credentials, and issuers can manage authorization by issuing or revoking credentials without the domain owner's involvement. diff --git a/docs/glossary.md b/docs/glossary.md index 5fada62..67684f1 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -1,102 +1,102 @@ -# Glossary - -## Amendment - -Amendments are new features or other changes to the functional behavior of XRP Ledger. - -**Other terms** -- *Feature* - in `xrpld`, used to refer to amendments. E.g.: - - `bool isFeatureEnabled(featureSingleAssetVault);` - -## CLOB - -Central Limit Order Book: list of offers for a pair. - -**Other terms** -- *Order book* (but we refer to order book as a book containing both CLOB and synthetic AMM offers) -- *Book* -- *LOB* - -## Currency - -A type of asset in XRP Ledger. There are three different types of assets: - -- **XRP** -- **IOU** -- **MPT** - -**Other terms** -- *Issue* - in `xrpld`, used to denote any currency. -- *Asset* - -**Other meanings** -- *Currency code* - three-letter code of an IOU. - -## IOU - -Currency issued by an account which balance is tracked in trust lines. - -**Other terms** -- *Trust line token* -- *Issue*, in `xrpld`, used as a term for a currency issued by an account, but not MPT (*MPTIssue*). -- *Issued Currency* - -## MPT - -Multi-purpose token. - -**Other terms** -- *MPTIssue* - in `xrpld` used to refer to a wrapper around MPT ID, especially when disambiguating from token (referred to as *Issue*) - -## Offer - -A limit order on the XRP Ledger decentralized exchange that specifies the maximum exchange rate at which the creator is willing to trade. An offer is defined by `takerGets` (what the offer creator provides) and `takerPays` (what they want to receive), and will only execute at a rate that is as favorable as, or better than, the rate specified. - -**Other terms** -- *Limit order* - traditional financial markets term for the same concept - -**Related terms** -- *Resting offer* - an offer that has been placed on the ledger but not yet consumed - -## Order Book - -A collection of CLOB and AMM synthetic offers for an asset pair. - -## Resting Offer - -An offer that has been placed in the order book and is waiting to be consumed. - -**Other terms** -- *Sitting offer* - alternative term for the same concept -- *Book offer* - used in some contexts to refer to offers in the order book - -## Quality - -The exchange rate, calculated as the ratio of input amount to output amount, including cost of transfer fees. For example, if converting 105 USD results in 100 EUR, the quality is 1.05. Lower quality values are better (less input required for the same output). - -Quality can represent the exchange rate of individual components (such as a single offer or liquidity source) or the composite exchange rate across multiple components in a path. - -In `xrpld`, quality is represented as a `Quality` class that encapsulates the input/output ratio and provides comparison operations for ranking. - -## Rippling - -The process where IOU payments flow through an intermediary account's trust lines to connect the sender and receiver. For example, if Alice holds USD from Issuer and wants to send to Bob who also trusts Issuer, the payment "ripples" through Issuer's account: Alice -> Issuer -> Bob. An account must not have the NoRipple flag set on a trust line for rippling to occur on that line. - -Rippling enables multi-hop IOU payments without requiring direct trust lines between the sender and receiver, as long as they both trust a common issuer or chain of intermediaries. - -## Trust Line - -Trust Lines are a bidirectional relationship between an issuer of a token and another account. - -**Other terms** -- *Trust* - in `xrpld`, used as a noun to describe a trust line. `TrustSet` is used to create a trust line, represented by `RippleState` ledger entry. -- *Ripple line* - seldomly used in `xrpld`. -**Related terms** -- *RippleState* - in `xrpld`, name for ledger entry representing a trust line. - -## XRP - -Native currency in XRP Ledger. - -**Other terms** -- *Native* - in `xrpld`, often used to disambiguate a currency as XRP. \ No newline at end of file +# Glossary + +## Amendment + +Amendments are new features or other changes to the functional behavior of XRP Ledger. + +**Other terms** +- *Feature* - in `xrpld`, used to refer to amendments. E.g.: + - `bool isFeatureEnabled(featureSingleAssetVault);` + +## CLOB + +Central Limit Order Book: list of offers for a pair. + +**Other terms** +- *Order book* (but we refer to order book as a book containing both CLOB and synthetic AMM offers) +- *Book* +- *LOB* + +## Currency + +A type of asset in XRP Ledger. There are three different types of assets: + +- **XRP** +- **IOU** +- **MPT** + +**Other terms** +- *Issue* - in `xrpld`, used to denote any currency. +- *Asset* + +**Other meanings** +- *Currency code* - three-letter code of an IOU. + +## IOU + +Currency issued by an account which balance is tracked in trust lines. + +**Other terms** +- *Trust line token* +- *Issue*, in `xrpld`, used as a term for a currency issued by an account, but not MPT (*MPTIssue*). +- *Issued Currency* + +## MPT + +Multi-purpose token. + +**Other terms** +- *MPTIssue* - in `xrpld` used to refer to a wrapper around MPT ID, especially when disambiguating from token (referred to as *Issue*) + +## Offer + +A limit order on the XRP Ledger decentralized exchange that specifies the maximum exchange rate at which the creator is willing to trade. An offer is defined by `takerGets` (what the offer creator provides) and `takerPays` (what they want to receive), and will only execute at a rate that is as favorable as, or better than, the rate specified. + +**Other terms** +- *Limit order* - traditional financial markets term for the same concept + +**Related terms** +- *Resting offer* - an offer that has been placed on the ledger but not yet consumed + +## Order Book + +A collection of CLOB and AMM synthetic offers for an asset pair. + +## Resting Offer + +An offer that has been placed in the order book and is waiting to be consumed. + +**Other terms** +- *Sitting offer* - alternative term for the same concept +- *Book offer* - used in some contexts to refer to offers in the order book + +## Quality + +The exchange rate, calculated as the ratio of input amount to output amount, including cost of transfer fees. For example, if converting 105 USD results in 100 EUR, the quality is 1.05. Lower quality values are better (less input required for the same output). + +Quality can represent the exchange rate of individual components (such as a single offer or liquidity source) or the composite exchange rate across multiple components in a path. + +In `xrpld`, quality is represented as a `Quality` class that encapsulates the input/output ratio and provides comparison operations for ranking. + +## Rippling + +The process where IOU payments flow through an intermediary account's trust lines to connect the sender and receiver. For example, if Alice holds USD from Issuer and wants to send to Bob who also trusts Issuer, the payment "ripples" through Issuer's account: Alice -> Issuer -> Bob. An account must not have the NoRipple flag set on a trust line for rippling to occur on that line. + +Rippling enables multi-hop IOU payments without requiring direct trust lines between the sender and receiver, as long as they both trust a common issuer or chain of intermediaries. + +## Trust Line + +Trust Lines are a bidirectional relationship between an issuer of a token and another account. + +**Other terms** +- *Trust* - in `xrpld`, used as a noun to describe a trust line. `TrustSet` is used to create a trust line, represented by `RippleState` ledger entry. +- *Ripple line* - seldomly used in `xrpld`. +**Related terms** +- *RippleState* - in `xrpld`, name for ledger entry representing a trust line. + +## XRP + +Native currency in XRP Ledger. + +**Other terms** +- *Native* - in `xrpld`, often used to disambiguate a currency as XRP. diff --git a/docs/offers/README.md b/docs/offers/README.md index 7634057..668b6d6 100644 --- a/docs/offers/README.md +++ b/docs/offers/README.md @@ -1,634 +1,634 @@ -# Index - -- [1. Introduction](#1-introduction) - - [1.1. Offers](#11-offers) - - [1.2. Offer Crossing](#12-offer-crossing) - - [1.2.1. Self-Crossing](#121-self-crossing) - - [1.2.2. Sell vs Buy Offers](#122-sell-vs-buy-offers) - - [1.2.3. Auto-bridging](#123-auto-bridging) - - [1.2.4. Creating the Residual Offer](#124-creating-the-residual-offer) - - [1.3. Rate Calculation](#13-rate-calculation) - - [1.3.1. TickSize Rounding](#131-ticksize-rounding) - - [1.4. Offer Deletion](#14-offer-deletion) - - [1.5. Domain and Hybrid Offers](#15-permissioned-dex) - - [1.5.1. Domain Offers](#151-domain-offers) - - [1.5.2. Hybrid Offers](#152-hybrid-offers) -- [2. Ledger Entries](#2-ledger-entries) - - [2.1. Offer Ledger Entry](#21-offer-ledger-entry) - - [2.1.1. Object Identifier](#211-object-identifier) - - [2.1.2. Fields](#212-fields) - - [2.1.2.1. Asset-Specific Fields](#2121-asset-specific-fields) - - [2.1.2.2. Domain-Specific Fields](#2122-domain-specific-fields) - - [2.1.2.3. Flags](#2123-flags) - - [2.1.3. Pseudo-accounts](#213-pseudo-accounts) - - [2.1.4. Ownership](#214-ownership) - - [2.1.5. Reserves](#215-reserves) - - [2.2. DirectoryNode Ledger Entry](#22-directorynode-ledger-entry) - - [2.2.1. Object Identifier](#221-object-identifier) - - [2.2.2. Directory Pages](#222-directory-pages) - - [2.2.3. Fields](#223-fields) -- [3. Transactions](#3-transactions) - - [3.1. Offer Transactions](#31-offer-transactions) - - [3.1.1. OfferCreate Transaction](#311-offercreate-transaction) - - [3.1.1.1. Failure Conditions](#3111-failure-conditions) - - [3.1.1.2. State Changes](#3112-state-changes) - - [3.1.2. OfferCancel Transaction](#312-offercancel-transaction) - - [3.1.2.1. Failure Conditions](#3121-failure-conditions) - - [3.1.2.2. State Changes](#3122-state-changes) - -# 1. Introduction - -Offers are limit orders on the XRP Ledger decentralized exchange (DEX). They execute only at an exchange rate that is as favorable as, or better than, the rate the offer creator specifies. -As part of the decentralized exchange, users can submit offers between any combination of asset types: [XRP](../glossary.md#xrp), [IOUs](../glossary.md#iou), and [MPTs](../glossary.md#mpt). MPTs must have the `lsfMPTCanTrade` flag set on their MPTokenIssuance to be tradable on the DEX. See [MPT Flags](../mpts/README.md#2121-flags) for details on MPT capability flags. - -The DEX supports both open order books (accessible to all accounts) and domain-specific order books (restricted to credential holders). During offer crossing and payments, domain offers only match within their domain, while hybrid offers can match in both environments. This Permissioned DEX functionality enables regulated trading for securities, institutional venues, and other scenarios requiring verified participants. See [PermissionedDomains documentation](../permissioned_domains/README.md) and [Domain and Hybrid Offers](#15-permissioned-dex) for details. - -When `xrpld` applies an `OfferCreate`, it first invokes the payment [flow engine](../flow/README.md) to try crossing with existing book depth; only any leftover remainder is placed as a resting order. - -## 1.1. Offers - -An offer is defined in terms of `takerGets` and `takerPays` parameters, which are named from the perspective of the taker - the party accepting the offer. If Alice creates an offer with `takerGets = 100 XRP` and `takerPays = 10 USD`, it means she is offering 100 XRP and wants to receive 10 USD in return. - -Alice has to have a positive amount in `takerGets` currency, unless she is the issuer of that asset (IOU issuers can issue trust line tokens on demand; MPT issuers can mint MPTs into circulation). She does not have to have the full amount to cover the offer. If her balance is lower than `takerGets`, the offer may still partially fill. - -## 1.2. Offer Crossing - -Because offers are limit orders, a new offer is first matched against existing offers on the book during crossing. It can be filled fully, partially, or not at all. Only the unfilled remainder is then placed on the order book as a resting offer. - -Crossing is done by calling the [flow engine](../flow/README.md) and passing a set of paths. All crossings will contain -the [default path](../path_finding/README.md#24-default-paths). If neither taker pays or taker gets is XRP, then an additional -`xrpCurrency` path is added to achieve auto-bridging. - -If transaction has `tfPassive` flag, it will only cross offers with strictly better quality than its own. -It will not cross offers of equal quality, making it more likely to remain on the order book. - -If the offer is `tfImmediateOrCancel` it will never be placed in the order book. It can be fully or partially filled during crossing, or not filled at all, but `Offer` ledger entry will never be created for it. - -If the offer is `tfFillOrKill` it will never be placed in the order book. It can either *fully* fill immediately or fail. - -During crossing, the new offer may be fully filled, partially filled, or not filled at all by existing offers in the order book. - -```mermaid ---- -title: Offer Creation ---- -flowchart LR - A((Offer Transaction)) --> B[Calculate Rate] - B --> C[Add default path to Paths] - C --> D{Can be
autobridged?} - D -->|yes| E[Add XRP currency to Paths] - D -->|no| F[Try crossing] - E --> F - - F --> G{Can fully fill?} - - %% Fully filled path - G -->|yes| X[Fill offer] - X --> Z((Transaction Done)) - - %% NOT fully filled: handle flags and partials - G -->|no| H{tfImmediateOrCancel?} - H -->|yes| X - H -->|no| I{tfFillOrKill?} - I -->|yes| Z - I -->|no| J{Can partially fill?} - J -->|yes| P[Fill partially] - P --> R[Create offer for remainder] - J -->|no| R[Create offer for remainder] - R --> Z -``` - -**Atomicity:** - -The fee and sequence number are applied to the base ledger view by the transactor before offer crossing begins. Both sandboxes below are built over that base view, so the fee is recorded outside of them and persists regardless of which one is applied: -- `sb`: the crossing results, the deletions of offers consumed or removed during crossing, and the new resting offer -- `sbCancel`: the deletion of offers marked for permanent removal during crossing, such as expired, already-unfunded, invalid, no-longer-in-domain, or directly self-crossable offers - -When the offer will not be placed (a `tfFillOrKill` offer that cannot fully cross, or a `tfImmediateOrCancel` offer that crosses nothing), `sbCancel` is applied instead of `sb`. This discards the crossing and placement work while keeping the fee and permanent offer cleanup. See [Ledger Views and Sandboxes](../transactions/README.md#5-ledger-views-and-sandboxes) for how sandboxes provide atomic state changes. - -### 1.2.1. Self-Crossing - -When a new offer would directly cross a resting offer owned by the same account, `xrpld` deletes the old offer instead of executing a trade against it. The old offer is not partially filled, and no assets are transferred between the account and itself.[^self-cross-removal] - -The self-cross removal rule applies when all of the following are true: - -1. Crossing is evaluating the default, direct path, not an auto-bridged path. -2. The resting offer's quality is equal to or better than the new offer's quality threshold. -3. The resting offer is owned by the account that submitted the new offer. - -The relative amounts of the offers are not considered. A smaller new offer can therefore delete one or more larger resting offers in full. Deleting self-owned offers does not reduce the new offer's amount and the new offer may then cross other accounts' liquidity. Any remainder may be placed on the order book normally. - -For an offer with the `tfPassive` flag, equal-quality resting offers do not meet the crossing threshold, so an equal-quality self-offer is not deleted by this rule. - -See [`isSelfCross` in the Flow step documentation](../flow/steps.md#531-foreachoffer-pseudo-code) for the implementation logic. - -[^self-cross-removal]: Self-cross removal: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/paths/BookStep.cpp#L399-L454). Amount-independent behavior with multiple larger resting offers: [`Offer_test.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/test/app/Offer_test.cpp#L3243-L3310) - -### 1.2.2. Sell vs Buy Offers - -An offer with the `tfSell` flag set is a **sell** offer. An offer without the `tfSell` flag is a **buy** offer. - -When selling, the offer will accept more than the specified `takerPays` amount to maximize the sale of `takerGets`. In the `xrpld` implementation, `takerPays` is capped at `STAmount::kMaxNative` for XRP[^cMaxNative], half the maximum representable IOU value (`STAmount::kMaxValue / 2`) for IOUs[^cMaxValue-iou], and half the maximum MPT amount (`kMaxMpTokenAmount / 2`) for MPTs[^maxMPTokenAmount-mpt]. The IOU and MPT amounts are halved to leave room for the transfer fee charged during crossing: an issuer's transfer rate can be as high as 200% (a 2.0 multiplier), so capping at half the maximum keeps the crossed amount representable.[^transfer-rate-max] XRP has no transfer rate, so its cap is not halved. - -The following examples demonstrate offer crossing behavior when a new offer is created and crosses with existing offers in the order book. - -**Example 1:** - -Alice is willing to sell her 20 USD for 100 XRP. She wants to sell all of her 20 USD. -Bob is willing to buy 10 USD for his 100 XRP. He does not want to buy more than 10 USD. - -**Offers:** - -- Alice's **sell** offer rests on the book first: - - Taker pays 100 XRP - - Taker gets 20 USD - -- Bob then submits a **buy** offer, which crosses Alice's resting offer: - - Taker pays 10 USD - - Taker gets 100 XRP - -**Result:** - -- Alice will get 50 XRP and pay 10 USD. This is the exchange rate that she offered. She did not manage to sell all of her 20 USD, but the offer was partially filled. -- Bob will get 10 USD and pay 50 XRP. This is a better exchange rate than he offered. He did not buy more than 10 USD that he originally wanted. - -**Example 2:** - -Alice is willing to buy 100 XRP for her 20 USD. Bob is willing to sell his 100 XRP for 10 USD, and he wants to sell all 100 XRP. - -**Offers:** - -- Alice's **buy** offer rests on the book first: - - Taker pays 100 XRP - - Taker gets 20 USD - -- Bob then submits a **sell** offer, which crosses Alice's resting offer: - - Taker pays 10 USD - - Taker gets 100 XRP - -**Result:** - -- Alice will get 100 XRP and pay 20 USD. This is the exchange rate that she offered. -- Bob will get 20 USD and pay 100 XRP. Because he was selling all of his 100 XRP, he got 20 USD for it. He sold his XRP for a better exchange rate than he hoped for. - -[^cMaxNative]: XRP maximum native value: [`STAmount.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STAmount.h#L55) -[^cMaxValue-iou]: IOU maximum value halved for transfer rate: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L436-L437) -[^maxMPTokenAmount-mpt]: MPT maximum amount halved for transfer rate: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L440), maximum defined in [`Protocol.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/Protocol.h#L234) -[^transfer-rate-max]: IOU transfer rate capped at 2.0 (200%): [`AccountSet.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/account/AccountSet.cpp#L128-L131) - -### 1.2.3. Auto-bridging - -Auto-bridging allows offers between two non-XRP currencies to execute through XRP as an intermediate currency. - -Auto-bridging is used only when both `takerPays` and `takerGets` are non-XRP currencies. When it applies, the [Flow engine](../flow/README.md) is invoked with two paths: -1. The [default](../path_finding/README.md#24-default-paths) direct path -2. An auto-bridging path with XRP as intermediate (e.g., USD -> XRP -> EUR)[^auto-bridging-path] - -The Flow engine evaluates both paths and selects the one(s) providing the best quality, allowing offers to execute through whichever route offers better pricing. - -[^auto-bridging-path]: Auto-bridging path construction: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L410-L412) -[^passive-threshold]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L394-L397) - -### 1.2.4. Creating the Residual Offer - -Before the offer is sent to the Flow engine for crossing, a **quality threshold** is calculated from `takerPays` and `takerGets`. This represents the minimum exchange rate at which the offer can be crossed. If `takerGets` is a non-XRP asset (IOU or MPT) and the offer creator is not the issuer, `takerGets` is adjusted by multiplying it by the issuer's transfer rate to account for transfer fees. The quality threshold is then calculated as `takerPays / adjusted takerGets`. For a passive offer (`tfPassive`), the threshold is then incremented so the offer crosses only strictly-better-quality offers.[^passive-threshold] - -The Flow engine returns the amount that was filled. During offer crossing, the offer owner pays transfer fees (see [transfer rates in flow steps](../flow/steps.md#213-quality-functions)). The transfer fee is deducted from the owner's balance but does not reduce the offer's stated amounts. For example, if the original `takerGets` was 100 USD and 50 USD was transferred with a 2% transfer fee, the owner pays 51 USD from their balance, but the remaining offer balance is 50 USD. - -The transfer rate is read from the issuer's settings (AccountRoot `TransferRate` field for IOUs, or the MPTokenIssuance `TransferFee` field for MPTs) and applied identically during offer crossing, maintaining consistent offer book semantics across all asset types. - -**Calculating the residual offer after partial filling:** - -Both calculations preserve the original offer's quality, the `takerGets : takerPays` ratio, so the unfilled remainder rests at the rate the creator specified. The rate used below is `takerGets / takerPays`, the reciprocal of the `takerPays / takerGets` rate defined in [Rate Calculation](#13-rate-calculation). - -**Buy offers** (no `tfSell` flag):[^buy-offer-residual] -1. Subtract consumed `takerPays` from original `takerPays` -2. Calculate remaining `takerGets` by multiplying remaining `takerPays` by this rate -3. Round `takerGets` up - -**Sell offers** (`tfSell` flag set):[^sell-offer-residual] -1. Subtract consumed `takerGets` from original `takerGets` (accounting for transfer rates) -2. Calculate remaining `takerPays` by dividing remaining `takerGets` by this rate -3. Round `takerPays` down - -**Special cases:** -- If the offer is not filled at all, the original offer is recorded on the ledger, unless it is an ImmediateOrCancel or FillOrKill offer (which are never placed) or the account lacks the reserve for a new offer[^offer-reserve] -- If, after partial filling, the signing account no longer has a positive balance in the `takerGets` currency, the remaining offer is not created[^no-balance-no-offer]. This check is skipped when `takerGets` is an MPT and the creator is its issuer (an issuer can supply the MPT without holding a balance), so the residual offer is still created in that case - -[^buy-offer-residual]: Buy offer residual calculation: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L527-L533) -[^sell-offer-residual]: Sell offer residual calculation: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L501-L520) -[^no-balance-no-offer]: No balance check after crossing: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L480-L486) -[^offer-reserve]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L835-L847) - -## 1.3. Rate Calculation - -The exchange rate for an offer is calculated before any crossing, so that potential partial filling does not affect the -intended rate. The rate is calculated as `takerPays / takerGets` (smaller is better for the taker). - -Different asset types have different internal representations: -- **XRP**: an integer number of drops, up to `kMaxNative` (9 * 10^18 drops).[^repr-xrp] -- **IOU**: a sign, an exponent (`-96` to `80`), and a mantissa. When non-zero, the mantissa is normalized to the range 10^15 to 10^16-1, always 16 significant decimal digits (a 54-bit value). This fixed-precision mantissa combined with a wide exponent lets an IOU represent both very large and very small amounts at 16 digits of precision.[^repr-iou] -- **MPT**: an unsigned integer, up to `kMaxMpTokenAmount` (2^63 - 1).[^repr-mpt] - -`xrpld` implementation normalizes the rate by packing the result into a 64-bit integer[^rate-packing]: -- Upper 8 bits: exponent + 100 -- Lower 56 bits: mantissa - -`getRate` returns the rate `0` (and the offer is not stored) in three cases: when `takerGets` is zero, when the computed rate rounds to zero (the offer is 'too good' to represent), or when the computation overflows. - -[^repr-xrp]: [`STAmount.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STAmount.h#L55) -[^repr-iou]: [`STAmount.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STAmount.h#L47-L53) -[^repr-mpt]: [`Protocol.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/Protocol.h#L234) -[^rate-packing]: [`STAmount.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/STAmount.cpp#L459-L481) - -### 1.3.1. TickSize Rounding - -In order to ensure that ranking of offers in order books requires a significant difference between exchange rates, -issuers can set `TickSize` field to their account. - -`TickSize` sets the number of significant decimal digits an offer's rate is rounded to. An issuer can set it to 0 (disabled) or to a value from 3 to 16.[^ticksize-range] If `TickSize` is present, the offer's rate is rounded up to that many significant digits.[^ticksize-round] If the two assets' issuers have different `TickSize` values, the smaller is used; if only one is set, that one is used. `TickSize` applies only to IOU sides: XRP and MPTs are integral types and never carry a `TickSize`, so they do not contribute one to the rounded rate.[^ticksize-integral] - -After the rate is rounded, one side of the offer is recomputed from the rounded rate, but only when that side is XRP or an IOU (an MPT side is left unchanged): -- For **sell offers** (`tfSell` flag): `takerPays` is recalculated based on the rounded rate, unless `takerPays` is an MPT -- For **buy offers** (no `tfSell` flag): `takerGets` is recalculated based on the rounded rate, unless `takerGets` is an MPT - -[^ticksize-range]: [`Quality.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/Quality.h#L97-L98) -[^ticksize-round]: [`Quality.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Quality.cpp#L134-L162) -[^ticksize-integral]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L668-L700) - -## 1.4. Offer deletion - -When creating an offer, the `OfferSequence` field can be optionally specified to cancel an existing offer before creating the new one. This allows atomic replacement of an offer in a single transaction. - -If `OfferSequence` is provided: -1. The system looks up the offer with the specified sequence number belonging to the signing account -2. If the offer is found, it is deleted via `offerDelete` -3. If the offer is not found (already consumed or removed), this is **not an error** - the transaction continues -4. Only after the cancellation (if any) does the system proceed with creating the new offer - -This mechanism is useful for updating an existing offer without the risk of having both the old and new offers active simultaneously. - -A domain or hybrid offer (one that sets `DomainID`) can use `OfferSequence` to cancel the signing account's own regular (non-domain) offer; see [State Changes](#3112-state-changes) for the amendment-gated details. - -Direct [self-cross removal](#121-self-crossing) is separate from `OfferSequence`. It happens automatically during crossing and does not require the transaction to identify an offer to cancel. - -## 1.5. Permissioned DEX - -PermissionedDomains enable credential-based access control for offers. When the `DomainID` field is specified in an OfferCreate transaction, the offer is placed in a domain-specific order book that only domain members can access. See [PermissionedDomains documentation](../permissioned_domains/README.md) for details on domain creation and access control. - -**Open Offers** are not a part of any domain. - -### 1.5.1. Domain Offers - -A **domain offer** is an offer created with the `DomainID` field set. Domain offers are placed exclusively in the domain's order book (separate from the open order book) and can only be created by accounts with domain access (domain owner or credential holders). Domain offers only match with other domain offers and hybrid offers within the same domain, and cannot be consumed by regular (non-domain) payments or offers.[^domain-book-segregation] - -### 1.5.2. Hybrid Offers - -A **hybrid offer** is an offer created with both the `DomainID` field set AND the `tfHybrid` flag enabled. Hybrid offers exist simultaneously in both the domain order book and the open order book, with a primary entry in the domain book and a secondary entry (via the `AdditionalBooks` field) in the open book. When a hybrid offer is created, it only crosses with offers in the domain book, since the `DomainID` is passed to the flow engine which uses that domain's order book. Once the hybrid offer is resting on the books, it can be consumed by both domain payments/offers (via the domain book entry) and open payments/offers (via the open book entry).[^hybrid-books] - -Under the `fixCleanup3_3_0` amendment, a resting hybrid offer's domain membership is re-validated only while the domain book is being walked. Losing domain access, for example through credential expiry, removes the offer during domain-book processing but leaves the open-book entry consumable. Without the amendment, the membership check ran during any book walk, so losing domain access also removed the hybrid offer during open-book processing.[^hybrid-eviction] - -[^domain-book-segregation]: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Indexes.cpp#L102-L110) -[^hybrid-books]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L561-L603) -[^hybrid-eviction]: [`OfferStream.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/paths/OfferStream.cpp#L253-L267) - -# 2. Ledger Entries - -Offers are stored on the ledger using `Offer` ledger entries, which are organized in the order book via `DirectoryNode` entries. - -Offers in the order book are indexed using `DirectoryNode` ledger entries, which organize offers by trading pair and quality level. Each offer is referenced in two directories: - -1. **Book Directory**: Groups all offers for a specific trading pair (e.g., USD/XRP) at a particular quality level -2. **Owner Directory**: Tracks all ledger objects owned by an account - -The book directory key is calculated by hashing the trading pair (asset identifiers: currency+issuer for IOUs, MPT ID for MPTs, plus optional domain ID), then the last 8 bytes are replaced with the 64-bit quality value (exchange rate). This means each quality level gets its own directory node, allowing efficient traversal from best to worst quality. - -When an offer is created, it stores references to both its book directory (`sfBookDirectory` field) and owner directory, enabling efficient order book traversal and account object enumeration. - -**Order Book Segregation:** - -The XRP Ledger maintains separate order book directories based on domain participation[^domain-book-segregation]: - -- **Open Order Books**: Standard directories without domain restrictions, computed as `hash(LedgerNameSpace::BookDir, asset_in, asset_out)`. Contains open offers and hybrid offers (via AdditionalBooks references). All accounts can create open offers. - -- **Domain Order Books**: Separate directories for permissioned domains, computed as `hash(LedgerNameSpace::BookDir, asset_in, asset_out, domainID)`. Contains domain offers (primary entries) and hybrid offers (primary entries). Only domain members can create offers in domain order books. - -- **Hybrid Offers**: Bridge both order books by maintaining a primary entry in the domain book and a secondary entry (via `AdditionalBooks`) in the open book. See [Hybrid Offers](#152-hybrid-offers) for crossing and consumption semantics. - -## 2.1. Offer Ledger Entry - -### 2.1.1. Object Identifier - -The key of the `Offer` object is the result -of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values -concatenated in order: - -- The `Offer` space key `0x006F` (lowercase `o`) -- The `AccountID` of the signing account. -- OfferCreate transaction sequence number, or `TicketSequence`. - -### 2.1.2. Fields - -Fields are described -in [Offer Fields](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/offer#offer-fields) - -#### 2.1.2.1. Asset-Specific Fields - -The `Offer` entry identifies its assets entirely through the `TakerPays` and `TakerGets` Amount fields, each of which embeds the asset: -- **XRP**: an integer amount of drops -- **IOU**: an amount carrying a currency code and issuer -- **MPT**: an amount carrying a 192-bit MPTID - -The Offer entry itself does not store separate `TakerPaysCurrency`/`TakerPaysIssuer`/`TakerPaysMPT` (or the `TakerGets` equivalents) fields.[^offer-asset-amounts] Those broken-out asset identifiers live on the book directory root page, not the offer (see [DirectoryNode Fields](#223-fields)). All combinations of distinct XRP, IOU, and MPT assets are supported; an offer cannot pay and receive the same asset. - -[^offer-asset-amounts]: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/ledger_entries.macro#L227-L240) - -#### 2.1.2.2. Domain-Specific Fields - -**DomainID** (UInt256, optional): The domain identifier for domain offers and hybrid offers. When present, the offer is placed in the domain's order book. - -**AdditionalBooks** (Array, optional): For hybrid offers only. Contains references to additional order book directories where the offer is also listed (specifically, the open order book). Each element includes: -- `BookDirectory` (UInt256): Order book directory hash for the additional book -- `BookNode` (UInt64): Page index within that directory - -This field allows hybrid offers to be discovered and consumed by both domain and open order book traversals. - -Under the `fixCleanup3_1_3` amendment, a valid hybrid offer must carry exactly one `AdditionalBooks` entry (the open order book). The `ValidPermissionedDEX` invariant rejects a hybrid offer whose `AdditionalBooks` array is missing, empty, or holds more than one entry. Before the amendment a present-but-empty array slipped through, since only a missing array or more than one entry failed the invariant.[^hybrid-additionalbooks-count] - -[^hybrid-additionalbooks-count]: [`PermissionedDEXInvariant.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp#L46-L75) - -Under the `fixCleanup3_2_0` amendment, when a hybrid offer partially crosses on placement, the open-book `BookDirectory` referenced here is keyed by the offer's original placement rate, so it shares the same quality (`sfExchangeRate`) as the primary domain `BookDirectory`. Before the amendment the open-book directory was keyed from the post-crossing amounts and could differ slightly due to rounding.[^hybrid-open-book-rate] - -[^hybrid-open-book-rate]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L944-L953) - -#### 2.1.2.3. Flags - -Flags are described -in [Offer Flags](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/offer#offer-flags) - -### 2.1.3. Pseudo-accounts - -Offer transactions (creating or canceling offers) do not create pseudo-accounts. However, offer crossing may modify pseudo-accounts such as AMMs when the offer crosses with AMM liquidity. - -### 2.1.4. Ownership - -The offer is stored in the ledger and tracked in an Owner Directory owned by the account submitting the OfferCreate transaction. Furthermore, the offer is also tracked in a Book Directory for the specific trading pair and quality level. The Owner Directory page is captured by the `sfOwnerNode` field. The Book Directory is identified by the `sfBookDirectory` field, and the page within that directory is captured by the `sfBookNode` field - -### 2.1.5. Reserves - -The `Offer` object costs one owner reserve for the account creating it. - -If the account has insufficient reserve before placing the offer: -- If the offer crosses with existing offers (meaning some liquidity was consumed), the transaction succeeds even with insufficient reserve, but the unfilled remainder is not placed on the order book[^offer-reserve] -- If the offer does not cross (nothing was consumed), the transaction fails with `tecINSUF_RESERVE_OFFER` - -This special behavior allows offers to succeed if they provide immediate value through crossing, even when the account cannot afford to place a standing offer on the order book. - -## 2.2. DirectoryNode Ledger Entry - -### 2.2.1. Object Identifier - -**Book Directory Root Page (Page 0)**: - -The first 192 bits are the first 192 bits of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values, concatenated in order.[^book-dir-hash] The exact concatenation order depends on the asset types involved: - -- **IOU + IOU** (includes XRP): `BOOK_DIR`, `takerPays` currency, `takerGets` currency, `takerPays` issuer, `takerGets` issuer, [`domainID`] -- **IOU + MPT**: `BOOK_DIR`, `takerPays` currency, `takerGets` MPT ID, `takerPays` issuer, [`domainID`] -- **MPT + IOU** (includes XRP): `BOOK_DIR`, `takerPays` MPT ID, `takerGets` currency, `takerGets` issuer, [`domainID`] -- **MPT + MPT**: `BOOK_DIR`, `takerPays` MPT ID, `takerGets` MPT ID, [`domainID`] - -The Book directory space key (`BOOK_DIR`) is `0x0042`. For XRP, the currency code is 160 bits of zeros and the issuer is 160 bits of zeros. The `domainID` is included only when specified (for permissioned domains). - -[^book-dir-hash]: Book directory hash computation: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Indexes.cpp#L102-L141) - -The last 64 bits encode the exchange rate (`takerPays / takerGets`) as a 64-bit value in big-endian format. - -**Owner Directory Root Page (Page 0)**: - -The key is the result of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values, concatenated in order: - -- The Owner directory space key (`0x004F`) -- The `AccountID` - -**Subsequent Pages (Pages 1+)**: - -The key is the result of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values, concatenated in order: - -- The DirectoryNode space key (`0x0064`) -- The ID of the root DirectoryNode -- The page number (integer 1 or higher) - -### 2.2.2. Directory Pages - -Each directory can span multiple pages to accommodate large numbers of entries: - -- **Maximum entries per page**: 32 entries -- **Maximum pages per directory**: 262,144 pages, unless the `fixDirectoryLimit` amendment is enabled (which removes this cap)[^dir-page-limit] - -Pages form a doubly-linked list structure: -- Root page (page 0) serves as the entry point -- `sfIndexNext`: Points to the next page in the chain -- `sfIndexPrevious`: Points to the previous page -- Last page's `sfIndexNext` points to root page (value 0) -- Root's `sfIndexPrevious` points to the last page number -- Subsequent pages have non-sequential keys (each page key is a hash, see [2.2.1](#221-object-identifier)), but they are addressed by a sequential page number (1, 2, 3, ...). `sfIndexNext`/`sfIndexPrevious` store page numbers, and `sfRootIndex` links each page back to the root.[^page-keylet] - -**Page Creation**: -- New offers are appended to the last page of the book directory (book directories preserve insertion order). Owner directories instead insert entries in sorted order, not appended.[^dir-append-insert] -- When a page reaches 32 entries, a new page is created and linked to the chain -- If creating a new page would exceed the page limit, the transaction fails with `tecDIR_FULL`. Before `fixDirectoryLimit` this limit is 262,144 pages; with `fixDirectoryLimit` enabled the cap is removed (pages are bounded only by the 64-bit page counter) - -**Page Deletion**: -- When the last entry is removed from a non-root page, the page is deleted -- Empty intermediate pages cause the chain to be repaired by updating adjacent pages' links -- The root page is only deleted when the entire directory becomes empty and `keepRoot` is false - -[^dir-page-limit]: [`ApplyView.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/ApplyView.cpp#L124-L129) -[^page-keylet]: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Indexes.cpp#L362-L369) -[^dir-append-insert]: [`ApplyView.h`](https://github.com/XRPLF/rippled/blob/3.3.0/include/xrpl/ledger/ApplyView.h#L326-L380) - -### 2.2.3. Fields - -**Common Fields** (all directories): -- `sfIndexes`: Vector of up to 32 object IDs (uint256 values) -- `sfRootIndex`: Points to the root directory node's key -- `sfIndexNext`: Optional, points to next page number (omitted if no next page) -- `sfIndexPrevious`: Optional, points to previous page number (omitted on root if it's the only page) - -**Book Directory Fields** (order books): -- `sfTakerPaysCurrency`: Currency code for takerPays (when asset is an IOU) -- `sfTakerPaysIssuer`: Issuer account ID for takerPays (when asset is an IOU) -- `sfTakerPaysMPT`: MPTID for takerPays (UInt192, when asset is an MPT) -- `sfTakerGetsCurrency`: Currency code for takerGets (when asset is an IOU) -- `sfTakerGetsIssuer`: Issuer account ID for takerGets (when asset is an IOU) -- `sfTakerGetsMPT`: MPTID for takerGets (UInt192, when asset is an MPT) -- `sfExchangeRate`: The quality level encoded as 64-bit integer -- `sfDomainID`: Optional, for permissioned domain offers - -Book directories include either currency/issuer fields or MPT fields depending on the asset types involved in the order book. For XRP, the currency code is used without an issuer field. - -**Owner Directory Fields**: -- `sfOwner`: The account that owns the objects - -# 3. Transactions - -## 3.1. Offer Transactions - -### 3.1.1. OfferCreate Transaction - -Fields are described -in [OfferCreate Fields](https://xrpl.org/docs/references/protocol/transactions/types/offercreate#offercreate-fields) - -Flags are described -in [OfferCreate flags](https://xrpl.org/docs/references/protocol/transactions/types/offercreate#offercreate-flags) - -Please note that when an `OfferCreate` transaction is placed, it may not necessarily create an [Offer ledger entry](#21-offer-ledger-entry). -Please refer to [offer crossing](#12-offer-crossing) for more information. - -#### 3.1.1.1. Failure Conditions - -Some errors are handled within [state changes](#3112-state-changes). - -For example, an `ImmediateOrCancel` offer that does not immediately get filled will return error code `tecKILLED`. -From a business logic perspective, this outcome is expected and acceptable. The `tec` prefix in the error code indicates that the transaction did not succeed, but it was still applied to a ledger and may have side effects. - -For this reason, certain `tec` outcomes are covered in the [state changes](#3112-state-changes) section of this document. - -**Static validation:** - -- `temDISABLED`: - - transaction contains field `DomainID` but [PermissionedDex](https://xrpl.org/resources/known-amendments#permissioneddex) amendment is not enabled - - either `takerPays` or `takerGets` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled -- `temINVALID_FLAG`: - - one of the specified flags is not one of [flags](#2123-flags). - - flag `tfHybrid` is specified, - but [PermissionedDex](https://xrpl.org/resources/known-amendments#permissioneddex) amendment is not enabled or - field `DomainID` is not present in the transaction. - - both `tfImmediateOrCancel` and `tfFillOrKill` flags are specified. -- `temMALFORMED`: field `DomainID` is present but is all zeros. User should omit the field if they do not want to specify a domain. Enforced under the `fixCleanup3_2_0` amendment.[^domainid-zero] -- `temBAD_EXPIRATION`: `Expiration` field is set to `0`. User should omit the field if they do not want to specify it. -- `temBAD_SEQUENCE`: `OfferSequence` is set to `0`. User should omit the field if they do not want to specify an offer to delete first. -- `temBAD_AMOUNT`: either `takerPays` or `takerGets` specifies XRP, but with mantissa bigger than `100000000000000000ull`. -- `temBAD_OFFER`: - - both `takerPays` and `takerGets` are XRP amounts. - - either `takerPays` or `takerGets` have value `0`. -- `temREDUNDANT`: `takerPays` and `takerGets` are the same asset -- `temBAD_CURRENCY`: either `takerPays` or `takerGets` is a non-native asset that uses the XRP currency code. -- `temBAD_ISSUER`: either `takerPays` or `takerGets` contain either an XRP with issuer ID, or an IOU without an issuer ID. - -**Validation against the ledger view:** - -- `terNO_ACCOUNT`: signing account does not exist -- `tecFROZEN`: either `takerPays` or `takerGets` is an IOU whose issuer has the `lsfGlobalFreeze` flag set. An offer cannot be created for a frozen issuer. For an MPT whose issuance is globally locked, the same check returns `tecLOCKED` instead.[^global-frozen] -- `tecUNFUNDED_OFFER`: signing account does not have a positive balance in `takerGets` currency and it is not the issuer of `takerGets` currency. Partially funding an offer is acceptable. For MPTs, unauthorized accounts (without `lsfMPTAuthorized` flag or without valid domain credentials when MPTokenIssuance has DomainID) are treated as having zero balance. -- `temBAD_SEQUENCE`: `OfferSequence` is equal to or greater than the signing account's next sequence number (you can only cancel an offer with a lower sequence).[^offer-bad-seq] -- `tecEXPIRED`: the `Expiration` field is before the close time of the previously closed ledger. This is unconditional (no longer gated on the DepositAuth amendment).[^offer-expired] -- **IOU-specific validations for `takerPays`** (only applies when `takerPays` is an IOU): - - `takerPays` issuer account does not exist: - - `terNO_ACCOUNT`: `tapRETRY` enabled - - `tecNO_ISSUER`: `tapRETRY` is not enabled. - - `takerPays` issuer account has a flag `lsfRequireAuth` and there is no trust line between signing account and the issuer account: - - `terNO_LINE`: `tapRETRY` enabled - - `tecNO_LINE`: `tapRETRY` is not enabled. - - `takerPays` issuer account has a flag `lsfRequireAuth` and there is a trust line between signing account and the issuer account, but it is not authorized:[^checkAcceptAsset-noauth] - - `terNO_AUTH`: `tapRETRY` enabled - - `tecNO_AUTH`: `tapRETRY` is not enabled. - - `tecFROZEN`: trust line between signing account and the `takerPays` issuer account is deeply frozen, either on low or high - side. -- `tecNO_AUTH`: `takerPays` is an MPT and the signing account is not authorized to hold the MPT. Authorization is checked via `lsfMPTAuthorized` flag on the holder's MPToken, or through valid domain credentials if the MPTokenIssuance has a DomainID set. See [DomainID and Authorization](../mpts/README.md#11-domainid-and-authorization) for details.[^checkAcceptAsset-mpt-auth] -- `tecNO_PERMISSION`: - - `DomainID` is specified but one of the following domain access requirements is not met: - - The specified domain must exist - - The offer creator must be in the domain (either the domain owner or hold a valid accepted credential that is not expired) - - MPT validation failure (see below) -- `tecOBJECT_NOT_FOUND`: MPT validation failure (see below) -- `tecNO_ISSUER`: MPT validation failure (see below) -- `tecLOCKED`: MPT validation failure (see below) - -**MPT-specific validations**: When either `takerPays` or `takerGets` is an MPT, the transaction is validated using [`canTrade`](../mpts/README.md#361-cantrade). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for complete details on validation logic and error conditions. - -[^checkAcceptAsset-noauth]: Unauthorized trust line returns auth errors: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L307) -[^checkAcceptAsset-mpt-auth]: MPT authorization via requireAuth with WeakAuth: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L330-L340), [`View.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp#L360-L384) -[^domainid-zero]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L99-L101) -[^domain-cancel-regular]: [`PermissionedDEXInvariant.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp#L41-L43), [finalize](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp#L105-L111) -[^offer-expired]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L222-L228), [doApply](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L651-L657) -[^offer-bad-seq]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L215-L221) -[^offercancel-bad-seq]: [`OfferCancel.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCancel.cpp#L34-L46) -[^fok-killed]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L807-L812), [`features.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/features.macro#L100) -[^ioc-killed]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L815-L825), [`features.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/features.macro#L132) -[^global-frozen]: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L59-L65) - -**Validation during doApply:** - -- `tecEXPIRED`: the `Expiration` field is before the close time of the previously closed ledger (re-checked during doApply in case the offer expired after preclaim). - -#### 3.1.1.2. State Changes - -- `Offer` object is **deleted**: - - If `OfferSequence` field is specified and the offer with that sequence exists, it is deleted. See [OfferCancel State Changes](#3122-state-changes) for details on the deletion process - - When the new offer sets `DomainID` (a domain or hybrid offer), the offer cancelled via `OfferSequence` may be the signing account's own regular (non-domain) offer. Under the `fixCleanup3_2_0` amendment this is permitted; before the amendment the `ValidPermissionedDEX` invariant treated the deleted regular offer as a violation and failed the transaction with `tecINVARIANT_FAILED`.[^domain-cancel-regular] - - During default-path crossing, any existing offer owned by the signing account that would directly cross the new offer is deleted in full, regardless of the relative amounts. See [Self-Crossing](#121-self-crossing) - - -- `Offer` object is **not created**: - - If offer is not fully crossed and it was submitted with `tfFillOrKill` flag, fail with `tecKILLED`. (`fix1578` is a retired amendment, so this is unconditional.)[^fok-killed] - - If offer is not at all crossed and it was submitted with `tfImmediateOrCancel` flag, fail with `tecKILLED`. (`ImmediateOfferKilled` is a retired amendment, so this is unconditional.)[^ioc-killed] - - If offer is not fully crossed and the signing account cannot cover the reserve of creating an Offer, fail with - `tecINSUF_RESERVE_OFFER`. - - If offer cannot be added to the OfferDirectory because it is full, fail with `tecDIR_FULL`. - - If the offer is partially filled at crossing and the signing account's `takerGets` balance is reduced to 0, the remaining offer is not created (the transaction still succeeds).[^no-balance-no-offer] - - -- `Offer` object is **created**: - - When the offer is not fully crossed and none of the not-created conditions above apply, the `Offer` is created with the remaining `takerGets` and `takerPays`. - - -- `DirectoryNode` object is **created or modified**: - - When an offer is created, it is added to two directories: - - **Owner Directory**: Added via `dirInsert` to `keylet::ownerDir(account)`. The page number is stored in the offer's `sfOwnerNode` field. - - **Book Directory**: Added via `dirAppend` to the book directory for the trading pair and quality level. The directory key is stored in `sfBookDirectory`, and the page number is stored in `sfBookNode`. - - Each newly created book-directory page (the root page or a subsequent page) is given these fields: - - **For IOU assets**: `sfTakerPaysCurrency` and `sfTakerPaysIssuer` (when `takerPays` is an IOU), `sfTakerGetsCurrency` and `sfTakerGetsIssuer` (when `takerGets` is an IOU) - - **For MPT assets**: `sfTakerPaysMPT` (when `takerPays` is an MPT), `sfTakerGetsMPT` (when `takerGets` is an MPT) - - **Always**: `sfExchangeRate` (the rate value before any crossing), and optionally `sfDomainID` - - If a directory page is full (32 entries), a new page is created and linked to the directory chain - - If creating a new page would exceed the page limit, the transaction fails with `tecDIR_FULL` (the 262,144-page cap applies only before the `fixDirectoryLimit` amendment)[^dir-page-limit] - - -- Order books are **registered** in `OrderBookDB` (if not already present): - - Trading pair registered for the offer's assets and domain. See [OrderBookDB documentation](../path_finding/README.md#26-orderbookdb) for details. - - -- `AccountRoot` object is **modified**: - - If `Offer` is deleted, decrement `sfOwnerCount` by 1, without going below `0`. - - If `Offer` is created, increment `sfOwnerCount` by 1, without overflowing ```std::uint32_t```. - -### 3.1.2. OfferCancel Transaction - -Fields are described -in [OfferCancel Fields](https://xrpl.org/docs/references/protocol/transactions/types/offercancel#offercancel-fields) - -#### 3.1.2.1. Failure Conditions - -**Static validation:** - -- `temINVALID_FLAG`: one of the specified flags is not one of common transaction flags -- `temBAD_SEQUENCE`: `OfferSequence` is set to `0` - -**Validation against the ledger view:** - -- `terNO_ACCOUNT`: signing account does not exist -- `temBAD_SEQUENCE`: `OfferSequence` is equal to or greater than the signing account's next sequence number[^offercancel-bad-seq] - -#### 3.1.2.2. State Changes - -- `Offer` object is **deleted**: - - If the offer with `OfferSequence` sequence number exists. If the offer does not exist, the transaction succeeds without deleting anything. - - When an offer is deleted via `offerDelete`: - - The offer is removed from its owner directory using `dirRemove(keylet::ownerDir(owner), sfOwnerNode, offer_index, false)` - - The offer is removed from its book directory using `dirRemove(keylet::page(book_directory), sfBookNode, offer_index, false)` - - If the offer has `sfAdditionalBooks` (hybrid offers), it is removed from those directories as well - - If removing the offer empties a non-root directory page, that page is deleted and the directory chain is repaired - -- `AccountRoot` object is **modified**: - - If `Offer` is deleted, decrement `sfOwnerCount` by 1, without going below `0`. +# Index + +- [1. Introduction](#1-introduction) + - [1.1. Offers](#11-offers) + - [1.2. Offer Crossing](#12-offer-crossing) + - [1.2.1. Self-Crossing](#121-self-crossing) + - [1.2.2. Sell vs Buy Offers](#122-sell-vs-buy-offers) + - [1.2.3. Auto-bridging](#123-auto-bridging) + - [1.2.4. Creating the Residual Offer](#124-creating-the-residual-offer) + - [1.3. Rate Calculation](#13-rate-calculation) + - [1.3.1. TickSize Rounding](#131-ticksize-rounding) + - [1.4. Offer Deletion](#14-offer-deletion) + - [1.5. Domain and Hybrid Offers](#15-permissioned-dex) + - [1.5.1. Domain Offers](#151-domain-offers) + - [1.5.2. Hybrid Offers](#152-hybrid-offers) +- [2. Ledger Entries](#2-ledger-entries) + - [2.1. Offer Ledger Entry](#21-offer-ledger-entry) + - [2.1.1. Object Identifier](#211-object-identifier) + - [2.1.2. Fields](#212-fields) + - [2.1.2.1. Asset-Specific Fields](#2121-asset-specific-fields) + - [2.1.2.2. Domain-Specific Fields](#2122-domain-specific-fields) + - [2.1.2.3. Flags](#2123-flags) + - [2.1.3. Pseudo-accounts](#213-pseudo-accounts) + - [2.1.4. Ownership](#214-ownership) + - [2.1.5. Reserves](#215-reserves) + - [2.2. DirectoryNode Ledger Entry](#22-directorynode-ledger-entry) + - [2.2.1. Object Identifier](#221-object-identifier) + - [2.2.2. Directory Pages](#222-directory-pages) + - [2.2.3. Fields](#223-fields) +- [3. Transactions](#3-transactions) + - [3.1. Offer Transactions](#31-offer-transactions) + - [3.1.1. OfferCreate Transaction](#311-offercreate-transaction) + - [3.1.1.1. Failure Conditions](#3111-failure-conditions) + - [3.1.1.2. State Changes](#3112-state-changes) + - [3.1.2. OfferCancel Transaction](#312-offercancel-transaction) + - [3.1.2.1. Failure Conditions](#3121-failure-conditions) + - [3.1.2.2. State Changes](#3122-state-changes) + +# 1. Introduction + +Offers are limit orders on the XRP Ledger decentralized exchange (DEX). They execute only at an exchange rate that is as favorable as, or better than, the rate the offer creator specifies. +As part of the decentralized exchange, users can submit offers between any combination of asset types: [XRP](../glossary.md#xrp), [IOUs](../glossary.md#iou), and [MPTs](../glossary.md#mpt). MPTs must have the `lsfMPTCanTrade` flag set on their MPTokenIssuance to be tradable on the DEX. See [MPT Flags](../mpts/README.md#2121-flags) for details on MPT capability flags. + +The DEX supports both open order books (accessible to all accounts) and domain-specific order books (restricted to credential holders). During offer crossing and payments, domain offers only match within their domain, while hybrid offers can match in both environments. This Permissioned DEX functionality enables regulated trading for securities, institutional venues, and other scenarios requiring verified participants. See [PermissionedDomains documentation](../permissioned_domains/README.md) and [Domain and Hybrid Offers](#15-permissioned-dex) for details. + +When `xrpld` applies an `OfferCreate`, it first invokes the payment [flow engine](../flow/README.md) to try crossing with existing book depth; only any leftover remainder is placed as a resting order. + +## 1.1. Offers + +An offer is defined in terms of `takerGets` and `takerPays` parameters, which are named from the perspective of the taker - the party accepting the offer. If Alice creates an offer with `takerGets = 100 XRP` and `takerPays = 10 USD`, it means she is offering 100 XRP and wants to receive 10 USD in return. + +Alice has to have a positive amount in `takerGets` currency, unless she is the issuer of that asset (IOU issuers can issue trust line tokens on demand; MPT issuers can mint MPTs into circulation). She does not have to have the full amount to cover the offer. If her balance is lower than `takerGets`, the offer may still partially fill. + +## 1.2. Offer Crossing + +Because offers are limit orders, a new offer is first matched against existing offers on the book during crossing. It can be filled fully, partially, or not at all. Only the unfilled remainder is then placed on the order book as a resting offer. + +Crossing is done by calling the [flow engine](../flow/README.md) and passing a set of paths. All crossings will contain +the [default path](../path_finding/README.md#24-default-paths). If neither taker pays or taker gets is XRP, then an additional +`xrpCurrency` path is added to achieve auto-bridging. + +If transaction has `tfPassive` flag, it will only cross offers with strictly better quality than its own. +It will not cross offers of equal quality, making it more likely to remain on the order book. + +If the offer is `tfImmediateOrCancel` it will never be placed in the order book. It can be fully or partially filled during crossing, or not filled at all, but `Offer` ledger entry will never be created for it. + +If the offer is `tfFillOrKill` it will never be placed in the order book. It can either *fully* fill immediately or fail. + +During crossing, the new offer may be fully filled, partially filled, or not filled at all by existing offers in the order book. + +```mermaid +--- +title: Offer Creation +--- +flowchart LR + A((Offer Transaction)) --> B[Calculate Rate] + B --> C[Add default path to Paths] + C --> D{Can be
autobridged?} + D -->|yes| E[Add XRP currency to Paths] + D -->|no| F[Try crossing] + E --> F + + F --> G{Can fully fill?} + + %% Fully filled path + G -->|yes| X[Fill offer] + X --> Z((Transaction Done)) + + %% NOT fully filled: handle flags and partials + G -->|no| H{tfImmediateOrCancel?} + H -->|yes| X + H -->|no| I{tfFillOrKill?} + I -->|yes| Z + I -->|no| J{Can partially fill?} + J -->|yes| P[Fill partially] + P --> R[Create offer for remainder] + J -->|no| R[Create offer for remainder] + R --> Z +``` + +**Atomicity:** + +The fee and sequence number are applied to the base ledger view by the transactor before offer crossing begins. Both sandboxes below are built over that base view, so the fee is recorded outside of them and persists regardless of which one is applied: +- `sb`: the crossing results, the deletions of offers consumed or removed during crossing, and the new resting offer +- `sbCancel`: the deletion of offers marked for permanent removal during crossing, such as expired, already-unfunded, invalid, no-longer-in-domain, or directly self-crossable offers + +When the offer will not be placed (a `tfFillOrKill` offer that cannot fully cross, or a `tfImmediateOrCancel` offer that crosses nothing), `sbCancel` is applied instead of `sb`. This discards the crossing and placement work while keeping the fee and permanent offer cleanup. See [Ledger Views and Sandboxes](../transactions/README.md#5-ledger-views-and-sandboxes) for how sandboxes provide atomic state changes. + +### 1.2.1. Self-Crossing + +When a new offer would directly cross a resting offer owned by the same account, `xrpld` deletes the old offer instead of executing a trade against it. The old offer is not partially filled, and no assets are transferred between the account and itself.[^self-cross-removal] + +The self-cross removal rule applies when all of the following are true: + +1. Crossing is evaluating the default, direct path, not an auto-bridged path. +2. The resting offer's quality is equal to or better than the new offer's quality threshold. +3. The resting offer is owned by the account that submitted the new offer. + +The relative amounts of the offers are not considered. A smaller new offer can therefore delete one or more larger resting offers in full. Deleting self-owned offers does not reduce the new offer's amount and the new offer may then cross other accounts' liquidity. Any remainder may be placed on the order book normally. + +For an offer with the `tfPassive` flag, equal-quality resting offers do not meet the crossing threshold, so an equal-quality self-offer is not deleted by this rule. + +See [`isSelfCross` in the Flow step documentation](../flow/steps.md#531-foreachoffer-pseudo-code) for the implementation logic. + +[^self-cross-removal]: Self-cross removal: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/paths/BookStep.cpp#L399-L454). Amount-independent behavior with multiple larger resting offers: [`Offer_test.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/test/app/Offer_test.cpp#L3243-L3310) + +### 1.2.2. Sell vs Buy Offers + +An offer with the `tfSell` flag set is a **sell** offer. An offer without the `tfSell` flag is a **buy** offer. + +When selling, the offer will accept more than the specified `takerPays` amount to maximize the sale of `takerGets`. In the `xrpld` implementation, `takerPays` is capped at `STAmount::kMaxNative` for XRP[^cMaxNative], half the maximum representable IOU value (`STAmount::kMaxValue / 2`) for IOUs[^cMaxValue-iou], and half the maximum MPT amount (`kMaxMpTokenAmount / 2`) for MPTs[^maxMPTokenAmount-mpt]. The IOU and MPT amounts are halved to leave room for the transfer fee charged during crossing: an issuer's transfer rate can be as high as 200% (a 2.0 multiplier), so capping at half the maximum keeps the crossed amount representable.[^transfer-rate-max] XRP has no transfer rate, so its cap is not halved. + +The following examples demonstrate offer crossing behavior when a new offer is created and crosses with existing offers in the order book. + +**Example 1:** + +Alice is willing to sell her 20 USD for 100 XRP. She wants to sell all of her 20 USD. +Bob is willing to buy 10 USD for his 100 XRP. He does not want to buy more than 10 USD. + +**Offers:** + +- Alice's **sell** offer rests on the book first: + - Taker pays 100 XRP + - Taker gets 20 USD + +- Bob then submits a **buy** offer, which crosses Alice's resting offer: + - Taker pays 10 USD + - Taker gets 100 XRP + +**Result:** + +- Alice will get 50 XRP and pay 10 USD. This is the exchange rate that she offered. She did not manage to sell all of her 20 USD, but the offer was partially filled. +- Bob will get 10 USD and pay 50 XRP. This is a better exchange rate than he offered. He did not buy more than 10 USD that he originally wanted. + +**Example 2:** + +Alice is willing to buy 100 XRP for her 20 USD. Bob is willing to sell his 100 XRP for 10 USD, and he wants to sell all 100 XRP. + +**Offers:** + +- Alice's **buy** offer rests on the book first: + - Taker pays 100 XRP + - Taker gets 20 USD + +- Bob then submits a **sell** offer, which crosses Alice's resting offer: + - Taker pays 10 USD + - Taker gets 100 XRP + +**Result:** + +- Alice will get 100 XRP and pay 20 USD. This is the exchange rate that she offered. +- Bob will get 20 USD and pay 100 XRP. Because he was selling all of his 100 XRP, he got 20 USD for it. He sold his XRP for a better exchange rate than he hoped for. + +[^cMaxNative]: XRP maximum native value: [`STAmount.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STAmount.h#L55) +[^cMaxValue-iou]: IOU maximum value halved for transfer rate: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L436-L437) +[^maxMPTokenAmount-mpt]: MPT maximum amount halved for transfer rate: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L440), maximum defined in [`Protocol.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/Protocol.h#L234) +[^transfer-rate-max]: IOU transfer rate capped at 2.0 (200%): [`AccountSet.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/account/AccountSet.cpp#L128-L131) + +### 1.2.3. Auto-bridging + +Auto-bridging allows offers between two non-XRP currencies to execute through XRP as an intermediate currency. + +Auto-bridging is used only when both `takerPays` and `takerGets` are non-XRP currencies. When it applies, the [Flow engine](../flow/README.md) is invoked with two paths: +1. The [default](../path_finding/README.md#24-default-paths) direct path +2. An auto-bridging path with XRP as intermediate (e.g., USD -> XRP -> EUR)[^auto-bridging-path] + +The Flow engine evaluates both paths and selects the one(s) providing the best quality, allowing offers to execute through whichever route offers better pricing. + +[^auto-bridging-path]: Auto-bridging path construction: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L410-L412) +[^passive-threshold]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L394-L397) + +### 1.2.4. Creating the Residual Offer + +Before the offer is sent to the Flow engine for crossing, a **quality threshold** is calculated from `takerPays` and `takerGets`. This represents the minimum exchange rate at which the offer can be crossed. If `takerGets` is a non-XRP asset (IOU or MPT) and the offer creator is not the issuer, `takerGets` is adjusted by multiplying it by the issuer's transfer rate to account for transfer fees. The quality threshold is then calculated as `takerPays / adjusted takerGets`. For a passive offer (`tfPassive`), the threshold is then incremented so the offer crosses only strictly-better-quality offers.[^passive-threshold] + +The Flow engine returns the amount that was filled. During offer crossing, the offer owner pays transfer fees (see [transfer rates in flow steps](../flow/steps.md#213-quality-functions)). The transfer fee is deducted from the owner's balance but does not reduce the offer's stated amounts. For example, if the original `takerGets` was 100 USD and 50 USD was transferred with a 2% transfer fee, the owner pays 51 USD from their balance, but the remaining offer balance is 50 USD. + +The transfer rate is read from the issuer's settings (AccountRoot `TransferRate` field for IOUs, or the MPTokenIssuance `TransferFee` field for MPTs) and applied identically during offer crossing, maintaining consistent offer book semantics across all asset types. + +**Calculating the residual offer after partial filling:** + +Both calculations preserve the original offer's quality, the `takerGets : takerPays` ratio, so the unfilled remainder rests at the rate the creator specified. The rate used below is `takerGets / takerPays`, the reciprocal of the `takerPays / takerGets` rate defined in [Rate Calculation](#13-rate-calculation). + +**Buy offers** (no `tfSell` flag):[^buy-offer-residual] +1. Subtract consumed `takerPays` from original `takerPays` +2. Calculate remaining `takerGets` by multiplying remaining `takerPays` by this rate +3. Round `takerGets` up + +**Sell offers** (`tfSell` flag set):[^sell-offer-residual] +1. Subtract consumed `takerGets` from original `takerGets` (accounting for transfer rates) +2. Calculate remaining `takerPays` by dividing remaining `takerGets` by this rate +3. Round `takerPays` down + +**Special cases:** +- If the offer is not filled at all, the original offer is recorded on the ledger, unless it is an ImmediateOrCancel or FillOrKill offer (which are never placed) or the account lacks the reserve for a new offer[^offer-reserve] +- If, after partial filling, the signing account no longer has a positive balance in the `takerGets` currency, the remaining offer is not created[^no-balance-no-offer]. This check is skipped when `takerGets` is an MPT and the creator is its issuer (an issuer can supply the MPT without holding a balance), so the residual offer is still created in that case + +[^buy-offer-residual]: Buy offer residual calculation: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L527-L533) +[^sell-offer-residual]: Sell offer residual calculation: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L501-L520) +[^no-balance-no-offer]: No balance check after crossing: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L480-L486) +[^offer-reserve]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L835-L847) + +## 1.3. Rate Calculation + +The exchange rate for an offer is calculated before any crossing, so that potential partial filling does not affect the +intended rate. The rate is calculated as `takerPays / takerGets` (smaller is better for the taker). + +Different asset types have different internal representations: +- **XRP**: an integer number of drops, up to `kMaxNative` (9 * 10^18 drops).[^repr-xrp] +- **IOU**: a sign, an exponent (`-96` to `80`), and a mantissa. When non-zero, the mantissa is normalized to the range 10^15 to 10^16-1, always 16 significant decimal digits (a 54-bit value). This fixed-precision mantissa combined with a wide exponent lets an IOU represent both very large and very small amounts at 16 digits of precision.[^repr-iou] +- **MPT**: an unsigned integer, up to `kMaxMpTokenAmount` (2^63 - 1).[^repr-mpt] + +`xrpld` implementation normalizes the rate by packing the result into a 64-bit integer[^rate-packing]: +- Upper 8 bits: exponent + 100 +- Lower 56 bits: mantissa + +`getRate` returns the rate `0` (and the offer is not stored) in three cases: when `takerGets` is zero, when the computed rate rounds to zero (the offer is 'too good' to represent), or when the computation overflows. + +[^repr-xrp]: [`STAmount.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STAmount.h#L55) +[^repr-iou]: [`STAmount.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STAmount.h#L47-L53) +[^repr-mpt]: [`Protocol.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/Protocol.h#L234) +[^rate-packing]: [`STAmount.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/STAmount.cpp#L459-L481) + +### 1.3.1. TickSize Rounding + +In order to ensure that ranking of offers in order books requires a significant difference between exchange rates, +issuers can set `TickSize` field to their account. + +`TickSize` sets the number of significant decimal digits an offer's rate is rounded to. An issuer can set it to 0 (disabled) or to a value from 3 to 16.[^ticksize-range] If `TickSize` is present, the offer's rate is rounded up to that many significant digits.[^ticksize-round] If the two assets' issuers have different `TickSize` values, the smaller is used; if only one is set, that one is used. `TickSize` applies only to IOU sides: XRP and MPTs are integral types and never carry a `TickSize`, so they do not contribute one to the rounded rate.[^ticksize-integral] + +After the rate is rounded, one side of the offer is recomputed from the rounded rate, but only when that side is XRP or an IOU (an MPT side is left unchanged): +- For **sell offers** (`tfSell` flag): `takerPays` is recalculated based on the rounded rate, unless `takerPays` is an MPT +- For **buy offers** (no `tfSell` flag): `takerGets` is recalculated based on the rounded rate, unless `takerGets` is an MPT + +[^ticksize-range]: [`Quality.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/Quality.h#L97-L98) +[^ticksize-round]: [`Quality.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Quality.cpp#L134-L162) +[^ticksize-integral]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L668-L700) + +## 1.4. Offer deletion + +When creating an offer, the `OfferSequence` field can be optionally specified to cancel an existing offer before creating the new one. This allows atomic replacement of an offer in a single transaction. + +If `OfferSequence` is provided: +1. The system looks up the offer with the specified sequence number belonging to the signing account +2. If the offer is found, it is deleted via `offerDelete` +3. If the offer is not found (already consumed or removed), this is **not an error** - the transaction continues +4. Only after the cancellation (if any) does the system proceed with creating the new offer + +This mechanism is useful for updating an existing offer without the risk of having both the old and new offers active simultaneously. + +A domain or hybrid offer (one that sets `DomainID`) can use `OfferSequence` to cancel the signing account's own regular (non-domain) offer; see [State Changes](#3112-state-changes) for the amendment-gated details. + +Direct [self-cross removal](#121-self-crossing) is separate from `OfferSequence`. It happens automatically during crossing and does not require the transaction to identify an offer to cancel. + +## 1.5. Permissioned DEX + +PermissionedDomains enable credential-based access control for offers. When the `DomainID` field is specified in an OfferCreate transaction, the offer is placed in a domain-specific order book that only domain members can access. See [PermissionedDomains documentation](../permissioned_domains/README.md) for details on domain creation and access control. + +**Open Offers** are not a part of any domain. + +### 1.5.1. Domain Offers + +A **domain offer** is an offer created with the `DomainID` field set. Domain offers are placed exclusively in the domain's order book (separate from the open order book) and can only be created by accounts with domain access (domain owner or credential holders). Domain offers only match with other domain offers and hybrid offers within the same domain, and cannot be consumed by regular (non-domain) payments or offers.[^domain-book-segregation] + +### 1.5.2. Hybrid Offers + +A **hybrid offer** is an offer created with both the `DomainID` field set AND the `tfHybrid` flag enabled. Hybrid offers exist simultaneously in both the domain order book and the open order book, with a primary entry in the domain book and a secondary entry (via the `AdditionalBooks` field) in the open book. When a hybrid offer is created, it only crosses with offers in the domain book, since the `DomainID` is passed to the flow engine which uses that domain's order book. Once the hybrid offer is resting on the books, it can be consumed by both domain payments/offers (via the domain book entry) and open payments/offers (via the open book entry).[^hybrid-books] + +Under the `fixCleanup3_3_0` amendment, a resting hybrid offer's domain membership is re-validated only while the domain book is being walked. Losing domain access, for example through credential expiry, removes the offer during domain-book processing but leaves the open-book entry consumable. Without the amendment, the membership check ran during any book walk, so losing domain access also removed the hybrid offer during open-book processing.[^hybrid-eviction] + +[^domain-book-segregation]: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Indexes.cpp#L102-L110) +[^hybrid-books]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L561-L603) +[^hybrid-eviction]: [`OfferStream.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/paths/OfferStream.cpp#L253-L267) + +# 2. Ledger Entries + +Offers are stored on the ledger using `Offer` ledger entries, which are organized in the order book via `DirectoryNode` entries. + +Offers in the order book are indexed using `DirectoryNode` ledger entries, which organize offers by trading pair and quality level. Each offer is referenced in two directories: + +1. **Book Directory**: Groups all offers for a specific trading pair (e.g., USD/XRP) at a particular quality level +2. **Owner Directory**: Tracks all ledger objects owned by an account + +The book directory key is calculated by hashing the trading pair (asset identifiers: currency+issuer for IOUs, MPT ID for MPTs, plus optional domain ID), then the last 8 bytes are replaced with the 64-bit quality value (exchange rate). This means each quality level gets its own directory node, allowing efficient traversal from best to worst quality. + +When an offer is created, it stores references to both its book directory (`sfBookDirectory` field) and owner directory, enabling efficient order book traversal and account object enumeration. + +**Order Book Segregation:** + +The XRP Ledger maintains separate order book directories based on domain participation[^domain-book-segregation]: + +- **Open Order Books**: Standard directories without domain restrictions, computed as `hash(LedgerNameSpace::BookDir, asset_in, asset_out)`. Contains open offers and hybrid offers (via AdditionalBooks references). All accounts can create open offers. + +- **Domain Order Books**: Separate directories for permissioned domains, computed as `hash(LedgerNameSpace::BookDir, asset_in, asset_out, domainID)`. Contains domain offers (primary entries) and hybrid offers (primary entries). Only domain members can create offers in domain order books. + +- **Hybrid Offers**: Bridge both order books by maintaining a primary entry in the domain book and a secondary entry (via `AdditionalBooks`) in the open book. See [Hybrid Offers](#152-hybrid-offers) for crossing and consumption semantics. + +## 2.1. Offer Ledger Entry + +### 2.1.1. Object Identifier + +The key of the `Offer` object is the result +of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values +concatenated in order: + +- The `Offer` space key `0x006F` (lowercase `o`) +- The `AccountID` of the signing account. +- OfferCreate transaction sequence number, or `TicketSequence`. + +### 2.1.2. Fields + +Fields are described +in [Offer Fields](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/offer#offer-fields) + +#### 2.1.2.1. Asset-Specific Fields + +The `Offer` entry identifies its assets entirely through the `TakerPays` and `TakerGets` Amount fields, each of which embeds the asset: +- **XRP**: an integer amount of drops +- **IOU**: an amount carrying a currency code and issuer +- **MPT**: an amount carrying a 192-bit MPTID + +The Offer entry itself does not store separate `TakerPaysCurrency`/`TakerPaysIssuer`/`TakerPaysMPT` (or the `TakerGets` equivalents) fields.[^offer-asset-amounts] Those broken-out asset identifiers live on the book directory root page, not the offer (see [DirectoryNode Fields](#223-fields)). All combinations of distinct XRP, IOU, and MPT assets are supported; an offer cannot pay and receive the same asset. + +[^offer-asset-amounts]: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/ledger_entries.macro#L227-L240) + +#### 2.1.2.2. Domain-Specific Fields + +**DomainID** (UInt256, optional): The domain identifier for domain offers and hybrid offers. When present, the offer is placed in the domain's order book. + +**AdditionalBooks** (Array, optional): For hybrid offers only. Contains references to additional order book directories where the offer is also listed (specifically, the open order book). Each element includes: +- `BookDirectory` (UInt256): Order book directory hash for the additional book +- `BookNode` (UInt64): Page index within that directory + +This field allows hybrid offers to be discovered and consumed by both domain and open order book traversals. + +Under the `fixCleanup3_1_3` amendment, a valid hybrid offer must carry exactly one `AdditionalBooks` entry (the open order book). The `ValidPermissionedDEX` invariant rejects a hybrid offer whose `AdditionalBooks` array is missing, empty, or holds more than one entry. Before the amendment a present-but-empty array slipped through, since only a missing array or more than one entry failed the invariant.[^hybrid-additionalbooks-count] + +[^hybrid-additionalbooks-count]: [`PermissionedDEXInvariant.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp#L46-L75) + +Under the `fixCleanup3_2_0` amendment, when a hybrid offer partially crosses on placement, the open-book `BookDirectory` referenced here is keyed by the offer's original placement rate, so it shares the same quality (`sfExchangeRate`) as the primary domain `BookDirectory`. Before the amendment the open-book directory was keyed from the post-crossing amounts and could differ slightly due to rounding.[^hybrid-open-book-rate] + +[^hybrid-open-book-rate]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L944-L953) + +#### 2.1.2.3. Flags + +Flags are described +in [Offer Flags](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/offer#offer-flags) + +### 2.1.3. Pseudo-accounts + +Offer transactions (creating or canceling offers) do not create pseudo-accounts. However, offer crossing may modify pseudo-accounts such as AMMs when the offer crosses with AMM liquidity. + +### 2.1.4. Ownership + +The offer is stored in the ledger and tracked in an Owner Directory owned by the account submitting the OfferCreate transaction. Furthermore, the offer is also tracked in a Book Directory for the specific trading pair and quality level. The Owner Directory page is captured by the `sfOwnerNode` field. The Book Directory is identified by the `sfBookDirectory` field, and the page within that directory is captured by the `sfBookNode` field + +### 2.1.5. Reserves + +The `Offer` object costs one owner reserve for the account creating it. + +If the account has insufficient reserve before placing the offer: +- If the offer crosses with existing offers (meaning some liquidity was consumed), the transaction succeeds even with insufficient reserve, but the unfilled remainder is not placed on the order book[^offer-reserve] +- If the offer does not cross (nothing was consumed), the transaction fails with `tecINSUF_RESERVE_OFFER` + +This special behavior allows offers to succeed if they provide immediate value through crossing, even when the account cannot afford to place a standing offer on the order book. + +## 2.2. DirectoryNode Ledger Entry + +### 2.2.1. Object Identifier + +**Book Directory Root Page (Page 0)**: + +The first 192 bits are the first 192 bits of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values, concatenated in order.[^book-dir-hash] The exact concatenation order depends on the asset types involved: + +- **IOU + IOU** (includes XRP): `BOOK_DIR`, `takerPays` currency, `takerGets` currency, `takerPays` issuer, `takerGets` issuer, [`domainID`] +- **IOU + MPT**: `BOOK_DIR`, `takerPays` currency, `takerGets` MPT ID, `takerPays` issuer, [`domainID`] +- **MPT + IOU** (includes XRP): `BOOK_DIR`, `takerPays` MPT ID, `takerGets` currency, `takerGets` issuer, [`domainID`] +- **MPT + MPT**: `BOOK_DIR`, `takerPays` MPT ID, `takerGets` MPT ID, [`domainID`] + +The Book directory space key (`BOOK_DIR`) is `0x0042`. For XRP, the currency code is 160 bits of zeros and the issuer is 160 bits of zeros. The `domainID` is included only when specified (for permissioned domains). + +[^book-dir-hash]: Book directory hash computation: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Indexes.cpp#L102-L141) + +The last 64 bits encode the exchange rate (`takerPays / takerGets`) as a 64-bit value in big-endian format. + +**Owner Directory Root Page (Page 0)**: + +The key is the result of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values, concatenated in order: + +- The Owner directory space key (`0x004F`) +- The `AccountID` + +**Subsequent Pages (Pages 1+)**: + +The key is the result of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values, concatenated in order: + +- The DirectoryNode space key (`0x0064`) +- The ID of the root DirectoryNode +- The page number (integer 1 or higher) + +### 2.2.2. Directory Pages + +Each directory can span multiple pages to accommodate large numbers of entries: + +- **Maximum entries per page**: 32 entries +- **Maximum pages per directory**: 262,144 pages, unless the `fixDirectoryLimit` amendment is enabled (which removes this cap)[^dir-page-limit] + +Pages form a doubly-linked list structure: +- Root page (page 0) serves as the entry point +- `sfIndexNext`: Points to the next page in the chain +- `sfIndexPrevious`: Points to the previous page +- Last page's `sfIndexNext` points to root page (value 0) +- Root's `sfIndexPrevious` points to the last page number +- Subsequent pages have non-sequential keys (each page key is a hash, see [2.2.1](#221-object-identifier)), but they are addressed by a sequential page number (1, 2, 3, ...). `sfIndexNext`/`sfIndexPrevious` store page numbers, and `sfRootIndex` links each page back to the root.[^page-keylet] + +**Page Creation**: +- New offers are appended to the last page of the book directory (book directories preserve insertion order). Owner directories instead insert entries in sorted order, not appended.[^dir-append-insert] +- When a page reaches 32 entries, a new page is created and linked to the chain +- If creating a new page would exceed the page limit, the transaction fails with `tecDIR_FULL`. Before `fixDirectoryLimit` this limit is 262,144 pages; with `fixDirectoryLimit` enabled the cap is removed (pages are bounded only by the 64-bit page counter) + +**Page Deletion**: +- When the last entry is removed from a non-root page, the page is deleted +- Empty intermediate pages cause the chain to be repaired by updating adjacent pages' links +- The root page is only deleted when the entire directory becomes empty and `keepRoot` is false + +[^dir-page-limit]: [`ApplyView.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/ApplyView.cpp#L124-L129) +[^page-keylet]: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/protocol/Indexes.cpp#L362-L369) +[^dir-append-insert]: [`ApplyView.h`](https://github.com/XRPLF/rippled/blob/3.3.0/include/xrpl/ledger/ApplyView.h#L326-L380) + +### 2.2.3. Fields + +**Common Fields** (all directories): +- `sfIndexes`: Vector of up to 32 object IDs (uint256 values) +- `sfRootIndex`: Points to the root directory node's key +- `sfIndexNext`: Optional, points to next page number (omitted if no next page) +- `sfIndexPrevious`: Optional, points to previous page number (omitted on root if it's the only page) + +**Book Directory Fields** (order books): +- `sfTakerPaysCurrency`: Currency code for takerPays (when asset is an IOU) +- `sfTakerPaysIssuer`: Issuer account ID for takerPays (when asset is an IOU) +- `sfTakerPaysMPT`: MPTID for takerPays (UInt192, when asset is an MPT) +- `sfTakerGetsCurrency`: Currency code for takerGets (when asset is an IOU) +- `sfTakerGetsIssuer`: Issuer account ID for takerGets (when asset is an IOU) +- `sfTakerGetsMPT`: MPTID for takerGets (UInt192, when asset is an MPT) +- `sfExchangeRate`: The quality level encoded as 64-bit integer +- `sfDomainID`: Optional, for permissioned domain offers + +Book directories include either currency/issuer fields or MPT fields depending on the asset types involved in the order book. For XRP, the currency code is used without an issuer field. + +**Owner Directory Fields**: +- `sfOwner`: The account that owns the objects + +# 3. Transactions + +## 3.1. Offer Transactions + +### 3.1.1. OfferCreate Transaction + +Fields are described +in [OfferCreate Fields](https://xrpl.org/docs/references/protocol/transactions/types/offercreate#offercreate-fields) + +Flags are described +in [OfferCreate flags](https://xrpl.org/docs/references/protocol/transactions/types/offercreate#offercreate-flags) + +Please note that when an `OfferCreate` transaction is placed, it may not necessarily create an [Offer ledger entry](#21-offer-ledger-entry). +Please refer to [offer crossing](#12-offer-crossing) for more information. + +#### 3.1.1.1. Failure Conditions + +Some errors are handled within [state changes](#3112-state-changes). + +For example, an `ImmediateOrCancel` offer that does not immediately get filled will return error code `tecKILLED`. +From a business logic perspective, this outcome is expected and acceptable. The `tec` prefix in the error code indicates that the transaction did not succeed, but it was still applied to a ledger and may have side effects. + +For this reason, certain `tec` outcomes are covered in the [state changes](#3112-state-changes) section of this document. + +**Static validation:** + +- `temDISABLED`: + - transaction contains field `DomainID` but [PermissionedDex](https://xrpl.org/resources/known-amendments#permissioneddex) amendment is not enabled + - either `takerPays` or `takerGets` is an MPT but [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled +- `temINVALID_FLAG`: + - one of the specified flags is not one of [flags](#2123-flags). + - flag `tfHybrid` is specified, + but [PermissionedDex](https://xrpl.org/resources/known-amendments#permissioneddex) amendment is not enabled or + field `DomainID` is not present in the transaction. + - both `tfImmediateOrCancel` and `tfFillOrKill` flags are specified. +- `temMALFORMED`: field `DomainID` is present but is all zeros. User should omit the field if they do not want to specify a domain. Enforced under the `fixCleanup3_2_0` amendment.[^domainid-zero] +- `temBAD_EXPIRATION`: `Expiration` field is set to `0`. User should omit the field if they do not want to specify it. +- `temBAD_SEQUENCE`: `OfferSequence` is set to `0`. User should omit the field if they do not want to specify an offer to delete first. +- `temBAD_AMOUNT`: either `takerPays` or `takerGets` specifies XRP, but with mantissa bigger than `100000000000000000ull`. +- `temBAD_OFFER`: + - both `takerPays` and `takerGets` are XRP amounts. + - either `takerPays` or `takerGets` have value `0`. +- `temREDUNDANT`: `takerPays` and `takerGets` are the same asset +- `temBAD_CURRENCY`: either `takerPays` or `takerGets` is a non-native asset that uses the XRP currency code. +- `temBAD_ISSUER`: either `takerPays` or `takerGets` contain either an XRP with issuer ID, or an IOU without an issuer ID. + +**Validation against the ledger view:** + +- `terNO_ACCOUNT`: signing account does not exist +- `tecFROZEN`: either `takerPays` or `takerGets` is an IOU whose issuer has the `lsfGlobalFreeze` flag set. An offer cannot be created for a frozen issuer. For an MPT whose issuance is globally locked, the same check returns `tecLOCKED` instead.[^global-frozen] +- `tecUNFUNDED_OFFER`: signing account does not have a positive balance in `takerGets` currency and it is not the issuer of `takerGets` currency. Partially funding an offer is acceptable. For MPTs, unauthorized accounts (without `lsfMPTAuthorized` flag or without valid domain credentials when MPTokenIssuance has DomainID) are treated as having zero balance. +- `temBAD_SEQUENCE`: `OfferSequence` is equal to or greater than the signing account's next sequence number (you can only cancel an offer with a lower sequence).[^offer-bad-seq] +- `tecEXPIRED`: the `Expiration` field is before the close time of the previously closed ledger. This is unconditional (no longer gated on the DepositAuth amendment).[^offer-expired] +- **IOU-specific validations for `takerPays`** (only applies when `takerPays` is an IOU): + - `takerPays` issuer account does not exist: + - `terNO_ACCOUNT`: `tapRETRY` enabled + - `tecNO_ISSUER`: `tapRETRY` is not enabled. + - `takerPays` issuer account has a flag `lsfRequireAuth` and there is no trust line between signing account and the issuer account: + - `terNO_LINE`: `tapRETRY` enabled + - `tecNO_LINE`: `tapRETRY` is not enabled. + - `takerPays` issuer account has a flag `lsfRequireAuth` and there is a trust line between signing account and the issuer account, but it is not authorized:[^checkAcceptAsset-noauth] + - `terNO_AUTH`: `tapRETRY` enabled + - `tecNO_AUTH`: `tapRETRY` is not enabled. + - `tecFROZEN`: trust line between signing account and the `takerPays` issuer account is deeply frozen, either on low or high + side. +- `tecNO_AUTH`: `takerPays` is an MPT and the signing account is not authorized to hold the MPT. Authorization is checked via `lsfMPTAuthorized` flag on the holder's MPToken, or through valid domain credentials if the MPTokenIssuance has a DomainID set. See [DomainID and Authorization](../mpts/README.md#11-domainid-and-authorization) for details.[^checkAcceptAsset-mpt-auth] +- `tecNO_PERMISSION`: + - `DomainID` is specified but one of the following domain access requirements is not met: + - The specified domain must exist + - The offer creator must be in the domain (either the domain owner or hold a valid accepted credential that is not expired) + - MPT validation failure (see below) +- `tecOBJECT_NOT_FOUND`: MPT validation failure (see below) +- `tecNO_ISSUER`: MPT validation failure (see below) +- `tecLOCKED`: MPT validation failure (see below) + +**MPT-specific validations**: When either `takerPays` or `takerGets` is an MPT, the transaction is validated using [`canTrade`](../mpts/README.md#361-cantrade). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for complete details on validation logic and error conditions. + +[^checkAcceptAsset-noauth]: Unauthorized trust line returns auth errors: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L307) +[^checkAcceptAsset-mpt-auth]: MPT authorization via requireAuth with WeakAuth: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L330-L340), [`View.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp#L360-L384) +[^domainid-zero]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L99-L101) +[^domain-cancel-regular]: [`PermissionedDEXInvariant.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp#L41-L43), [finalize](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp#L105-L111) +[^offer-expired]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L222-L228), [doApply](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L651-L657) +[^offer-bad-seq]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L215-L221) +[^offercancel-bad-seq]: [`OfferCancel.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCancel.cpp#L34-L46) +[^fok-killed]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L807-L812), [`features.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/features.macro#L100) +[^ioc-killed]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L815-L825), [`features.macro`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/detail/features.macro#L132) +[^global-frozen]: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L59-L65) + +**Validation during doApply:** + +- `tecEXPIRED`: the `Expiration` field is before the close time of the previously closed ledger (re-checked during doApply in case the offer expired after preclaim). + +#### 3.1.1.2. State Changes + +- `Offer` object is **deleted**: + - If `OfferSequence` field is specified and the offer with that sequence exists, it is deleted. See [OfferCancel State Changes](#3122-state-changes) for details on the deletion process + - When the new offer sets `DomainID` (a domain or hybrid offer), the offer cancelled via `OfferSequence` may be the signing account's own regular (non-domain) offer. Under the `fixCleanup3_2_0` amendment this is permitted; before the amendment the `ValidPermissionedDEX` invariant treated the deleted regular offer as a violation and failed the transaction with `tecINVARIANT_FAILED`.[^domain-cancel-regular] + - During default-path crossing, any existing offer owned by the signing account that would directly cross the new offer is deleted in full, regardless of the relative amounts. See [Self-Crossing](#121-self-crossing) + + +- `Offer` object is **not created**: + - If offer is not fully crossed and it was submitted with `tfFillOrKill` flag, fail with `tecKILLED`. (`fix1578` is a retired amendment, so this is unconditional.)[^fok-killed] + - If offer is not at all crossed and it was submitted with `tfImmediateOrCancel` flag, fail with `tecKILLED`. (`ImmediateOfferKilled` is a retired amendment, so this is unconditional.)[^ioc-killed] + - If offer is not fully crossed and the signing account cannot cover the reserve of creating an Offer, fail with + `tecINSUF_RESERVE_OFFER`. + - If offer cannot be added to the OfferDirectory because it is full, fail with `tecDIR_FULL`. + - If the offer is partially filled at crossing and the signing account's `takerGets` balance is reduced to 0, the remaining offer is not created (the transaction still succeeds).[^no-balance-no-offer] + + +- `Offer` object is **created**: + - When the offer is not fully crossed and none of the not-created conditions above apply, the `Offer` is created with the remaining `takerGets` and `takerPays`. + + +- `DirectoryNode` object is **created or modified**: + - When an offer is created, it is added to two directories: + - **Owner Directory**: Added via `dirInsert` to `keylet::ownerDir(account)`. The page number is stored in the offer's `sfOwnerNode` field. + - **Book Directory**: Added via `dirAppend` to the book directory for the trading pair and quality level. The directory key is stored in `sfBookDirectory`, and the page number is stored in `sfBookNode`. + - Each newly created book-directory page (the root page or a subsequent page) is given these fields: + - **For IOU assets**: `sfTakerPaysCurrency` and `sfTakerPaysIssuer` (when `takerPays` is an IOU), `sfTakerGetsCurrency` and `sfTakerGetsIssuer` (when `takerGets` is an IOU) + - **For MPT assets**: `sfTakerPaysMPT` (when `takerPays` is an MPT), `sfTakerGetsMPT` (when `takerGets` is an MPT) + - **Always**: `sfExchangeRate` (the rate value before any crossing), and optionally `sfDomainID` + - If a directory page is full (32 entries), a new page is created and linked to the directory chain + - If creating a new page would exceed the page limit, the transaction fails with `tecDIR_FULL` (the 262,144-page cap applies only before the `fixDirectoryLimit` amendment)[^dir-page-limit] + + +- Order books are **registered** in `OrderBookDB` (if not already present): + - Trading pair registered for the offer's assets and domain. See [OrderBookDB documentation](../path_finding/README.md#26-orderbookdb) for details. + + +- `AccountRoot` object is **modified**: + - If `Offer` is deleted, decrement `sfOwnerCount` by 1, without going below `0`. + - If `Offer` is created, increment `sfOwnerCount` by 1, without overflowing ```std::uint32_t```. + +### 3.1.2. OfferCancel Transaction + +Fields are described +in [OfferCancel Fields](https://xrpl.org/docs/references/protocol/transactions/types/offercancel#offercancel-fields) + +#### 3.1.2.1. Failure Conditions + +**Static validation:** + +- `temINVALID_FLAG`: one of the specified flags is not one of common transaction flags +- `temBAD_SEQUENCE`: `OfferSequence` is set to `0` + +**Validation against the ledger view:** + +- `terNO_ACCOUNT`: signing account does not exist +- `temBAD_SEQUENCE`: `OfferSequence` is equal to or greater than the signing account's next sequence number[^offercancel-bad-seq] + +#### 3.1.2.2. State Changes + +- `Offer` object is **deleted**: + - If the offer with `OfferSequence` sequence number exists. If the offer does not exist, the transaction succeeds without deleting anything. + - When an offer is deleted via `offerDelete`: + - The offer is removed from its owner directory using `dirRemove(keylet::ownerDir(owner), sfOwnerNode, offer_index, false)` + - The offer is removed from its book directory using `dirRemove(keylet::page(book_directory), sfBookNode, offer_index, false)` + - If the offer has `sfAdditionalBooks` (hybrid offers), it is removed from those directories as well + - If removing the offer empties a non-root directory page, that page is deleted and the directory chain is repaired + +- `AccountRoot` object is **modified**: + - If `Offer` is deleted, decrement `sfOwnerCount` by 1, without going below `0`. diff --git a/docs/overview/motivation.md b/docs/overview/motivation.md index 232c4f6..59d6c3d 100644 --- a/docs/overview/motivation.md +++ b/docs/overview/motivation.md @@ -1,34 +1,34 @@ -# Motivation - -XRP Ledger (XRPL) is a mature and widely used blockchain that distinguishes itself by supporting native multi-currency payments and integrating a decentralized exchange (DEX) into its execution layer. Unlike other platforms where such functionalities are implemented through smart contracts, XRPL's payment engine utilizes built-in mechanisms like trust-lines, order book, and AMMs to facilitate complex multi-currency payment. - -However, despite its sophistication and real-world usage, XRPL's payment engine lacks a formal specification. The logic underpinning key features-such as paths, trust-lines, and rippling is embedded deeply in the `xrpld` codebase. - -This specification aims to provide a single point of reference that can be used as canonical documentation that explains how the payment engine works at a conceptual and architectural level. - -The main goal of this work is to set the foundation for formally verifying the C++ implementation of the Payment Engine in `xrpld`. - -## Target Audience - -The target audience for this specification are: - -- **Researchers** who delve into XRP Ledger protocol in order to provide a formal verification of the components of the payment system. -- New and experienced `xrpld` **contributors** who need a detailed explanation of the Payment Engine and surrounding payment components. As a source of truth on the payments functionality in XRP Ledger, new developers can use it to onboard to the `xrpld` codebase, and existing developers can use it to build new features and identify and fix bugs in the system. -- **Users** and **integrators** of XRP Ledger who want a comprehensive understanding of how to utilize the Payment Engine and how to send payments most effectively. -- Anyone interested in implementing their own XRP Ledger client and validator, in another language, or with different design choices. - - -## Existing Resources - -XRP Ledger has existing resources that can be used to learn more about the Payment Engine. This includes: - -- [XRP Ledger Developer Resources: Documentation](https://xrpl.org/docs) -- [XRP Ledger Standards](https://github.com/XRPLF/XRPL-Standards) -- [Payment Engine System Design Overview](https://ripple.com/reports/Payment-Engine-System-Design.pdf) - -This specification augments existing resources and provides additional value to interested readers in the following ways: - -- It is a single, comprehensive source of truth for the entire payment system. -- While https://xrpl.org/docs provides an excellent reference to different transaction types and ledger entries, it does not go into detailed explanation of transaction validation and application logic. This code contains important invariants and is crucial to document to identify potential bugs and unexpected side-effects. -- While there is a good overview of the Payment Engine, this specification explains detailed logic and gives context to interaction between the Payment Engine and the pathfinding system. -- Pathfinding and path selection are complex topics and this specification provides explanation of both functional requirements and system design. \ No newline at end of file +# Motivation + +XRP Ledger (XRPL) is a mature and widely used blockchain that distinguishes itself by supporting native multi-currency payments and integrating a decentralized exchange (DEX) into its execution layer. Unlike other platforms where such functionalities are implemented through smart contracts, XRPL's payment engine utilizes built-in mechanisms like trust-lines, order book, and AMMs to facilitate complex multi-currency payment. + +However, despite its sophistication and real-world usage, XRPL's payment engine lacks a formal specification. The logic underpinning key features-such as paths, trust-lines, and rippling is embedded deeply in the `xrpld` codebase. + +This specification aims to provide a single point of reference that can be used as canonical documentation that explains how the payment engine works at a conceptual and architectural level. + +The main goal of this work is to set the foundation for formally verifying the C++ implementation of the Payment Engine in `xrpld`. + +## Target Audience + +The target audience for this specification are: + +- **Researchers** who delve into XRP Ledger protocol in order to provide a formal verification of the components of the payment system. +- New and experienced `xrpld` **contributors** who need a detailed explanation of the Payment Engine and surrounding payment components. As a source of truth on the payments functionality in XRP Ledger, new developers can use it to onboard to the `xrpld` codebase, and existing developers can use it to build new features and identify and fix bugs in the system. +- **Users** and **integrators** of XRP Ledger who want a comprehensive understanding of how to utilize the Payment Engine and how to send payments most effectively. +- Anyone interested in implementing their own XRP Ledger client and validator, in another language, or with different design choices. + + +## Existing Resources + +XRP Ledger has existing resources that can be used to learn more about the Payment Engine. This includes: + +- [XRP Ledger Developer Resources: Documentation](https://xrpl.org/docs) +- [XRP Ledger Standards](https://github.com/XRPLF/XRPL-Standards) +- [Payment Engine System Design Overview](https://ripple.com/reports/Payment-Engine-System-Design.pdf) + +This specification augments existing resources and provides additional value to interested readers in the following ways: + +- It is a single, comprehensive source of truth for the entire payment system. +- While https://xrpl.org/docs provides an excellent reference to different transaction types and ledger entries, it does not go into detailed explanation of transaction validation and application logic. This code contains important invariants and is crucial to document to identify potential bugs and unexpected side-effects. +- While there is a good overview of the Payment Engine, this specification explains detailed logic and gives context to interaction between the Payment Engine and the pathfinding system. +- Pathfinding and path selection are complex topics and this specification provides explanation of both functional requirements and system design. diff --git a/docs/overview/scope.md b/docs/overview/scope.md index 3fa5e1d..233eae6 100644 --- a/docs/overview/scope.md +++ b/docs/overview/scope.md @@ -1,25 +1,25 @@ -# Scope - -The **Payment Engine** is a feature of `xrpld` responsible for executing payment transactions and offer -crossings. Additionally, the Payment Engine serves as a component in pathfinding where it is used to evaluate paths. - -Term `Payment Engine` has been used synonymously with the **Flow** feature, an amendment enabled on the XRP Ledger since 2016. -This specification takes a broader view than Flow alone, as fully understanding the Payment Engine requires discussing -pathfinding, direct XRP payments, MPT payments, and AMMs. - -**Pathfinding** identifies how assets can be transferred between accounts using different types of intermediary steps. -The Payment Engine provides the logic to rate the quality of a path and carry out these cross-currency payments. - -Direct XRP-to-XRP transfers bypass Flow entirely, since they require no pathfinding or order book interaction. -However, these are still relevant to cover, as the code and logic around payment execution often overlap between -XRP-only and cross-currency cases. - -This specification also covers additional features of the payment system. The **AMM** amendment introduced automated market -makers as a source of liquidity transparent to the order book. The **MPT** (Multi-Purpose Transaction) amendment, -enabled as of October 2025, further expands the ways payments can be executed and will be -discussed briefly for completeness. - -This specification aims to explain the broader system surrounding the Payment Engine, including cross-currency payments, -direct XRP payments, offers, AMMs, and MPT payments. This wider view is necessary because, in practice, both the code -and the requirements for these features are often intertwined. - +# Scope + +The **Payment Engine** is a feature of `xrpld` responsible for executing payment transactions and offer +crossings. Additionally, the Payment Engine serves as a component in pathfinding where it is used to evaluate paths. + +Term `Payment Engine` has been used synonymously with the **Flow** feature, an amendment enabled on the XRP Ledger since 2016. +This specification takes a broader view than Flow alone, as fully understanding the Payment Engine requires discussing +pathfinding, direct XRP payments, MPT payments, and AMMs. + +**Pathfinding** identifies how assets can be transferred between accounts using different types of intermediary steps. +The Payment Engine provides the logic to rate the quality of a path and carry out these cross-currency payments. + +Direct XRP-to-XRP transfers bypass Flow entirely, since they require no pathfinding or order book interaction. +However, these are still relevant to cover, as the code and logic around payment execution often overlap between +XRP-only and cross-currency cases. + +This specification also covers additional features of the payment system. The **AMM** amendment introduced automated market +makers as a source of liquidity transparent to the order book. The **MPT** (Multi-Purpose Transaction) amendment, +enabled as of October 2025, further expands the ways payments can be executed and will be +discussed briefly for completeness. + +This specification aims to explain the broader system surrounding the Payment Engine, including cross-currency payments, +direct XRP payments, offers, AMMs, and MPT payments. This wider view is necessary because, in practice, both the code +and the requirements for these features are often intertwined. + diff --git a/docs/path_finding/README.md b/docs/path_finding/README.md index abf7b9c..fa5bfd0 100644 --- a/docs/path_finding/README.md +++ b/docs/path_finding/README.md @@ -1,1667 +1,1667 @@ -# Index - -- [1. Introduction](#1-introduction) - - [1.1. Paths](#11-paths) - - [1.1.1. Example: Issuing IOUs](#111-example-issuing-ious) - - [1.1.2. Example: Same Currency IOU](#112-example-same-currency-iou) - - [1.1.3. Example: Issuing and Redeeming MPTs](#113-example-issuing-and-redeeming-mpts) - - [1.1.4. Example: MPT Holder to Holder](#114-example-mpt-holder-to-holder) - - [1.1.5. Example: Different Currencies](#115-example-different-currencies) - - [1.1.6. Example: Same Currency Code IOU, Different Issuers](#116-example-same-currency-code-iou-different-issuers) - - [1.2. Path Types](#12-path-types) - - [1.3. Path Finding](#13-path-finding) - - [1.4. Algorithm](#14-algorithm) - - [1.4.1. Setup](#141-setup) - - [1.4.2. Path Type Expansion](#142-path-type-expansion) - - [1.4.3. Rank Paths](#143-rank-paths) - - [1.5. Structure](#15-structure) -- [2. Terminology and Concepts](#2-terminology-and-concepts) - - [2.1. Terminology](#21-terminology) - - [2.2. Payment Types](#22-payment-types) - - [2.3. Path Types](#23-path-types) - - [2.3.1. Node Types](#231-node-types) - - [2.4. Default Paths](#24-default-paths) -- [3. Pathfinder](#3-pathfinder) - - [3.1. Pathfinder Class](#31-pathfinder-class) - - [3.2. Path Elements](#32-path-elements) - - [3.3. Path](#33-path) -- [4. Path Discovery](#4-path-discovery) - - [4.1. findPaths Function](#41-findpaths-function) - - [4.2. addPathsForType](#42-addpathsfortype) - - [4.3. addLinks](#43-addlinks) - - [4.4. addLink](#44-addlink) - - [4.5. OrderBookDB](#45-orderbookdb) - - [4.6. AssetCache](#46-assetcache) -- [5. Path Ranking](#5-path-ranking) - - [5.1. computePathRanks](#51-computepathranks) - - [5.2. rankPaths](#52-rankpaths) - - [5.3. getPathLiquidity](#53-getpathliquidity) -- [6. Path Selection](#6-path-selection) -- [7. RPC Requests](#7-rpc-requests) - - [7.1. `ripple_path_find` RPC (Legacy)](#71-ripple_path_find-rpc-legacy) - - [7.2. `path_find` RPC](#72-path_find-rpc) - - [7.3. Source Currency Handling](#73-source-currency-handling) - -# 1. Introduction - -The XRP Ledger is a network where accounts are connected via [trust lines](../trust_lines/README.md), [offers](../offers/README.md), and [MPTs](../mpts/README.md). To send non-[XRP](../glossary.md#xrp) [currencies](../glossary.md#currency) or to perform currency conversions, payments often cannot go directly from source to destination. Instead, they must find routes through: - -- Direct XRP payments, trust lines and MPT payments -- Currency conversions through the decentralized exchange ([CLOBs](../glossary.md#clob) and [AMMs](../amms/README.md)) -- Multi-hop paths combining both - -**Path finding** discovers viable routes and returns them as **paths**. A path describes a potential route for value to flow, such as "Alice -> USD/EUR order book -> Bob" or "Alice -> USD/XRP order book -> XRP/EUR order book -> Bob". Each element in the path (an account or an order book) represents a location where value can move through. - -Path finding takes into account [domain-restricted order books](../permissioned_domains/README.md) when searching for routes. The permissioned DEX allows users to create domain-specific offers that are only accessible to accounts with valid credentials for that domain. When a domain is specified in a path finding request, the algorithm searches only within that domain's offers. - -The [**Flow engine**](../flow/README.md) then takes these paths and converts them into executable operations called **strands**. A strand is a sequence of **steps**, where each step is a concrete action that moves value between path elements. - -Path finding discovers **where** payments can go, while Flow figures out **how** to execute them. - -## 1.1. Paths - -**Paths** are sequences of intermediate steps that describe a route from source to destination. The examples below show common payment scenarios and the paths they require. - -### 1.1.1. Example: Issuing IOUs - -Issuer wants to send USD to Alice. Alice has a [trust line](../trust_lines/README.md) to Issuer for USD. The trust line is a direct connection so the path is: - -- **Path**: Issuer -> Alice - -### 1.1.2. Example: Same Currency IOU - -Alice wants to send USD issued by Issuer to Bob. Both Alice and Bob have a trust line to Issuer for USD. The payment has to go through Issuer, because Alice cannot issue a currency in Issuer's name: - -- **Path**: Alice -> Issuer -> Bob - -### 1.1.3. Example: Issuing and Redeeming MPTs - -Holder Alice holds an [MPT](../mpts/README.md) issued by Issuer. The issuer wants to send (mint) additional MPT to Alice: - -- **Path**: Issuer -> Alice - -If Alice wants to send the MPT back to the issuer (redeeming): - -- **Path**: Alice -> Issuer - -### 1.1.4. Example: MPT Holder to Holder - -Alice wants to send an MPT issued by Issuer to Bob. Both Alice and Bob are holders of the MPT. The payment path goes through the issuer. See [MPT Payment Execution](../payments/README.md#4-payment-execution-paths) for details on how holder-to-holder transfers are processed. - -- **Path**: Alice -> Issuer -> Bob - -### 1.1.5. Example: Different Currencies - -Alice wants to send USD to Bob and have Bob receive an MPT. Alice is not the issuer of USD and Bob is a holder of the MPT issued by MPT Issuer. -Since Alice is paying in a different currency than what Bob will get, the currency has to be exchanged. - -There are multiple ways in which Alice can achieve this transfer. For example, if there is a USD/MPT [order book](../glossary.md#order-book) (a set of offers and AMMs that can exchange one currency for another), it could be used to complete the payment. - -However, USD/MPT order book may not exist, or it may lack liquidity to perform the whole payment. In that case, two order books could be used - for example USD/XRP and then XRP/MPT - if both exist and have sufficient liquidity. - -Two possible routes a payment could take would be: - -- **Incomplete Path 1**: Alice -> [USD/MPT order book] -> Bob -- **Incomplete Path 2**: Alice -> [USD/XRP order book] -> [XRP/MPT order book] -> Bob - -However, these are not complete paths. Each payment that Alice makes in the issuer's USD has to be reflected on the trust line between her and USD Issuer. To consume the offer, she needs to send USD to the USD issuer, who will in turn send USD to the market maker who created the offer that is consumed (or multiple market makers if multiple offers are consumed). -To make a payment, she has to go through the issuer of USD to whom she has a trust line: - -- **Incomplete Path 1**: Alice -> USD Issuer -> [USD/MPT order book] -> Bob -- **Incomplete Path 2**: Alice -> USD Issuer -> [USD/XRP order book] -> [XRP/MPT order book] -> Bob - -Bob is a holder (not the issuer) of the MPT, so he also has to receive the payment through the MPT Issuer: - -- **Complete Path 1**: Alice -> USD Issuer -> [USD/MPT order book] -> MPT Issuer -> Bob -- **Complete Path 2**: Alice -> USD Issuer -> [USD/XRP order book] -> [XRP/MPT order book] -> MPT Issuer -> Bob - -### 1.1.6. Example: Same Currency Code IOU, Different Issuers - -Let's say that Alice wants to send USD to Bob. Alice has a USD trust line to Issuer A. Bob has an USD trust line to Issuer B. Issuer A and Issuer B have no trust lines between them, but there is an Exchanger who has USD trust lines to both Issuer A and Issuer B and NoRipple cleared on their trust lines, so payments can ripple through Exchanger. - -This payment can be completed via: - -- **Path**: Alice -> Issuer A -> Exchanger -> Issuer B -> Bob - -The payment is [**rippling**](../glossary.md#rippling) through Exchanger. Exchanger is taking an exchange risk between Issuer A's USD and Issuer B's USD value. - -## 1.2. Path Types - -The XRP Ledger has countless possible payment routes, but exploring all of them would be computationally infeasible. - -**Path types** constrain the search by defining templates for the structure of a route. Each path type specifies the sequence of hop types (accounts, order books, XRP bridges) that a path should follow, and the pathfinder fills in the template with concrete accounts and order books from the ledger. - -Each type is a sequence of building blocks: - -- **s** (source) - Start at the source account -- **a** (accounts) - Find accounts connected via trust lines or MPT holdings -- **b** (books) - Use an order book to exchange currencies -- **x** (XRP books) - Use an order book that outputs XRP, since XRP frequently serves as a bridge currency between other assets -- **f** (destination book) - Use an order book to get the destination currency -- **d** (destination) - Arrive at the destination account - -For example, the type `"sfd"` means: *"Start at source, find an order book to exchange into the destination currency, then deliver to destination."* - -A more complex type like `"saxfd"` means: *"Start at source, go through an intermediate account, use an order book to exchange to XRP, then use another order book to exchange to the destination currency, and deliver to destination."* - -Path types are predefined for every payment type. For example, XRP->NonXRP has one set of types, while NonXRP->NonXRP has another. See [Section 2.3](#23-path-types) for the full table, node type details, and search configuration. - -## 1.3. Path Finding - -Given a source and a destination, path finding uses path types to create actual paths that will be used to complete a payment. For example, when expanding `"sfd"`, the `"f"` gets translated into concrete order books - path finding will create a separate path for each feasible order book that can convert to the destination currency. - -Each path represents a different way value can flow from Alice's USD to Bob's EUR, with different exchange rates and liquidity characteristics. A single payment may need multiple paths to complete, because not every path will provide full liquidity. - -It is important to note that the path finding code in `xrpld` is not responsible for returning the full path. For example, in [different currencies example](#115-example-different-currencies), the path finding will only return: - -Path 1: - -- USD/EUR order book - -Path 2: - -- USD/XRP order book -- XRP/EUR order book - -It is the responsibility of the Flow engine to do the [path normalization](../flow/README.md#51-path-normalization) that will decide how to connect the source to the first element and how to connect the last element to the destination. - -Additionally, for IOU and MPT payments, path finding searches to the **effective destination** rather than the final destination. The effective destination is the issuer of the destination amount. -For example, if Bob is receiving EUR issued by EUR Issuer, path finding only needs to find paths that reach EUR Issuer. Similarly, if Bob is receiving an MPT, path finding finds paths that reach the MPT Issuer. Flow handles the final hop to Bob. For XRP payments, the effective destination is simply the destination account itself. - -## 1.4. Algorithm - -Path finding is a constrained graph search algorithm that explores the ledger's network to find viable payment routes. We will illustrate the algorithm using an example similar to [section 1.1.5](#115-example-different-currencies): Alice wants to send USD and Bob should receive an MPT. - -### 1.4.1. Setup - -- Alice has USD trust line to USD Issuer -- Bob is a holder of an MPT issued by MPT Issuer -- Available order books: USD/MPT, USD/XRP, XRP/MPT, USD/CAD, CAD/MPT, USD/JPY, JPY/CHF -- For this example, assume we only have the following path types defined for this payment type: `"safd"`, `"sbfd"`, `"saxfd"`, `"sabfd"` (in reality, the algorithm could explore more types based on the search depth and payment type) -- The [**default path**](#24-default-paths) is always tested separately and not included in path type expansion -- This is a non-permissioned payment (no domain specified), so all open order books are available for consideration - -### 1.4.2. Path Type Expansion - -Path type expansion works by incrementally building paths one node at a time. For each node in the path type (like `"s"`, `"a"`, `"f"`, `"d"`), the algorithm: -1. Queries the ledger to find all possible options for that node -2. Creates a new path branch for each option found -3. Adds all branches to a list of incomplete paths -4. Continues to the next node, expanding each path in the list - -When a path reaches the destination with the correct currency, it's marked as complete and added to the complete paths list. - -**Step 1: Determine Payment Type and Select Path Types** - -Payment type is NonXRP->NonXRP (different currencies), and the algorithm selects path types to explore based on the payment type: -- `"safd"`: source -> account -> destination book -> destination -- `"sbfd"`: source -> book -> destination book -> destination -- `"saxfd"`: source -> account -> XRP book -> destination book -> destination -- `"sabfd"`: source -> account -> book -> destination book -> destination - -**Step 2: Expand Path Type `"safd"`** - -Starting from Alice with USD, the algorithm will expand each node in the path type. - -- `"s"`: Start at Alice (USD), creates an empty path: `[]` -- `"sa"`: Query AssetCache for accounts connected via trust lines holding USD, which have enough liquidity and allow rippling. - - Finds: USD Issuer - - Incomplete paths so far: `[USD Issuer]` -- `"saf"`: Query OrderBookDB for any order books from Issuer USD to MPT destination - - Finds: USD/MPT - - MPT is the destination so adds `[USD Issuer, USD/MPT Book]` to complete paths -- `"safd"`: Has no incomplete paths to examine - -**Step 3: Expand Path Type `"sbfd"`** - -Starting from Alice with USD: -- `"s"`: Start at Alice (USD) -- `"sb"`: Query OrderBookDB for any order books that accept USD as input. Because no source issuer was specified, the source asset's issuer defaults to Alice herself[^source-issuer-default]. No order books exist for USD.Alice, so no books are found. - - Terminates (`"sbf"` and `"sbfd"` have no incomplete paths to examine) - -[^source-issuer-default]: Source issuer defaults to source account: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L274-L277) - -**Step 4: Expand Path Type: `"saxfd"`** -- `"s"`: Start at Alice (USD) -- `"sa"`: Query AssetCache for accounts connected via trust lines holding USD, which have enough liquidity and allow rippling. - - Finds: USD Issuer - - Incomplete paths so far: `[USD Issuer]` -- `"sax"`: Query OrderBookDB for any order book that will convert Issuer USD convert to XRP - - Finds: Issuer USD -> XRP - - Incomplete paths so far: `[USD Issuer, USD/XRP Book]` -- `"saxf"`: Query OrderBookDB for any order book that will convert to MPT - - Finds: Issuer XRP -> MPT - - MPT is the destination so adds to complete paths: `[USD Issuer, USD/XRP Book, XRP/MPT book]` -- `"saxfd"`: Has no incomplete paths to examine - -**Step 5: Expand Path Type: `"sabfd"`** -- `"s"`: Start at Alice (USD) -- `"sa"`: Finds that `"sa"` has already created an incomplete path `[USD Issuer]` (in step 4) -- `"sab"`: Query OrderBookDB for any order book that will convert from Issuer USD - - Finds: Issuer USD -> JPY, Issuer USD -> MPT, Issuer USD -> XRP, Issuer USD -> CAD - - Adds `[USD Issuer, USD/MPT Book]` to complete paths, as MPT is the destination - - This step will add both the books it finds, but also the issuer account for IOUs and MPTs if the issuer is not the final destination - - Incomplete paths so far: `[USD Issuer, USD/XRP Book]`, `[USD Issuer, USD/JPY Book, JPY Issuer]`, `[USD Issuer, USD/CAD Book, CAD Issuer]` -- `"sabf"`: Query OrderBookDB for any order book that will convert from previous incomplete path to MPT: - - `[USD Issuer, USD/JPY book, JPY Issuer]` branch: - - Finds JPY/CHF Book. CHF is not the destination asset - - Rejects this path and terminates. - - `[USD Issuer, USD/XRP Book]` branch: - - Finds XRP/MPT Book. While we found this in `"saxfd"` already, we have not seen it in this path type - - MPT is the destination so tries to add `[USD Issuer, USD/XRP Book, XRP/MPT Book]` to complete paths. However, since this path is already in completed paths, it is ignored - - `[USD Issuer, USD/CAD Book, CAD Issuer]` branch: - - Finds CAD/MPT Book. The path ends with `[... USD/CAD Book, CAD Issuer]`, so the redundant `CAD Issuer` account is replaced with `CAD/MPT Book` - - MPT is the destination so adds `[USD Issuer, USD/CAD Book, CAD/MPT Book]` to complete paths -- `"sabfd"`: Has no incomplete paths to examine - -*Complete Path 1:* `[USD Issuer, USD/MPT Book]` -*Complete Path 2:* `[USD Issuer, USD/XRP Book, XRP/MPT book]` -*Complete Path 3:* `[USD Issuer, USD/CAD Book, CAD/MPT Book]` - -### 1.4.3. Rank Paths - -Now that there is a set of feasible paths, the algorithm ranks them. - -The algorithm begins by testing the [default path](#24-default-paths). If the default path returns some liquidity, it will be deducted from the remaining liquidity used to test discovered paths. - -The algorithm then simulates each discovered path to measure its [quality](../flow/README.md#21-quality) and liquidity: -- Simulate Path 1 `[USD Issuer, USD/MPT Book]`: - - Quality: 1.05 (costs 105 USD to get 100 MPT) - - Liquidity: 1000 MPT capacity -- Simulate Path 2 `[USD Issuer, USD/XRP Book, XRP/MPT book]`: - - Quality: 1.04 (costs 104 USD to get 100 MPT) - - Liquidity: 750 MPT capacity -- Simulate Path 3 `[USD Issuer, USD/CAD Book, CAD/MPT Book]`: - - Quality: 1.06 (costs 106 USD to get 100 MPT) - - Liquidity: 500 MPT capacity - -Unless the user is trying to convert the entire possible amount of an asset, when it checks the liquidity of each path, the algorithm checks that each path can deliver at least 1/6th of the total amount[^min-liquidity], to prevent returning paths that can return very small liquidity. The total amount is divided by 6 because maxPaths is 4[^max-paths], and two is added. - -Paths are ranked by quality first (lower cost is better), then by liquidity (higher is better), then by path length (shorter is better)[^rank-sort]. In this example, quality alone determines the order: Path 2 (1.04), Path 1 (1.05), Path 3 (1.06). When the user is converting the entire possible amount of an asset, quality is ignored and paths are ranked by liquidity first. - -[^min-liquidity]: Minimum liquidity calculation: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L182-L186) - -[^max-paths]: Maximum paths constant: [`TransactionSign.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/TransactionSign.cpp#L317-L321) - -[^rank-sort]: Path ranking comparator: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L582-L597) - -## 1.5. Structure - -The algorithm executes four main phases: - -**1. Path Discovery ([Section 4](#4-path-discovery))** - -The [`findPaths`](#41-findpaths-function) function determines the payment type (XRP->NonXRP, NonXRP->XRP, NonXRP->NonXRP, etc.) and selects appropriate [path types](#23-path-types) to explore. Each path type is a template like `"sfd"` or `"saxfd"` that describes a routing strategy. The algorithm expands these templates into concrete paths by recursively querying the ledger through [`addPathsForType`](#42-addpathsfortype), which calls [`addLinks`](#43-addlinks) and [`addLink`](#44-addlink) to build paths step by step. - -As it explores, `addLink` queries [AssetCache](#46-assetcache) to find accounts connected by trust lines and MPTs, and [OrderBookDB](#45-orderbookdb) to find available order books for currency conversion (including domain-restricted order books when a domain is specified). It filters out invalid options (loops, trust lines with insufficient liquidity, `NoRipple` violations, unauthorized MPT holders) and prioritizes promising routes by ranking accounts by their "paths out" score (how many onward connections they have). Complete paths that reach the destination currency at the effective destination account are stored in `mCompletePaths`. - -**2. Path Ranking ([Section 5](#5-path-ranking))** - -The [`computePathRanks`](#51-computepathranks) function evaluates discovered paths by simulating their execution through the Flow engine. It first tests the default path to determine `mRemainingAmount` - the liquidity still needed beyond what the default provides. This ensures paths are evaluated for their incremental value. - -The [`rankPaths`](#52-rankpaths) function then tests each path in `mCompletePaths` by calling [`getPathLiquidity`](#53-getpathliquidity), which uses the Flow engine to simulate payment execution on a sandbox ledger. Paths are scored and sorted by quality (exchange rate), liquidity (capacity), and length (hop count). In normal mode, better quality is prioritized. In convert-all mode (when discovering maximum liquidity), quality is ignored and only liquidity matters. - -**3. Path Selection ([Section 6](#6-path-selection))** - -The [`getBestPaths`](#6-path-selection) function selects the optimal set of paths from the ranked results. It merges rankings from discovered paths and any extra paths provided by the caller, then iteratively selects paths by comparing quality first, then liquidity. It validates issuer constraints (ensuring non-default paths route through the correct issuer for IOUs and MPTs when needed) and applies different selection rules based on remaining slots: filling slots greedily when multiple remain, requiring the last path to cover all remaining liquidity, and optionally saving a "full liquidity path" that can handle the entire payment alone. - -**4. Payment Execution** - -The selected paths are returned to the caller ([RPC handler](#7-rpc-requests) or [Payment transaction](../payments/README.md)) and passed to the [Flow engine](../flow/README.md) for actual payment execution. Flow performs path normalization to add source and destination accounts, then executes the payment by processing each path as a strand of steps. - -```mermaid -flowchart LR - rpc((RPC/Payment)) - pathfinder[Pathfinder] - findPaths[1. Path Discovery
findPaths] - computePathRanks[2. Path Ranking
computePathRanks] - getBestPaths[3. Path Selection
getBestPaths] - flow[4. Payment Execution
Flow] - - rpc --> pathfinder - pathfinder --> findPaths - findPaths --> computePathRanks - computePathRanks --> getBestPaths - getBestPaths --> flow -``` - -**Domain Parameter for Permissioned DEX:** - -Path finding supports an optional `domain` parameter that enables permissioned DEX functionality. When a domain is specified, the pathfinder restricts order book queries to only include offers that belong to that domain's order book. This domain value is: -- Passed into the Pathfinder constructor and stored as `mDomain`[^pathfinder-domain-constructor] -- Forwarded to OrderBookDB queries in `addLink` when discovering available books[^pathfinder-domain-orderbook] (see [Section 4.4](#44-addlink)) -- Passed to RippleCalc and Flow during path ranking and execution[^pathfinder-domain-flow] (see [Section 5.1](#51-computepathranks)) - -[^pathfinder-domain-constructor]: Pathfinder domain parameter storage: [`Pathfinder.cpp:220,230`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L220-L230) -[^pathfinder-domain-orderbook]: OrderBookDB domain filtering flow: `addLink` ([`Pathfinder.cpp:995`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L995)) calls `getPathsOut` ([`Pathfinder.cpp:1145`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1145)), which queries OrderBookDB with domain parameter ([`Pathfinder.cpp:786`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L786)) -[^pathfinder-domain-flow]: Domain passed to Flow: [`Pathfinder.cpp:411`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L411) - -# 2. Terminology and Concepts - -## 2.1. Terminology - -**Path Elements** are the building blocks that describe a location in a path. Each path element can contain: -- An **account ID** (for rippling through trust lines or MPT connections) -- A **currency** (for specifying what currency flows through this point) -- An **issuer ID** (for non-XRP currencies, who issues the currency) -- An **MPTID** (for MPT currencies, identifies the specific MPT) - -A path element can be either: -- An **account element** - represents an account that holds balances (has account ID set) -- An **offer element** - represents an order book for currency exchange (no account ID, just currency/issuer or MPTID) - -**Paths** are sequences of path elements that describe a complete route from source to destination. For example: -- `[Alice] -> [USD IssuerA] -> [Book: USD/IssuerA -> EUR/IssuerB] -> [EUR/IssuerB] -> [Bob]` - -**Steps** are the executable operations created by the Flow engine when it [converts paths into strands](../flow/README.md#52-path-to-strand-conversion). Path finding creates paths and path elements, not steps or strands, but it relies on the Flow engine to rank the paths. - -The relationship is: **Node Types** (search strategy) -> **Path Elements** (route description) -> **Steps** (executable operations) - -**Liquidity** refers to the capacity of a path - how much value it can move in a single payment. Paths with higher liquidity can deliver more to the destination. - -**Effective Destination** is the account where path finding actually searches to, which differs from the final destination for IOU and MPT payments. For XRP payments, effective destination equals the destination account. For IOU and MPT payments, effective destination is the issuer of the destination amount, since paths only need to reach the issuer - Flow handles the final hop to the destination through path normalization. - -**Complete Path** is a path that successfully reaches the effective destination with the correct destination currency. Incomplete paths are discarded during path discovery. - -**Convert-all Mode** is activated when the destination amount equals the maximum possible value for that currency. In this mode, path finding discovers maximum available liquidity rather than targeting a specific amount, and prioritizes liquidity over quality when ranking paths. - -## 2.2. Payment Types - -The pathfinder categorizes each payment request into one of five types: - -| PaymentType | Description | Example | -|-------------|------------------------------|----------------------------------------| -| `PaymentType.XrpToXrp` | XRP to XRP payment | Alice sends XRP to Bob | -| `PaymentType.XrpToNonXrp` | XRP to IOU or MPT payment | Alice sends XRP, Bob receives MPT | -| `PaymentType.NonXrpToXrp` | IOU or MPT to XRP payment | Alice sends USD or MPT, Bob receives XRP | -| `PaymentType.NonXrpToSame` | Same IOU or MPT payment | Alice sends USD, Bob receives USD (same asset) | -| `PaymentType.NonXrpToNonXrp` | Different IOU or MPT payment | Alice sends EUR, Bob receives USD | - -While `PaymentType.XrpToXrp` is defined as a payment type and is initialized with an empty path type list, XRP->XRP payments **never actually invoke the path finding or flow system**. The Payment transactor detects XRP->XRP payments and processes them as direct balance transfers, bypassing both path finding and the Flow engine entirely. - -## 2.3. Path Types - -Each payment type has a predefined table of path types at different search levels (costs). Higher search levels explore more complex paths. - -Example types for `PaymentType.XrpToNonXrp`: - -| Cost | Type | Path Structure | -|------|----------|--------------------------------------------------------------| -| 1 | `sfd` | Source -> Destination Book -> Destination | -| 3 | `sfad` | Source -> Destination Book -> Account -> Destination | -| 5 | `sfaad` | Source -> Destination Book -> Account -> Account -> Destination | -| 6 | `sbfd` | Source -> Book -> Destination Book -> Destination | -| 8 | `sbafd` | Source -> Book -> Account -> Destination Book -> Destination | - -The path finding algorithm searches through these types based on the requested search depth. A search level of 0 finds no paths, while higher levels (4-7 typical, 10 maximum) explore increasingly complex routing options. - -For the complete list of path types for each payment type, see `Pathfinder::initPathTable()`[^init-path-table]. - -[^init-path-table]: Path table initialization: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1365-L1430) - -**Configuration** - -The search depth can be configured in `xrpld.cfg`: - -| Parameter | Description | Default | Recommended | -|-----------|-------------|---------|-------------| -| `path_search` | Default search aggressiveness | 2 | 7 (for advanced path finding) | -| `path_search_fast` | Minimum search aggressiveness | 2 | 2 (for advanced path finding) | -| `path_search_max` | Maximum search aggressiveness | 3 | 10 (for advanced path finding) | -| `path_search_old` | Search level for legacy path finding interfaces | 2 | 7 (for advanced path finding) | - -Higher values can exponentially increase resource usage. Setting `path_search_max` to 0 disables path finding entirely. On a server configured as a validator (one with `[validation_seed]` or `[validator_token]`), `path_search_max` defaults to 0 (path finding disabled) unless explicitly set. - -### 2.3.1. Node Types - -Path types are constructed from a sequence of node types. Each node type tells the path finding algorithm what kind of connection to explore at that step in the path: - -**`NodeType.Source` (code: `s`)** - The source account - -This represents the starting point of the payment. The source is always the first node in any path type. When path finding expands this node, it creates a single empty path representing the starting position at the source account. - -**`NodeType.Accounts` (code: `a`)** - Accounts connected via trust lines or MPTs - -When path finding encounters an `a` node, it expands to neighboring accounts connected to the current position via [trust lines](../trust_lines/README.md) or [MPTs](../mpts/README.md). The actual account selection involves filtering by NoRipple flags, liquidity, and authorization, then ranking candidates by their number of viable outgoing paths. See [Section 4.4](#44-addlink) for details. - -**`NodeType.Books` (code: `b`)** - Order books for currency conversion - -When path finding encounters a `b` node, it queries [OrderBookDB](#45-orderbookdb) for all order books that accept the current currency as input, allowing the path to exchange into a different currency. See [Section 4.4](#44-addlink) for details on book expansion, including how output issuers are handled. - -**`NodeType.XrpBook` (code: `x`)** - Order book to XRP - -A specialized version of `NodeType.Books` that only considers order books that convert the current currency to XRP. XRP often serves as a bridge currency between other currencies, and limiting to XRP books reduces the search space. [OrderBookDB](#45-orderbookdb) maintains a separate `xrpBooks` cache (and `xrpDomainBooks` for permissioned DEX) for faster lookups. - -**`NodeType.DestBook` (code: `f`)** - Order book to destination currency - -This is another specialized version of `NodeType.Books` that only considers order books that output the destination currency. The `f` stands for "final" book. When path finding encounters an `f` node, it only looks for order books that convert the current currency into whatever currency the destination wants to receive. This ensures the path ends with the correct currency. - -For example, if the destination wants EUR, an `f` node will only consider order books like USD/EUR, XRP/EUR, GBP/EUR, etc. - -**`NodeType.Destination` (code: `d`)** - The destination account - -The destination is always the last node in any path type. When path finding encounters a `d` node, it searches for account connections to the effective destination. For IOUs and MPTs, the effective destination is the issuer, not the final recipient. If a preceding `f` step already ended at the issuer, the path is already complete and `d` has nothing to add. When it does fire, `d` adds the issuer as a trust line or MPT hop, completing the path via rippling. - - -## 2.4. Default Paths - -A **default path** is the direct route between source and destination that does not need to be explicitly specified. The default path is: - -- **For same-currency IOU or MPT payments**: Direct transfer between source and destination through the issuer -- **For cross-currency payments**: Uses the order book between the source currency and the destination currency - -Unless the Payment transaction contains `tfNoRippleDirect`, the Flow engine always attempts the default path, even when explicit paths are provided. The default path can fail (e.g., no trust line exists, no order book available), in which case the Flow engine continues with any explicit paths. - -Default path is an empty path that is passed to [Path Normalization](../flow/README.md). - -# 3. Pathfinder - -The Pathfinder class is responsible for orchestrating finding of paths. It creates a Path set - a vector of Paths. Paths consist of Path Elements. -The Pathfinder uses path types to systematically explore the ledger, building STPath objects from STPathElement components and collecting them into an STPathSet. - -```mermaid -classDiagram - class Pathfinder { - +STPathSet mCompletePaths - +AssetCache cache - +findPaths() - +computePathRanks() - +getBestPaths() - } - - class STPathSet { - +vector~STPath~ paths - } - - class STPath { - +vector~STPathElement~ mPath - +hasSeen() - +push_back() - } - - class STPathElement { - +unsigned int mType - +AccountID mAccountID - +PathAsset mAssetID - +AccountID mIssuerID - } - - Pathfinder "1" --> "1" STPathSet : produces - STPathSet "1" *-- "0..*" STPath : contains - STPath "1" *-- "1..*" STPathElement : contains -``` -*Figure: Key components of Pathfinder* - - -## 3.1. Pathfinder Class - -The `Pathfinder` class is the entry point for path finding. It is constructed with: - -> [!IMPORTANT] -> Parameter names and definitions are simplified to provide an overview. They do not map 1:1 to the C++ implementation, but are intended to make the pseudocode in later sections easier to follow. - -| Parameter | Description | Required | -|-----------|--------------------------------------------------------------------|----------| -| `cache` | AssetCache containing ledger state, trust line, and MPT information | ✅ | -| `srcAccount` | Source account ID - the account sending funds | ✅ | -| `dstAccount` | Destination account ID - the account receiving funds | ✅ | -| `srcPathAsset` | Asset the source wants to spend | ✅ | -| `srcIssuer` | Issuer for the source currency | ❌ | -| `dstAmount` | The amount to be delivered to the destination | ✅ | -| `srcAmount` | Maximum amount the source is willing to spend | ❌ | -| `domain` | Domain identifier for permissioned DEX | ❌ | -| `app` | Application reference for accessing ledger state | ✅ | - -The pathfinder maintains these key variables: -- `app` - Application reference which can be used to fetch `OrderBookDB` -- `convert_all_` - Boolean flag indicating "convert all" mode (find maximum liquidity instead of exact amount). Set to true when destination amount equals the maximum possible value for that currency -- `mSrcAccount` - Source account -- `mSrcPathAsset` - Asset the source wants to spend, derived from `srcPathAsset` constructor parameter -- `mDstAccount` - Destination account (the account that will ultimately receive the payment) -- `mEffectiveDst` - The account where paths must end. For XRP destinations, this is `mDstAccount`. For IOU and MPT destinations, this is the issuer of the destination amount. Paths discovered by path finding end at this account, not at `mDstAccount`. The Flow engine later handles the final hop from `mEffectiveDst` to `mDstAccount` through path normalization. -- `mCompletePaths` - Collection of all complete paths found -- `mPathRanks` - Rankings of paths based on quality and liquidity -- `mPaths` - Cache of paths organized by PathType -- `mPathsOutCountMap` - Cache of "paths out" counts for each Issue -- `mPathTable` - Static (file-level) table of [path types](#23-path-types), shared across all Pathfinder instances -- `mSource` - STPathElement representing the starting point for path discovery. Computed in `findPaths`: if the source asset has a non-XRP issuer (`mSrcIssuer`), the element's account and issuer are set to that issuer; otherwise, they are set to `mSrcAccount`. For XRP, the issuer is the zero account. Used by `addLink` as the implicit first element when the current path is empty. -- `mRemainingAmount` - Amount remaining to deliver after accounting for default path contribution -- `mAssetCache` - [AssetCache](#46-assetcache) for querying trust line and MPT information - -## 3.2. Path Elements - -An `STPathElement`[^stpathelement] is the data structure representing a single step in a payment path. It is a class containing: - -- `mType`[^mtype] (unsigned int) - Bitmask indicating which fields are present: - - `0x01` (typeAccount)[^typeaccount] - Has account field - - `0x10` (typeCurrency)[^typecurrency] - Has currency field - - `0x20` (typeIssuer)[^typeissuer] - Has issuer field - - `0x40` (typeMPT)[^typempt] - Has MPT field -- `mAccountID` (AccountID) - The account (if typeAccount bit is set) -- `mAssetID` (PathAsset) - Holds either a Currency or an MPTID -- `mIssuerID` (AccountID) - The issuer (if typeIssuer bit is set) - -[^stpathelement]: STPathElement class definition: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STPathSet.h#L17) -[^mtype]: mType field: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STPathSet.h#L19) -[^typeaccount]: typeAccount constant: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STPathSet.h#L32) -[^typecurrency]: typeCurrency constant: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STPathSet.h#L33) -[^typeissuer]: typeIssuer constant: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STPathSet.h#L34) -[^typempt]: typeMPT constant: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STPathSet.h#L35) - -**Types of path elements:** - -**Account element** (`type = 0x01`): -``` -{type: 0x01, account: aliceAccountID} -``` -Represents an account in a path - either an intermediate account for rippling or the destination account. Created during pathfinding[^account-element-creation] when adding accounts to incomplete paths. - -**Offer/Book element for XRP** (`type = 0x10`): -``` -{type: 0x10, currency: "XRP"} -``` -Represents a conversion to XRP via order book. Has currency only[^xrp-book-element-creation] (0x10). - -**Offer/Book element for IOUs** (`type = 0x30`): -``` -{type: 0x30, currency: "USD", issuer: issuerAccountID} -``` -Represents a currency conversion via order book. Has both currency and issuer[^iou-book-element-creation] (0x10 | 0x20 = 0x30). - -**Offer/Book element for MPTs** (`type = 0x60`): -``` -{type: 0x60, mptid: mptID, issuer: issuerAccountID} -``` -Represents an MPT order book. Has both MPT identifier and issuer (0x40 | 0x20 = 0x60). - -**Account+Asset element** (`type = 0x31` for IOUs, `0x61` for MPTs): -``` -{type: 0x31, account: sourceAccountID, currency: "USD", issuer: issuerAccountID} -``` -Represents the source account holding a specific asset. **Always added as the first element**[^source-element-normalization] during path normalization (in the Flow engine's `toStrand` function). Has account, currency, and issuer (0x01 | 0x10 | 0x20 = 0x31) or account, MPT, and issuer (0x01 | 0x40 | 0x20 = 0x61). -The issuer in the first element is set to source, and the next element connects it to a particular issuer. - -[^account-element-creation]: Account element creation during pathfinding: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1202-L1203) -[^xrp-book-element-creation]: XRP book element creation during pathfinding: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1225-L1226) -[^iou-book-element-creation]: IOU book element creation during pathfinding: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1287-L1291) -[^source-element-normalization]: Source element added during path normalization: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/paths/PaySteps.cpp#L262-L273) - - -## 3.3. Path - -An `STPath` represents a single payment path. It is a vector of [STPathElements](#32-path-elements). - -**Key methods:** - -- `size()` - Returns the number of path elements in the path -- `empty()` - Returns true if path has no elements -- `push_back(element)` - Adds an element to the end of the path -- `back()` - Returns the last element in the path -- `hasSeen(account, asset, issuer)` - Checks if this combination already appears in the path (loop detection). The asset parameter is a PathAsset which can hold either Currency (for IOUs) or MPTID (for MPTs). - -For example, a normalized XRP -> USD path would look like: - -``` -[0] Type=0x31 (Account Currency Issuer ) Account: sourceAccountID Currency: XRP Issuer: rrrrrrrrrrrr -[1] Type=0x1 (Account ) Account: rrrrrrrrrrrr -[2] Type=0x30 (Currency Issuer ) Currency: USD Issuer: issuerAccountID -[3] Type=0x1 (Account ) Account: issuerAccountID -[4] Type=0x1 (Account ) Account: destinationAccountId -``` - -An `STPathSet` is a collection of multiple `STPath` objects, representing alternative paths that can be used simultaneously for a payment. - - -# 4. Path Discovery - -The path discovery process works by expanding **path types** into a **tree of concrete paths**. -The core algorithm takes a type like `"sfad"` and expands it step-by-step: first it builds all paths matching `"s"`, then extends those to match `"sf"`, then `"sfa"`, and finally `"sfad"`. At each step, it queries the ledger (AssetCache for trust lines and MPTs, OrderBookDB for order books) to find what's actually available, creating branches for each viable option while filtering out loops and insufficient liquidity. - -Consider a payment where the source holds USD and wants to deliver EUR.Issuer to the destination. The pathfinder uses type `"sfad"` (source -> destination book -> account -> destination): - -The type `"sfad"` expands as follows: -1. **s** (source) - Start with source account -2. **f** (destination book) - Query OrderBookDB for order books from USD that output the destination currency (EUR). Finds USD->EUR.Issuer book -3. **a** (accounts) - Query AssetCache for accounts holding EUR.Issuer. Finds Alice (has liquidity), Bob (zero balance, filtered out), and Carol (has liquidity) -4. **d** (destination) - Try to complete each path by reaching the effective destination (Issuer): - - Alice has a trust line to Issuer -> path complete (blue) - - Carol has no trust line to Issuer -> filtered out (red) - -```mermaid -graph TD - Start["source"] --> Book1["source
USD->EUR.Issuer book"] - - Book1 --> Acct1["source
USD->EUR.Issuer book
Alice"] - Book1 --> Filtered1["source
USD->EUR.Issuer book
Bob (no liquidity)"] - Book1 --> Acct3["source
USD->EUR.Issuer book
Carol"] - - Acct1 --> Complete1["source
USD->EUR.Issuer book
Alice
Issuer"] - - Acct3 --> Filtered2["source
USD->EUR.Issuer book
Carol (no path to Issuer)"] - - style Complete1 fill:blue - style Filtered1 fill:red - style Filtered2 fill:red -``` - -The algorithm uses: -- **Filtering** - Rejects loops, insufficient liquidity, NoRipple violations before creating branches -- **Ranking** - Prioritizes destination connections and high "paths out" scores -- **Limits** - Maximum 1000 complete paths; when expanding accounts, up to 50 candidates from the source account, up to 10 candidates from any other account - - -**Implementation:** - -1. **`findPaths`** - Entry point that: - - Determines payment type (XRP->NonXRP, NonXRP->XRP, NonXRP->NonXRP, etc.) and iterates through all path types for that payment type - - For each type, calls `addPathsForType` to expand it - -2. **`addPathsForType`** - Recursive function that: - - Takes a type like `"sfad"` (source -> book -> account -> destination) and builds it incrementally by first building `"s"`, then `"sf"`, then `"sfa"`, then `"sfad"` - - For each step, calls `addLinks` to expand the last node type - - Returns a list of paths (complete and incomplete) for that type, which is cached in `mPaths` for reuse - -3. **`addLinks`** - Simple wrapper that: - - Takes a set of incomplete paths and calls `addLink` once for each path - -4. **`addLink`** - Core expansion logic that: - - Takes one incomplete path and flags indicating what type of expansion to perform (add accounts via `a` node type, or add books via `b`/`f` node types) - - Based on the flags: - - If expanding accounts (`a`): queries **`AssetCache`** to find connected accounts via trust lines or MPTs - - If expanding books (`b`/`f`): queries **`OrderBookDB`** to find available order books (from offers and AMMs) - - For each viable option found, creates a new branch by appending path elements to `incompletePaths` - - **When a path reaches the destination with the correct currency, adds it directly to `mCompletePaths`** - - Filters out loops, NoRipple violations, and insufficient liquidity - -The entire flow can be interrupted by using `continueCallback`. This callback allows the caller to interrupt path finding by returning `false`. It is checked at multiple points during path discovery. - -- **[`path_find` subscriptions](#72-path_find-rpc)** provides a callback that checks if the WebSocket client is still connected -- **[`ripple_path_find`](#71-ripple_path_find-rpc-legacy)** and **[transaction signing with `build_path`](../payments/README.md#421-path-finding)** do NOT provide a callback - - -## 4.1. findPaths Function - -The `findPaths` function[^find-paths] is the main path discovery engine. It searches for paths from `mSrcAccount` to `mEffectiveDst`. - -[^find-paths]: Main path discovery function: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L241-L380) - -- **For XRP payments**: `mEffectiveDst` equals `mDstAccount`, so paths go directly to the destination -- **For IOU and MPT payments**: `mEffectiveDst` is the issuer of the destination amount. Path finding only needs to reach the issuer - the Flow engine will handle the final hop from issuer to destination through path normalization. - -**Parameters:** - -| Parameter | Description | Required | -|-----------|-------------|----------| -| `searchLevel` | Maximum cost/depth to search | ✅ | -| `continueCallback` | Optional callback to check if search should continue | ❌ | - -The function validates the payment request, determines [payment type](#22-payment-types) from source and destination currencies and iterates through path types for that payment type and calls addPathsForType, making sure not to exceed the `searchLevel`. - -All found paths are stored in PathFinder object state, so this function only loops over types without much responsibility itself. - -### 4.1.1. findPaths Pseudo-Code - -```python -def findPaths(searchLevel, continueCallback) -> bool: - # Validate payment request - if mDstAmount == 0: - # Destination amount is 0 - return False - - if mSrcAccount == mDstAccount and mDstAccount == mEffectiveDst and mSrcPathAsset == mDstAmount.asset(): - # No need to send to same account with same currency - return False - - if mSrcAccount == mEffectiveDst and mSrcPathAsset == mDstAmount.asset(): - # Default path might work, but any additional path would loop back to source - # (since paths must end at mEffectiveDst which is the source) - return True - - if not mLedger: - # No ledger to search - return False - - if not accountExists(mSrcAccount): - # Source account has to exist. Destination does not if we are sending XRP to it. - return False - - if mEffectiveDst != mDstAccount and not accountExists(mEffectiveDst): - # Issuer account has to exist - return False - - if not accountExists(mDstAccount) and (not isXRP(mDstAmount) or mDstAmount < getAccountReserve()): - # New account must be funded with XRP meeting minimum reserve - return False - - # Build the source element used by addLink when the path is empty. - # If the source asset has a non-XRP issuer, start from the issuer's account; - # otherwise start from the source account. - if mSrcIssuer and not isXRP(mSrcPathAsset) and not isXRP(mSrcIssuer): - account = mSrcIssuer - else: - account = mSrcAccount - issuer = xrpAccount() if isXRP(mSrcPathAsset) else account - mSource = STPathElement(account, mSrcPathAsset, issuer) - - # Determine payment type (one of: PaymentType.XrpToXrp, PaymentType.XrpToNonXrp, etc.) - paymentType = determinePaymentType(mSrcPathAsset, mDstAmount.asset()) - - # Search for paths using types for this payment type - for costedPath in mPathTable[paymentType]: - if continueCallback.shouldBreak(): - return False - if costedPath.searchLevel <= searchLevel: - # costedPath.type is a PathType (sequence of node types like "sfd", "sfad", etc.) - addPathsForType(costedPath.type, continueCallback) - if len(mCompletePaths) > PATHFINDER_MAX_COMPLETE_PATHS: # 1000 - break - - return True -``` - -## 4.2. addPathsForType - -`addPathsForType`[^add-paths-for-type] takes a `PathType` like `"sfad"` and converts it into concrete paths by building incrementally, one node type at a time. - -[^add-paths-for-type]: Incremental path building function: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L872-L943) - -**Parameters:** - -| Parameter | Description | Required | -|-----------|-------------|----------| -| `pathType` | Sequence of node types (e.g., `"sfd"`, `"saxfd"`) | ✅ | -| `continueCallback` | Optional callback to check if search should continue | ❌ | - -The function works recursively. When asked to build `"sfad"`, it first checks if `"sfad"` was already built and cached in `mPaths` - if so, it returns immediately. Otherwise, it recursively strips off the last character until it hits the empty string: - -- `"sfad"` -> `"sfa"` -> `"sf"` -> `"s"` -> `""` (base case returns empty list) - -Now the recursion unwinds and each level builds paths by extending what its parent returned. Each node type maps to a specific expansion strategy: - -- **`"s"`**: Returns a single empty path (the starting point at source) -- **`"sf"`**: Takes that empty path and calls `addLinks` with flags to query OrderBookDB for order books from the source currency that output the destination currency. If 5 such books exist, returns 5 paths. -- **`"sfa"`**: Takes those 5 paths and calls `addLinks` with flags to query AssetCache for accounts holding the book's output currency. If each book leads to 3 accounts, returns 15 paths (5 * 3). -- **`"sfad"`**: Takes those 15 paths and calls `addLinks` with flags to find the destination account. Only paths reaching the destination with the correct currency are not discarded. - -The flags passed to `addLinks` tell `addLink` what to query (accounts via AssetCache or books via OrderBookDB) and what filters to apply (only XRP books, only destination currency, only destination account, etc.). Each node type (`s`, `a`, `b`, `x`, `f`, `d`) uses different flags to control this behavior. - -Finally, the result is stored in `mPaths[pathType]` and returned. - -### 4.2.1. addPathsForType Pseudo-Code - -```python -def addPathsForType(pathType, continueCallback) -> list[STPath]: - # pathType is a sequence of node types like "sfd" or "saxfd" - - # Check cache - if pathType in mPaths: - return mPaths[pathType] - - # Base case - empty path type - if len(pathType) == 0: - mPaths[pathType] = [] - return mPaths[pathType] - - if continueCallback.shouldBreak(): - return [] - - # Recursive case - build parent paths first - parentPathType = pathType[:-1] # Remove last node type (e.g., "sfd" -> "sf") - parentPaths = addPathsForType(parentPathType, continueCallback) - - # Add final node type to parent paths - nodeType = pathType[-1] # Get last node type (e.g., 'd' from "sfd") - pathsOut = [] - - if nodeType == NodeType.Source: - pathsOut = [empty_path] - elif nodeType == NodeType.Accounts: - addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_ACCOUNTS, continueCallback=continueCallback) - elif nodeType == NodeType.Books: - addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_BOOKS, continueCallback=continueCallback) - elif nodeType == NodeType.XrpBook: - addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_BOOKS | afOB_XRP, continueCallback=continueCallback) - elif nodeType == NodeType.DestBook: - addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_BOOKS | afOB_LAST, continueCallback=continueCallback) - elif nodeType == NodeType.Destination: - addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_ACCOUNTS | afAC_LAST, continueCallback=continueCallback) - - mPaths[pathType] = pathsOut - return pathsOut -``` - -## 4.3. addLinks - -`addLinks`[^add-links] is a simple wrapper that calls `addLink` for each path in a set. - -[^add-links]: Wrapper function for batch path extension: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L857-L870) - -**Parameters:** - -| Parameter | Description | Required | -|-----------|-------------|----------| -| `currentPaths` | Set of incomplete paths to extend | ✅ | -| `incompletePaths` | Output list where new paths are added | ✅ | -| `addFlags` | Flags controlling expansion behavior (e.g., `afADD_ACCOUNTS`, `afADD_BOOKS`) | ✅ | -| `continueCallback` | Optional callback to check if search should continue | ❌ | - -### 4.3.1. addLinks Pseudo-Code - -```python -def addLinks(currentPaths, incompletePaths, addFlags, continueCallback): - for path in currentPaths: - if continueCallback.shouldBreak(): - return - addLink(currentPath=path, incompletePaths=incompletePaths, addFlags=addFlags, continueCallback=continueCallback) -``` - -## 4.4. addLink - -`addLink`[^add-link] is where the actual path expansion happens - it's the function that queries the ledger and creates new path branches. - -[^add-link]: Core path expansion function: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L972-L1301) - -**Parameters:** - -| Parameter | Description | Required | -|-----------|-------------|----------| -| `currentPath` | Single incomplete path to extend | ✅ | -| `incompletePaths` | Output list where new paths are added | ✅ | -| `addFlags` | Flags controlling expansion behavior (e.g., `afADD_ACCOUNTS`, `afADD_BOOKS`, `afOB_XRP`, `afOB_LAST`, `afAC_LAST`) | ✅ | -| `continueCallback` | Optional callback to check if search should continue | ❌ | - -Every partial path has an **endpoint**, derived from its last [path element](#32-path-elements) (or from the source if the path is empty)[^addlink-endpoint]. The endpoint provides an account, an asset (currency or MPTID), and an issuer, which `addLink` uses to determine where to search for the next hop. - -[^addlink-endpoint]: Endpoint extraction from partial path: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1001-L1005) - -The function examines the path's current endpoint (which account and currency/asset) and uses `addFlags` to determine which ledger data source to query and what filters to apply. It produces two kinds of output: paths that reach the effective destination with the correct asset are added to `mCompletePaths`, while paths that still need further extension are added to `incompletePaths`. `addPathsForType` feeds incomplete paths back into `addLink` for the next expansion round, and complete paths proceed to [path ranking](#5-path-ranking), where they are simulated through the Flow engine to measure quality and liquidity. - -**Flag meanings:** - -- **`afADD_ACCOUNTS`** - Queries AssetCache for trust lines and MPT holdings to find connected accounts -- **`afADD_BOOKS`** - Queries OrderBookDB to find order books where the current asset is the input (TakerPays) -- **`afOB_XRP`** (modifier for `afADD_BOOKS`) - Restricts books to only those outputting XRP -- **`afOB_LAST`** (modifier for `afADD_BOOKS`) - Restricts books to only those outputting the destination asset -- **`afAC_LAST`** (modifier for `afADD_ACCOUNTS`) - Restricts accounts to only the effective destination - -The function's behavior varies significantly between account expansion and book expansion: - -**Account expansion** (`afADD_ACCOUNTS`): - -When the path's current endpoint is on XRP and the destination amount is XRP and the current path is non-empty, it adds the path to complete paths.[^xrp-endpoint-check] Empty paths are not added because they would represent XRP->XRP payments and those do not require pathfinding. - -[^xrp-endpoint-check]: XRP endpoint completion check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1019-L1027) - -For non-XRP endpoints, the function queries for trust lines or MPTs connected to the account at the path's current endpoint. - -For IOU currencies, `addLink` calls `AssetCache::getRippleLines`[^get-ripple-lines-call] to fetch trust lines for the endpoint account. The returned trust lines cover all currencies; `addLink` later filters them to match the current currency via `correctAsset`[^currency-match]. These trust lines are candidates for the next hop in the payment path, subject to the checks described below. - -The query is pre-filtered using `LineDirection`, which hints to `getRippleLines` which trust lines are needed. Rippling through an account is blocked when that account has NoRipple set on **both** its incoming and outgoing trust lines. `addLink` uses this rule at two levels: - -- `addLink` calls `isNoRippleOut(currentPath)`[^is-no-ripple-out] to check whether the trust line between the previous account in the path and the endpoint account has NoRipple set on the endpoint account's side. -- If `isNoRippleOut` returns true, `getRippleLines` is called with `LineDirection::incoming`[^noripple-direction], which requests only trust lines where the endpoint account does **not** have NoRipple set[^get-ripple-lines-direction]. However, this is a best effort optimization. The same account may be reached via different partial paths during pathfinding, and if its full set of trust lines was already cached from an earlier call, `AssetCache` returns those instead of fetching the filtered subset, to avoid duplicate storage[^asset-cache-superset]. -- If `isNoRippleOut` returns false, `getRippleLines` is called with `LineDirection::outgoing`, which returns all trust lines. - -Regardless of which set `getRippleLines` returned, `addLink` performs a per-candidate check that provides the actual gating: if `isNoRippleOut` was true and the candidate trust line also has NoRipple set on the endpoint account's side (`asset.getNoRipple()`), the candidate is skipped[^noripple-candidate-check]. - -For MPTs, the function queries for MPTs associated with the current account. The peer account is always the issuer, extracted from the MPTID[^mpt-peer-issuer], so MPT account expansion only navigates from holder to issuer. The reverse direction (issuer to holder) is never needed because pathfinding only needs to reach `mEffectiveDst` (the issuer for non-XRP destinations); the flow engine handles the final hop from issuer to destination holder through path normalization[^mpt-no-reverse]. - -Each asset connection undergoes these checks in order: - -1. **Currency/asset match**[^currency-match] - The asset must match the path's current currency code or MPTID -2. **Destination account bypass**[^dest-bypass] - When the destination issuer differs from the destination account, skips the destination account (pathfinding only needs to reach the issuer, not the final recipient) -3. **Destination-only filtering**[^dest-only] - When `afAC_LAST` is set, rejects all accounts except the effective destination (the issuer) -4. **Loop detection**[^loop-detection] - Rejects accounts already visited using `hasSeen(account, asset, issuer)` -5. **Liquidity and NoRipple check**[^liquidity-check]: - - For trust lines: Rejects if (balance <= 0 AND (no peer limit OR peer limit exhausted OR unauthorized when `lsfRequireAuth` is set)) OR (both the previous link and current link have NoRipple set) - - For MPTs: Rejects if zero balance OR maxed out OR not authorized -6. **Source loop prevention**[^source-loop] - Rejects accounts that would loop back to the source - -[^currency-match]: Currency/asset match check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1082-L1092) -[^dest-bypass]: Destination account bypass check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1069-L1073) -[^dest-only]: Destination-only filtering check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1077-L1080) -[^loop-detection]: Loop detection using hasSeen: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1110) -[^liquidity-check]: Liquidity and NoRipple check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1093-L1108) -[^source-loop]: Source loop prevention check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1138-L1141) -[^get-ripple-lines-call]: getRippleLines call in addLink: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1161-L1163) -[^is-no-ripple-out]: isNoRippleOut checks whether the last account in the path has NoRipple set on its outgoing link: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L958-L979) -[^noripple-direction]: Trust line fetch direction based on NoRipple: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1159-L1166) -[^get-ripple-lines-direction]: LineDirection::incoming excludes trust lines where the account has NoRipple set: [`TrustLine.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/TrustLine.cpp#L61) -[^noripple-candidate-check]: Per-candidate NoRipple check in addLink: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1101) -[^asset-cache-superset]: AssetCache returns the outgoing superset when incoming is requested but outgoing is already cached: [`AssetCache.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/AssetCache.cpp#L78-L87) -[^getpathsout]: getPathsOut computes the paths out score for an account: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L748-L835) -[^getpathsout-auth]: getPathsOut checks lsfRequireAuth on the candidate account: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L771-L775) -[^getpathsout-booksize]: Score starts with order book size: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L786) -[^getpathsout-destination-bonus]: Destination bonus of +10000: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L800-L804) -[^getpathsout-frozen]: Global freeze check in getPathsOut: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L776-L784) -[^getpathsout-iou-loop]: IOU trust line scoring loop: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L788-L830) -[^getpathsout-noripple]: getPathsOut skips trust lines where the peer has NoRipple set: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L805-L806) -[^getpathsout-freeze]: getPathsOut skips trust lines where the peer has frozen the line: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L807-L808) -[^getpathsout-mpt-loop]: MPT scoring loop: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L813-L831) -[^getpathsout-mpt-match]: MPT ID match check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L818-L820) -[^getpathsout-mpt-balance]: MPT zero balance or maxed out check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L818-L820) -[^getpathsout-mpt-auth]: MPT authorization check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L818-L820) -[^getpathsout-mpt-destination]: MPT destination bonus of +10000: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L821-L825) -[^getpathsout-mpt-frozen]: MPT frozen check (redundant with outer freeze check): [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L826-L827) -[^getpathsout-mpt-count]: MPT count increment: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L827-L828) -[^compare-account-candidate]: compareAccountCandidate sorts by priority descending, then account ID descending: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L108-L124) -[^dest-complete-path]: Destination account with matching asset completes the path: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1121-L1130) -[^dest-high-priority]: Destination account with non-matching asset receives high priority directly: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1132-L1136) -[^getpathsout-zero-filter]: Candidates with score 0 are not added: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1152) -[^candidate-extend]: Selected candidates are extended into incomplete paths: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1196-L1206) -[^mpt-peer-issuer]: MPT peer is always the issuer, extracted from the MPTID: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1057-L1059), [`MPTIssue.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/MPTIssue.h#L84-L93) -[^mpt-no-reverse]: Pathfinding targets `mEffectiveDst` (the issuer for non-XRP destinations), so it never needs to navigate from issuer to holder. The flow engine handles the final hop to the destination holder via path normalization: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/paths/PaySteps.cpp#L261-L320) - -Accounts that pass all filters become candidates. When the candidate is the destination account and the current asset matches the destination asset, the path is complete[^dest-complete-path]. When the candidate is the destination account but the asset does not match, it receives a score of 10000 directly, bypassing `getPathsOut()`[^dest-high-priority]. All other candidates are scored by `getPathsOut()`[^getpathsout], which counts the number of viable onward connections from that account in the current asset. - -Candidates that score 0 are excluded entirely[^getpathsout-zero-filter], because an account with no viable onward connections (e.g., globally frozen, or all trust lines are frozen, unauthorized, or have NoRipple set) would be a dead end in the path. Non-zero scores determine priority during sorting. In the rules below, **Skipped** means the connection does not count toward the score (+0): - -1. If the account is globally frozen for the asset, no scoring happens and the count stays 0[^getpathsout-frozen]. -2. Otherwise, the function checks whether the account has `lsfRequireAuth` set[^getpathsout-auth]. -3. The score starts with the number of order books available for the asset[^getpathsout-booksize]. -4. For IOU currencies, the function iterates over trust lines from `getRippleLines(account, direction)` and for each trust line[^getpathsout-iou-loop]: - - **Skipped** if the trust line currency does not match the current asset - - **Skipped** if the balance is zero or negative and the peer has no available credit, or if `lsfRequireAuth` is set (step 2) and the trust line is not authorized - - **+10000** if the current asset matches the destination asset and the peer is the destination account[^getpathsout-destination-bonus] - - **Skipped** if the peer has NoRipple set on its side of the trust line[^getpathsout-noripple] - - **Skipped** if the peer has frozen the trust line[^getpathsout-freeze] - - **+1** otherwise -5. For MPTs, the function iterates over MPTs from `getMPTs(account)` and for each MPT[^getpathsout-mpt-loop]: - - **Skipped** if the MPT ID does not match the current asset[^getpathsout-mpt-match] - - **Skipped** if zero balance or maxed out[^getpathsout-mpt-balance] - - **Skipped** if authorization is required (step 2)[^getpathsout-mpt-auth] - - **+10000** if the current asset matches the destination asset and the peer is the destination account[^getpathsout-mpt-destination] - - **Skipped** if frozen (redundant with the outer freeze check in step 1, but present in the code)[^getpathsout-mpt-frozen] - - **+1** otherwise[^getpathsout-mpt-count] - -`addLink` then sorts all candidates by score descending, with account ID as a tiebreaker[^compare-account-candidate], and selects the top 50 if expanding from the source account, or top 10 otherwise. For each selected candidate, `addLink` extends the current path by appending the candidate as an account element and adds the extended path to `incompletePaths`[^candidate-extend]. These incomplete paths are fed back into `addLink` by `addPathsForType` for further expansion in subsequent rounds. - -**Book expansion** (`afADD_BOOKS`): - -When `afOB_XRP` is set, the function checks whether an order book exists from the current asset to XRP (using domain filtering if configured). If found, it adds an XRP book element to the path. - -Without `afOB_XRP`, the function queries all order books where the current asset is the input currency (TakerPays). Each book undergoes these filters: - -1. **Output loop detection** - Rejects books whose output asset/issuer was already seen using `hasSeen(xrpAccount(), book.out, book.out.getIssuer())` -2. **Origin issuer check** - Rejects books that would create a loop back to the source issuer -3. **Destination asset filter** - When `afOB_LAST` is set, rejects books not outputting the destination asset - -For books outputting XRP, the function adds an XRP book element. If the destination amount is XRP, this completes the path; otherwise the path is added to `incompletePaths` for further expansion. - -For books outputting non-XRP assets, the function performs an additional check using `hasSeen(book.out.getIssuer(), book.out, book.out.getIssuer())` to prevent issuer loops. It then adds the book element, with an optimization: if the path already has a book -> account -> book pattern, it replaces the redundant intermediate account with the new book element. - -After adding the book element, the function determines whether the path is complete. If the destination requires reaching a specific issuer (non-XRP destination), the function checks whether the book's output issuer matches. When the issuer matches the destination account but differs from the effective destination, the path is rejected (this indicates an issuer bypass violation). When the issuer matches the effective destination and the asset matches, the path is complete. Otherwise, the function appends the issuer's account element. - -### 4.4.1. addLink pseudoCode - -```python -def addLink(currentPath, incompletePaths, addFlags, continueCallback): - pathEnd = currentPath.back() if currentPath else mSource - endPathAsset = pathEnd.getPathAsset() - endAccount = pathEnd.getAccountID() - isOnXRP = isXRP(endPathAsset) - hasEffectiveDst = mEffectiveDst != mDstAccount - destOnly = afAC_LAST in addFlags - - if afADD_ACCOUNTS in addFlags: - if isOnXRP: - if mDstAmount.isXRP() and not currentPath.empty(): - mCompletePaths.add(currentPath) # Complete XRP->XRP path - elif endAccount.exists(): - # Check if the trust line between the previous account and the endpoint account - # has NoRipple set on the endpoint account's side - noRippleOut = isNoRippleOut(currentPath) - direction = LineDirection.incoming if noRippleOut else LineDirection.outgoing - - # Get trust lines or MPTs from current account, based on asset type. - # For IOUs, getRippleLines returns trust lines filtered by direction: - # incoming = only lines where the account does not have NoRipple set (best effort, see AssetCache) - # outgoing = all trust lines - # For MPTs, getMPTs returns all MPT holdings for the account. - if isIOU(endPathAsset): - assets = mAssetCache.getRippleLines(endAccount, direction) - else: - assets = mAssetCache.getMPTs(endAccount) - candidates = [] - - for asset in assets: - if continueCallback.shouldBreak(): - return - - # Get peer account (for trust lines) or issuer account (for MPTs) - peerAccount = asset.getAccountIDPeer() if isTrustLine(asset) else asset.getIssuer() - - # Skip if issuer bypass - if hasEffectiveDst and peerAccount == mDstAccount: - continue - - # Check if this is the destination - isDestination = peerAccount == mEffectiveDst - - # Destination-only filter - if destOnly and not isDestination: - continue - - # Skip if asset does not match the path's current currency/MPTID - if not correctAsset(asset, endPathAsset): - continue - - # Skip if creates loop - if currentPath.hasSeen(peerAccount, endPathAsset, peerAccount): - continue - - # Skip if insufficient liquidity or NoRipple violation - # For trust lines: (balance > 0 OR (available credit AND authorized)) - # AND NOT (noRippleOut AND asset.getNoRipple()) - # For MPTs: balance > 0 AND not maxed out AND authorized - if not hasLiquidity(asset): - continue - - # Handle destination account - if isDestination: - if endPathAsset == mDstAmount.asset(): - if not currentPath.empty(): - mCompletePaths.add(currentPath) # Complete path - elif not destOnly: - candidates.add({priority: HIGH_PRIORITY, account: peerAccount}) - # Skip if going back to source - elif peerAccount == mSrcAccount: - continue - else: - # Rank by paths out - pathsOut = getPathsOut(endPathAsset, peerAccount) - if pathsOut > 0: - candidates.add({priority: pathsOut, account: peerAccount}) - - # Sort and select top candidates - candidates.sort(by: priority descending, then account ID descending) - maxCandidates = 10 if endAccount != mSrcAccount else 50 - - for candidate in candidates[:maxCandidates]: - if continueCallback.shouldBreak(): - return - newPath = currentPath + [accountElement(candidate.account)] - incompletePaths.add(newPath) - - if afADD_BOOKS in addFlags: - if afOB_XRP in addFlags: - # Only add book to XRP (includes domain filtering if mDomain is set) - if not isOnXRP and app.orderBookDB.isBookToXRP(endAsset, mDomain): - incompletePaths.add(currentPath.append(xrpBookElement)) - else: - # Add all viable order books (includes domain filtering if mDomain is set) - books = app.orderBookDB.getBooksByTakerPays(endAsset, mDomain) - - for book in books: - if currentPath.hasSeen(xrpAccount(), book.out, book.out.getIssuer()): - continue - if issueMatchesOrigin(book.out): - continue - # afOB_LAST keeps only books whose output token matches the destination. - # equalTokens compares currency/MPTID and ignores the issuer. - if afOB_LAST in addFlags and not equalTokens(book.out, mDstAmount.asset()): - continue - - newPath = currentPath.append(bookElement(book)) - - if isXRP(book.out): - if mDstAmount.isXRP(): - mCompletePaths.add(newPath) # Complete path - else: - incompletePaths.add(newPath) - elif not currentPath.hasSeen(book.out.getIssuer(), book.out, book.out.getIssuer()): - if hasEffectiveDst and book.out.getIssuer() == mDstAccount and equalTokens(book.out, mDstAmount.asset()): - continue # Skipped required issuer - elif book.out.getIssuer() == mEffectiveDst and book.out.asset == mDstAmount.asset(): - mCompletePaths.add(newPath) # Complete path - else: - # If the path already ends in an account, the issuer-bearing element - # replaces that trailing account rather than being appended after it. - incompletePaths.add(newPath.append(issuerAccount(book.out))) -``` - -## 4.5. OrderBookDB - -`OrderBookDB` serves as an **in-memory index** that catalogs all available trading pairs (order books) on the XRP Ledger. This index enables path finding to rapidly discover which currency conversions are available without scanning the entire ledger for each query. - -OrderBookDB maintains four separate indexes for both offer-based order books and AMM pools: - -- **`allBooks`**: Maps each asset to all assets it can be traded for (includes both offers and AMMs in open order books) -- **`xrpBooks`**: Set of all assets that have a direct trading pair with XRP (subset of `allBooks` for fast XRP bridge lookups) -- **`domainBooks`**: Maps (asset, domainID) pairs to tradeable assets (permissioned order books with domain restrictions, offers only) -- **`xrpDomainBooks`**: Set of (asset, domainID) pairs that have direct XRP trading pairs in permissioned books (offers only) - -### 4.5.1. OrderBookDB Construction - -On startup, `OrderBookDB.update()` ([OrderBookDBImpl.cpp:91-219](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/app/ledger/OrderBookDBImpl.cpp#L91-L219)) scans the entire ledger looking for two types of entries: - -**1. Order book directories (`ltDIR_NODE` with `sfExchangeRate`):** - -Order book directories are created when offers are placed via `OfferCreate` transaction. Each directory represents a specific quality level (exchange rate) for a trading pair. - -When OrderBookDB finds an order book directory (identified by `ltDIR_NODE` type with `sfExchangeRate` field at root), it extracts the trading pair: - -- **TakerPays asset**: Retrieved from `sfTakerPaysCurrency` + `sfTakerPaysIssuer` fields (for IOUs and XRPs) or `sfTakerPaysMPT` field (for MPTs) -- **TakerGets asset**: Retrieved from `sfTakerGetsCurrency` + `sfTakerGetsIssuer` fields (for IOUs and XRPs) or `sfTakerGetsMPT` field (for MPTs) - -The book is registered based on whether it has domain restrictions: - -- **Without domain** (`sfDomainID` not present): - - Registered in `allBooks` - - If TakerGets is XRP: also registered in `xrpBooks` -- **With domain** (`sfDomainID` present): - - Registered in `domainBooks` (indexed by asset + domainID) - - If TakerGets is XRP: also registered in `xrpDomainBooks` - -**2. AMM ledger entries (`ltAMM`):** - -AMM instances are created by `AMMCreate` transaction and provide liquidity without traditional order book directories. Each AMM holds two assets and can facilitate trades in both directions. - -When OrderBookDB finds an AMM (identified by `ltAMM` type), it extracts the two pool assets from `sfAsset` and `sfAsset2` fields. Both trading directions are registered: - -- `asset1 -> asset2` is registered in `allBooks` -- `asset2 -> asset1` is registered in `allBooks` -- If either asset is XRP, the other asset is also registered in `xrpBooks` - -Unlike offer-based order books, AMMs are discovered directly from their `ltAMM` ledger entries without needing order book directory entries (`ltDIR_NODE`). Both offer-based and AMM-based liquidity are indexed together, allowing path finding to treat them uniformly when searching for currency conversion options. - -## 4.6. AssetCache - -`AssetCache` is an **in-memory cache** that provides fast access to trust line and MPT information during path finding. Each AssetCache is tied to a specific ledger view. For `path_find` subscriptions, a single AssetCache is shared across all path finding operations during batch processing (when a ledger changes and there are multiple `path_find` connections open), then deallocated when the batch completes. For one-shot requests like `ripple_path_find`, a new AssetCache is created for each request. - -Assets (trust lines and MPTs) are fetched and cached on-demand for specific accounts as path finding explores the network. - -The AssetCache provides two query methods: -- `getRippleLines(accountID, direction)`[^get-ripple-lines-impl]: Returns trust lines for an account. The `direction` parameter controls filtering: - - `LineDirection::outgoing`: returns all trust lines for the account. - - `LineDirection::incoming`: returns only trust lines where the account does **not** have NoRipple set on its side[^get-trust-line-items-filter]. - - To avoid storing two copies per account, the cache keeps at most one set[^asset-cache-superset]: - - If the full (`outgoing`) set is already cached when `incoming` is requested, the full set is returned. `addLink` relies on its per-candidate NoRipple check to filter out the extra trust lines. - - If the `incoming` subset is cached when `outgoing` is requested, the subset is discarded and the full set is rebuilt[^asset-cache-rebuild]. -- `getMPTs(accountID)`: Returns MPTs held by an account - -[^get-ripple-lines-impl]: AssetCache::getRippleLines: [`AssetCache.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/AssetCache.cpp#L38-L106) -[^get-trust-line-items-filter]: getTrustLineItems filters by direction, excluding trust lines with NoRipple when incoming: [`TrustLine.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/TrustLine.cpp#L61) -[^asset-cache-rebuild]: AssetCache discards incoming subset when outgoing is requested: [`AssetCache.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/AssetCache.cpp#L66-L76) - -# 5. Path Ranking - -After path discovery completes, the pathfinder must evaluate which paths are worth using. Not all discovered paths have sufficient liquidity or good exchange rates - some may have already been consumed by the default path, while others may be too inefficient to be useful. - -## 5.1. computePathRanks - -The `computePathRanks` function evaluates path quality and liquidity by simulating payment execution through the Flow engine. - -**Parameters:** - -| Parameter | Description | Required | -|--------------------|-------------------------------------------------------|----------| -| `maxPaths` | Maximum number of paths to rank and return | ✅ | -| `continueCallback` | Optional callback to check if ranking should continue | ❌ | - -The function performs two key steps: - -**1. Account for the default path** - -The default path is the direct route between source and destination that Flow always attempts first (unless the `tfNoRippleDirect` flag is set). Before ranking discovered paths, path finding must determine how much liquidity the default path provides. - -To measure this, `computePathRanks` calls `RippleCalc.rippleCalculate()` with an empty path set and partial payment enabled[^default-path-partial], which in turn calls Flow and tests only the default path. Partial payment is enabled so the default path can deliver whatever liquidity it has, even if it cannot cover the full amount. RippleCalc simulates payment execution and returns: -- `actualAmountIn` - How much was consumed from the source -- `actualAmountOut` - How much was delivered to the destination -- Result code indicating success or failure - -If the default path succeeds, its delivery is subtracted from `mRemainingAmount`[^remaining-amount-init] to calculate the additional liquidity still needed beyond what the default path provides. - -[^default-path-partial]: Default path tested with partial payment enabled: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L462) -[^remaining-amount-init]: `mRemainingAmount` initialized via `convertAmount`, which returns the largest possible amount in convert-all mode or the destination amount otherwise: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L454) (`convertAmount` defined in [`PathfinderUtils.h`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/PathfinderUtils.h#L19-L26)) - -By accounting for the default path first, path finding ensures that discovered paths are evaluated for their **incremental value** - what they contribute beyond the baseline liquidity that Flow will attempt anyway. - -**2. Rank all discovered paths** - -With `mRemainingAmount` calculated, `computePathRanks` calls `rankPaths` to evaluate all paths in `mCompletePaths`. Each path is tested by simulating its execution through Flow, and successful paths are scored based on quality (exchange rate), liquidity (capacity), and length (number of hops). These rankings determine which paths `getBestPaths` will ultimately select for the payment. - -### 5.1.1. computePathRanks Pseudo-Code - -```python -def computePathRanks(maxPaths: int, continueCallback): - # convertAmount returns the largest possible amount if convert_all_ is true (to find max liquidity) - # otherwise returns mDstAmount unchanged - mRemainingAmount = convertAmount(mDstAmount, convert_all_) - - # The default path is the direct path that always exists (source -> destination) - # We test it first to see how much it can deliver, then rank additional paths - # based on what they add beyond the default - sandbox = PaymentSandbox(mLedger) - inputs = Input(partialPaymentAllowed=True) - rc = RippleCalc.rippleCalculate( - view=sandbox, - maxAmountIn=mSrcAmount, - deliver=mRemainingAmount, - account=mDstAccount, - issuer=mSrcAccount, - paths=[], # Empty path set = test default path only - domain=mDomain, - inputs=inputs - ) - - if rc.success(): - mRemainingAmount -= rc.actualAmountOut - - # Rank all found paths - rankPaths(maxPaths, mCompletePaths, mPathRanks, continueCallback) -``` - -### 5.1.2. rippleCalculate Pseudo-Code - -`RippleCalc.rippleCalculate()` is a wrapper function that calls the [Flow engine](../flow/README.md) to simulate payment execution. - -**Parameters:** - -| Parameter | Description | Required | -|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------| -| `view` | PaymentSandbox view of the ledger for simulation | ✅ | -| `maxAmountIn` | Maximum amount willing to spend from source (SendMax) | ✅ | -| `deliver` | Amount to deliver to destination | ✅ | -| `account` | Destination account ID | ✅ | -| `issuer` | Source account ID | ✅ | -| `paths` | Set of paths to test | ✅ | -| `domain` | Optional domain ID | ❌ | -| `inputs` | Optional Input struct containing flags: `defaultPathsAllowed` (whether to test default path), `partialPaymentAllowed` (whether partial delivery is acceptable), `limitQuality` (whether to enforce quality limit) | ❌ | - -```python -def rippleCalculate(view, maxAmountIn, deliver, account, issuer, paths, domain, inputs=None): - # Create sandbox for simulation - sandbox = PaymentSandbox(view) - - # Extract parameters from inputs or use defaults - defaultPaths = inputs.defaultPathsAllowed if inputs else True - partialPayment = inputs.partialPaymentAllowed if inputs else False - - # Calculate quality limit if requested - qualityLimit = None - if inputs and inputs.limitQuality and maxAmountIn > 0: - qualityLimit = Quality(maxAmountIn / deliver) - - # Determine sendMax (if different from source account's native currency) - sendMax = maxAmountIn if (maxAmountIn >= 0 or - maxAmountIn.currency != deliver.currency or - maxAmountIn.issuer != issuer) else None - - # Call Flow engine to execute payment simulation - flowResult = flow( - sandbox, - deliver, - issuer, - account, - paths, - defaultPaths=defaultPaths, - partialPayment=partialPayment, - limitQuality=qualityLimit, - sendMax=sendMax, - domain=domain - ) - - # Commit the simulated consumption back to the caller's view, so that sequential - # rippleCalculate calls (e.g. the two calls in getPathLiquidity) measure liquidity - # incrementally on the depleted state rather than double-counting it. - sandbox.apply(view) - - return { - result: flowResult.result, - actualAmountIn: flowResult.actualAmountIn, - actualAmountOut: flowResult.actualAmountOut - } -``` - -## 5.2. rankPaths - -The `rankPaths` function evaluates each discovered path by testing its liquidity and quality, then sorts them to identify the best paths for the payment. - -**Parameters:** - -| Parameter | Description | Required | -|--------------------|----------------------------------------------------------------|----------| -| `maxPaths` | Maximum number of paths to rank | ✅ | -| `paths` | Set of complete paths to evaluate (typically `mCompletePaths`) | ✅ | -| `rankedPaths` | Output vector where ranked paths are stored | ✅ | -| `continueCallback` | Optional callback to check if ranking should continue | ❌ | - -The function first calculates a minimum liquidity threshold that each path must meet to be worth including. This threshold serves as a quality filter, preventing the pathfinder from wasting computational resources ranking paths that contribute only negligible amounts to the payment. - -When not in convert-all mode, this threshold is `dstAmount / (maxPaths + 2)`, where `maxPaths` is hardcoded to **4** in the `xrpld` implementation. This ensures each path can deliver at least a meaningful fraction of the destination amount. In convert-all mode, the threshold is set to the largest possible amount to find maximum available liquidity. - -Convert-all mode is triggered when the destination amount equals the maximum possible value for that currency (checked via `convertAllCheck(mDstAmount)`). This mode is used when discovering maximum available liquidity rather than targeting a specific amount - for example, when a user wants to convert their entire balance of one currency to another. In convert-all mode, path finding prioritizes liquidity over quality, finding paths that can move the most value regardless of exchange rates. - -For each path in the input set, `rankPaths` calls `getPathLiquidity` to simulate its execution and measure how much it can deliver. Paths that fail or cannot meet the minimum threshold are discarded. Successful paths are recorded as `PathRank` entries containing their quality (exchange rate), liquidity (capacity), length (hop count), and original index. - -Finally, the function sorts all ranked paths using multiple criteria in order of importance: quality (better exchange rates first, unless in convert-all mode), liquidity (higher capacity first), length (shorter paths first), and index (as a tie breaker). This sorted ranking determines which paths `getBestPaths` will ultimately select for the payment. - -### 5.2.1. rankPaths Pseudo-Code - -```python -def rankPaths(maxPaths, paths, rankedPaths, continueCallback): - rankedPaths.clear() - - if convert_all_: - minDstAmount = largestAmount(dstAmount) - else: - minDstAmount = dstAmount / (maxPaths + 2) - - for i, path in enumerate(paths): - if continueCallback.shouldBreak(): - return - - ter, liquidity, quality = getPathLiquidity(path, minDstAmount) - - if ter == tesSUCCESS: - rankedPaths.add({ - quality: quality, - length: len(path), - liquidity: liquidity, - index: i - }) - - # Sort by quality, liquidity, length - rankedPaths.sort(key=lambda rank: ( - rank.quality if not convert_all_ else 0, # Quality first (unless convert_all_) - -rank.liquidity, # Higher liquidity better (negative for desc sort) - rank.length, # Shorter better - -rank.index # Tie breaker - )) -``` - -## 5.3. getPathLiquidity - -The `getPathLiquidity` function determines how much liquidity a single path can provide by simulating its execution through the Flow engine. - -The [**Flow engine**](../flow/README.md) takes paths and converts them into executable operations called **strands**. A strand is a sequence of **steps**, where each step is a concrete action that moves value between path elements. Path finding uses Flow to simulate execution and measure how much liquidity each path can actually deliver. - -**Parameters:** - -| Parameter | Description | Required | -|----------------|--------------------------------------------------------------|----------| -| `path` | The path to test for liquidity | ✅ | -| `minDstAmount` | Minimum amount the path must deliver to be considered viable | ✅ | - -The function calls `RippleCalc.rippleCalculate()` to simulate payment execution along the path, with default paths explicitly disabled so only the specific path's liquidity is measured[^getpathliq-no-default]. RippleCalc uses the Flow engine to execute a payment simulation on a sandbox ledger (a copy of the ledger that can be modified without affecting the real ledger state), returning how much was consumed from the source (`actualAmountIn`), how much was delivered to the destination (`actualAmountOut`), and whether the payment succeeded. The first call tests whether the path can deliver at least `minDstAmount`. In convert-all mode, partial payment is allowed to find the maximum available liquidity. In normal mode, the path must deliver exactly the minimum amount or it's rejected. - -[^getpathliq-no-default]: Default paths disabled in getPathLiquidity: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L394) - -If the first call succeeds and we are not in convert-all mode, the function makes a second call to probe for additional liquidity beyond the minimum. This second call attempts to deliver `(dstAmount - amountOut)` with partial payment allowed, discovering how much more the path can provide beyond the minimum threshold. - -The function returns the total liquidity the path can deliver and its initial quality (exchange rate), calculated as `actualAmountIn / actualAmountOut` from the first call. Paths that fail to meet the minimum threshold return an error code instead. - -### 5.3.1. getPathLiquidity Pseudo-Code - -```python -def getPathLiquidity(path, minDstAmount): - pathSet = [path] - sandbox = PaymentSandbox(ledger) - - # Test minimum liquidity (default paths disabled, test only this path) - inputs = Input(defaultPathsAllowed=False, partialPaymentAllowed=convert_all_) - rc = RippleCalc.rippleCalculate( - view=sandbox, - maxAmountIn=srcAmount, - deliver=minDstAmount, - account=dstAccount, - issuer=srcAccount, - paths=pathSet, - domain=domain, - inputs=inputs - ) - - if rc.result != tesSUCCESS: - return rc.result - - quality = rc.actualAmountIn / rc.actualAmountOut - amountOut = rc.actualAmountOut - - if not convert_all_: - # Test remaining liquidity - inputs2 = Input(defaultPathsAllowed=False, partialPaymentAllowed=True) - rc = RippleCalc.rippleCalculate( - view=sandbox, - maxAmountIn=srcAmount, - deliver=dstAmount - amountOut, - account=dstAccount, - issuer=srcAccount, - paths=pathSet, - domain=domain, - inputs=inputs2 - ) - - if rc.result == tesSUCCESS: - amountOut += rc.actualAmountOut - - return (tesSUCCESS, amountOut, quality) -``` - -# 6. Path Selection - -After discovering and ranking paths, `getBestPaths` selects the optimal set of paths to use for the payment. - -In the [RPC layer](#7-rpc-requests), path finding may be called multiple times for the same payment as the ledger state changes (e.g., in `path_find` subscriptions that continuously update). When path finding runs again, previously discovered paths are passed in as `extraPaths` so they can be merged with newly discovered paths, ensuring the best overall set is selected. - -`getBestPaths` takes: - -| Parameter | Description | Required | -|---------------------|---------------------------------------------------------------|----------| -| `maxPaths` | Maximum number of paths to return | ✅ | -| `fullLiquidityPath` | Output parameter for a path that can handle full amount alone | ✅ | -| `extraPaths` | Paths from previous path finding runs (empty for first run) | ✅ | -| `srcIssuer` | Source issuer for validation | ✅ | -| `continueCallback` | Optional callback to check if search should continue | ❌ | - -The function works by merging two sets of ranked paths: -1. **`mPathRanks`** - Paths discovered in the current path finding run (already ranked by `computePathRanks`) -2. **`extraPathRanks`** - Paths from `extraPaths`, which are ranked by calling `rankPaths` at the start of this function - -At each iteration, the function selects the best path from either set (prioritizing better quality, then better liquidity). If both quality and liquidity are identical, both paths are advanced to handle potential duplicates. Selected paths go through issuer constraint validation and slot management rules before being added to the result. - -**Issuer Constraint Validation:** - -During path discovery, the Pathfinder searches for paths of a specific currency (like USD) without constraining which issuer. This allows it to discover all possible USD paths efficiently. However, in the [RPC layer](#7-rpc-requests), path finding is called separately for each source asset the sender holds (USD from IssuerA, USD from IssuerB, etc.). When `getBestPaths` is called for a specific issuer's currency, it must filter the discovered paths to only include those that route through that specific issuer. - -For example, when searching for paths using USD from IssuerA, the Pathfinder discovers all USD paths. But only paths that explicitly route through IssuerA should be returned - otherwise Alice might spend USD from a different issuer she doesn't hold, or the payment might fail. - -To enforce this, `getBestPaths` validates paths from the discovered set (not extra paths, which are assumed already validated): if the source currency issuer is not the source account itself, discovered paths must start with the issuer account element. Paths that don't start with the issuer are skipped - this includes default paths and paths from simpler path types like `"sfd"` that go directly to books without routing through an account first. When a path does start with the issuer, that initial issuer element is removed before being added to the result, since the Flow engine will add it back during path normalization. - -**Path Slot Management:** - -A "path slot" refers to one position in the result set - `getBestPaths` returns up to `maxPaths` paths, so there are `maxPaths` slots available to fill. - -The function applies different selection rules depending on how many path slots remain. If more than one slot is available (`pathsLeft > 1`), it adds the path to the result, subtracts its liquidity from the remaining amount needed, and continues. If only one slot remains (`pathsLeft == 1`), it only adds the path if its liquidity can cover the entire remaining amount - this ensures the last path is useful. If no slots remain (`pathsLeft == 0`) but a path can handle the full `mDstAmount` by itself, it's saved as `fullLiquidityPath` for potential use as a single-path alternative. - -## 6.1. getBestPaths Pseudo-Code - -```python -def getBestPaths(maxPaths, fullLiquidityPath, extraPaths, srcIssuer, continueCallback): - # Rank extra paths - extraPathRanks = [] - rankPaths(maxPaths, extraPaths, extraPathRanks, continueCallback) - - bestPaths = [] - remaining = mRemainingAmount - issuerIsSender = isXRP(srcAsset) or (srcIssuer == srcAccount) - - i = 0 # index into mPathRanks - j = 0 # index into extraPathRanks - - # Merge and select best paths - while i < len(mPathRanks) or j < len(extraPathRanks): - # Reset per-iteration flags (the C++ declares these inside the loop body) - usePath = False - useExtra = False - startsWithIssuer = False - - # Determine which path to use next - if i >= len(mPathRanks): - useExtra = True - elif j >= len(extraPathRanks): - usePath = True - elif extraPathRanks[j].quality < mPathRanks[i].quality: - useExtra = True - elif extraPathRanks[j].quality > mPathRanks[i].quality: - usePath = True - elif extraPathRanks[j].liquidity > mPathRanks[i].liquidity: - useExtra = True - elif extraPathRanks[j].liquidity < mPathRanks[i].liquidity: - usePath = True - else: - usePath = True - useExtra = True # Both might be same path - - rank = mPathRanks[i] if usePath else extraPathRanks[j] - path = mCompletePaths[rank.index] if usePath else extraPaths[rank.index] - - if useExtra: - j += 1 - if usePath: - i += 1 - - pathsLeft = maxPaths - len(bestPaths) - - if pathsLeft == 0 and not fullLiquidityPath.empty(): - break - - # Validate issuer constraint - if not issuerIsSender and usePath: - if isDefaultPath(path) or path[0].getAccountID() != srcIssuer: - continue # Skip paths that don't start with issuer - startsWithIssuer = True - - # Apply selection rules - if pathsLeft > 1 or (pathsLeft > 0 and rank.liquidity >= remaining): - # Add to best paths - pathsLeft -= 1 - remaining -= rank.liquidity - bestPaths.add(removeIssuer(path) if startsWithIssuer else path) - - elif pathsLeft == 0 and rank.liquidity >= dstAmount and fullLiquidityPath.empty(): - # Found extra path that can handle full amount - fullLiquidityPath = (removeIssuer(path) if startsWithIssuer else path) - - return bestPaths -``` - -# 7. RPC Requests - -Both `path_find` and `ripple_path_find` RPCs support an optional `domain` parameter (256-bit hex string) for permissioned DEX functionality. When specified, path finding restricts order book queries to only include offers within the specified domain. - -## 7.1. `ripple_path_find` RPC (Legacy) - -The `ripple_path_find` RPC command is the legacy interface for one-shot path finding requests. It is **deprecated** but still supported. - -**The request flow works as follows:** - -1. Client sends a `ripple_path_find` request with source, destination, and amount -2. Server creates a path finding job and enqueues it -3. The RPC handler uses a **coroutine** that yields while waiting for path finding to complete -4. When path finding finishes, the coroutine resumes and returns the result - -**continueCallback usage:** - -`ripple_path_find` does NOT provide a `continueCallback` when calling `Pathfinder::findPaths()`. It runs the path finding synchronously without interruption checking. This is acceptable because: -- It's a one-shot request (not streaming updates) -- The coroutine mechanism already handles shutdown gracefully -- The search completes relatively quickly with typical search levels - -## 7.2. `path_find` RPC - -The `path_find` RPC command is the modern interface for path finding with three subcommands: - -- `path_find create` - Creates a path finding subscription -- `path_find status` - Gets current status of an active subscription -- `path_find close` - Closes an active subscription - -**The subscription flow works as follows:** - -1. Client creates a subscription with `path_find create` -2. Server creates a `PathRequest` associated with the WebSocket connection -3. The `PathRequest` continuously updates as ledgers close, sending updates to the client -4. Client receives streaming path updates until they close the subscription or disconnect - -**continueCallback usage:** - -`path_find` subscriptions do provide a `continueCallback` to `Pathfinder::findPaths()`: - -```cpp -auto continueCallback = [&getSubscriber, &request]() { - return (bool)getSubscriber(request); -}; -``` - -This callback: -- Returns `true` if the subscriber (WebSocket client) is still connected -- Returns `false` if the client has disconnected -- Allows path finding to abort immediately if the client is no longer listening - -This is critical for subscriptions because: -- path finding can take significant time at high search levels -- Multiple subscriptions may be active simultaneously -- No point computing paths if the client disconnected - -## 7.3. Source Currency Handling - -Both `path_find` and `ripple_path_find` RPCs support the `source_currencies` parameter, which controls which currencies the pathfinder considers as potential sources for funding the payment. - -The `source_currencies` handling happens in the `PathRequest` layer (RPC handler), **not** in the core Pathfinder algorithm. The PathRequest creates one Pathfinder instance per source currency, reusing it across different issuers of that currency: - -```mermaid -flowchart TD - RPC["path_find / ripple_path_find RPC"] - PR["PathRequest
(processes source_currencies parameter)"] - PF1["Pathfinder #1
(source currency: USD)"] - PF2["Pathfinder #2
(source currency: EUR)"] - PF3["Pathfinder #3
(source currency: XRP)"] - EXEC1["findPaths() -> computePathRanks() -> getBestPaths()"] - EXEC2["findPaths() -> computePathRanks() -> getBestPaths()"] - EXEC3["findPaths() -> computePathRanks() -> getBestPaths()"] - - RPC --> PR - PR -->|creates| PF1 - PR -->|creates| PF2 - PR -->|creates| PF3 - PF1 --> EXEC1 - PF2 --> EXEC2 - PF3 --> EXEC3 -``` - -**When `source_currencies` is specified:** - -The client provides an array of currency/issuer pairs (up to 18 currencies): - -```json -{ - "source_currencies": [ - {"currency": "USD", "issuer": "rIssuer1..."}, - {"currency": "EUR", "issuer": "rIssuer2..."}, - {"currency": "XRP"}, - {"mpt_issuance_id": "00000001B2..."} - ] -} -``` - -For each source asset: -1. PathRequest looks up or creates a Pathfinder for that source currency (the cache is keyed by currency, so the Pathfinder is reused across issuers of the same currency) -2. Path discovery (`findPaths`/`computePathRanks`) runs once per currency; path selection (`getBestPaths`) then runs per source asset, filtering the discovered paths to that asset's issuer -3. Results are collected in a hash map keyed by the issuer-qualified asset: `mContext[issue] = pathSet` - -PathRequest does not create a separate Pathfinder for each issuer of an IOU; it reuses one Pathfinder per currency and runs a separate `getBestPaths` call per issuer to filter the shared discovered paths to that issuer. - -**When `source_currencies` is NOT specified:** - -PathRequest auto-discovers source currencies in this order: - -1. **If `send_max` is provided**: Use only the `send_max` currency/issuer -2. **Otherwise, scan the source account** by calling `accountSourceAssets()`: - - Always includes XRP - - Scans all outgoing trust lines and MPTs from the source account - - Includes a currency if either: - - Account has positive balance (has asset to send), OR - - Peer extends credit AND there's available credit remaining (can issue more) - - Limited to 88 currencies maximum (`max_auto_src_cur`, limited at RPC layer) - - Excludes currencies matching the destination currency (when source == destination account) +# Index + +- [1. Introduction](#1-introduction) + - [1.1. Paths](#11-paths) + - [1.1.1. Example: Issuing IOUs](#111-example-issuing-ious) + - [1.1.2. Example: Same Currency IOU](#112-example-same-currency-iou) + - [1.1.3. Example: Issuing and Redeeming MPTs](#113-example-issuing-and-redeeming-mpts) + - [1.1.4. Example: MPT Holder to Holder](#114-example-mpt-holder-to-holder) + - [1.1.5. Example: Different Currencies](#115-example-different-currencies) + - [1.1.6. Example: Same Currency Code IOU, Different Issuers](#116-example-same-currency-code-iou-different-issuers) + - [1.2. Path Types](#12-path-types) + - [1.3. Path Finding](#13-path-finding) + - [1.4. Algorithm](#14-algorithm) + - [1.4.1. Setup](#141-setup) + - [1.4.2. Path Type Expansion](#142-path-type-expansion) + - [1.4.3. Rank Paths](#143-rank-paths) + - [1.5. Structure](#15-structure) +- [2. Terminology and Concepts](#2-terminology-and-concepts) + - [2.1. Terminology](#21-terminology) + - [2.2. Payment Types](#22-payment-types) + - [2.3. Path Types](#23-path-types) + - [2.3.1. Node Types](#231-node-types) + - [2.4. Default Paths](#24-default-paths) +- [3. Pathfinder](#3-pathfinder) + - [3.1. Pathfinder Class](#31-pathfinder-class) + - [3.2. Path Elements](#32-path-elements) + - [3.3. Path](#33-path) +- [4. Path Discovery](#4-path-discovery) + - [4.1. findPaths Function](#41-findpaths-function) + - [4.2. addPathsForType](#42-addpathsfortype) + - [4.3. addLinks](#43-addlinks) + - [4.4. addLink](#44-addlink) + - [4.5. OrderBookDB](#45-orderbookdb) + - [4.6. AssetCache](#46-assetcache) +- [5. Path Ranking](#5-path-ranking) + - [5.1. computePathRanks](#51-computepathranks) + - [5.2. rankPaths](#52-rankpaths) + - [5.3. getPathLiquidity](#53-getpathliquidity) +- [6. Path Selection](#6-path-selection) +- [7. RPC Requests](#7-rpc-requests) + - [7.1. `ripple_path_find` RPC (Legacy)](#71-ripple_path_find-rpc-legacy) + - [7.2. `path_find` RPC](#72-path_find-rpc) + - [7.3. Source Currency Handling](#73-source-currency-handling) + +# 1. Introduction + +The XRP Ledger is a network where accounts are connected via [trust lines](../trust_lines/README.md), [offers](../offers/README.md), and [MPTs](../mpts/README.md). To send non-[XRP](../glossary.md#xrp) [currencies](../glossary.md#currency) or to perform currency conversions, payments often cannot go directly from source to destination. Instead, they must find routes through: + +- Direct XRP payments, trust lines and MPT payments +- Currency conversions through the decentralized exchange ([CLOBs](../glossary.md#clob) and [AMMs](../amms/README.md)) +- Multi-hop paths combining both + +**Path finding** discovers viable routes and returns them as **paths**. A path describes a potential route for value to flow, such as "Alice -> USD/EUR order book -> Bob" or "Alice -> USD/XRP order book -> XRP/EUR order book -> Bob". Each element in the path (an account or an order book) represents a location where value can move through. + +Path finding takes into account [domain-restricted order books](../permissioned_domains/README.md) when searching for routes. The permissioned DEX allows users to create domain-specific offers that are only accessible to accounts with valid credentials for that domain. When a domain is specified in a path finding request, the algorithm searches only within that domain's offers. + +The [**Flow engine**](../flow/README.md) then takes these paths and converts them into executable operations called **strands**. A strand is a sequence of **steps**, where each step is a concrete action that moves value between path elements. + +Path finding discovers **where** payments can go, while Flow figures out **how** to execute them. + +## 1.1. Paths + +**Paths** are sequences of intermediate steps that describe a route from source to destination. The examples below show common payment scenarios and the paths they require. + +### 1.1.1. Example: Issuing IOUs + +Issuer wants to send USD to Alice. Alice has a [trust line](../trust_lines/README.md) to Issuer for USD. The trust line is a direct connection so the path is: + +- **Path**: Issuer -> Alice + +### 1.1.2. Example: Same Currency IOU + +Alice wants to send USD issued by Issuer to Bob. Both Alice and Bob have a trust line to Issuer for USD. The payment has to go through Issuer, because Alice cannot issue a currency in Issuer's name: + +- **Path**: Alice -> Issuer -> Bob + +### 1.1.3. Example: Issuing and Redeeming MPTs + +Holder Alice holds an [MPT](../mpts/README.md) issued by Issuer. The issuer wants to send (mint) additional MPT to Alice: + +- **Path**: Issuer -> Alice + +If Alice wants to send the MPT back to the issuer (redeeming): + +- **Path**: Alice -> Issuer + +### 1.1.4. Example: MPT Holder to Holder + +Alice wants to send an MPT issued by Issuer to Bob. Both Alice and Bob are holders of the MPT. The payment path goes through the issuer. See [MPT Payment Execution](../payments/README.md#4-payment-execution-paths) for details on how holder-to-holder transfers are processed. + +- **Path**: Alice -> Issuer -> Bob + +### 1.1.5. Example: Different Currencies + +Alice wants to send USD to Bob and have Bob receive an MPT. Alice is not the issuer of USD and Bob is a holder of the MPT issued by MPT Issuer. +Since Alice is paying in a different currency than what Bob will get, the currency has to be exchanged. + +There are multiple ways in which Alice can achieve this transfer. For example, if there is a USD/MPT [order book](../glossary.md#order-book) (a set of offers and AMMs that can exchange one currency for another), it could be used to complete the payment. + +However, USD/MPT order book may not exist, or it may lack liquidity to perform the whole payment. In that case, two order books could be used - for example USD/XRP and then XRP/MPT - if both exist and have sufficient liquidity. + +Two possible routes a payment could take would be: + +- **Incomplete Path 1**: Alice -> [USD/MPT order book] -> Bob +- **Incomplete Path 2**: Alice -> [USD/XRP order book] -> [XRP/MPT order book] -> Bob + +However, these are not complete paths. Each payment that Alice makes in the issuer's USD has to be reflected on the trust line between her and USD Issuer. To consume the offer, she needs to send USD to the USD issuer, who will in turn send USD to the market maker who created the offer that is consumed (or multiple market makers if multiple offers are consumed). +To make a payment, she has to go through the issuer of USD to whom she has a trust line: + +- **Incomplete Path 1**: Alice -> USD Issuer -> [USD/MPT order book] -> Bob +- **Incomplete Path 2**: Alice -> USD Issuer -> [USD/XRP order book] -> [XRP/MPT order book] -> Bob + +Bob is a holder (not the issuer) of the MPT, so he also has to receive the payment through the MPT Issuer: + +- **Complete Path 1**: Alice -> USD Issuer -> [USD/MPT order book] -> MPT Issuer -> Bob +- **Complete Path 2**: Alice -> USD Issuer -> [USD/XRP order book] -> [XRP/MPT order book] -> MPT Issuer -> Bob + +### 1.1.6. Example: Same Currency Code IOU, Different Issuers + +Let's say that Alice wants to send USD to Bob. Alice has a USD trust line to Issuer A. Bob has an USD trust line to Issuer B. Issuer A and Issuer B have no trust lines between them, but there is an Exchanger who has USD trust lines to both Issuer A and Issuer B and NoRipple cleared on their trust lines, so payments can ripple through Exchanger. + +This payment can be completed via: + +- **Path**: Alice -> Issuer A -> Exchanger -> Issuer B -> Bob + +The payment is [**rippling**](../glossary.md#rippling) through Exchanger. Exchanger is taking an exchange risk between Issuer A's USD and Issuer B's USD value. + +## 1.2. Path Types + +The XRP Ledger has countless possible payment routes, but exploring all of them would be computationally infeasible. + +**Path types** constrain the search by defining templates for the structure of a route. Each path type specifies the sequence of hop types (accounts, order books, XRP bridges) that a path should follow, and the pathfinder fills in the template with concrete accounts and order books from the ledger. + +Each type is a sequence of building blocks: + +- **s** (source) - Start at the source account +- **a** (accounts) - Find accounts connected via trust lines or MPT holdings +- **b** (books) - Use an order book to exchange currencies +- **x** (XRP books) - Use an order book that outputs XRP, since XRP frequently serves as a bridge currency between other assets +- **f** (destination book) - Use an order book to get the destination currency +- **d** (destination) - Arrive at the destination account + +For example, the type `"sfd"` means: *"Start at source, find an order book to exchange into the destination currency, then deliver to destination."* + +A more complex type like `"saxfd"` means: *"Start at source, go through an intermediate account, use an order book to exchange to XRP, then use another order book to exchange to the destination currency, and deliver to destination."* + +Path types are predefined for every payment type. For example, XRP->NonXRP has one set of types, while NonXRP->NonXRP has another. See [Section 2.3](#23-path-types) for the full table, node type details, and search configuration. + +## 1.3. Path Finding + +Given a source and a destination, path finding uses path types to create actual paths that will be used to complete a payment. For example, when expanding `"sfd"`, the `"f"` gets translated into concrete order books - path finding will create a separate path for each feasible order book that can convert to the destination currency. + +Each path represents a different way value can flow from Alice's USD to Bob's EUR, with different exchange rates and liquidity characteristics. A single payment may need multiple paths to complete, because not every path will provide full liquidity. + +It is important to note that the path finding code in `xrpld` is not responsible for returning the full path. For example, in [different currencies example](#115-example-different-currencies), the path finding will only return: + +Path 1: + +- USD/EUR order book + +Path 2: + +- USD/XRP order book +- XRP/EUR order book + +It is the responsibility of the Flow engine to do the [path normalization](../flow/README.md#51-path-normalization) that will decide how to connect the source to the first element and how to connect the last element to the destination. + +Additionally, for IOU and MPT payments, path finding searches to the **effective destination** rather than the final destination. The effective destination is the issuer of the destination amount. +For example, if Bob is receiving EUR issued by EUR Issuer, path finding only needs to find paths that reach EUR Issuer. Similarly, if Bob is receiving an MPT, path finding finds paths that reach the MPT Issuer. Flow handles the final hop to Bob. For XRP payments, the effective destination is simply the destination account itself. + +## 1.4. Algorithm + +Path finding is a constrained graph search algorithm that explores the ledger's network to find viable payment routes. We will illustrate the algorithm using an example similar to [section 1.1.5](#115-example-different-currencies): Alice wants to send USD and Bob should receive an MPT. + +### 1.4.1. Setup + +- Alice has USD trust line to USD Issuer +- Bob is a holder of an MPT issued by MPT Issuer +- Available order books: USD/MPT, USD/XRP, XRP/MPT, USD/CAD, CAD/MPT, USD/JPY, JPY/CHF +- For this example, assume we only have the following path types defined for this payment type: `"safd"`, `"sbfd"`, `"saxfd"`, `"sabfd"` (in reality, the algorithm could explore more types based on the search depth and payment type) +- The [**default path**](#24-default-paths) is always tested separately and not included in path type expansion +- This is a non-permissioned payment (no domain specified), so all open order books are available for consideration + +### 1.4.2. Path Type Expansion + +Path type expansion works by incrementally building paths one node at a time. For each node in the path type (like `"s"`, `"a"`, `"f"`, `"d"`), the algorithm: +1. Queries the ledger to find all possible options for that node +2. Creates a new path branch for each option found +3. Adds all branches to a list of incomplete paths +4. Continues to the next node, expanding each path in the list + +When a path reaches the destination with the correct currency, it's marked as complete and added to the complete paths list. + +**Step 1: Determine Payment Type and Select Path Types** + +Payment type is NonXRP->NonXRP (different currencies), and the algorithm selects path types to explore based on the payment type: +- `"safd"`: source -> account -> destination book -> destination +- `"sbfd"`: source -> book -> destination book -> destination +- `"saxfd"`: source -> account -> XRP book -> destination book -> destination +- `"sabfd"`: source -> account -> book -> destination book -> destination + +**Step 2: Expand Path Type `"safd"`** + +Starting from Alice with USD, the algorithm will expand each node in the path type. + +- `"s"`: Start at Alice (USD), creates an empty path: `[]` +- `"sa"`: Query AssetCache for accounts connected via trust lines holding USD, which have enough liquidity and allow rippling. + - Finds: USD Issuer + - Incomplete paths so far: `[USD Issuer]` +- `"saf"`: Query OrderBookDB for any order books from Issuer USD to MPT destination + - Finds: USD/MPT + - MPT is the destination so adds `[USD Issuer, USD/MPT Book]` to complete paths +- `"safd"`: Has no incomplete paths to examine + +**Step 3: Expand Path Type `"sbfd"`** + +Starting from Alice with USD: +- `"s"`: Start at Alice (USD) +- `"sb"`: Query OrderBookDB for any order books that accept USD as input. Because no source issuer was specified, the source asset's issuer defaults to Alice herself[^source-issuer-default]. No order books exist for USD.Alice, so no books are found. + - Terminates (`"sbf"` and `"sbfd"` have no incomplete paths to examine) + +[^source-issuer-default]: Source issuer defaults to source account: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L274-L277) + +**Step 4: Expand Path Type: `"saxfd"`** +- `"s"`: Start at Alice (USD) +- `"sa"`: Query AssetCache for accounts connected via trust lines holding USD, which have enough liquidity and allow rippling. + - Finds: USD Issuer + - Incomplete paths so far: `[USD Issuer]` +- `"sax"`: Query OrderBookDB for any order book that will convert Issuer USD convert to XRP + - Finds: Issuer USD -> XRP + - Incomplete paths so far: `[USD Issuer, USD/XRP Book]` +- `"saxf"`: Query OrderBookDB for any order book that will convert to MPT + - Finds: Issuer XRP -> MPT + - MPT is the destination so adds to complete paths: `[USD Issuer, USD/XRP Book, XRP/MPT book]` +- `"saxfd"`: Has no incomplete paths to examine + +**Step 5: Expand Path Type: `"sabfd"`** +- `"s"`: Start at Alice (USD) +- `"sa"`: Finds that `"sa"` has already created an incomplete path `[USD Issuer]` (in step 4) +- `"sab"`: Query OrderBookDB for any order book that will convert from Issuer USD + - Finds: Issuer USD -> JPY, Issuer USD -> MPT, Issuer USD -> XRP, Issuer USD -> CAD + - Adds `[USD Issuer, USD/MPT Book]` to complete paths, as MPT is the destination + - This step will add both the books it finds, but also the issuer account for IOUs and MPTs if the issuer is not the final destination + - Incomplete paths so far: `[USD Issuer, USD/XRP Book]`, `[USD Issuer, USD/JPY Book, JPY Issuer]`, `[USD Issuer, USD/CAD Book, CAD Issuer]` +- `"sabf"`: Query OrderBookDB for any order book that will convert from previous incomplete path to MPT: + - `[USD Issuer, USD/JPY book, JPY Issuer]` branch: + - Finds JPY/CHF Book. CHF is not the destination asset + - Rejects this path and terminates. + - `[USD Issuer, USD/XRP Book]` branch: + - Finds XRP/MPT Book. While we found this in `"saxfd"` already, we have not seen it in this path type + - MPT is the destination so tries to add `[USD Issuer, USD/XRP Book, XRP/MPT Book]` to complete paths. However, since this path is already in completed paths, it is ignored + - `[USD Issuer, USD/CAD Book, CAD Issuer]` branch: + - Finds CAD/MPT Book. The path ends with `[... USD/CAD Book, CAD Issuer]`, so the redundant `CAD Issuer` account is replaced with `CAD/MPT Book` + - MPT is the destination so adds `[USD Issuer, USD/CAD Book, CAD/MPT Book]` to complete paths +- `"sabfd"`: Has no incomplete paths to examine + +*Complete Path 1:* `[USD Issuer, USD/MPT Book]` +*Complete Path 2:* `[USD Issuer, USD/XRP Book, XRP/MPT book]` +*Complete Path 3:* `[USD Issuer, USD/CAD Book, CAD/MPT Book]` + +### 1.4.3. Rank Paths + +Now that there is a set of feasible paths, the algorithm ranks them. + +The algorithm begins by testing the [default path](#24-default-paths). If the default path returns some liquidity, it will be deducted from the remaining liquidity used to test discovered paths. + +The algorithm then simulates each discovered path to measure its [quality](../flow/README.md#21-quality) and liquidity: +- Simulate Path 1 `[USD Issuer, USD/MPT Book]`: + - Quality: 1.05 (costs 105 USD to get 100 MPT) + - Liquidity: 1000 MPT capacity +- Simulate Path 2 `[USD Issuer, USD/XRP Book, XRP/MPT book]`: + - Quality: 1.04 (costs 104 USD to get 100 MPT) + - Liquidity: 750 MPT capacity +- Simulate Path 3 `[USD Issuer, USD/CAD Book, CAD/MPT Book]`: + - Quality: 1.06 (costs 106 USD to get 100 MPT) + - Liquidity: 500 MPT capacity + +Unless the user is trying to convert the entire possible amount of an asset, when it checks the liquidity of each path, the algorithm checks that each path can deliver at least 1/6th of the total amount[^min-liquidity], to prevent returning paths that can return very small liquidity. The total amount is divided by 6 because maxPaths is 4[^max-paths], and two is added. + +Paths are ranked by quality first (lower cost is better), then by liquidity (higher is better), then by path length (shorter is better)[^rank-sort]. In this example, quality alone determines the order: Path 2 (1.04), Path 1 (1.05), Path 3 (1.06). When the user is converting the entire possible amount of an asset, quality is ignored and paths are ranked by liquidity first. + +[^min-liquidity]: Minimum liquidity calculation: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L182-L186) + +[^max-paths]: Maximum paths constant: [`TransactionSign.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/TransactionSign.cpp#L317-L321) + +[^rank-sort]: Path ranking comparator: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L582-L597) + +## 1.5. Structure + +The algorithm executes four main phases: + +**1. Path Discovery ([Section 4](#4-path-discovery))** + +The [`findPaths`](#41-findpaths-function) function determines the payment type (XRP->NonXRP, NonXRP->XRP, NonXRP->NonXRP, etc.) and selects appropriate [path types](#23-path-types) to explore. Each path type is a template like `"sfd"` or `"saxfd"` that describes a routing strategy. The algorithm expands these templates into concrete paths by recursively querying the ledger through [`addPathsForType`](#42-addpathsfortype), which calls [`addLinks`](#43-addlinks) and [`addLink`](#44-addlink) to build paths step by step. + +As it explores, `addLink` queries [AssetCache](#46-assetcache) to find accounts connected by trust lines and MPTs, and [OrderBookDB](#45-orderbookdb) to find available order books for currency conversion (including domain-restricted order books when a domain is specified). It filters out invalid options (loops, trust lines with insufficient liquidity, `NoRipple` violations, unauthorized MPT holders) and prioritizes promising routes by ranking accounts by their "paths out" score (how many onward connections they have). Complete paths that reach the destination currency at the effective destination account are stored in `mCompletePaths`. + +**2. Path Ranking ([Section 5](#5-path-ranking))** + +The [`computePathRanks`](#51-computepathranks) function evaluates discovered paths by simulating their execution through the Flow engine. It first tests the default path to determine `mRemainingAmount` - the liquidity still needed beyond what the default provides. This ensures paths are evaluated for their incremental value. + +The [`rankPaths`](#52-rankpaths) function then tests each path in `mCompletePaths` by calling [`getPathLiquidity`](#53-getpathliquidity), which uses the Flow engine to simulate payment execution on a sandbox ledger. Paths are scored and sorted by quality (exchange rate), liquidity (capacity), and length (hop count). In normal mode, better quality is prioritized. In convert-all mode (when discovering maximum liquidity), quality is ignored and only liquidity matters. + +**3. Path Selection ([Section 6](#6-path-selection))** + +The [`getBestPaths`](#6-path-selection) function selects the optimal set of paths from the ranked results. It merges rankings from discovered paths and any extra paths provided by the caller, then iteratively selects paths by comparing quality first, then liquidity. It validates issuer constraints (ensuring non-default paths route through the correct issuer for IOUs and MPTs when needed) and applies different selection rules based on remaining slots: filling slots greedily when multiple remain, requiring the last path to cover all remaining liquidity, and optionally saving a "full liquidity path" that can handle the entire payment alone. + +**4. Payment Execution** + +The selected paths are returned to the caller ([RPC handler](#7-rpc-requests) or [Payment transaction](../payments/README.md)) and passed to the [Flow engine](../flow/README.md) for actual payment execution. Flow performs path normalization to add source and destination accounts, then executes the payment by processing each path as a strand of steps. + +```mermaid +flowchart LR + rpc((RPC/Payment)) + pathfinder[Pathfinder] + findPaths[1. Path Discovery
findPaths] + computePathRanks[2. Path Ranking
computePathRanks] + getBestPaths[3. Path Selection
getBestPaths] + flow[4. Payment Execution
Flow] + + rpc --> pathfinder + pathfinder --> findPaths + findPaths --> computePathRanks + computePathRanks --> getBestPaths + getBestPaths --> flow +``` + +**Domain Parameter for Permissioned DEX:** + +Path finding supports an optional `domain` parameter that enables permissioned DEX functionality. When a domain is specified, the pathfinder restricts order book queries to only include offers that belong to that domain's order book. This domain value is: +- Passed into the Pathfinder constructor and stored as `mDomain`[^pathfinder-domain-constructor] +- Forwarded to OrderBookDB queries in `addLink` when discovering available books[^pathfinder-domain-orderbook] (see [Section 4.4](#44-addlink)) +- Passed to RippleCalc and Flow during path ranking and execution[^pathfinder-domain-flow] (see [Section 5.1](#51-computepathranks)) + +[^pathfinder-domain-constructor]: Pathfinder domain parameter storage: [`Pathfinder.cpp:220,230`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L220-L230) +[^pathfinder-domain-orderbook]: OrderBookDB domain filtering flow: `addLink` ([`Pathfinder.cpp:995`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L995)) calls `getPathsOut` ([`Pathfinder.cpp:1145`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1145)), which queries OrderBookDB with domain parameter ([`Pathfinder.cpp:786`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L786)) +[^pathfinder-domain-flow]: Domain passed to Flow: [`Pathfinder.cpp:411`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L411) + +# 2. Terminology and Concepts + +## 2.1. Terminology + +**Path Elements** are the building blocks that describe a location in a path. Each path element can contain: +- An **account ID** (for rippling through trust lines or MPT connections) +- A **currency** (for specifying what currency flows through this point) +- An **issuer ID** (for non-XRP currencies, who issues the currency) +- An **MPTID** (for MPT currencies, identifies the specific MPT) + +A path element can be either: +- An **account element** - represents an account that holds balances (has account ID set) +- An **offer element** - represents an order book for currency exchange (no account ID, just currency/issuer or MPTID) + +**Paths** are sequences of path elements that describe a complete route from source to destination. For example: +- `[Alice] -> [USD IssuerA] -> [Book: USD/IssuerA -> EUR/IssuerB] -> [EUR/IssuerB] -> [Bob]` + +**Steps** are the executable operations created by the Flow engine when it [converts paths into strands](../flow/README.md#52-path-to-strand-conversion). Path finding creates paths and path elements, not steps or strands, but it relies on the Flow engine to rank the paths. + +The relationship is: **Node Types** (search strategy) -> **Path Elements** (route description) -> **Steps** (executable operations) + +**Liquidity** refers to the capacity of a path - how much value it can move in a single payment. Paths with higher liquidity can deliver more to the destination. + +**Effective Destination** is the account where path finding actually searches to, which differs from the final destination for IOU and MPT payments. For XRP payments, effective destination equals the destination account. For IOU and MPT payments, effective destination is the issuer of the destination amount, since paths only need to reach the issuer - Flow handles the final hop to the destination through path normalization. + +**Complete Path** is a path that successfully reaches the effective destination with the correct destination currency. Incomplete paths are discarded during path discovery. + +**Convert-all Mode** is activated when the destination amount equals the maximum possible value for that currency. In this mode, path finding discovers maximum available liquidity rather than targeting a specific amount, and prioritizes liquidity over quality when ranking paths. + +## 2.2. Payment Types + +The pathfinder categorizes each payment request into one of five types: + +| PaymentType | Description | Example | +|-------------|------------------------------|----------------------------------------| +| `PaymentType.XrpToXrp` | XRP to XRP payment | Alice sends XRP to Bob | +| `PaymentType.XrpToNonXrp` | XRP to IOU or MPT payment | Alice sends XRP, Bob receives MPT | +| `PaymentType.NonXrpToXrp` | IOU or MPT to XRP payment | Alice sends USD or MPT, Bob receives XRP | +| `PaymentType.NonXrpToSame` | Same IOU or MPT payment | Alice sends USD, Bob receives USD (same asset) | +| `PaymentType.NonXrpToNonXrp` | Different IOU or MPT payment | Alice sends EUR, Bob receives USD | + +While `PaymentType.XrpToXrp` is defined as a payment type and is initialized with an empty path type list, XRP->XRP payments **never actually invoke the path finding or flow system**. The Payment transactor detects XRP->XRP payments and processes them as direct balance transfers, bypassing both path finding and the Flow engine entirely. + +## 2.3. Path Types + +Each payment type has a predefined table of path types at different search levels (costs). Higher search levels explore more complex paths. + +Example types for `PaymentType.XrpToNonXrp`: + +| Cost | Type | Path Structure | +|------|----------|--------------------------------------------------------------| +| 1 | `sfd` | Source -> Destination Book -> Destination | +| 3 | `sfad` | Source -> Destination Book -> Account -> Destination | +| 5 | `sfaad` | Source -> Destination Book -> Account -> Account -> Destination | +| 6 | `sbfd` | Source -> Book -> Destination Book -> Destination | +| 8 | `sbafd` | Source -> Book -> Account -> Destination Book -> Destination | + +The path finding algorithm searches through these types based on the requested search depth. A search level of 0 finds no paths, while higher levels (4-7 typical, 10 maximum) explore increasingly complex routing options. + +For the complete list of path types for each payment type, see `Pathfinder::initPathTable()`[^init-path-table]. + +[^init-path-table]: Path table initialization: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1365-L1430) + +**Configuration** + +The search depth can be configured in `xrpld.cfg`: + +| Parameter | Description | Default | Recommended | +|-----------|-------------|---------|-------------| +| `path_search` | Default search aggressiveness | 2 | 7 (for advanced path finding) | +| `path_search_fast` | Minimum search aggressiveness | 2 | 2 (for advanced path finding) | +| `path_search_max` | Maximum search aggressiveness | 3 | 10 (for advanced path finding) | +| `path_search_old` | Search level for legacy path finding interfaces | 2 | 7 (for advanced path finding) | + +Higher values can exponentially increase resource usage. Setting `path_search_max` to 0 disables path finding entirely. On a server configured as a validator (one with `[validation_seed]` or `[validator_token]`), `path_search_max` defaults to 0 (path finding disabled) unless explicitly set. + +### 2.3.1. Node Types + +Path types are constructed from a sequence of node types. Each node type tells the path finding algorithm what kind of connection to explore at that step in the path: + +**`NodeType.Source` (code: `s`)** - The source account + +This represents the starting point of the payment. The source is always the first node in any path type. When path finding expands this node, it creates a single empty path representing the starting position at the source account. + +**`NodeType.Accounts` (code: `a`)** - Accounts connected via trust lines or MPTs + +When path finding encounters an `a` node, it expands to neighboring accounts connected to the current position via [trust lines](../trust_lines/README.md) or [MPTs](../mpts/README.md). The actual account selection involves filtering by NoRipple flags, liquidity, and authorization, then ranking candidates by their number of viable outgoing paths. See [Section 4.4](#44-addlink) for details. + +**`NodeType.Books` (code: `b`)** - Order books for currency conversion + +When path finding encounters a `b` node, it queries [OrderBookDB](#45-orderbookdb) for all order books that accept the current currency as input, allowing the path to exchange into a different currency. See [Section 4.4](#44-addlink) for details on book expansion, including how output issuers are handled. + +**`NodeType.XrpBook` (code: `x`)** - Order book to XRP + +A specialized version of `NodeType.Books` that only considers order books that convert the current currency to XRP. XRP often serves as a bridge currency between other currencies, and limiting to XRP books reduces the search space. [OrderBookDB](#45-orderbookdb) maintains a separate `xrpBooks` cache (and `xrpDomainBooks` for permissioned DEX) for faster lookups. + +**`NodeType.DestBook` (code: `f`)** - Order book to destination currency + +This is another specialized version of `NodeType.Books` that only considers order books that output the destination currency. The `f` stands for "final" book. When path finding encounters an `f` node, it only looks for order books that convert the current currency into whatever currency the destination wants to receive. This ensures the path ends with the correct currency. + +For example, if the destination wants EUR, an `f` node will only consider order books like USD/EUR, XRP/EUR, GBP/EUR, etc. + +**`NodeType.Destination` (code: `d`)** - The destination account + +The destination is always the last node in any path type. When path finding encounters a `d` node, it searches for account connections to the effective destination. For IOUs and MPTs, the effective destination is the issuer, not the final recipient. If a preceding `f` step already ended at the issuer, the path is already complete and `d` has nothing to add. When it does fire, `d` adds the issuer as a trust line or MPT hop, completing the path via rippling. + + +## 2.4. Default Paths + +A **default path** is the direct route between source and destination that does not need to be explicitly specified. The default path is: + +- **For same-currency IOU or MPT payments**: Direct transfer between source and destination through the issuer +- **For cross-currency payments**: Uses the order book between the source currency and the destination currency + +Unless the Payment transaction contains `tfNoRippleDirect`, the Flow engine always attempts the default path, even when explicit paths are provided. The default path can fail (e.g., no trust line exists, no order book available), in which case the Flow engine continues with any explicit paths. + +Default path is an empty path that is passed to [Path Normalization](../flow/README.md). + +# 3. Pathfinder + +The Pathfinder class is responsible for orchestrating finding of paths. It creates a Path set - a vector of Paths. Paths consist of Path Elements. +The Pathfinder uses path types to systematically explore the ledger, building STPath objects from STPathElement components and collecting them into an STPathSet. + +```mermaid +classDiagram + class Pathfinder { + +STPathSet mCompletePaths + +AssetCache cache + +findPaths() + +computePathRanks() + +getBestPaths() + } + + class STPathSet { + +vector~STPath~ paths + } + + class STPath { + +vector~STPathElement~ mPath + +hasSeen() + +push_back() + } + + class STPathElement { + +unsigned int mType + +AccountID mAccountID + +PathAsset mAssetID + +AccountID mIssuerID + } + + Pathfinder "1" --> "1" STPathSet : produces + STPathSet "1" *-- "0..*" STPath : contains + STPath "1" *-- "1..*" STPathElement : contains +``` +*Figure: Key components of Pathfinder* + + +## 3.1. Pathfinder Class + +The `Pathfinder` class is the entry point for path finding. It is constructed with: + +> [!IMPORTANT] +> Parameter names and definitions are simplified to provide an overview. They do not map 1:1 to the C++ implementation, but are intended to make the pseudocode in later sections easier to follow. + +| Parameter | Description | Required | +|-----------|--------------------------------------------------------------------|----------| +| `cache` | AssetCache containing ledger state, trust line, and MPT information | ✅ | +| `srcAccount` | Source account ID - the account sending funds | ✅ | +| `dstAccount` | Destination account ID - the account receiving funds | ✅ | +| `srcPathAsset` | Asset the source wants to spend | ✅ | +| `srcIssuer` | Issuer for the source currency | ❌ | +| `dstAmount` | The amount to be delivered to the destination | ✅ | +| `srcAmount` | Maximum amount the source is willing to spend | ❌ | +| `domain` | Domain identifier for permissioned DEX | ❌ | +| `app` | Application reference for accessing ledger state | ✅ | + +The pathfinder maintains these key variables: +- `app` - Application reference which can be used to fetch `OrderBookDB` +- `convert_all_` - Boolean flag indicating "convert all" mode (find maximum liquidity instead of exact amount). Set to true when destination amount equals the maximum possible value for that currency +- `mSrcAccount` - Source account +- `mSrcPathAsset` - Asset the source wants to spend, derived from `srcPathAsset` constructor parameter +- `mDstAccount` - Destination account (the account that will ultimately receive the payment) +- `mEffectiveDst` - The account where paths must end. For XRP destinations, this is `mDstAccount`. For IOU and MPT destinations, this is the issuer of the destination amount. Paths discovered by path finding end at this account, not at `mDstAccount`. The Flow engine later handles the final hop from `mEffectiveDst` to `mDstAccount` through path normalization. +- `mCompletePaths` - Collection of all complete paths found +- `mPathRanks` - Rankings of paths based on quality and liquidity +- `mPaths` - Cache of paths organized by PathType +- `mPathsOutCountMap` - Cache of "paths out" counts for each Issue +- `mPathTable` - Static (file-level) table of [path types](#23-path-types), shared across all Pathfinder instances +- `mSource` - STPathElement representing the starting point for path discovery. Computed in `findPaths`: if the source asset has a non-XRP issuer (`mSrcIssuer`), the element's account and issuer are set to that issuer; otherwise, they are set to `mSrcAccount`. For XRP, the issuer is the zero account. Used by `addLink` as the implicit first element when the current path is empty. +- `mRemainingAmount` - Amount remaining to deliver after accounting for default path contribution +- `mAssetCache` - [AssetCache](#46-assetcache) for querying trust line and MPT information + +## 3.2. Path Elements + +An `STPathElement`[^stpathelement] is the data structure representing a single step in a payment path. It is a class containing: + +- `mType`[^mtype] (unsigned int) - Bitmask indicating which fields are present: + - `0x01` (typeAccount)[^typeaccount] - Has account field + - `0x10` (typeCurrency)[^typecurrency] - Has currency field + - `0x20` (typeIssuer)[^typeissuer] - Has issuer field + - `0x40` (typeMPT)[^typempt] - Has MPT field +- `mAccountID` (AccountID) - The account (if typeAccount bit is set) +- `mAssetID` (PathAsset) - Holds either a Currency or an MPTID +- `mIssuerID` (AccountID) - The issuer (if typeIssuer bit is set) + +[^stpathelement]: STPathElement class definition: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STPathSet.h#L17) +[^mtype]: mType field: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STPathSet.h#L19) +[^typeaccount]: typeAccount constant: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STPathSet.h#L32) +[^typecurrency]: typeCurrency constant: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STPathSet.h#L33) +[^typeissuer]: typeIssuer constant: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STPathSet.h#L34) +[^typempt]: typeMPT constant: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/STPathSet.h#L35) + +**Types of path elements:** + +**Account element** (`type = 0x01`): +``` +{type: 0x01, account: aliceAccountID} +``` +Represents an account in a path - either an intermediate account for rippling or the destination account. Created during pathfinding[^account-element-creation] when adding accounts to incomplete paths. + +**Offer/Book element for XRP** (`type = 0x10`): +``` +{type: 0x10, currency: "XRP"} +``` +Represents a conversion to XRP via order book. Has currency only[^xrp-book-element-creation] (0x10). + +**Offer/Book element for IOUs** (`type = 0x30`): +``` +{type: 0x30, currency: "USD", issuer: issuerAccountID} +``` +Represents a currency conversion via order book. Has both currency and issuer[^iou-book-element-creation] (0x10 | 0x20 = 0x30). + +**Offer/Book element for MPTs** (`type = 0x60`): +``` +{type: 0x60, mptid: mptID, issuer: issuerAccountID} +``` +Represents an MPT order book. Has both MPT identifier and issuer (0x40 | 0x20 = 0x60). + +**Account+Asset element** (`type = 0x31` for IOUs, `0x61` for MPTs): +``` +{type: 0x31, account: sourceAccountID, currency: "USD", issuer: issuerAccountID} +``` +Represents the source account holding a specific asset. **Always added as the first element**[^source-element-normalization] during path normalization (in the Flow engine's `toStrand` function). Has account, currency, and issuer (0x01 | 0x10 | 0x20 = 0x31) or account, MPT, and issuer (0x01 | 0x40 | 0x20 = 0x61). +The issuer in the first element is set to source, and the next element connects it to a particular issuer. + +[^account-element-creation]: Account element creation during pathfinding: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1202-L1203) +[^xrp-book-element-creation]: XRP book element creation during pathfinding: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1225-L1226) +[^iou-book-element-creation]: IOU book element creation during pathfinding: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1287-L1291) +[^source-element-normalization]: Source element added during path normalization: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/paths/PaySteps.cpp#L262-L273) + + +## 3.3. Path + +An `STPath` represents a single payment path. It is a vector of [STPathElements](#32-path-elements). + +**Key methods:** + +- `size()` - Returns the number of path elements in the path +- `empty()` - Returns true if path has no elements +- `push_back(element)` - Adds an element to the end of the path +- `back()` - Returns the last element in the path +- `hasSeen(account, asset, issuer)` - Checks if this combination already appears in the path (loop detection). The asset parameter is a PathAsset which can hold either Currency (for IOUs) or MPTID (for MPTs). + +For example, a normalized XRP -> USD path would look like: + +``` +[0] Type=0x31 (Account Currency Issuer ) Account: sourceAccountID Currency: XRP Issuer: rrrrrrrrrrrr +[1] Type=0x1 (Account ) Account: rrrrrrrrrrrr +[2] Type=0x30 (Currency Issuer ) Currency: USD Issuer: issuerAccountID +[3] Type=0x1 (Account ) Account: issuerAccountID +[4] Type=0x1 (Account ) Account: destinationAccountId +``` + +An `STPathSet` is a collection of multiple `STPath` objects, representing alternative paths that can be used simultaneously for a payment. + + +# 4. Path Discovery + +The path discovery process works by expanding **path types** into a **tree of concrete paths**. +The core algorithm takes a type like `"sfad"` and expands it step-by-step: first it builds all paths matching `"s"`, then extends those to match `"sf"`, then `"sfa"`, and finally `"sfad"`. At each step, it queries the ledger (AssetCache for trust lines and MPTs, OrderBookDB for order books) to find what's actually available, creating branches for each viable option while filtering out loops and insufficient liquidity. + +Consider a payment where the source holds USD and wants to deliver EUR.Issuer to the destination. The pathfinder uses type `"sfad"` (source -> destination book -> account -> destination): + +The type `"sfad"` expands as follows: +1. **s** (source) - Start with source account +2. **f** (destination book) - Query OrderBookDB for order books from USD that output the destination currency (EUR). Finds USD->EUR.Issuer book +3. **a** (accounts) - Query AssetCache for accounts holding EUR.Issuer. Finds Alice (has liquidity), Bob (zero balance, filtered out), and Carol (has liquidity) +4. **d** (destination) - Try to complete each path by reaching the effective destination (Issuer): + - Alice has a trust line to Issuer -> path complete (blue) + - Carol has no trust line to Issuer -> filtered out (red) + +```mermaid +graph TD + Start["source"] --> Book1["source
USD->EUR.Issuer book"] + + Book1 --> Acct1["source
USD->EUR.Issuer book
Alice"] + Book1 --> Filtered1["source
USD->EUR.Issuer book
Bob (no liquidity)"] + Book1 --> Acct3["source
USD->EUR.Issuer book
Carol"] + + Acct1 --> Complete1["source
USD->EUR.Issuer book
Alice
Issuer"] + + Acct3 --> Filtered2["source
USD->EUR.Issuer book
Carol (no path to Issuer)"] + + style Complete1 fill:blue + style Filtered1 fill:red + style Filtered2 fill:red +``` + +The algorithm uses: +- **Filtering** - Rejects loops, insufficient liquidity, NoRipple violations before creating branches +- **Ranking** - Prioritizes destination connections and high "paths out" scores +- **Limits** - Maximum 1000 complete paths; when expanding accounts, up to 50 candidates from the source account, up to 10 candidates from any other account + + +**Implementation:** + +1. **`findPaths`** - Entry point that: + - Determines payment type (XRP->NonXRP, NonXRP->XRP, NonXRP->NonXRP, etc.) and iterates through all path types for that payment type + - For each type, calls `addPathsForType` to expand it + +2. **`addPathsForType`** - Recursive function that: + - Takes a type like `"sfad"` (source -> book -> account -> destination) and builds it incrementally by first building `"s"`, then `"sf"`, then `"sfa"`, then `"sfad"` + - For each step, calls `addLinks` to expand the last node type + - Returns a list of paths (complete and incomplete) for that type, which is cached in `mPaths` for reuse + +3. **`addLinks`** - Simple wrapper that: + - Takes a set of incomplete paths and calls `addLink` once for each path + +4. **`addLink`** - Core expansion logic that: + - Takes one incomplete path and flags indicating what type of expansion to perform (add accounts via `a` node type, or add books via `b`/`f` node types) + - Based on the flags: + - If expanding accounts (`a`): queries **`AssetCache`** to find connected accounts via trust lines or MPTs + - If expanding books (`b`/`f`): queries **`OrderBookDB`** to find available order books (from offers and AMMs) + - For each viable option found, creates a new branch by appending path elements to `incompletePaths` + - **When a path reaches the destination with the correct currency, adds it directly to `mCompletePaths`** + - Filters out loops, NoRipple violations, and insufficient liquidity + +The entire flow can be interrupted by using `continueCallback`. This callback allows the caller to interrupt path finding by returning `false`. It is checked at multiple points during path discovery. + +- **[`path_find` subscriptions](#72-path_find-rpc)** provides a callback that checks if the WebSocket client is still connected +- **[`ripple_path_find`](#71-ripple_path_find-rpc-legacy)** and **[transaction signing with `build_path`](../payments/README.md#421-path-finding)** do NOT provide a callback + + +## 4.1. findPaths Function + +The `findPaths` function[^find-paths] is the main path discovery engine. It searches for paths from `mSrcAccount` to `mEffectiveDst`. + +[^find-paths]: Main path discovery function: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L241-L380) + +- **For XRP payments**: `mEffectiveDst` equals `mDstAccount`, so paths go directly to the destination +- **For IOU and MPT payments**: `mEffectiveDst` is the issuer of the destination amount. Path finding only needs to reach the issuer - the Flow engine will handle the final hop from issuer to destination through path normalization. + +**Parameters:** + +| Parameter | Description | Required | +|-----------|-------------|----------| +| `searchLevel` | Maximum cost/depth to search | ✅ | +| `continueCallback` | Optional callback to check if search should continue | ❌ | + +The function validates the payment request, determines [payment type](#22-payment-types) from source and destination currencies and iterates through path types for that payment type and calls addPathsForType, making sure not to exceed the `searchLevel`. + +All found paths are stored in PathFinder object state, so this function only loops over types without much responsibility itself. + +### 4.1.1. findPaths Pseudo-Code + +```python +def findPaths(searchLevel, continueCallback) -> bool: + # Validate payment request + if mDstAmount == 0: + # Destination amount is 0 + return False + + if mSrcAccount == mDstAccount and mDstAccount == mEffectiveDst and mSrcPathAsset == mDstAmount.asset(): + # No need to send to same account with same currency + return False + + if mSrcAccount == mEffectiveDst and mSrcPathAsset == mDstAmount.asset(): + # Default path might work, but any additional path would loop back to source + # (since paths must end at mEffectiveDst which is the source) + return True + + if not mLedger: + # No ledger to search + return False + + if not accountExists(mSrcAccount): + # Source account has to exist. Destination does not if we are sending XRP to it. + return False + + if mEffectiveDst != mDstAccount and not accountExists(mEffectiveDst): + # Issuer account has to exist + return False + + if not accountExists(mDstAccount) and (not isXRP(mDstAmount) or mDstAmount < getAccountReserve()): + # New account must be funded with XRP meeting minimum reserve + return False + + # Build the source element used by addLink when the path is empty. + # If the source asset has a non-XRP issuer, start from the issuer's account; + # otherwise start from the source account. + if mSrcIssuer and not isXRP(mSrcPathAsset) and not isXRP(mSrcIssuer): + account = mSrcIssuer + else: + account = mSrcAccount + issuer = xrpAccount() if isXRP(mSrcPathAsset) else account + mSource = STPathElement(account, mSrcPathAsset, issuer) + + # Determine payment type (one of: PaymentType.XrpToXrp, PaymentType.XrpToNonXrp, etc.) + paymentType = determinePaymentType(mSrcPathAsset, mDstAmount.asset()) + + # Search for paths using types for this payment type + for costedPath in mPathTable[paymentType]: + if continueCallback.shouldBreak(): + return False + if costedPath.searchLevel <= searchLevel: + # costedPath.type is a PathType (sequence of node types like "sfd", "sfad", etc.) + addPathsForType(costedPath.type, continueCallback) + if len(mCompletePaths) > PATHFINDER_MAX_COMPLETE_PATHS: # 1000 + break + + return True +``` + +## 4.2. addPathsForType + +`addPathsForType`[^add-paths-for-type] takes a `PathType` like `"sfad"` and converts it into concrete paths by building incrementally, one node type at a time. + +[^add-paths-for-type]: Incremental path building function: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L872-L943) + +**Parameters:** + +| Parameter | Description | Required | +|-----------|-------------|----------| +| `pathType` | Sequence of node types (e.g., `"sfd"`, `"saxfd"`) | ✅ | +| `continueCallback` | Optional callback to check if search should continue | ❌ | + +The function works recursively. When asked to build `"sfad"`, it first checks if `"sfad"` was already built and cached in `mPaths` - if so, it returns immediately. Otherwise, it recursively strips off the last character until it hits the empty string: + +- `"sfad"` -> `"sfa"` -> `"sf"` -> `"s"` -> `""` (base case returns empty list) + +Now the recursion unwinds and each level builds paths by extending what its parent returned. Each node type maps to a specific expansion strategy: + +- **`"s"`**: Returns a single empty path (the starting point at source) +- **`"sf"`**: Takes that empty path and calls `addLinks` with flags to query OrderBookDB for order books from the source currency that output the destination currency. If 5 such books exist, returns 5 paths. +- **`"sfa"`**: Takes those 5 paths and calls `addLinks` with flags to query AssetCache for accounts holding the book's output currency. If each book leads to 3 accounts, returns 15 paths (5 * 3). +- **`"sfad"`**: Takes those 15 paths and calls `addLinks` with flags to find the destination account. Only paths reaching the destination with the correct currency are not discarded. + +The flags passed to `addLinks` tell `addLink` what to query (accounts via AssetCache or books via OrderBookDB) and what filters to apply (only XRP books, only destination currency, only destination account, etc.). Each node type (`s`, `a`, `b`, `x`, `f`, `d`) uses different flags to control this behavior. + +Finally, the result is stored in `mPaths[pathType]` and returned. + +### 4.2.1. addPathsForType Pseudo-Code + +```python +def addPathsForType(pathType, continueCallback) -> list[STPath]: + # pathType is a sequence of node types like "sfd" or "saxfd" + + # Check cache + if pathType in mPaths: + return mPaths[pathType] + + # Base case - empty path type + if len(pathType) == 0: + mPaths[pathType] = [] + return mPaths[pathType] + + if continueCallback.shouldBreak(): + return [] + + # Recursive case - build parent paths first + parentPathType = pathType[:-1] # Remove last node type (e.g., "sfd" -> "sf") + parentPaths = addPathsForType(parentPathType, continueCallback) + + # Add final node type to parent paths + nodeType = pathType[-1] # Get last node type (e.g., 'd' from "sfd") + pathsOut = [] + + if nodeType == NodeType.Source: + pathsOut = [empty_path] + elif nodeType == NodeType.Accounts: + addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_ACCOUNTS, continueCallback=continueCallback) + elif nodeType == NodeType.Books: + addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_BOOKS, continueCallback=continueCallback) + elif nodeType == NodeType.XrpBook: + addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_BOOKS | afOB_XRP, continueCallback=continueCallback) + elif nodeType == NodeType.DestBook: + addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_BOOKS | afOB_LAST, continueCallback=continueCallback) + elif nodeType == NodeType.Destination: + addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_ACCOUNTS | afAC_LAST, continueCallback=continueCallback) + + mPaths[pathType] = pathsOut + return pathsOut +``` + +## 4.3. addLinks + +`addLinks`[^add-links] is a simple wrapper that calls `addLink` for each path in a set. + +[^add-links]: Wrapper function for batch path extension: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L857-L870) + +**Parameters:** + +| Parameter | Description | Required | +|-----------|-------------|----------| +| `currentPaths` | Set of incomplete paths to extend | ✅ | +| `incompletePaths` | Output list where new paths are added | ✅ | +| `addFlags` | Flags controlling expansion behavior (e.g., `afADD_ACCOUNTS`, `afADD_BOOKS`) | ✅ | +| `continueCallback` | Optional callback to check if search should continue | ❌ | + +### 4.3.1. addLinks Pseudo-Code + +```python +def addLinks(currentPaths, incompletePaths, addFlags, continueCallback): + for path in currentPaths: + if continueCallback.shouldBreak(): + return + addLink(currentPath=path, incompletePaths=incompletePaths, addFlags=addFlags, continueCallback=continueCallback) +``` + +## 4.4. addLink + +`addLink`[^add-link] is where the actual path expansion happens - it's the function that queries the ledger and creates new path branches. + +[^add-link]: Core path expansion function: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L972-L1301) + +**Parameters:** + +| Parameter | Description | Required | +|-----------|-------------|----------| +| `currentPath` | Single incomplete path to extend | ✅ | +| `incompletePaths` | Output list where new paths are added | ✅ | +| `addFlags` | Flags controlling expansion behavior (e.g., `afADD_ACCOUNTS`, `afADD_BOOKS`, `afOB_XRP`, `afOB_LAST`, `afAC_LAST`) | ✅ | +| `continueCallback` | Optional callback to check if search should continue | ❌ | + +Every partial path has an **endpoint**, derived from its last [path element](#32-path-elements) (or from the source if the path is empty)[^addlink-endpoint]. The endpoint provides an account, an asset (currency or MPTID), and an issuer, which `addLink` uses to determine where to search for the next hop. + +[^addlink-endpoint]: Endpoint extraction from partial path: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1001-L1005) + +The function examines the path's current endpoint (which account and currency/asset) and uses `addFlags` to determine which ledger data source to query and what filters to apply. It produces two kinds of output: paths that reach the effective destination with the correct asset are added to `mCompletePaths`, while paths that still need further extension are added to `incompletePaths`. `addPathsForType` feeds incomplete paths back into `addLink` for the next expansion round, and complete paths proceed to [path ranking](#5-path-ranking), where they are simulated through the Flow engine to measure quality and liquidity. + +**Flag meanings:** + +- **`afADD_ACCOUNTS`** - Queries AssetCache for trust lines and MPT holdings to find connected accounts +- **`afADD_BOOKS`** - Queries OrderBookDB to find order books where the current asset is the input (TakerPays) +- **`afOB_XRP`** (modifier for `afADD_BOOKS`) - Restricts books to only those outputting XRP +- **`afOB_LAST`** (modifier for `afADD_BOOKS`) - Restricts books to only those outputting the destination asset +- **`afAC_LAST`** (modifier for `afADD_ACCOUNTS`) - Restricts accounts to only the effective destination + +The function's behavior varies significantly between account expansion and book expansion: + +**Account expansion** (`afADD_ACCOUNTS`): + +When the path's current endpoint is on XRP and the destination amount is XRP and the current path is non-empty, it adds the path to complete paths.[^xrp-endpoint-check] Empty paths are not added because they would represent XRP->XRP payments and those do not require pathfinding. + +[^xrp-endpoint-check]: XRP endpoint completion check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1019-L1027) + +For non-XRP endpoints, the function queries for trust lines or MPTs connected to the account at the path's current endpoint. + +For IOU currencies, `addLink` calls `AssetCache::getRippleLines`[^get-ripple-lines-call] to fetch trust lines for the endpoint account. The returned trust lines cover all currencies; `addLink` later filters them to match the current currency via `correctAsset`[^currency-match]. These trust lines are candidates for the next hop in the payment path, subject to the checks described below. + +The query is pre-filtered using `LineDirection`, which hints to `getRippleLines` which trust lines are needed. Rippling through an account is blocked when that account has NoRipple set on **both** its incoming and outgoing trust lines. `addLink` uses this rule at two levels: + +- `addLink` calls `isNoRippleOut(currentPath)`[^is-no-ripple-out] to check whether the trust line between the previous account in the path and the endpoint account has NoRipple set on the endpoint account's side. +- If `isNoRippleOut` returns true, `getRippleLines` is called with `LineDirection::incoming`[^noripple-direction], which requests only trust lines where the endpoint account does **not** have NoRipple set[^get-ripple-lines-direction]. However, this is a best effort optimization. The same account may be reached via different partial paths during pathfinding, and if its full set of trust lines was already cached from an earlier call, `AssetCache` returns those instead of fetching the filtered subset, to avoid duplicate storage[^asset-cache-superset]. +- If `isNoRippleOut` returns false, `getRippleLines` is called with `LineDirection::outgoing`, which returns all trust lines. + +Regardless of which set `getRippleLines` returned, `addLink` performs a per-candidate check that provides the actual gating: if `isNoRippleOut` was true and the candidate trust line also has NoRipple set on the endpoint account's side (`asset.getNoRipple()`), the candidate is skipped[^noripple-candidate-check]. + +For MPTs, the function queries for MPTs associated with the current account. The peer account is always the issuer, extracted from the MPTID[^mpt-peer-issuer], so MPT account expansion only navigates from holder to issuer. The reverse direction (issuer to holder) is never needed because pathfinding only needs to reach `mEffectiveDst` (the issuer for non-XRP destinations); the flow engine handles the final hop from issuer to destination holder through path normalization[^mpt-no-reverse]. + +Each asset connection undergoes these checks in order: + +1. **Currency/asset match**[^currency-match] - The asset must match the path's current currency code or MPTID +2. **Destination account bypass**[^dest-bypass] - When the destination issuer differs from the destination account, skips the destination account (pathfinding only needs to reach the issuer, not the final recipient) +3. **Destination-only filtering**[^dest-only] - When `afAC_LAST` is set, rejects all accounts except the effective destination (the issuer) +4. **Loop detection**[^loop-detection] - Rejects accounts already visited using `hasSeen(account, asset, issuer)` +5. **Liquidity and NoRipple check**[^liquidity-check]: + - For trust lines: Rejects if (balance <= 0 AND (no peer limit OR peer limit exhausted OR unauthorized when `lsfRequireAuth` is set)) OR (both the previous link and current link have NoRipple set) + - For MPTs: Rejects if zero balance OR maxed out OR not authorized +6. **Source loop prevention**[^source-loop] - Rejects accounts that would loop back to the source + +[^currency-match]: Currency/asset match check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1082-L1092) +[^dest-bypass]: Destination account bypass check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1069-L1073) +[^dest-only]: Destination-only filtering check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1077-L1080) +[^loop-detection]: Loop detection using hasSeen: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1110) +[^liquidity-check]: Liquidity and NoRipple check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1093-L1108) +[^source-loop]: Source loop prevention check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1138-L1141) +[^get-ripple-lines-call]: getRippleLines call in addLink: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1161-L1163) +[^is-no-ripple-out]: isNoRippleOut checks whether the last account in the path has NoRipple set on its outgoing link: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L958-L979) +[^noripple-direction]: Trust line fetch direction based on NoRipple: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1159-L1166) +[^get-ripple-lines-direction]: LineDirection::incoming excludes trust lines where the account has NoRipple set: [`TrustLine.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/TrustLine.cpp#L61) +[^noripple-candidate-check]: Per-candidate NoRipple check in addLink: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1101) +[^asset-cache-superset]: AssetCache returns the outgoing superset when incoming is requested but outgoing is already cached: [`AssetCache.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/AssetCache.cpp#L78-L87) +[^getpathsout]: getPathsOut computes the paths out score for an account: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L748-L835) +[^getpathsout-auth]: getPathsOut checks lsfRequireAuth on the candidate account: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L771-L775) +[^getpathsout-booksize]: Score starts with order book size: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L786) +[^getpathsout-destination-bonus]: Destination bonus of +10000: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L800-L804) +[^getpathsout-frozen]: Global freeze check in getPathsOut: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L776-L784) +[^getpathsout-iou-loop]: IOU trust line scoring loop: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L788-L830) +[^getpathsout-noripple]: getPathsOut skips trust lines where the peer has NoRipple set: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L805-L806) +[^getpathsout-freeze]: getPathsOut skips trust lines where the peer has frozen the line: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L807-L808) +[^getpathsout-mpt-loop]: MPT scoring loop: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L813-L831) +[^getpathsout-mpt-match]: MPT ID match check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L818-L820) +[^getpathsout-mpt-balance]: MPT zero balance or maxed out check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L818-L820) +[^getpathsout-mpt-auth]: MPT authorization check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L818-L820) +[^getpathsout-mpt-destination]: MPT destination bonus of +10000: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L821-L825) +[^getpathsout-mpt-frozen]: MPT frozen check (redundant with outer freeze check): [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L826-L827) +[^getpathsout-mpt-count]: MPT count increment: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/xrpld/rpc/detail/Pathfinder.cpp#L827-L828) +[^compare-account-candidate]: compareAccountCandidate sorts by priority descending, then account ID descending: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L108-L124) +[^dest-complete-path]: Destination account with matching asset completes the path: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1121-L1130) +[^dest-high-priority]: Destination account with non-matching asset receives high priority directly: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1132-L1136) +[^getpathsout-zero-filter]: Candidates with score 0 are not added: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1152) +[^candidate-extend]: Selected candidates are extended into incomplete paths: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1196-L1206) +[^mpt-peer-issuer]: MPT peer is always the issuer, extracted from the MPTID: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L1057-L1059), [`MPTIssue.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/MPTIssue.h#L84-L93) +[^mpt-no-reverse]: Pathfinding targets `mEffectiveDst` (the issuer for non-XRP destinations), so it never needs to navigate from issuer to holder. The flow engine handles the final hop to the destination holder via path normalization: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/paths/PaySteps.cpp#L261-L320) + +Accounts that pass all filters become candidates. When the candidate is the destination account and the current asset matches the destination asset, the path is complete[^dest-complete-path]. When the candidate is the destination account but the asset does not match, it receives a score of 10000 directly, bypassing `getPathsOut()`[^dest-high-priority]. All other candidates are scored by `getPathsOut()`[^getpathsout], which counts the number of viable onward connections from that account in the current asset. + +Candidates that score 0 are excluded entirely[^getpathsout-zero-filter], because an account with no viable onward connections (e.g., globally frozen, or all trust lines are frozen, unauthorized, or have NoRipple set) would be a dead end in the path. Non-zero scores determine priority during sorting. In the rules below, **Skipped** means the connection does not count toward the score (+0): + +1. If the account is globally frozen for the asset, no scoring happens and the count stays 0[^getpathsout-frozen]. +2. Otherwise, the function checks whether the account has `lsfRequireAuth` set[^getpathsout-auth]. +3. The score starts with the number of order books available for the asset[^getpathsout-booksize]. +4. For IOU currencies, the function iterates over trust lines from `getRippleLines(account, direction)` and for each trust line[^getpathsout-iou-loop]: + - **Skipped** if the trust line currency does not match the current asset + - **Skipped** if the balance is zero or negative and the peer has no available credit, or if `lsfRequireAuth` is set (step 2) and the trust line is not authorized + - **+10000** if the current asset matches the destination asset and the peer is the destination account[^getpathsout-destination-bonus] + - **Skipped** if the peer has NoRipple set on its side of the trust line[^getpathsout-noripple] + - **Skipped** if the peer has frozen the trust line[^getpathsout-freeze] + - **+1** otherwise +5. For MPTs, the function iterates over MPTs from `getMPTs(account)` and for each MPT[^getpathsout-mpt-loop]: + - **Skipped** if the MPT ID does not match the current asset[^getpathsout-mpt-match] + - **Skipped** if zero balance or maxed out[^getpathsout-mpt-balance] + - **Skipped** if authorization is required (step 2)[^getpathsout-mpt-auth] + - **+10000** if the current asset matches the destination asset and the peer is the destination account[^getpathsout-mpt-destination] + - **Skipped** if frozen (redundant with the outer freeze check in step 1, but present in the code)[^getpathsout-mpt-frozen] + - **+1** otherwise[^getpathsout-mpt-count] + +`addLink` then sorts all candidates by score descending, with account ID as a tiebreaker[^compare-account-candidate], and selects the top 50 if expanding from the source account, or top 10 otherwise. For each selected candidate, `addLink` extends the current path by appending the candidate as an account element and adds the extended path to `incompletePaths`[^candidate-extend]. These incomplete paths are fed back into `addLink` by `addPathsForType` for further expansion in subsequent rounds. + +**Book expansion** (`afADD_BOOKS`): + +When `afOB_XRP` is set, the function checks whether an order book exists from the current asset to XRP (using domain filtering if configured). If found, it adds an XRP book element to the path. + +Without `afOB_XRP`, the function queries all order books where the current asset is the input currency (TakerPays). Each book undergoes these filters: + +1. **Output loop detection** - Rejects books whose output asset/issuer was already seen using `hasSeen(xrpAccount(), book.out, book.out.getIssuer())` +2. **Origin issuer check** - Rejects books that would create a loop back to the source issuer +3. **Destination asset filter** - When `afOB_LAST` is set, rejects books not outputting the destination asset + +For books outputting XRP, the function adds an XRP book element. If the destination amount is XRP, this completes the path; otherwise the path is added to `incompletePaths` for further expansion. + +For books outputting non-XRP assets, the function performs an additional check using `hasSeen(book.out.getIssuer(), book.out, book.out.getIssuer())` to prevent issuer loops. It then adds the book element, with an optimization: if the path already has a book -> account -> book pattern, it replaces the redundant intermediate account with the new book element. + +After adding the book element, the function determines whether the path is complete. If the destination requires reaching a specific issuer (non-XRP destination), the function checks whether the book's output issuer matches. When the issuer matches the destination account but differs from the effective destination, the path is rejected (this indicates an issuer bypass violation). When the issuer matches the effective destination and the asset matches, the path is complete. Otherwise, the function appends the issuer's account element. + +### 4.4.1. addLink pseudoCode + +```python +def addLink(currentPath, incompletePaths, addFlags, continueCallback): + pathEnd = currentPath.back() if currentPath else mSource + endPathAsset = pathEnd.getPathAsset() + endAccount = pathEnd.getAccountID() + isOnXRP = isXRP(endPathAsset) + hasEffectiveDst = mEffectiveDst != mDstAccount + destOnly = afAC_LAST in addFlags + + if afADD_ACCOUNTS in addFlags: + if isOnXRP: + if mDstAmount.isXRP() and not currentPath.empty(): + mCompletePaths.add(currentPath) # Complete XRP->XRP path + elif endAccount.exists(): + # Check if the trust line between the previous account and the endpoint account + # has NoRipple set on the endpoint account's side + noRippleOut = isNoRippleOut(currentPath) + direction = LineDirection.incoming if noRippleOut else LineDirection.outgoing + + # Get trust lines or MPTs from current account, based on asset type. + # For IOUs, getRippleLines returns trust lines filtered by direction: + # incoming = only lines where the account does not have NoRipple set (best effort, see AssetCache) + # outgoing = all trust lines + # For MPTs, getMPTs returns all MPT holdings for the account. + if isIOU(endPathAsset): + assets = mAssetCache.getRippleLines(endAccount, direction) + else: + assets = mAssetCache.getMPTs(endAccount) + candidates = [] + + for asset in assets: + if continueCallback.shouldBreak(): + return + + # Get peer account (for trust lines) or issuer account (for MPTs) + peerAccount = asset.getAccountIDPeer() if isTrustLine(asset) else asset.getIssuer() + + # Skip if issuer bypass + if hasEffectiveDst and peerAccount == mDstAccount: + continue + + # Check if this is the destination + isDestination = peerAccount == mEffectiveDst + + # Destination-only filter + if destOnly and not isDestination: + continue + + # Skip if asset does not match the path's current currency/MPTID + if not correctAsset(asset, endPathAsset): + continue + + # Skip if creates loop + if currentPath.hasSeen(peerAccount, endPathAsset, peerAccount): + continue + + # Skip if insufficient liquidity or NoRipple violation + # For trust lines: (balance > 0 OR (available credit AND authorized)) + # AND NOT (noRippleOut AND asset.getNoRipple()) + # For MPTs: balance > 0 AND not maxed out AND authorized + if not hasLiquidity(asset): + continue + + # Handle destination account + if isDestination: + if endPathAsset == mDstAmount.asset(): + if not currentPath.empty(): + mCompletePaths.add(currentPath) # Complete path + elif not destOnly: + candidates.add({priority: HIGH_PRIORITY, account: peerAccount}) + # Skip if going back to source + elif peerAccount == mSrcAccount: + continue + else: + # Rank by paths out + pathsOut = getPathsOut(endPathAsset, peerAccount) + if pathsOut > 0: + candidates.add({priority: pathsOut, account: peerAccount}) + + # Sort and select top candidates + candidates.sort(by: priority descending, then account ID descending) + maxCandidates = 10 if endAccount != mSrcAccount else 50 + + for candidate in candidates[:maxCandidates]: + if continueCallback.shouldBreak(): + return + newPath = currentPath + [accountElement(candidate.account)] + incompletePaths.add(newPath) + + if afADD_BOOKS in addFlags: + if afOB_XRP in addFlags: + # Only add book to XRP (includes domain filtering if mDomain is set) + if not isOnXRP and app.orderBookDB.isBookToXRP(endAsset, mDomain): + incompletePaths.add(currentPath.append(xrpBookElement)) + else: + # Add all viable order books (includes domain filtering if mDomain is set) + books = app.orderBookDB.getBooksByTakerPays(endAsset, mDomain) + + for book in books: + if currentPath.hasSeen(xrpAccount(), book.out, book.out.getIssuer()): + continue + if issueMatchesOrigin(book.out): + continue + # afOB_LAST keeps only books whose output token matches the destination. + # equalTokens compares currency/MPTID and ignores the issuer. + if afOB_LAST in addFlags and not equalTokens(book.out, mDstAmount.asset()): + continue + + newPath = currentPath.append(bookElement(book)) + + if isXRP(book.out): + if mDstAmount.isXRP(): + mCompletePaths.add(newPath) # Complete path + else: + incompletePaths.add(newPath) + elif not currentPath.hasSeen(book.out.getIssuer(), book.out, book.out.getIssuer()): + if hasEffectiveDst and book.out.getIssuer() == mDstAccount and equalTokens(book.out, mDstAmount.asset()): + continue # Skipped required issuer + elif book.out.getIssuer() == mEffectiveDst and book.out.asset == mDstAmount.asset(): + mCompletePaths.add(newPath) # Complete path + else: + # If the path already ends in an account, the issuer-bearing element + # replaces that trailing account rather than being appended after it. + incompletePaths.add(newPath.append(issuerAccount(book.out))) +``` + +## 4.5. OrderBookDB + +`OrderBookDB` serves as an **in-memory index** that catalogs all available trading pairs (order books) on the XRP Ledger. This index enables path finding to rapidly discover which currency conversions are available without scanning the entire ledger for each query. + +OrderBookDB maintains four separate indexes for both offer-based order books and AMM pools: + +- **`allBooks`**: Maps each asset to all assets it can be traded for (includes both offers and AMMs in open order books) +- **`xrpBooks`**: Set of all assets that have a direct trading pair with XRP (subset of `allBooks` for fast XRP bridge lookups) +- **`domainBooks`**: Maps (asset, domainID) pairs to tradeable assets (permissioned order books with domain restrictions, offers only) +- **`xrpDomainBooks`**: Set of (asset, domainID) pairs that have direct XRP trading pairs in permissioned books (offers only) + +### 4.5.1. OrderBookDB Construction + +On startup, `OrderBookDB.update()` ([OrderBookDBImpl.cpp:91-219](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/app/ledger/OrderBookDBImpl.cpp#L91-L219)) scans the entire ledger looking for two types of entries: + +**1. Order book directories (`ltDIR_NODE` with `sfExchangeRate`):** + +Order book directories are created when offers are placed via `OfferCreate` transaction. Each directory represents a specific quality level (exchange rate) for a trading pair. + +When OrderBookDB finds an order book directory (identified by `ltDIR_NODE` type with `sfExchangeRate` field at root), it extracts the trading pair: + +- **TakerPays asset**: Retrieved from `sfTakerPaysCurrency` + `sfTakerPaysIssuer` fields (for IOUs and XRPs) or `sfTakerPaysMPT` field (for MPTs) +- **TakerGets asset**: Retrieved from `sfTakerGetsCurrency` + `sfTakerGetsIssuer` fields (for IOUs and XRPs) or `sfTakerGetsMPT` field (for MPTs) + +The book is registered based on whether it has domain restrictions: + +- **Without domain** (`sfDomainID` not present): + - Registered in `allBooks` + - If TakerGets is XRP: also registered in `xrpBooks` +- **With domain** (`sfDomainID` present): + - Registered in `domainBooks` (indexed by asset + domainID) + - If TakerGets is XRP: also registered in `xrpDomainBooks` + +**2. AMM ledger entries (`ltAMM`):** + +AMM instances are created by `AMMCreate` transaction and provide liquidity without traditional order book directories. Each AMM holds two assets and can facilitate trades in both directions. + +When OrderBookDB finds an AMM (identified by `ltAMM` type), it extracts the two pool assets from `sfAsset` and `sfAsset2` fields. Both trading directions are registered: + +- `asset1 -> asset2` is registered in `allBooks` +- `asset2 -> asset1` is registered in `allBooks` +- If either asset is XRP, the other asset is also registered in `xrpBooks` + +Unlike offer-based order books, AMMs are discovered directly from their `ltAMM` ledger entries without needing order book directory entries (`ltDIR_NODE`). Both offer-based and AMM-based liquidity are indexed together, allowing path finding to treat them uniformly when searching for currency conversion options. + +## 4.6. AssetCache + +`AssetCache` is an **in-memory cache** that provides fast access to trust line and MPT information during path finding. Each AssetCache is tied to a specific ledger view. For `path_find` subscriptions, a single AssetCache is shared across all path finding operations during batch processing (when a ledger changes and there are multiple `path_find` connections open), then deallocated when the batch completes. For one-shot requests like `ripple_path_find`, a new AssetCache is created for each request. + +Assets (trust lines and MPTs) are fetched and cached on-demand for specific accounts as path finding explores the network. + +The AssetCache provides two query methods: +- `getRippleLines(accountID, direction)`[^get-ripple-lines-impl]: Returns trust lines for an account. The `direction` parameter controls filtering: + - `LineDirection::outgoing`: returns all trust lines for the account. + - `LineDirection::incoming`: returns only trust lines where the account does **not** have NoRipple set on its side[^get-trust-line-items-filter]. + + To avoid storing two copies per account, the cache keeps at most one set[^asset-cache-superset]: + - If the full (`outgoing`) set is already cached when `incoming` is requested, the full set is returned. `addLink` relies on its per-candidate NoRipple check to filter out the extra trust lines. + - If the `incoming` subset is cached when `outgoing` is requested, the subset is discarded and the full set is rebuilt[^asset-cache-rebuild]. +- `getMPTs(accountID)`: Returns MPTs held by an account + +[^get-ripple-lines-impl]: AssetCache::getRippleLines: [`AssetCache.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/AssetCache.cpp#L38-L106) +[^get-trust-line-items-filter]: getTrustLineItems filters by direction, excluding trust lines with NoRipple when incoming: [`TrustLine.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/TrustLine.cpp#L61) +[^asset-cache-rebuild]: AssetCache discards incoming subset when outgoing is requested: [`AssetCache.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/AssetCache.cpp#L66-L76) + +# 5. Path Ranking + +After path discovery completes, the pathfinder must evaluate which paths are worth using. Not all discovered paths have sufficient liquidity or good exchange rates - some may have already been consumed by the default path, while others may be too inefficient to be useful. + +## 5.1. computePathRanks + +The `computePathRanks` function evaluates path quality and liquidity by simulating payment execution through the Flow engine. + +**Parameters:** + +| Parameter | Description | Required | +|--------------------|-------------------------------------------------------|----------| +| `maxPaths` | Maximum number of paths to rank and return | ✅ | +| `continueCallback` | Optional callback to check if ranking should continue | ❌ | + +The function performs two key steps: + +**1. Account for the default path** + +The default path is the direct route between source and destination that Flow always attempts first (unless the `tfNoRippleDirect` flag is set). Before ranking discovered paths, path finding must determine how much liquidity the default path provides. + +To measure this, `computePathRanks` calls `RippleCalc.rippleCalculate()` with an empty path set and partial payment enabled[^default-path-partial], which in turn calls Flow and tests only the default path. Partial payment is enabled so the default path can deliver whatever liquidity it has, even if it cannot cover the full amount. RippleCalc simulates payment execution and returns: +- `actualAmountIn` - How much was consumed from the source +- `actualAmountOut` - How much was delivered to the destination +- Result code indicating success or failure + +If the default path succeeds, its delivery is subtracted from `mRemainingAmount`[^remaining-amount-init] to calculate the additional liquidity still needed beyond what the default path provides. + +[^default-path-partial]: Default path tested with partial payment enabled: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L462) +[^remaining-amount-init]: `mRemainingAmount` initialized via `convertAmount`, which returns the largest possible amount in convert-all mode or the destination amount otherwise: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L454) (`convertAmount` defined in [`PathfinderUtils.h`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/PathfinderUtils.h#L19-L26)) + +By accounting for the default path first, path finding ensures that discovered paths are evaluated for their **incremental value** - what they contribute beyond the baseline liquidity that Flow will attempt anyway. + +**2. Rank all discovered paths** + +With `mRemainingAmount` calculated, `computePathRanks` calls `rankPaths` to evaluate all paths in `mCompletePaths`. Each path is tested by simulating its execution through Flow, and successful paths are scored based on quality (exchange rate), liquidity (capacity), and length (number of hops). These rankings determine which paths `getBestPaths` will ultimately select for the payment. + +### 5.1.1. computePathRanks Pseudo-Code + +```python +def computePathRanks(maxPaths: int, continueCallback): + # convertAmount returns the largest possible amount if convert_all_ is true (to find max liquidity) + # otherwise returns mDstAmount unchanged + mRemainingAmount = convertAmount(mDstAmount, convert_all_) + + # The default path is the direct path that always exists (source -> destination) + # We test it first to see how much it can deliver, then rank additional paths + # based on what they add beyond the default + sandbox = PaymentSandbox(mLedger) + inputs = Input(partialPaymentAllowed=True) + rc = RippleCalc.rippleCalculate( + view=sandbox, + maxAmountIn=mSrcAmount, + deliver=mRemainingAmount, + account=mDstAccount, + issuer=mSrcAccount, + paths=[], # Empty path set = test default path only + domain=mDomain, + inputs=inputs + ) + + if rc.success(): + mRemainingAmount -= rc.actualAmountOut + + # Rank all found paths + rankPaths(maxPaths, mCompletePaths, mPathRanks, continueCallback) +``` + +### 5.1.2. rippleCalculate Pseudo-Code + +`RippleCalc.rippleCalculate()` is a wrapper function that calls the [Flow engine](../flow/README.md) to simulate payment execution. + +**Parameters:** + +| Parameter | Description | Required | +|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------| +| `view` | PaymentSandbox view of the ledger for simulation | ✅ | +| `maxAmountIn` | Maximum amount willing to spend from source (SendMax) | ✅ | +| `deliver` | Amount to deliver to destination | ✅ | +| `account` | Destination account ID | ✅ | +| `issuer` | Source account ID | ✅ | +| `paths` | Set of paths to test | ✅ | +| `domain` | Optional domain ID | ❌ | +| `inputs` | Optional Input struct containing flags: `defaultPathsAllowed` (whether to test default path), `partialPaymentAllowed` (whether partial delivery is acceptable), `limitQuality` (whether to enforce quality limit) | ❌ | + +```python +def rippleCalculate(view, maxAmountIn, deliver, account, issuer, paths, domain, inputs=None): + # Create sandbox for simulation + sandbox = PaymentSandbox(view) + + # Extract parameters from inputs or use defaults + defaultPaths = inputs.defaultPathsAllowed if inputs else True + partialPayment = inputs.partialPaymentAllowed if inputs else False + + # Calculate quality limit if requested + qualityLimit = None + if inputs and inputs.limitQuality and maxAmountIn > 0: + qualityLimit = Quality(maxAmountIn / deliver) + + # Determine sendMax (if different from source account's native currency) + sendMax = maxAmountIn if (maxAmountIn >= 0 or + maxAmountIn.currency != deliver.currency or + maxAmountIn.issuer != issuer) else None + + # Call Flow engine to execute payment simulation + flowResult = flow( + sandbox, + deliver, + issuer, + account, + paths, + defaultPaths=defaultPaths, + partialPayment=partialPayment, + limitQuality=qualityLimit, + sendMax=sendMax, + domain=domain + ) + + # Commit the simulated consumption back to the caller's view, so that sequential + # rippleCalculate calls (e.g. the two calls in getPathLiquidity) measure liquidity + # incrementally on the depleted state rather than double-counting it. + sandbox.apply(view) + + return { + result: flowResult.result, + actualAmountIn: flowResult.actualAmountIn, + actualAmountOut: flowResult.actualAmountOut + } +``` + +## 5.2. rankPaths + +The `rankPaths` function evaluates each discovered path by testing its liquidity and quality, then sorts them to identify the best paths for the payment. + +**Parameters:** + +| Parameter | Description | Required | +|--------------------|----------------------------------------------------------------|----------| +| `maxPaths` | Maximum number of paths to rank | ✅ | +| `paths` | Set of complete paths to evaluate (typically `mCompletePaths`) | ✅ | +| `rankedPaths` | Output vector where ranked paths are stored | ✅ | +| `continueCallback` | Optional callback to check if ranking should continue | ❌ | + +The function first calculates a minimum liquidity threshold that each path must meet to be worth including. This threshold serves as a quality filter, preventing the pathfinder from wasting computational resources ranking paths that contribute only negligible amounts to the payment. + +When not in convert-all mode, this threshold is `dstAmount / (maxPaths + 2)`, where `maxPaths` is hardcoded to **4** in the `xrpld` implementation. This ensures each path can deliver at least a meaningful fraction of the destination amount. In convert-all mode, the threshold is set to the largest possible amount to find maximum available liquidity. + +Convert-all mode is triggered when the destination amount equals the maximum possible value for that currency (checked via `convertAllCheck(mDstAmount)`). This mode is used when discovering maximum available liquidity rather than targeting a specific amount - for example, when a user wants to convert their entire balance of one currency to another. In convert-all mode, path finding prioritizes liquidity over quality, finding paths that can move the most value regardless of exchange rates. + +For each path in the input set, `rankPaths` calls `getPathLiquidity` to simulate its execution and measure how much it can deliver. Paths that fail or cannot meet the minimum threshold are discarded. Successful paths are recorded as `PathRank` entries containing their quality (exchange rate), liquidity (capacity), length (hop count), and original index. + +Finally, the function sorts all ranked paths using multiple criteria in order of importance: quality (better exchange rates first, unless in convert-all mode), liquidity (higher capacity first), length (shorter paths first), and index (as a tie breaker). This sorted ranking determines which paths `getBestPaths` will ultimately select for the payment. + +### 5.2.1. rankPaths Pseudo-Code + +```python +def rankPaths(maxPaths, paths, rankedPaths, continueCallback): + rankedPaths.clear() + + if convert_all_: + minDstAmount = largestAmount(dstAmount) + else: + minDstAmount = dstAmount / (maxPaths + 2) + + for i, path in enumerate(paths): + if continueCallback.shouldBreak(): + return + + ter, liquidity, quality = getPathLiquidity(path, minDstAmount) + + if ter == tesSUCCESS: + rankedPaths.add({ + quality: quality, + length: len(path), + liquidity: liquidity, + index: i + }) + + # Sort by quality, liquidity, length + rankedPaths.sort(key=lambda rank: ( + rank.quality if not convert_all_ else 0, # Quality first (unless convert_all_) + -rank.liquidity, # Higher liquidity better (negative for desc sort) + rank.length, # Shorter better + -rank.index # Tie breaker + )) +``` + +## 5.3. getPathLiquidity + +The `getPathLiquidity` function determines how much liquidity a single path can provide by simulating its execution through the Flow engine. + +The [**Flow engine**](../flow/README.md) takes paths and converts them into executable operations called **strands**. A strand is a sequence of **steps**, where each step is a concrete action that moves value between path elements. Path finding uses Flow to simulate execution and measure how much liquidity each path can actually deliver. + +**Parameters:** + +| Parameter | Description | Required | +|----------------|--------------------------------------------------------------|----------| +| `path` | The path to test for liquidity | ✅ | +| `minDstAmount` | Minimum amount the path must deliver to be considered viable | ✅ | + +The function calls `RippleCalc.rippleCalculate()` to simulate payment execution along the path, with default paths explicitly disabled so only the specific path's liquidity is measured[^getpathliq-no-default]. RippleCalc uses the Flow engine to execute a payment simulation on a sandbox ledger (a copy of the ledger that can be modified without affecting the real ledger state), returning how much was consumed from the source (`actualAmountIn`), how much was delivered to the destination (`actualAmountOut`), and whether the payment succeeded. The first call tests whether the path can deliver at least `minDstAmount`. In convert-all mode, partial payment is allowed to find the maximum available liquidity. In normal mode, the path must deliver exactly the minimum amount or it's rejected. + +[^getpathliq-no-default]: Default paths disabled in getPathLiquidity: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/Pathfinder.cpp#L394) + +If the first call succeeds and we are not in convert-all mode, the function makes a second call to probe for additional liquidity beyond the minimum. This second call attempts to deliver `(dstAmount - amountOut)` with partial payment allowed, discovering how much more the path can provide beyond the minimum threshold. + +The function returns the total liquidity the path can deliver and its initial quality (exchange rate), calculated as `actualAmountIn / actualAmountOut` from the first call. Paths that fail to meet the minimum threshold return an error code instead. + +### 5.3.1. getPathLiquidity Pseudo-Code + +```python +def getPathLiquidity(path, minDstAmount): + pathSet = [path] + sandbox = PaymentSandbox(ledger) + + # Test minimum liquidity (default paths disabled, test only this path) + inputs = Input(defaultPathsAllowed=False, partialPaymentAllowed=convert_all_) + rc = RippleCalc.rippleCalculate( + view=sandbox, + maxAmountIn=srcAmount, + deliver=minDstAmount, + account=dstAccount, + issuer=srcAccount, + paths=pathSet, + domain=domain, + inputs=inputs + ) + + if rc.result != tesSUCCESS: + return rc.result + + quality = rc.actualAmountIn / rc.actualAmountOut + amountOut = rc.actualAmountOut + + if not convert_all_: + # Test remaining liquidity + inputs2 = Input(defaultPathsAllowed=False, partialPaymentAllowed=True) + rc = RippleCalc.rippleCalculate( + view=sandbox, + maxAmountIn=srcAmount, + deliver=dstAmount - amountOut, + account=dstAccount, + issuer=srcAccount, + paths=pathSet, + domain=domain, + inputs=inputs2 + ) + + if rc.result == tesSUCCESS: + amountOut += rc.actualAmountOut + + return (tesSUCCESS, amountOut, quality) +``` + +# 6. Path Selection + +After discovering and ranking paths, `getBestPaths` selects the optimal set of paths to use for the payment. + +In the [RPC layer](#7-rpc-requests), path finding may be called multiple times for the same payment as the ledger state changes (e.g., in `path_find` subscriptions that continuously update). When path finding runs again, previously discovered paths are passed in as `extraPaths` so they can be merged with newly discovered paths, ensuring the best overall set is selected. + +`getBestPaths` takes: + +| Parameter | Description | Required | +|---------------------|---------------------------------------------------------------|----------| +| `maxPaths` | Maximum number of paths to return | ✅ | +| `fullLiquidityPath` | Output parameter for a path that can handle full amount alone | ✅ | +| `extraPaths` | Paths from previous path finding runs (empty for first run) | ✅ | +| `srcIssuer` | Source issuer for validation | ✅ | +| `continueCallback` | Optional callback to check if search should continue | ❌ | + +The function works by merging two sets of ranked paths: +1. **`mPathRanks`** - Paths discovered in the current path finding run (already ranked by `computePathRanks`) +2. **`extraPathRanks`** - Paths from `extraPaths`, which are ranked by calling `rankPaths` at the start of this function + +At each iteration, the function selects the best path from either set (prioritizing better quality, then better liquidity). If both quality and liquidity are identical, both paths are advanced to handle potential duplicates. Selected paths go through issuer constraint validation and slot management rules before being added to the result. + +**Issuer Constraint Validation:** + +During path discovery, the Pathfinder searches for paths of a specific currency (like USD) without constraining which issuer. This allows it to discover all possible USD paths efficiently. However, in the [RPC layer](#7-rpc-requests), path finding is called separately for each source asset the sender holds (USD from IssuerA, USD from IssuerB, etc.). When `getBestPaths` is called for a specific issuer's currency, it must filter the discovered paths to only include those that route through that specific issuer. + +For example, when searching for paths using USD from IssuerA, the Pathfinder discovers all USD paths. But only paths that explicitly route through IssuerA should be returned - otherwise Alice might spend USD from a different issuer she doesn't hold, or the payment might fail. + +To enforce this, `getBestPaths` validates paths from the discovered set (not extra paths, which are assumed already validated): if the source currency issuer is not the source account itself, discovered paths must start with the issuer account element. Paths that don't start with the issuer are skipped - this includes default paths and paths from simpler path types like `"sfd"` that go directly to books without routing through an account first. When a path does start with the issuer, that initial issuer element is removed before being added to the result, since the Flow engine will add it back during path normalization. + +**Path Slot Management:** + +A "path slot" refers to one position in the result set - `getBestPaths` returns up to `maxPaths` paths, so there are `maxPaths` slots available to fill. + +The function applies different selection rules depending on how many path slots remain. If more than one slot is available (`pathsLeft > 1`), it adds the path to the result, subtracts its liquidity from the remaining amount needed, and continues. If only one slot remains (`pathsLeft == 1`), it only adds the path if its liquidity can cover the entire remaining amount - this ensures the last path is useful. If no slots remain (`pathsLeft == 0`) but a path can handle the full `mDstAmount` by itself, it's saved as `fullLiquidityPath` for potential use as a single-path alternative. + +## 6.1. getBestPaths Pseudo-Code + +```python +def getBestPaths(maxPaths, fullLiquidityPath, extraPaths, srcIssuer, continueCallback): + # Rank extra paths + extraPathRanks = [] + rankPaths(maxPaths, extraPaths, extraPathRanks, continueCallback) + + bestPaths = [] + remaining = mRemainingAmount + issuerIsSender = isXRP(srcAsset) or (srcIssuer == srcAccount) + + i = 0 # index into mPathRanks + j = 0 # index into extraPathRanks + + # Merge and select best paths + while i < len(mPathRanks) or j < len(extraPathRanks): + # Reset per-iteration flags (the C++ declares these inside the loop body) + usePath = False + useExtra = False + startsWithIssuer = False + + # Determine which path to use next + if i >= len(mPathRanks): + useExtra = True + elif j >= len(extraPathRanks): + usePath = True + elif extraPathRanks[j].quality < mPathRanks[i].quality: + useExtra = True + elif extraPathRanks[j].quality > mPathRanks[i].quality: + usePath = True + elif extraPathRanks[j].liquidity > mPathRanks[i].liquidity: + useExtra = True + elif extraPathRanks[j].liquidity < mPathRanks[i].liquidity: + usePath = True + else: + usePath = True + useExtra = True # Both might be same path + + rank = mPathRanks[i] if usePath else extraPathRanks[j] + path = mCompletePaths[rank.index] if usePath else extraPaths[rank.index] + + if useExtra: + j += 1 + if usePath: + i += 1 + + pathsLeft = maxPaths - len(bestPaths) + + if pathsLeft == 0 and not fullLiquidityPath.empty(): + break + + # Validate issuer constraint + if not issuerIsSender and usePath: + if isDefaultPath(path) or path[0].getAccountID() != srcIssuer: + continue # Skip paths that don't start with issuer + startsWithIssuer = True + + # Apply selection rules + if pathsLeft > 1 or (pathsLeft > 0 and rank.liquidity >= remaining): + # Add to best paths + pathsLeft -= 1 + remaining -= rank.liquidity + bestPaths.add(removeIssuer(path) if startsWithIssuer else path) + + elif pathsLeft == 0 and rank.liquidity >= dstAmount and fullLiquidityPath.empty(): + # Found extra path that can handle full amount + fullLiquidityPath = (removeIssuer(path) if startsWithIssuer else path) + + return bestPaths +``` + +# 7. RPC Requests + +Both `path_find` and `ripple_path_find` RPCs support an optional `domain` parameter (256-bit hex string) for permissioned DEX functionality. When specified, path finding restricts order book queries to only include offers within the specified domain. + +## 7.1. `ripple_path_find` RPC (Legacy) + +The `ripple_path_find` RPC command is the legacy interface for one-shot path finding requests. It is **deprecated** but still supported. + +**The request flow works as follows:** + +1. Client sends a `ripple_path_find` request with source, destination, and amount +2. Server creates a path finding job and enqueues it +3. The RPC handler uses a **coroutine** that yields while waiting for path finding to complete +4. When path finding finishes, the coroutine resumes and returns the result + +**continueCallback usage:** + +`ripple_path_find` does NOT provide a `continueCallback` when calling `Pathfinder::findPaths()`. It runs the path finding synchronously without interruption checking. This is acceptable because: +- It's a one-shot request (not streaming updates) +- The coroutine mechanism already handles shutdown gracefully +- The search completes relatively quickly with typical search levels + +## 7.2. `path_find` RPC + +The `path_find` RPC command is the modern interface for path finding with three subcommands: + +- `path_find create` - Creates a path finding subscription +- `path_find status` - Gets current status of an active subscription +- `path_find close` - Closes an active subscription + +**The subscription flow works as follows:** + +1. Client creates a subscription with `path_find create` +2. Server creates a `PathRequest` associated with the WebSocket connection +3. The `PathRequest` continuously updates as ledgers close, sending updates to the client +4. Client receives streaming path updates until they close the subscription or disconnect + +**continueCallback usage:** + +`path_find` subscriptions do provide a `continueCallback` to `Pathfinder::findPaths()`: + +```cpp +auto continueCallback = [&getSubscriber, &request]() { + return (bool)getSubscriber(request); +}; +``` + +This callback: +- Returns `true` if the subscriber (WebSocket client) is still connected +- Returns `false` if the client has disconnected +- Allows path finding to abort immediately if the client is no longer listening + +This is critical for subscriptions because: +- path finding can take significant time at high search levels +- Multiple subscriptions may be active simultaneously +- No point computing paths if the client disconnected + +## 7.3. Source Currency Handling + +Both `path_find` and `ripple_path_find` RPCs support the `source_currencies` parameter, which controls which currencies the pathfinder considers as potential sources for funding the payment. + +The `source_currencies` handling happens in the `PathRequest` layer (RPC handler), **not** in the core Pathfinder algorithm. The PathRequest creates one Pathfinder instance per source currency, reusing it across different issuers of that currency: + +```mermaid +flowchart TD + RPC["path_find / ripple_path_find RPC"] + PR["PathRequest
(processes source_currencies parameter)"] + PF1["Pathfinder #1
(source currency: USD)"] + PF2["Pathfinder #2
(source currency: EUR)"] + PF3["Pathfinder #3
(source currency: XRP)"] + EXEC1["findPaths() -> computePathRanks() -> getBestPaths()"] + EXEC2["findPaths() -> computePathRanks() -> getBestPaths()"] + EXEC3["findPaths() -> computePathRanks() -> getBestPaths()"] + + RPC --> PR + PR -->|creates| PF1 + PR -->|creates| PF2 + PR -->|creates| PF3 + PF1 --> EXEC1 + PF2 --> EXEC2 + PF3 --> EXEC3 +``` + +**When `source_currencies` is specified:** + +The client provides an array of currency/issuer pairs (up to 18 currencies): + +```json +{ + "source_currencies": [ + {"currency": "USD", "issuer": "rIssuer1..."}, + {"currency": "EUR", "issuer": "rIssuer2..."}, + {"currency": "XRP"}, + {"mpt_issuance_id": "00000001B2..."} + ] +} +``` + +For each source asset: +1. PathRequest looks up or creates a Pathfinder for that source currency (the cache is keyed by currency, so the Pathfinder is reused across issuers of the same currency) +2. Path discovery (`findPaths`/`computePathRanks`) runs once per currency; path selection (`getBestPaths`) then runs per source asset, filtering the discovered paths to that asset's issuer +3. Results are collected in a hash map keyed by the issuer-qualified asset: `mContext[issue] = pathSet` + +PathRequest does not create a separate Pathfinder for each issuer of an IOU; it reuses one Pathfinder per currency and runs a separate `getBestPaths` call per issuer to filter the shared discovered paths to that issuer. + +**When `source_currencies` is NOT specified:** + +PathRequest auto-discovers source currencies in this order: + +1. **If `send_max` is provided**: Use only the `send_max` currency/issuer +2. **Otherwise, scan the source account** by calling `accountSourceAssets()`: + - Always includes XRP + - Scans all outgoing trust lines and MPTs from the source account + - Includes a currency if either: + - Account has positive balance (has asset to send), OR + - Peer extends credit AND there's available credit remaining (can issue more) + - Limited to 88 currencies maximum (`max_auto_src_cur`, limited at RPC layer) + - Excludes currencies matching the destination currency (when source == destination account) diff --git a/docs/payments/README.md b/docs/payments/README.md index bf27e9a..17dfec0 100644 --- a/docs/payments/README.md +++ b/docs/payments/README.md @@ -1,364 +1,364 @@ -# Index - -- [1. Introduction](#1-introduction) -- [2. Ledger Entries](#2-ledger-entries) - - [2.1. AccountRoot Ledger Entry](#21-accountroot-ledger-entry) - - [2.1.1. Object Identifier](#211-object-identifier) - - [2.1.2. Fields](#212-fields) - - [2.1.2.1. Flags](#2121-flags) - - [2.1.3. Reserves](#213-reserves) - - [2.2. RippleState Ledger Entry](#22-ripplestate-ledger-entry) - - [2.3. MPT Ledger Entries](#23-mpt-ledger-entries) - - [2.4. AMM Ledger Entries](#24-amm-ledger-entries) -- [3. Transactions](#3-transactions) - - [3.1. Payment Transaction](#31-payment-transaction) - - [3.1.1. Failure Conditions](#311-failure-conditions) - - [3.1.2. State Changes](#312-state-changes) -- [4. Payment Execution Paths](#4-payment-execution-paths) - - [4.1. Direct XRP Payment Execution](#41-direct-xrp-payment-execution) - - [4.2. Cross-Currency Payment Execution](#42-cross-currency-payment-execution) - - [4.2.1. Path Finding](#421-path-finding) - - [4.2.2. Flow Execution](#422-flow-execution) - -# 1. Introduction - -Payments are the fundamental mechanism for transferring value on the XRP Ledger. They enable the movement of XRP, -IOUs, and Multi-Purpose Tokens (MPTs) between accounts, either directly or through intermediary paths that -leverage trust lines and order books. - -A Payment transaction can operate in different modes: - -1. [Direct XRP Payment](#41-direct-xrp-payment-execution): Simple transfer of XRP from one account to another -2. [Cross-Currency Payment](#42-cross-currency-payment-execution): Converting one currency to another through intermediary steps, using paths through the decentralized exchange. With the [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment, cross-currency payments support all combinations of XRP, tokens, and MPTs. -3. Since [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2), MPT->MPT payments use the cross-currency payment execution path. See [MPT Payment Execution](../mpts/README.md#4-mpt-payment-execution) for a description of MPT transfer mechanics. Before MPTokensV2, the Payment transactor performed these steps directly rather than through the cross-currency engine. - -The payment system uses the path finding algorithm and the Payment Engine to discover the most efficient routes for cross-currency transactions, -automatically handling currency conversion, fees, and liquidity constraints. Payments can optionally specify a `DomainID` field; when specified, the payment will consume offers only from that domain's order book (domain offers and hybrid offers within that domain). Without a `DomainID`, payments consume from the open order book (open offers and hybrid offers), not from any domain's order book. See [Domain and Hybrid Offers](../offers/README.md#15-permissioned-dex) and [PermissionedDomains documentation](../permissioned_domains/README.md) for details. - -Payments execute either fully or partially (when `tfPartialPayment` flag is set), or fail with an error code if insufficient liquidity exists. - -# 2. Ledger Entries - -The Payment transaction interacts with different ledger entry types depending on the payment mode: - -- **Direct XRP payments**: Modify only `AccountRoot` entries to update XRP balances -- **Direct MPT payments**: Modify `MPToken` entries (sender and receiver balances), and `MPTokenIssuance` entry (to track outstanding amount and transfer fee burns) -- **Cross-currency payments**: May modify `AccountRoot`, `RippleState` (trust lines), `DirectoryNode` (owner directories and order book directories), `Offer` (consuming or partially consuming offers from the order book), `AMM` (when AMM liquidity is used for swaps), `MPTokenIssuance` entry (to track outstanding amount), and `MPToken` entries for sender and receiver holders - -## 2.1. AccountRoot Ledger Entry - -The `AccountRoot` ledger entry represents an account on the XRP Ledger and stores the account's XRP balance, sequence -number, and various flags that control account behavior. - -### 2.1.1. Object Identifier - -The key of the `AccountRoot` object is the result -of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values -concatenated in order: - -- The `AccountRoot` space key `0x0061` (lowercase `a`) -- The `AccountID` of the account. - -### 2.1.2. Fields - -Please -see [AccountRoot Fields](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/accountroot#accountroot-fields) - -#### 2.1.2.1. Flags - -Please -see [AccountRoot Flags](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/accountroot#accountroot-flags) - -Key flags relevant to payments: - -- `lsfRequireDestTag`: Requires incoming payments to include a destination tag -- `lsfDepositAuth`: Requires authorization for incoming payments (except XRP under specific conditions) -- `lsfPasswordSpent`: Tracks whether the account has used its one-time - free [SetRegularKey](https://xrpl.org/docs/references/protocol/transactions/types/setregularkey) transaction. Cleared - when the account receives a direct XRP payment to allow another free regular key change - -### 2.1.3. Reserves - -See [Reserves](https://xrpl.org/docs/concepts/accounts/reserves) for complete details on reserve requirements. - -**Payment-specific reserve behavior:** - -When a Payment transaction creates a new destination account (destination does not exist), the payment must deliver at -least the base reserve amount in XRP. If the XRP amount is below the base reserve, the payment fails with -`tecNO_DST_INSUF_XRP`. - -Under the `Sponsor` amendment, a payment carrying the `tfSponsorCreatedAccount` flag can create the destination account with any positive XRP amount, as small as one drop. The source account then sponsors the new account's base reserve (see the [transactions documentation](../transactions/README.md)). - -## 2.2. RippleState Ledger Entry - -See [Trust Lines Documentation](../trust_lines/README.md#21-ripplestate-ledger-entry) for complete details on -`RippleState` ledger entry. - -## 2.3. MPT Ledger Entries - -See [MPT Documentation](../mpts/README.md#2-ledger-entries) for complete details on MPT related ledger entries. - -## 2.4. AMM Ledger Entries - -See [AMM Documentation](../amms/README.md#2-ledger-entries) for complete details on AMM related ledger entries. - - -# 3. Transactions - -## 3.1. Payment Transaction - -The Payment transaction transfers value from one account to another, supporting [XRP](../glossary.md#xrp), [IOUs](../glossary.md#iou), and [MPTs](../glossary.md#mpt). -It can operate as a simple direct transfer or use pathfinding for cross-currency conversions. - -Fields are described -in [Payment Fields](https://xrpl.org/docs/references/protocol/transactions/types/payment#payment-fields) - -Flags are described -in [Payment Flags](https://xrpl.org/docs/references/protocol/transactions/types/payment#payment-flags) - -**Automatic Pathfinding with `build_path`** - -The `sign` and `submit` RPC commands accept a `build_path` parameter that triggers automatic pathfinding before signing/submitting the transaction: - -```json -{ - "command": "sign", - "tx_json": { - "TransactionType": "Payment", - "Account": "rSource...", - "Destination": "rDest...", - "Amount": { - "currency": "USD", - "value": "100", - "issuer": "rGateway..." - } - }, - "build_path": true -} -``` - -When `build_path` is `true`: - -- [Pathfinding](../path_finding/README.md) runs automatically using the `path_search_old` configuration value -- Up to 4 best paths are found and inserted into the `Paths` field[^build-path] -- Rejected if `Paths` is already specified -- Rejected for XRP-to-XRP payments - -[^build-path]: [`TransactionSign.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/TransactionSign.cpp#L315-L321) - -### 3.1.1. Failure Conditions - -**Static validation**[^static-validation] - -[^static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L86-L93), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L97-L109), [`preflight`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L113-L287) -[^sponsor-created-account]: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L125-L138), [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L399-L423), [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L495-L521) - -The following preflight failure conditions apply. Cases that depend on a specific amendment are noted inline: - -- `temDISABLED`: - - transaction contains `sfCredentialIDs` and the [Credentials](https://xrpl.org/resources/known-amendments#credentials) amendment is not enabled. - - transaction contains `sfDomainID` and the [PermissionedDEX](https://xrpl.org/resources/known-amendments#permissioneddex) amendment is not enabled. - - `Amount` is an MPT and the [MPTokensV1](https://xrpl.org/resources/known-amendments#mptokensv1) amendment is not enabled. - - transaction contains `tfSponsorCreatedAccount` and the `Sponsor` amendment is not enabled. -- `temINVALID_FLAG`: - - transaction flags contain invalid flags for the payment type. - - `tfSponsorCreatedAccount` is combined with `tfNoRippleDirect`, `tfPartialPayment`, or `tfLimitQuality`. -- `temINVALID`: `tfSponsorCreatedAccount` with a `SendMax` or `Paths` field.[^sponsor-created-account] -- `temMALFORMED`: - - `sfCredentialIDs` array is empty or exceeds maximum size of 8. To leave credential IDs out, leave out the entire field. - - `sfCredentialIDs` array contains duplicate credential IDs - - `sfDomainID` is present but is all zeros. To omit the domain, leave out the entire field. Enforced under the `fixCleanup3_2_0` amendment.[^domainid-zero] -- `temBAD_AMOUNT`: - - `Amount` is not XRP and the `tfSponsorCreatedAccount` flag is set. - - `Amount` is XRP and mantissa is bigger than `100000000000000000ull`. - - `SendMax` is XRP and mantissa is bigger than `100000000000000000ull`.[^isLegalNet-sendmax] - - `SendMax` is specified but is negative or zero. - - `Amount` is negative or zero. - - `DeliverMin` is specified without `tfPartialPayment`. - - `DeliverMin` is XRP and mantissa is bigger than `100000000000000000ull`, or `DeliverMin` is negative or zero.[^delivermin-checks] - - `DeliverMin` asset (currency and issuer, or MPT issuance) differs from the `Amount` asset. - - `DeliverMin` is greater than `Amount`. - - Any `STAmount` field in the transaction is non-canonical (fails `isLegalNet` or `isLegalMPT`); a universal check applied to all transaction types under the `fixCleanup3_2_0` amendment.[^preflight-universal] -- `temBAD_CURRENCY`: either `Amount` or `SendMax` (or the implied source amount) is a non-native IOU that uses the XRP currency code, or (with MPTokensV2 enabled) an MPT whose issuer account is all zero. -- `temDST_NEEDED`: destination account is not specified. -- `temREDUNDANT`: payment is from account to itself with the same currency or MPT issuance and no `Paths` field. `Paths` are required because perhaps they will allow arbitrage. -- `temBAD_SEND_XRP_MAX`: XRP->XRP payment specifies `SendMax`. -- `temBAD_SEND_XRP_PATHS`: XRP->XRP payment specifies `Paths`. -- `temBAD_SEND_XRP_PARTIAL`: XRP->XRP payment has `tfPartialPayment` flag. -- `temBAD_SEND_XRP_LIMIT`: XRP->XRP payment has `tfLimitQuality` flag, or MPT->MPT payment has `tfLimitQuality` flag (only if [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled). -- `temBAD_SEND_XRP_NO_DIRECT`: XRP->XRP payment has `tfNoRippleDirect` flag, or MPT->MPT payment has `tfNoRippleDirect` flag (only if [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled). - -**Validation against the ledger view**[^preclaim-validation] - -[^preclaim-validation]: Validation against ledger view (preclaim): [`checkGranularSemantics`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L290-L356), [`preclaim`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L359-L469) -[^isLegalNet-sendmax]: Both Amount and SendMax checked via isLegalNet: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L160) -[^delivermin-checks]: DeliverMin checked for legal amount and positive value: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L247-L266) -[^domainid-zero]: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L128-L132) -[^preflight-universal]: [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/Transactor.cpp#L260-L267) - -- Destination account does not exist: - - `tecNO_DST`: payment is not XRP - - `telNO_DST_PARTIAL`: `tfPartialPayment` flag is set (XRP payments, since a non-XRP payment fails with `tecNO_DST` first). User cannot fund a new account with a partial payment. Inside a batch (parent batch ID present with `BatchV1_1` enabled), this returns `tefNO_DST_PARTIAL` instead. - - `tecNO_DST_INSUF_XRP`: XRP amount is below reserve (waived when `tfSponsorCreatedAccount` is set: any positive amount funds the account and the source sponsors its base reserve). - - `tecNO_SPONSOR_PERMISSION`: `tfSponsorCreatedAccount` is set but the destination account already exists. -- `tecDST_TAG_NEEDED`: destination account has `lsfRequireDestTag` flag set and transaction did not specify `DestinationTag` field. -- `telBAD_PATH_COUNT`: - - the `Paths` field contains more than 6 paths. - - any `Path` in `Paths` has more than 8 elements. - - Inside a batch (parent batch ID present with `BatchV1_1` enabled), these cases return `tefBAD_PATH_COUNT` instead. -- `tecBAD_CREDENTIALS`: Credential validation failed: - - Any credential ID in `sfCredentialIDs` doesn't exist in the ledger - - Any credential doesn't belong to the source account - - Any credential isn't accepted (missing `lsfAccepted` flag) -- `tecNO_PERMISSION`: `DomainID` is present and either sending or receiving account is not in Domain (not the domain owner and does not hold a valid, non-expired accepted credential). -- `terNO_DELEGATE_PERMISSION`: Transaction specifies a delegate but: - - The delegate authorization doesn't exist in the ledger - - The delegate doesn't have transaction-level permission for Payment - - For granular permissions (`PaymentMint`/`PaymentBurn`, `PermissionDelegationV1_1` amendment): the transaction carries a field or flag outside the granular templates (for example `DeliverMin`, `DomainID`, `Paths`, or any non-universal flag), or `SendMax` names a different asset than `Amount`, or `Amount` is XRP - - For granular permissions with an IOU `Amount`: the issuer is not one of the two endpoints, or the trust line between source and destination does not exist, or `PaymentMint` is held but the payment redeems (the destination's trust limit is not positive or the source currently holds the destination's IOUs), or `PaymentBurn` is held but the source is not currently the holder - - For granular permissions with an MPT `Amount`: `PaymentMint` requires the source to be the MPT issuer and `PaymentBurn` requires the destination to be the MPT issuer - -**Validation during doApply** - -**Direct XRP Payments:**[^direct-xrp-payment] - -[^direct-xrp-payment]: Direct XRP payment execution: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L682-L763) - -- `tefINTERNAL`: Source account does not exist. -- `tecUNFUNDED_PAYMENT`: sending the payment would leave the source account below its required reserve. Under the `Sponsor` amendment the reserve is sponsorship-aware: objects covered by a sponsor stop counting, and objects or accounts the source sponsors are added. When the source is the fee payer, it must cover `Amount` plus the larger of the reserve and the fee. When the fee payer is a delegate or a fee sponsor, the source covers only `Amount` plus the reserve. -- `tecNO_PERMISSION`: Destination is a pseudo-account. -- If the destination has the `lsfDepositAuth` flag set: - - Payment succeeds if source == destination (paying yourself) - - Payment succeeds if destination balance <= base reserve AND payment amount <= base reserve (prevents account - wedging) - - Otherwise, deposit preauthorization is verified and may fail with: - - `tecEXPIRED`: Any credential in `sfCredentialIDs` is expired - - `tecNO_PERMISSION`: Source is not deposit preauthorized by destination (either by account or by credentials) - -**Cross-Currency Payments:** - -- If the destination has the `lsfDepositAuth` flag set, deposit preauthorization is verified and may fail with: - - `tecEXPIRED`: Any credential in `sfCredentialIDs` is expired - - `tecNO_PERMISSION`: Source is not deposit preauthorized by destination (either by account or by credentials) -- RippleCalc, which is a thin wrapper that calls the [Flow engine](../flow/README.md), is invoked to execute the provided payment paths. Flow may fail during path conversion or execution. See [Flow Validation and Error Codes](../flow/README.md#8-validation-and-error-codes) for complete details on error codes that can be returned during cross-currency payment execution. -- `tecPATH_PARTIAL`: `DeliverMin` was specified and the delivered amount is less than `DeliverMin` - -### 3.1.2. State Changes - -**XRP Payments:** - -- `AccountRoot` object is **modified**: - - Source account: Balance decreased by payment amount - - Destination account (if exists): Balance increased by delivered amount - - Destination account (if exists and has `lsfPasswordSpent` flag): Clear the flag - -- `AccountRoot` object is **created**: - - When the destination account does not exist - - Fields set: - - `Account`: Destination account ID - - `Balance`: Payment amount - - `Sequence`: the sequence of the ledger in which the account is created - - `Sponsor`: Set to the source account (only with `tfSponsorCreatedAccount`; the source's `SponsoringAccountCount` is incremented)[^sponsor-created-account] - -**Cross-Currency Payments:** - -State changes for cross-currency payments depend on the execution path determined by RippleCalc/Flow. -See [Flow documentation](../flow/README.md) for detailed mechanics of path execution. - -- `AccountRoot` objects are **modified**: - - Source account: Balance decreased by actual amount consumed - - Destination account: Balance increased by delivered amount - - Intermediate accounts in payment path: Balances adjusted according to path execution - -- `RippleState` objects are **modified**: - - Trust lines along the payment path: Balances updated according to transfers - - Trust line quality and flags may affect transfer amounts - -- `Offer` objects may be **modified** or **deleted**: - - When: BookStep consumes order book liquidity - - Modified: Offer is partially consumed, amounts reduced - - Deleted: Offer is fully consumed or becomes unfunded - -- `DirectoryNode` objects may be **modified**: - - When: Offers are consumed and removed from order book directories - - Directory entries updated to reflect consumed offers - -- `AMM` objects may be **modified**: - - When: AMM liquidity is used for currency conversion - - AMM pool balances adjusted according to swap amounts - -**MPT Payments:** - -- Source's `MPToken` is **modified** (if source is not the issuer): - - `MPTAmount`: Decreased by sent amount (including transfer fee if applicable) - -- Destination's `MPToken` is **modified** (if destination is not the issuer): - - `MPTAmount`: Increased by received amount - -- Transfer fee effect (when neither source nor destination is the issuer): - - `MPTokenIssuance` `OutstandingAmount`: Decreased by transfer fee amount - - The fee is effectively burned from circulation, reducing total supply - -- `MPTokenIssuance` is **modified**: - - When source is issuer: `OutstandingAmount` increased by sent amount (issuer minting tokens) - - When destination is issuer: `OutstandingAmount` decreased by received amount (tokens burned, removed from circulation) - - When neither is issuer: `OutstandingAmount` decreased by the transfer fee amount (if the MPT has a transfer fee; unchanged if it has none) - -**Important**: -- The receiver must already have an `MPToken` entry before receiving a payment (unless the receiver is the issuer). If the receiver has no `MPToken` and is not the issuer, the payment fails with `tecNO_AUTH`. Holders create their `MPToken` entries using the `MPTokenAuthorize` transaction. -- For an MPT that requires authorization (`lsfMPTRequireAuth`), AMM, Vault, and LoanBroker pseudo-accounts are treated as authorized without needing `lsfMPTAuthorized` on their `MPToken` (under the SingleAssetVault or MPTokensV2 amendment). The `MPToken` must still exist. -- The issuer never has an `MPToken` entry and cannot hold a balance of their own issuance. When MPTs are sent to the issuer, they are burned from circulation by decreasing `OutstandingAmount`. - -# 4. Payment Execution Paths - -All payment types are processed through the Payment transaction but follow different execution paths based on the -currencies and parameters involved: - -- **Direct XRP payments**: Execute simple balance transfers when `Amount` is XRP and no `SendMax` and no `Paths` are specified -- **Cross-currency payments**: Invoke the [Flow engine](../flow/README.md) when `SendMax` is specified, `Paths` are provided, or `Amount` is an IOU (or an MPT when the MPTokensV2 amendment is enabled). -- **Direct MPT payments**: Execute MPToken transfers when `Amount` holds an MPT issue and MPTokensV2 amendment is not enabled - -The Payment transaction determines which path to take during the `doApply` phase based on these conditions. - -## 4.1. Direct XRP Payment Execution - -Direct XRP payments are the simplest payment type, transferring XRP directly from one account to another without any intermediate steps or currency conversion. The execution varies based on whether the destination account exists. - -**When the destination account exists**: The payment decreases the source account's `Balance` by the payment amount and increases the destination account's `Balance` by the same amount. If the destination account has the `lsfPasswordSpent` flag set, it is cleared to allow another free `SetRegularKey` transaction. - -**When the destination account does not exist**: A new `AccountRoot` entry is created for the destination with the payment amount as its initial balance. The account's `Sequence` is set to the current ledger sequence. The payment must meet the base reserve requirement (see [Reserves](#213-reserves)), or it fails with `tecNO_DST_INSUF_XRP`. With `tfSponsorCreatedAccount` (`Sponsor` amendment), the base reserve requirement is waived: any positive amount creates the account, the source is recorded as its sponsor, and the source's reserve requirement grows by one base reserve. - -All validation checks are performed before execution, including reserve requirements, deposit authorization, and destination tags. See [Failure Conditions](#311-failure-conditions) for complete validation rules. - -## 4.2. Cross-Currency Payment Execution - -Cross-currency payments convert one currency to another through intermediary steps, leveraging MPTs, trust lines, order books, and AMM liquidity. These payments are executed when the payment specifies `SendMax`, includes `Paths`, or has a non-XRP `Amount` (an IOU, or an MPT when the MPTokensV2 amendment is enabled). - -Cross-currency payment execution involves two complementary components: path finding and flow execution. - -### 4.2.1. Path Finding - -Before a payment can execute, viable routes from source to destination must be discovered. This is handled by the [Path Finding Protocol](../path_finding/README.md), which searches the ledger graph to find potential paths through trust lines (for tokens), order books (for XRP, tokens, and MPTs), and AMM pools (for XRP, tokens, and MPTs). - -Path finding can be initiated in three ways: - -- **User-specified paths**: Clients can provide explicit `Paths` in the Payment transaction, bypassing path finding entirely -- **RPC path finding**: Clients use RPC endpoints to discover paths before submitting the transaction: - - [`path_find`](../path_finding/README.md#72-path_find-rpc) - Modern interface with streaming subscriptions (`path_find create`, `path_find status`, `path_find close`) - - [`ripple_path_find`](../path_finding/README.md#71-ripple_path_find-rpc-legacy) - Legacy one-shot path finding (deprecated but still supported) -- **Default paths**: When no paths are specified, the payment engine automatically generates simple default paths (direct trust line to issuer, XRP-bridged paths) - -The RPC endpoints accept parameters like `source_account`, `destination_account`, `destination_amount`, and optionally `source_currencies` (to constrain which currencies the pathfinder considers as potential sources). For each source currency, the pathfinder creates a separate search instance, discovers paths through the ledger graph, simulates each path to measure quality and liquidity, ranks paths by quality, and returns the best paths to the client. - -### 4.2.2. Flow Execution - -Once paths are available (whether from RPC, user-specified, or default paths), the Payment transaction executes by delegating to the [Flow engine](../flow/README.md). Flow converts the paths into **strands** - sequences of executable **steps** that move value: - -- **DirectStepI**: Transfers tokens between accounts via trust lines -- **BookStep**: Converts currencies through order books and AMM pools -- **XRPEndpointStep**: Transfers XRP to/from accounts -- **MPTEndpointStep**: Transfers MPTs to/from accounts - -Flow [ranks strands by quality](../flow/README.md#62-qualityupperbound) (exchange rate) and [iteratively consumes liquidity](../flow/README.md#6-iterative-strands-evaluation-strandsflow) from the best available strands, dynamically re-ranking as their quality changes, until the payment amount is satisfied or all liquidity is exhausted. Each strand evaluation uses a [two-pass algorithm](../flow/README.md#7-single-strand-evaluation-strandflow): a reverse pass works backwards from the desired output to calculate required input, then a forward pass verifies and executes the actual transfer. - -See [Flow documentation](../flow/README.md) for detailed mechanics of path execution, strand evaluation, and step-by-step state changes. \ No newline at end of file +# Index + +- [1. Introduction](#1-introduction) +- [2. Ledger Entries](#2-ledger-entries) + - [2.1. AccountRoot Ledger Entry](#21-accountroot-ledger-entry) + - [2.1.1. Object Identifier](#211-object-identifier) + - [2.1.2. Fields](#212-fields) + - [2.1.2.1. Flags](#2121-flags) + - [2.1.3. Reserves](#213-reserves) + - [2.2. RippleState Ledger Entry](#22-ripplestate-ledger-entry) + - [2.3. MPT Ledger Entries](#23-mpt-ledger-entries) + - [2.4. AMM Ledger Entries](#24-amm-ledger-entries) +- [3. Transactions](#3-transactions) + - [3.1. Payment Transaction](#31-payment-transaction) + - [3.1.1. Failure Conditions](#311-failure-conditions) + - [3.1.2. State Changes](#312-state-changes) +- [4. Payment Execution Paths](#4-payment-execution-paths) + - [4.1. Direct XRP Payment Execution](#41-direct-xrp-payment-execution) + - [4.2. Cross-Currency Payment Execution](#42-cross-currency-payment-execution) + - [4.2.1. Path Finding](#421-path-finding) + - [4.2.2. Flow Execution](#422-flow-execution) + +# 1. Introduction + +Payments are the fundamental mechanism for transferring value on the XRP Ledger. They enable the movement of XRP, +IOUs, and Multi-Purpose Tokens (MPTs) between accounts, either directly or through intermediary paths that +leverage trust lines and order books. + +A Payment transaction can operate in different modes: + +1. [Direct XRP Payment](#41-direct-xrp-payment-execution): Simple transfer of XRP from one account to another +2. [Cross-Currency Payment](#42-cross-currency-payment-execution): Converting one currency to another through intermediary steps, using paths through the decentralized exchange. With the [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment, cross-currency payments support all combinations of XRP, tokens, and MPTs. +3. Since [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2), MPT->MPT payments use the cross-currency payment execution path. See [MPT Payment Execution](../mpts/README.md#4-mpt-payment-execution) for a description of MPT transfer mechanics. Before MPTokensV2, the Payment transactor performed these steps directly rather than through the cross-currency engine. + +The payment system uses the path finding algorithm and the Payment Engine to discover the most efficient routes for cross-currency transactions, +automatically handling currency conversion, fees, and liquidity constraints. Payments can optionally specify a `DomainID` field; when specified, the payment will consume offers only from that domain's order book (domain offers and hybrid offers within that domain). Without a `DomainID`, payments consume from the open order book (open offers and hybrid offers), not from any domain's order book. See [Domain and Hybrid Offers](../offers/README.md#15-permissioned-dex) and [PermissionedDomains documentation](../permissioned_domains/README.md) for details. + +Payments execute either fully or partially (when `tfPartialPayment` flag is set), or fail with an error code if insufficient liquidity exists. + +# 2. Ledger Entries + +The Payment transaction interacts with different ledger entry types depending on the payment mode: + +- **Direct XRP payments**: Modify only `AccountRoot` entries to update XRP balances +- **Direct MPT payments**: Modify `MPToken` entries (sender and receiver balances), and `MPTokenIssuance` entry (to track outstanding amount and transfer fee burns) +- **Cross-currency payments**: May modify `AccountRoot`, `RippleState` (trust lines), `DirectoryNode` (owner directories and order book directories), `Offer` (consuming or partially consuming offers from the order book), `AMM` (when AMM liquidity is used for swaps), `MPTokenIssuance` entry (to track outstanding amount), and `MPToken` entries for sender and receiver holders + +## 2.1. AccountRoot Ledger Entry + +The `AccountRoot` ledger entry represents an account on the XRP Ledger and stores the account's XRP balance, sequence +number, and various flags that control account behavior. + +### 2.1.1. Object Identifier + +The key of the `AccountRoot` object is the result +of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values +concatenated in order: + +- The `AccountRoot` space key `0x0061` (lowercase `a`) +- The `AccountID` of the account. + +### 2.1.2. Fields + +Please +see [AccountRoot Fields](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/accountroot#accountroot-fields) + +#### 2.1.2.1. Flags + +Please +see [AccountRoot Flags](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/accountroot#accountroot-flags) + +Key flags relevant to payments: + +- `lsfRequireDestTag`: Requires incoming payments to include a destination tag +- `lsfDepositAuth`: Requires authorization for incoming payments (except XRP under specific conditions) +- `lsfPasswordSpent`: Tracks whether the account has used its one-time + free [SetRegularKey](https://xrpl.org/docs/references/protocol/transactions/types/setregularkey) transaction. Cleared + when the account receives a direct XRP payment to allow another free regular key change + +### 2.1.3. Reserves + +See [Reserves](https://xrpl.org/docs/concepts/accounts/reserves) for complete details on reserve requirements. + +**Payment-specific reserve behavior:** + +When a Payment transaction creates a new destination account (destination does not exist), the payment must deliver at +least the base reserve amount in XRP. If the XRP amount is below the base reserve, the payment fails with +`tecNO_DST_INSUF_XRP`. + +Under the `Sponsor` amendment, a payment carrying the `tfSponsorCreatedAccount` flag can create the destination account with any positive XRP amount, as small as one drop. The source account then sponsors the new account's base reserve (see the [transactions documentation](../transactions/README.md)). + +## 2.2. RippleState Ledger Entry + +See [Trust Lines Documentation](../trust_lines/README.md#21-ripplestate-ledger-entry) for complete details on +`RippleState` ledger entry. + +## 2.3. MPT Ledger Entries + +See [MPT Documentation](../mpts/README.md#2-ledger-entries) for complete details on MPT related ledger entries. + +## 2.4. AMM Ledger Entries + +See [AMM Documentation](../amms/README.md#2-ledger-entries) for complete details on AMM related ledger entries. + + +# 3. Transactions + +## 3.1. Payment Transaction + +The Payment transaction transfers value from one account to another, supporting [XRP](../glossary.md#xrp), [IOUs](../glossary.md#iou), and [MPTs](../glossary.md#mpt). +It can operate as a simple direct transfer or use pathfinding for cross-currency conversions. + +Fields are described +in [Payment Fields](https://xrpl.org/docs/references/protocol/transactions/types/payment#payment-fields) + +Flags are described +in [Payment Flags](https://xrpl.org/docs/references/protocol/transactions/types/payment#payment-flags) + +**Automatic Pathfinding with `build_path`** + +The `sign` and `submit` RPC commands accept a `build_path` parameter that triggers automatic pathfinding before signing/submitting the transaction: + +```json +{ + "command": "sign", + "tx_json": { + "TransactionType": "Payment", + "Account": "rSource...", + "Destination": "rDest...", + "Amount": { + "currency": "USD", + "value": "100", + "issuer": "rGateway..." + } + }, + "build_path": true +} +``` + +When `build_path` is `true`: + +- [Pathfinding](../path_finding/README.md) runs automatically using the `path_search_old` configuration value +- Up to 4 best paths are found and inserted into the `Paths` field[^build-path] +- Rejected if `Paths` is already specified +- Rejected for XRP-to-XRP payments + +[^build-path]: [`TransactionSign.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/xrpld/rpc/detail/TransactionSign.cpp#L315-L321) + +### 3.1.1. Failure Conditions + +**Static validation**[^static-validation] + +[^static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L86-L93), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L97-L109), [`preflight`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L113-L287) +[^sponsor-created-account]: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L125-L138), [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L399-L423), [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L495-L521) + +The following preflight failure conditions apply. Cases that depend on a specific amendment are noted inline: + +- `temDISABLED`: + - transaction contains `sfCredentialIDs` and the [Credentials](https://xrpl.org/resources/known-amendments#credentials) amendment is not enabled. + - transaction contains `sfDomainID` and the [PermissionedDEX](https://xrpl.org/resources/known-amendments#permissioneddex) amendment is not enabled. + - `Amount` is an MPT and the [MPTokensV1](https://xrpl.org/resources/known-amendments#mptokensv1) amendment is not enabled. + - transaction contains `tfSponsorCreatedAccount` and the `Sponsor` amendment is not enabled. +- `temINVALID_FLAG`: + - transaction flags contain invalid flags for the payment type. + - `tfSponsorCreatedAccount` is combined with `tfNoRippleDirect`, `tfPartialPayment`, or `tfLimitQuality`. +- `temINVALID`: `tfSponsorCreatedAccount` with a `SendMax` or `Paths` field.[^sponsor-created-account] +- `temMALFORMED`: + - `sfCredentialIDs` array is empty or exceeds maximum size of 8. To leave credential IDs out, leave out the entire field. + - `sfCredentialIDs` array contains duplicate credential IDs + - `sfDomainID` is present but is all zeros. To omit the domain, leave out the entire field. Enforced under the `fixCleanup3_2_0` amendment.[^domainid-zero] +- `temBAD_AMOUNT`: + - `Amount` is not XRP and the `tfSponsorCreatedAccount` flag is set. + - `Amount` is XRP and mantissa is bigger than `100000000000000000ull`. + - `SendMax` is XRP and mantissa is bigger than `100000000000000000ull`.[^isLegalNet-sendmax] + - `SendMax` is specified but is negative or zero. + - `Amount` is negative or zero. + - `DeliverMin` is specified without `tfPartialPayment`. + - `DeliverMin` is XRP and mantissa is bigger than `100000000000000000ull`, or `DeliverMin` is negative or zero.[^delivermin-checks] + - `DeliverMin` asset (currency and issuer, or MPT issuance) differs from the `Amount` asset. + - `DeliverMin` is greater than `Amount`. + - Any `STAmount` field in the transaction is non-canonical (fails `isLegalNet` or `isLegalMPT`); a universal check applied to all transaction types under the `fixCleanup3_2_0` amendment.[^preflight-universal] +- `temBAD_CURRENCY`: either `Amount` or `SendMax` (or the implied source amount) is a non-native IOU that uses the XRP currency code, or (with MPTokensV2 enabled) an MPT whose issuer account is all zero. +- `temDST_NEEDED`: destination account is not specified. +- `temREDUNDANT`: payment is from account to itself with the same currency or MPT issuance and no `Paths` field. `Paths` are required because perhaps they will allow arbitrage. +- `temBAD_SEND_XRP_MAX`: XRP->XRP payment specifies `SendMax`. +- `temBAD_SEND_XRP_PATHS`: XRP->XRP payment specifies `Paths`. +- `temBAD_SEND_XRP_PARTIAL`: XRP->XRP payment has `tfPartialPayment` flag. +- `temBAD_SEND_XRP_LIMIT`: XRP->XRP payment has `tfLimitQuality` flag, or MPT->MPT payment has `tfLimitQuality` flag (only if [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled). +- `temBAD_SEND_XRP_NO_DIRECT`: XRP->XRP payment has `tfNoRippleDirect` flag, or MPT->MPT payment has `tfNoRippleDirect` flag (only if [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is not enabled). + +**Validation against the ledger view**[^preclaim-validation] + +[^preclaim-validation]: Validation against ledger view (preclaim): [`checkGranularSemantics`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L290-L356), [`preclaim`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L359-L469) +[^isLegalNet-sendmax]: Both Amount and SendMax checked via isLegalNet: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L160) +[^delivermin-checks]: DeliverMin checked for legal amount and positive value: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L247-L266) +[^domainid-zero]: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L128-L132) +[^preflight-universal]: [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/Transactor.cpp#L260-L267) + +- Destination account does not exist: + - `tecNO_DST`: payment is not XRP + - `telNO_DST_PARTIAL`: `tfPartialPayment` flag is set (XRP payments, since a non-XRP payment fails with `tecNO_DST` first). User cannot fund a new account with a partial payment. Inside a batch (parent batch ID present with `BatchV1_1` enabled), this returns `tefNO_DST_PARTIAL` instead. + - `tecNO_DST_INSUF_XRP`: XRP amount is below reserve (waived when `tfSponsorCreatedAccount` is set: any positive amount funds the account and the source sponsors its base reserve). + - `tecNO_SPONSOR_PERMISSION`: `tfSponsorCreatedAccount` is set but the destination account already exists. +- `tecDST_TAG_NEEDED`: destination account has `lsfRequireDestTag` flag set and transaction did not specify `DestinationTag` field. +- `telBAD_PATH_COUNT`: + - the `Paths` field contains more than 6 paths. + - any `Path` in `Paths` has more than 8 elements. + - Inside a batch (parent batch ID present with `BatchV1_1` enabled), these cases return `tefBAD_PATH_COUNT` instead. +- `tecBAD_CREDENTIALS`: Credential validation failed: + - Any credential ID in `sfCredentialIDs` doesn't exist in the ledger + - Any credential doesn't belong to the source account + - Any credential isn't accepted (missing `lsfAccepted` flag) +- `tecNO_PERMISSION`: `DomainID` is present and either sending or receiving account is not in Domain (not the domain owner and does not hold a valid, non-expired accepted credential). +- `terNO_DELEGATE_PERMISSION`: Transaction specifies a delegate but: + - The delegate authorization doesn't exist in the ledger + - The delegate doesn't have transaction-level permission for Payment + - For granular permissions (`PaymentMint`/`PaymentBurn`, `PermissionDelegationV1_1` amendment): the transaction carries a field or flag outside the granular templates (for example `DeliverMin`, `DomainID`, `Paths`, or any non-universal flag), or `SendMax` names a different asset than `Amount`, or `Amount` is XRP + - For granular permissions with an IOU `Amount`: the issuer is not one of the two endpoints, or the trust line between source and destination does not exist, or `PaymentMint` is held but the payment redeems (the destination's trust limit is not positive or the source currently holds the destination's IOUs), or `PaymentBurn` is held but the source is not currently the holder + - For granular permissions with an MPT `Amount`: `PaymentMint` requires the source to be the MPT issuer and `PaymentBurn` requires the destination to be the MPT issuer + +**Validation during doApply** + +**Direct XRP Payments:**[^direct-xrp-payment] + +[^direct-xrp-payment]: Direct XRP payment execution: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/payment/Payment.cpp#L682-L763) + +- `tefINTERNAL`: Source account does not exist. +- `tecUNFUNDED_PAYMENT`: sending the payment would leave the source account below its required reserve. Under the `Sponsor` amendment the reserve is sponsorship-aware: objects covered by a sponsor stop counting, and objects or accounts the source sponsors are added. When the source is the fee payer, it must cover `Amount` plus the larger of the reserve and the fee. When the fee payer is a delegate or a fee sponsor, the source covers only `Amount` plus the reserve. +- `tecNO_PERMISSION`: Destination is a pseudo-account. +- If the destination has the `lsfDepositAuth` flag set: + - Payment succeeds if source == destination (paying yourself) + - Payment succeeds if destination balance <= base reserve AND payment amount <= base reserve (prevents account + wedging) + - Otherwise, deposit preauthorization is verified and may fail with: + - `tecEXPIRED`: Any credential in `sfCredentialIDs` is expired + - `tecNO_PERMISSION`: Source is not deposit preauthorized by destination (either by account or by credentials) + +**Cross-Currency Payments:** + +- If the destination has the `lsfDepositAuth` flag set, deposit preauthorization is verified and may fail with: + - `tecEXPIRED`: Any credential in `sfCredentialIDs` is expired + - `tecNO_PERMISSION`: Source is not deposit preauthorized by destination (either by account or by credentials) +- RippleCalc, which is a thin wrapper that calls the [Flow engine](../flow/README.md), is invoked to execute the provided payment paths. Flow may fail during path conversion or execution. See [Flow Validation and Error Codes](../flow/README.md#8-validation-and-error-codes) for complete details on error codes that can be returned during cross-currency payment execution. +- `tecPATH_PARTIAL`: `DeliverMin` was specified and the delivered amount is less than `DeliverMin` + +### 3.1.2. State Changes + +**XRP Payments:** + +- `AccountRoot` object is **modified**: + - Source account: Balance decreased by payment amount + - Destination account (if exists): Balance increased by delivered amount + - Destination account (if exists and has `lsfPasswordSpent` flag): Clear the flag + +- `AccountRoot` object is **created**: + - When the destination account does not exist + - Fields set: + - `Account`: Destination account ID + - `Balance`: Payment amount + - `Sequence`: the sequence of the ledger in which the account is created + - `Sponsor`: Set to the source account (only with `tfSponsorCreatedAccount`; the source's `SponsoringAccountCount` is incremented)[^sponsor-created-account] + +**Cross-Currency Payments:** + +State changes for cross-currency payments depend on the execution path determined by RippleCalc/Flow. +See [Flow documentation](../flow/README.md) for detailed mechanics of path execution. + +- `AccountRoot` objects are **modified**: + - Source account: Balance decreased by actual amount consumed + - Destination account: Balance increased by delivered amount + - Intermediate accounts in payment path: Balances adjusted according to path execution + +- `RippleState` objects are **modified**: + - Trust lines along the payment path: Balances updated according to transfers + - Trust line quality and flags may affect transfer amounts + +- `Offer` objects may be **modified** or **deleted**: + - When: BookStep consumes order book liquidity + - Modified: Offer is partially consumed, amounts reduced + - Deleted: Offer is fully consumed or becomes unfunded + +- `DirectoryNode` objects may be **modified**: + - When: Offers are consumed and removed from order book directories + - Directory entries updated to reflect consumed offers + +- `AMM` objects may be **modified**: + - When: AMM liquidity is used for currency conversion + - AMM pool balances adjusted according to swap amounts + +**MPT Payments:** + +- Source's `MPToken` is **modified** (if source is not the issuer): + - `MPTAmount`: Decreased by sent amount (including transfer fee if applicable) + +- Destination's `MPToken` is **modified** (if destination is not the issuer): + - `MPTAmount`: Increased by received amount + +- Transfer fee effect (when neither source nor destination is the issuer): + - `MPTokenIssuance` `OutstandingAmount`: Decreased by transfer fee amount + - The fee is effectively burned from circulation, reducing total supply + +- `MPTokenIssuance` is **modified**: + - When source is issuer: `OutstandingAmount` increased by sent amount (issuer minting tokens) + - When destination is issuer: `OutstandingAmount` decreased by received amount (tokens burned, removed from circulation) + - When neither is issuer: `OutstandingAmount` decreased by the transfer fee amount (if the MPT has a transfer fee; unchanged if it has none) + +**Important**: +- The receiver must already have an `MPToken` entry before receiving a payment (unless the receiver is the issuer). If the receiver has no `MPToken` and is not the issuer, the payment fails with `tecNO_AUTH`. Holders create their `MPToken` entries using the `MPTokenAuthorize` transaction. +- For an MPT that requires authorization (`lsfMPTRequireAuth`), AMM, Vault, and LoanBroker pseudo-accounts are treated as authorized without needing `lsfMPTAuthorized` on their `MPToken` (under the SingleAssetVault or MPTokensV2 amendment). The `MPToken` must still exist. +- The issuer never has an `MPToken` entry and cannot hold a balance of their own issuance. When MPTs are sent to the issuer, they are burned from circulation by decreasing `OutstandingAmount`. + +# 4. Payment Execution Paths + +All payment types are processed through the Payment transaction but follow different execution paths based on the +currencies and parameters involved: + +- **Direct XRP payments**: Execute simple balance transfers when `Amount` is XRP and no `SendMax` and no `Paths` are specified +- **Cross-currency payments**: Invoke the [Flow engine](../flow/README.md) when `SendMax` is specified, `Paths` are provided, or `Amount` is an IOU (or an MPT when the MPTokensV2 amendment is enabled). +- **Direct MPT payments**: Execute MPToken transfers when `Amount` holds an MPT issue and MPTokensV2 amendment is not enabled + +The Payment transaction determines which path to take during the `doApply` phase based on these conditions. + +## 4.1. Direct XRP Payment Execution + +Direct XRP payments are the simplest payment type, transferring XRP directly from one account to another without any intermediate steps or currency conversion. The execution varies based on whether the destination account exists. + +**When the destination account exists**: The payment decreases the source account's `Balance` by the payment amount and increases the destination account's `Balance` by the same amount. If the destination account has the `lsfPasswordSpent` flag set, it is cleared to allow another free `SetRegularKey` transaction. + +**When the destination account does not exist**: A new `AccountRoot` entry is created for the destination with the payment amount as its initial balance. The account's `Sequence` is set to the current ledger sequence. The payment must meet the base reserve requirement (see [Reserves](#213-reserves)), or it fails with `tecNO_DST_INSUF_XRP`. With `tfSponsorCreatedAccount` (`Sponsor` amendment), the base reserve requirement is waived: any positive amount creates the account, the source is recorded as its sponsor, and the source's reserve requirement grows by one base reserve. + +All validation checks are performed before execution, including reserve requirements, deposit authorization, and destination tags. See [Failure Conditions](#311-failure-conditions) for complete validation rules. + +## 4.2. Cross-Currency Payment Execution + +Cross-currency payments convert one currency to another through intermediary steps, leveraging MPTs, trust lines, order books, and AMM liquidity. These payments are executed when the payment specifies `SendMax`, includes `Paths`, or has a non-XRP `Amount` (an IOU, or an MPT when the MPTokensV2 amendment is enabled). + +Cross-currency payment execution involves two complementary components: path finding and flow execution. + +### 4.2.1. Path Finding + +Before a payment can execute, viable routes from source to destination must be discovered. This is handled by the [Path Finding Protocol](../path_finding/README.md), which searches the ledger graph to find potential paths through trust lines (for tokens), order books (for XRP, tokens, and MPTs), and AMM pools (for XRP, tokens, and MPTs). + +Path finding can be initiated in three ways: + +- **User-specified paths**: Clients can provide explicit `Paths` in the Payment transaction, bypassing path finding entirely +- **RPC path finding**: Clients use RPC endpoints to discover paths before submitting the transaction: + - [`path_find`](../path_finding/README.md#72-path_find-rpc) - Modern interface with streaming subscriptions (`path_find create`, `path_find status`, `path_find close`) + - [`ripple_path_find`](../path_finding/README.md#71-ripple_path_find-rpc-legacy) - Legacy one-shot path finding (deprecated but still supported) +- **Default paths**: When no paths are specified, the payment engine automatically generates simple default paths (direct trust line to issuer, XRP-bridged paths) + +The RPC endpoints accept parameters like `source_account`, `destination_account`, `destination_amount`, and optionally `source_currencies` (to constrain which currencies the pathfinder considers as potential sources). For each source currency, the pathfinder creates a separate search instance, discovers paths through the ledger graph, simulates each path to measure quality and liquidity, ranks paths by quality, and returns the best paths to the client. + +### 4.2.2. Flow Execution + +Once paths are available (whether from RPC, user-specified, or default paths), the Payment transaction executes by delegating to the [Flow engine](../flow/README.md). Flow converts the paths into **strands** - sequences of executable **steps** that move value: + +- **DirectStepI**: Transfers tokens between accounts via trust lines +- **BookStep**: Converts currencies through order books and AMM pools +- **XRPEndpointStep**: Transfers XRP to/from accounts +- **MPTEndpointStep**: Transfers MPTs to/from accounts + +Flow [ranks strands by quality](../flow/README.md#62-qualityupperbound) (exchange rate) and [iteratively consumes liquidity](../flow/README.md#6-iterative-strands-evaluation-strandsflow) from the best available strands, dynamically re-ranking as their quality changes, until the payment amount is satisfied or all liquidity is exhausted. Each strand evaluation uses a [two-pass algorithm](../flow/README.md#7-single-strand-evaluation-strandflow): a reverse pass works backwards from the desired output to calculate required input, then a forward pass verifies and executes the actual transfer. + +See [Flow documentation](../flow/README.md) for detailed mechanics of path execution, strand evaluation, and step-by-step state changes. diff --git a/docs/permissioned_domains/README.md b/docs/permissioned_domains/README.md index 6558442..2c4d24e 100644 --- a/docs/permissioned_domains/README.md +++ b/docs/permissioned_domains/README.md @@ -1,258 +1,258 @@ -# Index - -- [1. Introduction](#1-introduction) - - [1.1. Key Concepts](#11-key-concepts) -- [2. Ledger Entries](#2-ledger-entries) - - [2.1. PermissionedDomain Ledger Entry](#21-permissioneddomain-ledger-entry) - - [2.1.1. Object Identifier](#211-object-identifier) - - [2.1.2. Fields](#212-fields) - - [2.1.3. Pseudo-accounts](#213-pseudo-accounts) - - [2.1.4. Ownership](#214-ownership) - - [2.1.5. Reserves](#215-reserves) - - [2.2. Offer Ledger Entry](#22-offer-ledger-entry) - - [2.2.1. Domain Field](#221-domain-field) - - [2.2.2. Hybrid Offer Fields](#222-hybrid-offer-fields) - - [2.2.2.1. Flags](#2221-flags) -- [3. Transactions](#3-transactions) - - [3.1. PermissionedDomainSet Transaction](#31-permissioneddomainset-transaction) - - [3.1.1. Failure Conditions](#311-failure-conditions) - - [3.1.2. State Changes](#312-state-changes) - - [3.2. PermissionedDomainDelete Transaction](#32-permissioneddomaindelete-transaction) - - [3.2.1. Failure Conditions](#321-failure-conditions) - - [3.2.2. State Changes](#322-state-changes) -- [4. Access Control](#4-access-control) - - [4.1. Domain Membership](#41-domain-membership) - - [4.2. Credential Verification](#42-credential-verification) - -# 1. Introduction - -PermissionedDomains enable credential-based access control for decentralized exchange activity on the XRP Ledger. A domain owner creates a PermissionedDomain specifying which credentials are required, and only accounts holding those credentials can place offers within that domain. This creates segregated order books where trading activity is restricted to authorized participants. Domain restrictions also apply to cross-currency payments that carry a `DomainID`, both the sender and receiver must be in the domain (see [§4.1 Domain Membership](#41-domain-membership)). - -Domain offers support all asset types available on the XRP Ledger: XRP, tokens (issued currencies), and MPTs (Multi-Purpose Tokens, which require the `MPTokensV2` amendment). Any trading pair can be restricted to a permissioned domain. Note that domain offers cross only against the permissioned limit order book; automated market maker (AMM) pools are not consulted for domain crossing.[^amm-no-domain] Under the `fixCleanup3_3_0` amendment, AMM liquidity is also excluded from a domain book's quality estimate, so path ranking matches what domain crossing can deliver.[^amm-no-domain-estimate] - -For example, a securities exchange creates a PermissionedDomain requiring "accredited_investor" credentials from a regulatory authority. When Alice wants to trade: -1. Domain Setup: ExchangeAccountID submits PermissionedDomainSet with: `AcceptedCredentials=[{Issuer: RegulatorAccountID, CredentialType: "accredited_investor"}]` -2. Alice obtains credential: RegulatorAccountID creates and Alice accepts the credential (see [Credentials documentation](../credentials/README.md)) -3. Alice places offer: AliceAccountID submits OfferCreate with `DomainID=ExchangeDomainID` -4. Ledger verification: Checks Alice holds accepted credential from RegulatorAccountID of type "accredited_investor" and not expired -5. Offer placement: Alice's offer is placed in the domain's order book, matching with other domain offers and hybrid offers - -The domain owner always has access to their own domain. All other participants must hold valid credentials. Credentials can be revoked (via expiration or deletion), automatically removing access without the domain owner's involvement. - -[^amm-no-domain]: AMM pools are not consulted when a book has a domain: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/paths/BookStep.cpp#L820-L822) -[^amm-no-domain-estimate]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/paths/BookStep.cpp#L904-L917) - -## 1.1. Terminology and Concepts - -**Domain Owner**: The account that creates and controls the PermissionedDomain. The owner can update the accepted credentials list or delete the domain. The owner always has access to place offers in their own domain regardless of credentials. - -**Domain ID**: A unique 256-bit identifier for the domain, computed as `hash(PERMISSIONED_DOMAIN_NAMESPACE, owner_account, creation_sequence)`. This ID is immutable and used to reference the domain in OfferCreate transactions. - -**AcceptedCredentials**: An array (maximum 10 entries) specifying which credentials grant access to the domain. Each entry contains an Issuer and CredentialType. An account holding any credential matching any entry in this array gains access. - -**Domain Offer**: An offer created with the Domain field set, placed exclusively in the domain's order book. Only accounts with domain access can create domain offers, and domain offers only match with other domain offers or hybrid offers. Domain offers support all asset types: XRP, tokens, and MPTs. - -**Hybrid Offer**: An offer with both the Domain field set and tfHybrid flag enabled. Hybrid offers exist simultaneously in both the domain order book and the open (regular) order book, providing liquidity bridging between permissioned and open markets. - -**Open Offer**: A regular offer without the Domain field, placed in the standard open order book. Open offers are accessible to all accounts and match only with other open offers or hybrid offers. - -# 2. Ledger Entries - -## 2.1. PermissionedDomain Ledger Entry - -### 2.1.1. Object Identifier - -**Type Code**: `ltPERMISSIONED_DOMAIN` = `0x0082` - -**Domain ID Calculation**: `hash(PERMISSIONED_DOMAIN_NAMESPACE, owner_account, creation_sequence)` - -The domain ID is computed at creation using the owner's account and the sequence number consumed by the creating transaction. This can be `Sequence`, or its `TicketSequence` when submitted via a Ticket[^pd-seq]. - -[^pd-seq]: Domain ID and the stored `Sequence` use the transaction's effective sequence (ticket-aware) under the `fixCleanup3_1_3` amendment: [`PermissionedDomainSet.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp#L113-L115) - -### 2.1.2. Fields - -| Field Name | Type | Required | Description | -|-----------------------|-----------|--------------------|-----------------------------------------------| -| `Owner` | AccountID | :heavy_check_mark: | The account that owns this domain | -| `Sequence` | UInt32 | :heavy_check_mark: | Sequence consumed by the creating transaction (`Sequence`, or `TicketSequence` if ticketed) | -| `AcceptedCredentials` | Array | :heavy_check_mark: | Credentials that grant domain access (max 10) | -| `OwnerNode` | UInt64 | :heavy_check_mark: | Owner directory page index | -| `PreviousTxnID` | Hash256 | :heavy_check_mark: | Previous transaction hash | -| `PreviousTxnLgrSeq` | UInt32 | :heavy_check_mark: | Previous transaction ledger sequence | - -**AcceptedCredentials Array Structure**: Each element is an object containing: -- `Issuer` (AccountID): The credential issuer account -- `CredentialType` (Blob): The credential type identifier (max 64 bytes) - -Credentials are sorted by (Issuer, CredentialType) to ensure deterministic storage order. - -### 2.1.3. Pseudo-accounts - -PermissionedDomain transactions (creating, updating, or deleting domains) do not create pseudo-accounts. - -### 2.1.4. Ownership - -PermissionedDomain objects are owned by the account specified in the Owner field. The domain appears in the owner's directory via the OwnerNode field. Only the owner can update or delete the domain. - -### 2.1.5. Reserves - -Creating a PermissionedDomain increases the owner's object count by 1, requiring one owner reserve increment. Deleting the domain decreases the owner count and releases the reserve. - -## 2.2. Offer Ledger Entry - -### 2.2.1. Domain Field - -**Field Name**: `DomainID` (optional, Hash256) - -When present on an Offer ledger entry, this field indicates the offer exists in a permissioned domain's order book. The DomainID must reference an existing PermissionedDomain ledger entry. - -### 2.2.2. Hybrid Offer Fields - -#### 2.2.2.1. Flags - -| Flag Name | Hex Value | Description | -|-------------|--------------|--------------------------------------------------| -| `lsfHybrid` | `0x00040000` | Offer exists in both domain and open order books | - -**AdditionalBooks Field** (Array, optional): Present on hybrid offers, contains references to additional order book directories where the offer appears. Each array element is an object with: -- `BookDirectory` (Hash256): Order book directory hash -- `BookNode` (UInt64): Page index within the directory - -Under the `fixCleanup3_2_0` amendment, when a hybrid offer partially crosses on placement, the open-book `BookDirectory` listed here is keyed by the offer's original placement rate, so it shares the same quality (`ExchangeRate`) as the primary domain `BookDirectory`. Before the amendment the open-book directory was keyed from the post-crossing amounts and could differ slightly due to rounding.[^pd-hybrid-rate] - -[^pd-hybrid-rate]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L944-L953) - -Under the `fixCleanup3_3_0` amendment, a resting hybrid offer's domain membership is re-validated only while the domain book is being walked. Losing domain access, for example through credential expiry, removes the offer during domain-book processing but leaves the open-book entry consumable. Without the amendment, the membership check ran during any book walk, so losing domain access also removed the hybrid offer during open-book processing.[^pd-hybrid-eviction] - -[^pd-hybrid-eviction]: [`OfferStream.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/paths/OfferStream.cpp#L253-L267) - -# 3. Transactions - -## 3.1. PermissionedDomainSet Transaction - -Creates a new PermissionedDomain (when DomainID is omitted) or updates an existing domain's AcceptedCredentials (when DomainID is provided). - -| Field Name | Required? | JSON Type | Internal Type | Description | -|-----------------------|:------------------:|:---------:|:-------------:|:--------------------------------------------| -| `TransactionType` | :heavy_check_mark: | String | UInt16 | Must be `"PermissionedDomainSet"` | -| `Account` | :heavy_check_mark: | String | AccountID | Transaction sender | -| `Fee` | :heavy_check_mark: | String | Amount | Transaction fee | -| `DomainID` | | String | UInt256 | Domain to update (omit for creation) | -| `AcceptedCredentials` | :heavy_check_mark: | Array | Array | Credentials granting domain access (max 10) | - -**AcceptedCredentials Array**: Each element must contain: -- `Issuer` (AccountID): Credential issuer -- `CredentialType` (Blob): Credential type (max 64 bytes) - -### 3.1.1. Failure Conditions - -**Static validation**: -- `temDISABLED`: featurePermissionedDomains or featureCredentials not enabled -- `temARRAY_EMPTY`: AcceptedCredentials array is empty -- `temARRAY_TOO_LARGE`: AcceptedCredentials exceeds 10 entries -- `temINVALID_ACCOUNT_ID`: AcceptedCredentials contains invalid issuer account id -- `temMALFORMED`: - - AcceptedCredentials contains CredentialType that is empty or exceeds 64 bytes - - AcceptedCredentials contains duplicate credentials - - DomainID is all zeros (update case) - -**Validation against the ledger view**: -- `tefINTERNAL`: Account does not exist -- `tecNO_ISSUER`: AcceptedCredentials contains issuer that does not exist -- `tecNO_ENTRY`: DomainID provided but domain does not exist (update case) -- `tecNO_PERMISSION`: DomainID provided but Account is not domain owner (update case) - -**Validation during doApply**: -- `tefINTERNAL`: Failed to create domain SLE (creation case) -- `tecINSUFFICIENT_RESERVE`: Insufficient reserve for owner count increase (creation case) -- `tecDIR_FULL`: Owner directory is full (creation case) - -### 3.1.2. State Changes - -**If DomainID is omitted (creation)**: -- `PermissionedDomain` object is **created** with: - - `Owner`: set to Account - - `Sequence`: set to the transaction's effective sequence (its `Sequence`, or `TicketSequence` if ticketed) - - `AcceptedCredentials`: sorted credentials array - - `OwnerNode`: page index in owner directory -- `Owner`'s owner count is **incremented** by 1 -- `DirectoryNode` entry is **added** to owner's directory - -**If DomainID is provided (update)**: -- `PermissionedDomain` object is **updated**: - - `AcceptedCredentials`: replaced with new sorted credentials array - - `PreviousTxnID` and `PreviousTxnLgrSeq`: updated - -## 3.2. PermissionedDomainDelete Transaction - -Deletes a PermissionedDomain. Only the domain owner can delete their domain. - -| Field Name | Required? | JSON Type | Internal Type | Description | -|------------|:---------:|:---------:|:-------------:|:------------| -| `TransactionType` | :heavy_check_mark: | String | UInt16 | Must be `"PermissionedDomainDelete"` | -| `Account` | :heavy_check_mark: | String | AccountID | Transaction sender (must be domain owner) | -| `Fee` | :heavy_check_mark: | String | Amount | Transaction fee | -| `DomainID` | :heavy_check_mark: | String | UInt256 | Domain to delete | - -### 3.2.1. Failure Conditions - -**Static validation**: -- `temDISABLED`: featurePermissionedDomains not enabled -- `temMALFORMED`: DomainID is all zeros - -**Validation against the ledger view**: -- `tecNO_ENTRY`: DomainID does not exist -- `tecNO_PERMISSION`: Account is not domain owner - -**Validation during doApply**: -- `tefBAD_LEDGER`: Unable to remove directory entry - -### 3.2.2. State Changes - -- `PermissionedDomain` object is **deleted** (removed from ledger) -- `Owner`'s owner count is **decremented** by 1 -- `DirectoryNode` entry is **removed** from owner's directory - -Note: Deleting a domain does not remove existing offers from the order book. Those offers remain in the ledger but become unfunded for domain payments because domain membership verification fails when the domain no longer exists. - -# 4. Access Control - -## 4.1. Domain Membership - -An account is considered "in domain" if either condition is met: - -1. **Owner Access**: Account is the domain owner (specified in PermissionedDomain.Owner field) -2. **Credential Access**: Account holds at least one accepted credential where: - - Subject = Account - - Issuer matches one entry in AcceptedCredentials - - CredentialType matches the same entry in AcceptedCredentials - - lsfAccepted flag is set (0x00010000) - - Credential is not expired (checked against parentCloseTime) - -Domain membership is checked at: -- OfferCreate preclaim: Offer creator must be in domain -- Payment preclaim: Both payer (Account) and payee (Destination) must be in domain (when DomainID field is present) -- Offer matching: Offer creator must remain in domain (otherwise offer becomes unfunded) - -## 4.2. Credential Verification - -The ledger verifies domain access using the `accountInDomain()` function: - -``` -Function: accountInDomain(view, account, domainID) -1. Lookup PermissionedDomain by domainID -2. If account == domain.Owner: return TRUE -3. For each credential in domain.AcceptedCredentials: - a. Lookup Credential(account, credential.Issuer, credential.CredentialType) - b. If credential exists AND lsfAccepted is set AND not expired: - return TRUE -4. Return FALSE -``` - -**Expiration Check**: Credential expiration is compared against the ledger's `parentCloseTime`. Expired credentials are treated as if they don't exist for domain access purposes. During transaction apply, an expired credential encountered while verifying domain membership is also deleted to reclaim its reserve; under the `fixCleanup3_1_3` amendment, if that deletion fails the transaction halts and returns the propagated error (e.g. `tecINTERNAL`) instead of continuing the membership check.[^pd-expiry-delete] - -[^pd-expiry-delete]: [`removeExpired`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/CredentialHelpers.cpp#L61-L64), [`verifyValidDomain`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/CredentialHelpers.cpp#L332-L334) - -**Performance**: Verification iterates through the domain's AcceptedCredentials array (max 10 entries), performing one ledger lookup per credential until a valid match is found. - +# Index + +- [1. Introduction](#1-introduction) + - [1.1. Key Concepts](#11-key-concepts) +- [2. Ledger Entries](#2-ledger-entries) + - [2.1. PermissionedDomain Ledger Entry](#21-permissioneddomain-ledger-entry) + - [2.1.1. Object Identifier](#211-object-identifier) + - [2.1.2. Fields](#212-fields) + - [2.1.3. Pseudo-accounts](#213-pseudo-accounts) + - [2.1.4. Ownership](#214-ownership) + - [2.1.5. Reserves](#215-reserves) + - [2.2. Offer Ledger Entry](#22-offer-ledger-entry) + - [2.2.1. Domain Field](#221-domain-field) + - [2.2.2. Hybrid Offer Fields](#222-hybrid-offer-fields) + - [2.2.2.1. Flags](#2221-flags) +- [3. Transactions](#3-transactions) + - [3.1. PermissionedDomainSet Transaction](#31-permissioneddomainset-transaction) + - [3.1.1. Failure Conditions](#311-failure-conditions) + - [3.1.2. State Changes](#312-state-changes) + - [3.2. PermissionedDomainDelete Transaction](#32-permissioneddomaindelete-transaction) + - [3.2.1. Failure Conditions](#321-failure-conditions) + - [3.2.2. State Changes](#322-state-changes) +- [4. Access Control](#4-access-control) + - [4.1. Domain Membership](#41-domain-membership) + - [4.2. Credential Verification](#42-credential-verification) + +# 1. Introduction + +PermissionedDomains enable credential-based access control for decentralized exchange activity on the XRP Ledger. A domain owner creates a PermissionedDomain specifying which credentials are required, and only accounts holding those credentials can place offers within that domain. This creates segregated order books where trading activity is restricted to authorized participants. Domain restrictions also apply to cross-currency payments that carry a `DomainID`, both the sender and receiver must be in the domain (see [§4.1 Domain Membership](#41-domain-membership)). + +Domain offers support all asset types available on the XRP Ledger: XRP, tokens (issued currencies), and MPTs (Multi-Purpose Tokens, which require the `MPTokensV2` amendment). Any trading pair can be restricted to a permissioned domain. Note that domain offers cross only against the permissioned limit order book; automated market maker (AMM) pools are not consulted for domain crossing.[^amm-no-domain] Under the `fixCleanup3_3_0` amendment, AMM liquidity is also excluded from a domain book's quality estimate, so path ranking matches what domain crossing can deliver.[^amm-no-domain-estimate] + +For example, a securities exchange creates a PermissionedDomain requiring "accredited_investor" credentials from a regulatory authority. When Alice wants to trade: +1. Domain Setup: ExchangeAccountID submits PermissionedDomainSet with: `AcceptedCredentials=[{Issuer: RegulatorAccountID, CredentialType: "accredited_investor"}]` +2. Alice obtains credential: RegulatorAccountID creates and Alice accepts the credential (see [Credentials documentation](../credentials/README.md)) +3. Alice places offer: AliceAccountID submits OfferCreate with `DomainID=ExchangeDomainID` +4. Ledger verification: Checks Alice holds accepted credential from RegulatorAccountID of type "accredited_investor" and not expired +5. Offer placement: Alice's offer is placed in the domain's order book, matching with other domain offers and hybrid offers + +The domain owner always has access to their own domain. All other participants must hold valid credentials. Credentials can be revoked (via expiration or deletion), automatically removing access without the domain owner's involvement. + +[^amm-no-domain]: AMM pools are not consulted when a book has a domain: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/paths/BookStep.cpp#L820-L822) +[^amm-no-domain-estimate]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/paths/BookStep.cpp#L904-L917) + +## 1.1. Terminology and Concepts + +**Domain Owner**: The account that creates and controls the PermissionedDomain. The owner can update the accepted credentials list or delete the domain. The owner always has access to place offers in their own domain regardless of credentials. + +**Domain ID**: A unique 256-bit identifier for the domain, computed as `hash(PERMISSIONED_DOMAIN_NAMESPACE, owner_account, creation_sequence)`. This ID is immutable and used to reference the domain in OfferCreate transactions. + +**AcceptedCredentials**: An array (maximum 10 entries) specifying which credentials grant access to the domain. Each entry contains an Issuer and CredentialType. An account holding any credential matching any entry in this array gains access. + +**Domain Offer**: An offer created with the Domain field set, placed exclusively in the domain's order book. Only accounts with domain access can create domain offers, and domain offers only match with other domain offers or hybrid offers. Domain offers support all asset types: XRP, tokens, and MPTs. + +**Hybrid Offer**: An offer with both the Domain field set and tfHybrid flag enabled. Hybrid offers exist simultaneously in both the domain order book and the open (regular) order book, providing liquidity bridging between permissioned and open markets. + +**Open Offer**: A regular offer without the Domain field, placed in the standard open order book. Open offers are accessible to all accounts and match only with other open offers or hybrid offers. + +# 2. Ledger Entries + +## 2.1. PermissionedDomain Ledger Entry + +### 2.1.1. Object Identifier + +**Type Code**: `ltPERMISSIONED_DOMAIN` = `0x0082` + +**Domain ID Calculation**: `hash(PERMISSIONED_DOMAIN_NAMESPACE, owner_account, creation_sequence)` + +The domain ID is computed at creation using the owner's account and the sequence number consumed by the creating transaction. This can be `Sequence`, or its `TicketSequence` when submitted via a Ticket[^pd-seq]. + +[^pd-seq]: Domain ID and the stored `Sequence` use the transaction's effective sequence (ticket-aware) under the `fixCleanup3_1_3` amendment: [`PermissionedDomainSet.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp#L113-L115) + +### 2.1.2. Fields + +| Field Name | Type | Required | Description | +|-----------------------|-----------|--------------------|-----------------------------------------------| +| `Owner` | AccountID | :heavy_check_mark: | The account that owns this domain | +| `Sequence` | UInt32 | :heavy_check_mark: | Sequence consumed by the creating transaction (`Sequence`, or `TicketSequence` if ticketed) | +| `AcceptedCredentials` | Array | :heavy_check_mark: | Credentials that grant domain access (max 10) | +| `OwnerNode` | UInt64 | :heavy_check_mark: | Owner directory page index | +| `PreviousTxnID` | Hash256 | :heavy_check_mark: | Previous transaction hash | +| `PreviousTxnLgrSeq` | UInt32 | :heavy_check_mark: | Previous transaction ledger sequence | + +**AcceptedCredentials Array Structure**: Each element is an object containing: +- `Issuer` (AccountID): The credential issuer account +- `CredentialType` (Blob): The credential type identifier (max 64 bytes) + +Credentials are sorted by (Issuer, CredentialType) to ensure deterministic storage order. + +### 2.1.3. Pseudo-accounts + +PermissionedDomain transactions (creating, updating, or deleting domains) do not create pseudo-accounts. + +### 2.1.4. Ownership + +PermissionedDomain objects are owned by the account specified in the Owner field. The domain appears in the owner's directory via the OwnerNode field. Only the owner can update or delete the domain. + +### 2.1.5. Reserves + +Creating a PermissionedDomain increases the owner's object count by 1, requiring one owner reserve increment. Deleting the domain decreases the owner count and releases the reserve. + +## 2.2. Offer Ledger Entry + +### 2.2.1. Domain Field + +**Field Name**: `DomainID` (optional, Hash256) + +When present on an Offer ledger entry, this field indicates the offer exists in a permissioned domain's order book. The DomainID must reference an existing PermissionedDomain ledger entry. + +### 2.2.2. Hybrid Offer Fields + +#### 2.2.2.1. Flags + +| Flag Name | Hex Value | Description | +|-------------|--------------|--------------------------------------------------| +| `lsfHybrid` | `0x00040000` | Offer exists in both domain and open order books | + +**AdditionalBooks Field** (Array, optional): Present on hybrid offers, contains references to additional order book directories where the offer appears. Each array element is an object with: +- `BookDirectory` (Hash256): Order book directory hash +- `BookNode` (UInt64): Page index within the directory + +Under the `fixCleanup3_2_0` amendment, when a hybrid offer partially crosses on placement, the open-book `BookDirectory` listed here is keyed by the offer's original placement rate, so it shares the same quality (`ExchangeRate`) as the primary domain `BookDirectory`. Before the amendment the open-book directory was keyed from the post-crossing amounts and could differ slightly due to rounding.[^pd-hybrid-rate] + +[^pd-hybrid-rate]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L944-L953) + +Under the `fixCleanup3_3_0` amendment, a resting hybrid offer's domain membership is re-validated only while the domain book is being walked. Losing domain access, for example through credential expiry, removes the offer during domain-book processing but leaves the open-book entry consumable. Without the amendment, the membership check ran during any book walk, so losing domain access also removed the hybrid offer during open-book processing.[^pd-hybrid-eviction] + +[^pd-hybrid-eviction]: [`OfferStream.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/paths/OfferStream.cpp#L253-L267) + +# 3. Transactions + +## 3.1. PermissionedDomainSet Transaction + +Creates a new PermissionedDomain (when DomainID is omitted) or updates an existing domain's AcceptedCredentials (when DomainID is provided). + +| Field Name | Required? | JSON Type | Internal Type | Description | +|-----------------------|:------------------:|:---------:|:-------------:|:--------------------------------------------| +| `TransactionType` | :heavy_check_mark: | String | UInt16 | Must be `"PermissionedDomainSet"` | +| `Account` | :heavy_check_mark: | String | AccountID | Transaction sender | +| `Fee` | :heavy_check_mark: | String | Amount | Transaction fee | +| `DomainID` | | String | UInt256 | Domain to update (omit for creation) | +| `AcceptedCredentials` | :heavy_check_mark: | Array | Array | Credentials granting domain access (max 10) | + +**AcceptedCredentials Array**: Each element must contain: +- `Issuer` (AccountID): Credential issuer +- `CredentialType` (Blob): Credential type (max 64 bytes) + +### 3.1.1. Failure Conditions + +**Static validation**: +- `temDISABLED`: featurePermissionedDomains or featureCredentials not enabled +- `temARRAY_EMPTY`: AcceptedCredentials array is empty +- `temARRAY_TOO_LARGE`: AcceptedCredentials exceeds 10 entries +- `temINVALID_ACCOUNT_ID`: AcceptedCredentials contains invalid issuer account id +- `temMALFORMED`: + - AcceptedCredentials contains CredentialType that is empty or exceeds 64 bytes + - AcceptedCredentials contains duplicate credentials + - DomainID is all zeros (update case) + +**Validation against the ledger view**: +- `tefINTERNAL`: Account does not exist +- `tecNO_ISSUER`: AcceptedCredentials contains issuer that does not exist +- `tecNO_ENTRY`: DomainID provided but domain does not exist (update case) +- `tecNO_PERMISSION`: DomainID provided but Account is not domain owner (update case) + +**Validation during doApply**: +- `tefINTERNAL`: Failed to create domain SLE (creation case) +- `tecINSUFFICIENT_RESERVE`: Insufficient reserve for owner count increase (creation case) +- `tecDIR_FULL`: Owner directory is full (creation case) + +### 3.1.2. State Changes + +**If DomainID is omitted (creation)**: +- `PermissionedDomain` object is **created** with: + - `Owner`: set to Account + - `Sequence`: set to the transaction's effective sequence (its `Sequence`, or `TicketSequence` if ticketed) + - `AcceptedCredentials`: sorted credentials array + - `OwnerNode`: page index in owner directory +- `Owner`'s owner count is **incremented** by 1 +- `DirectoryNode` entry is **added** to owner's directory + +**If DomainID is provided (update)**: +- `PermissionedDomain` object is **updated**: + - `AcceptedCredentials`: replaced with new sorted credentials array + - `PreviousTxnID` and `PreviousTxnLgrSeq`: updated + +## 3.2. PermissionedDomainDelete Transaction + +Deletes a PermissionedDomain. Only the domain owner can delete their domain. + +| Field Name | Required? | JSON Type | Internal Type | Description | +|------------|:---------:|:---------:|:-------------:|:------------| +| `TransactionType` | :heavy_check_mark: | String | UInt16 | Must be `"PermissionedDomainDelete"` | +| `Account` | :heavy_check_mark: | String | AccountID | Transaction sender (must be domain owner) | +| `Fee` | :heavy_check_mark: | String | Amount | Transaction fee | +| `DomainID` | :heavy_check_mark: | String | UInt256 | Domain to delete | + +### 3.2.1. Failure Conditions + +**Static validation**: +- `temDISABLED`: featurePermissionedDomains not enabled +- `temMALFORMED`: DomainID is all zeros + +**Validation against the ledger view**: +- `tecNO_ENTRY`: DomainID does not exist +- `tecNO_PERMISSION`: Account is not domain owner + +**Validation during doApply**: +- `tefBAD_LEDGER`: Unable to remove directory entry + +### 3.2.2. State Changes + +- `PermissionedDomain` object is **deleted** (removed from ledger) +- `Owner`'s owner count is **decremented** by 1 +- `DirectoryNode` entry is **removed** from owner's directory + +Note: Deleting a domain does not remove existing offers from the order book. Those offers remain in the ledger but become unfunded for domain payments because domain membership verification fails when the domain no longer exists. + +# 4. Access Control + +## 4.1. Domain Membership + +An account is considered "in domain" if either condition is met: + +1. **Owner Access**: Account is the domain owner (specified in PermissionedDomain.Owner field) +2. **Credential Access**: Account holds at least one accepted credential where: + - Subject = Account + - Issuer matches one entry in AcceptedCredentials + - CredentialType matches the same entry in AcceptedCredentials + - lsfAccepted flag is set (0x00010000) + - Credential is not expired (checked against parentCloseTime) + +Domain membership is checked at: +- OfferCreate preclaim: Offer creator must be in domain +- Payment preclaim: Both payer (Account) and payee (Destination) must be in domain (when DomainID field is present) +- Offer matching: Offer creator must remain in domain (otherwise offer becomes unfunded) + +## 4.2. Credential Verification + +The ledger verifies domain access using the `accountInDomain()` function: + +``` +Function: accountInDomain(view, account, domainID) +1. Lookup PermissionedDomain by domainID +2. If account == domain.Owner: return TRUE +3. For each credential in domain.AcceptedCredentials: + a. Lookup Credential(account, credential.Issuer, credential.CredentialType) + b. If credential exists AND lsfAccepted is set AND not expired: + return TRUE +4. Return FALSE +``` + +**Expiration Check**: Credential expiration is compared against the ledger's `parentCloseTime`. Expired credentials are treated as if they don't exist for domain access purposes. During transaction apply, an expired credential encountered while verifying domain membership is also deleted to reclaim its reserve; under the `fixCleanup3_1_3` amendment, if that deletion fails the transaction halts and returns the propagated error (e.g. `tecINTERNAL`) instead of continuing the membership check.[^pd-expiry-delete] + +[^pd-expiry-delete]: [`removeExpired`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/CredentialHelpers.cpp#L61-L64), [`verifyValidDomain`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/CredentialHelpers.cpp#L332-L334) + +**Performance**: Verification iterates through the domain's AcceptedCredentials array (max 10 entries), performing one ledger lookup per credential until a valid match is found. + diff --git a/docs/transactions/README.md b/docs/transactions/README.md index b6c21ce..4ea3d83 100644 --- a/docs/transactions/README.md +++ b/docs/transactions/README.md @@ -1,609 +1,609 @@ -# Index - -- [1. Introduction](#1-introduction) -- [2. System Design](#2-system-design) - - [2.1. Transaction Processing Classes](#21-transaction-processing-classes) - - [2.2. Processing Flow](#22-processing-flow) -- [3. Transaction Processing Pipeline](#3-transaction-processing-pipeline) - - [3.1. Preflight](#31-preflight) - - [3.2. Preclaim](#32-preclaim) - - [3.3. DoApply](#33-doapply) -- [4. Transaction Result Codes](#4-transaction-result-codes) -- [5. Ledger Views and Sandboxes](#5-ledger-views-and-sandboxes) - - [5.1. Atomic Application](#51-atomic-application) - - [5.1.1. Conditional Atomicity](#511-conditional-atomicity) -- [6. Fees and Reserves](#6-fees-and-reserves) -- [7. Sponsorship (Sponsor Amendment, XLS-68)](#7-sponsorship-sponsor-amendment-xls-68) -- [8. Batch Transactions (BatchV1_1 Amendment)](#8-batch-transactions-batchv1_1-amendment) - -# 1. Introduction - - -> [!IMPORTANT] -> N.B.: Transaction processing in `xrpld` is a complex system. This document presents a simplified view focused on providing sufficient context for understanding payment-related documentation. It covers the essential concepts and mechanisms without exhaustively detailing every aspect of transaction processing. - -Transactions are the mechanism for modifying the XRP Ledger state. New transactions representing user intent enter the network exclusively through RPC submission - clients submit transactions via commands like `submit` or `submit_multisigned` to a `xrpld` server. Once a transaction passes initial validation, it is relayed to other nodes through peer-to-peer propagation via `TMTransaction` protocol messages. - -Every transaction, regardless of how it arrived at a node, goes through the same three-phase processing pipeline: preflight (static validation), preclaim (ledger-based validation), and doApply (execution). All transaction types inherit from the `Transactor` base class, which provides the common infrastructure for these validation and execution stages. Both RPC-submitted and peer-propagated transactions converge at `processTransaction`, which orchestrates the preflight, preclaim, and doApply stages. - -When a ledger closes, consensus determines which transactions are included and each server independently computes the same deterministic transaction order. Transactions are then applied in multiple passes to ensure all transactions that can successfully execute are included in the ledger, with early passes allowing retries for transactions that may succeed after other transactions are applied. - -# 2. System Design - -## 2.1. Transaction Processing Classes - -The diagram below shows the key classes involved in transaction processing. Methods shown are commonly used during transaction validation and execution, not an exhaustive list. - -```mermaid -classDiagram - class STObject { - <> - +getAccountID(SField) - +isFieldPresent(SField) - +getFieldAmount(SField) - +isFlag(uint32_t) - } - - class STTx { - +getTransactionID() - +getTxnType() - +getSeqProxy() - +getSigningPubKey() - +checkSign() - } - - class Transactor { - <> - #ApplyContext ctx_ - #AccountID accountID_ - #XRPAmount preFeeBalance_ - +operator()() ApplyResult - +apply() TER - +doApply()* TER - +preclaim()$ TER - +checkSeqProxy()$ NotTEC - +checkFee()$ TER - +checkSign()$ NotTEC - } - - class Payment { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class OfferCreate { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class OfferCancel { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class TrustSet { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class AMMCreate { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class AMMDeposit { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class AMMWithdraw { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class AMMVote { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class AMMBid { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class AMMDelete { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class CredentialCreate { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class CredentialAccept { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class CredentialDelete { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class PermissionedDomainSet { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class PermissionedDomainDelete { - +preflight()$ NotTEC - +preclaim()$ TER - +doApply() TER - } - - class PreflightContext { - +Application app - +STTx tx - +Rules rules - +ApplyFlags flags - } - - class PreclaimContext { - +Application app - +ReadView view - +STTx tx - +TER preflightResult - +ApplyFlags flags - } - - class ApplyContext { - +Application app - +OpenView view - +STTx tx - +TER preclaimResult - } - - class PreflightResult { - +STTx tx - +TxConsequences consequences - +NotTEC ter - } - - class PreclaimResult { - +ReadView view - +STTx tx - +TER ter - +bool likelyToClaimFee - } - - class ApplyResult { - +TER ter - +bool applied - +TxMeta metadata - } - - STTx --|> STObject : inherits - STTx --> Transactor : processed by - Transactor <|-- Payment - Transactor <|-- OfferCreate - Transactor <|-- OfferCancel - Transactor <|-- TrustSet - Transactor <|-- AMMCreate - Transactor <|-- AMMDeposit - Transactor <|-- AMMWithdraw - Transactor <|-- AMMVote - Transactor <|-- AMMBid - Transactor <|-- AMMDelete - Transactor <|-- CredentialCreate - Transactor <|-- CredentialAccept - Transactor <|-- CredentialDelete - Transactor <|-- PermissionedDomainSet - Transactor <|-- PermissionedDomainDelete - - PreflightContext --> PreflightResult : used to create - PreclaimContext --> PreclaimResult : used to create - ApplyContext --> ApplyResult : used to create -``` -*Figure: Simplified Transaction Class Diagram showing payment-related Transactors* - -## 2.2. Processing Flow - -Transaction processing follows a three-phase pipeline: preflight (static validation), preclaim (ledger-based validation), and doApply (execution). Each phase can fail and return an error to the client. The `Transactor` base class coordinates this flow by calling into derived transaction classes at specific validation and execution points. - -The table below shows the main functions called during each phase. The "Implemented By" column indicates whether the function is implemented in `applySteps.cpp` (the top-level orchestrator for each phase), the `Transactor` base class (providing common behavior for all transactions), or the `Derived` transaction-specific class (e.g., `Payment`, `AMMCreate`). "Transactor (overridable)" means the base class provides a default implementation that derived classes may optionally override. - -| Phase | Function | Implemented By | Description | -|---------------|-------------------------------|---------------------------|--------------------------------------------------------------------------| -| **Preflight** | `invokePreflight()` | Transactor | Orchestrates preflight phase: checks tx type feature, calls other checks | -| | `checkExtraFeatures()` | Transactor (overridable) | Check if optional fields require specific amendments | -| | `preflight1()` | Transactor | Basic validation (account, fee, flags) - calls `preflight0()` | -| | `preflightUniversal()` | Transactor | Cross-cutting amount validation (gated by `fixCleanup3_2_0`) | -| | `preflight()` | Derived | **Required override** - transaction-specific static validation | -| | `preflight2()` | Transactor | Signature validation | -| | `preflightSigValidated()` | Transactor (overridable) | Optional post-signature validation | -| **Preclaim** | `invokePreclaim()` | applySteps.cpp | Orchestrates preclaim phase | -| | `checkSeqProxy()` | Transactor | Validate sequence number or ticket | -| | `checkPriorTxAndLastLedger()` | Transactor | Check prior transaction and last ledger sequence | -| | `checkSponsor()` | Transactor | Validate the sponsor account and any signatureless `Sponsorship` authorization (`Sponsor` amendment) | -| | `invokeCheckPermission()` | Transactor | Verify account permissions (delegate transaction-level and granular permissions) | -| | `checkSign()` | Transactor | Verify signature authorization | -| | `checkFee()` | Transactor | Verify the fee payer has sufficient balance for the fee | -| | `preclaim()` | Derived | Transaction-specific ledger-based validation | -| **Apply** | `doApply()` | applySteps.cpp | Orchestrates apply phase | -| | `operator()()` | Transactor | Entry point, exception handling | -| | `apply()` | Transactor | Orchestrates doApply flow | -| | `preCompute()` | Transactor | Per-transaction setup (validates account) | -| | `consumeSeqProxy()` | Transactor | Consume sequence or delete ticket | -| | `payFee()` | Transactor | Deduct the transaction fee from the fee payer | -| | `doApply()` | Derived | **Required override** - transaction-specific execution | - - -# 3. Transaction Processing Pipeline - -Every transaction is processed through three distinct phases: - -``` -preflight -> preclaim -> doApply -``` - -## 3.1. Preflight - -**Purpose**: Static validation - checks that don't require ledger state - -**Context**: PreflightContext -- `app`: Application instance -- `tx`: Transaction being validated -- `rules`: Amendment rules in effect -- `flags`: Apply flags - -**Validation flow**: - -Preflight validation is orchestrated by `Transactor::invokePreflight()` which calls the following stages in order: - -1. **Transaction Type Feature Check**: Verify the transaction type itself is enabled - - Check if transaction type requires a specific amendment (via `Permission::getInstance().getTxFeature()`) - - Return `temDISABLED` if required amendment is not enabled - -2. **checkExtraFeatures()**: Check optional field amendments (Transactor base class method) - - Each transaction can override to check if optional fields require specific amendments - - Called before preflight1, allows early rejection based on amendment rules - - Example: Payment checks if `sfCredentialIDs` field requires `featureCredentials` amendment - - Example: OfferCreate checks if `sfDomainID` field requires `featurePermissionedDEX` amendment - - Returns `false` (causes `temDISABLED`) if required amendments are not enabled - - Returns `true` by default (base class implementation) - -3. **preflight1()**: Account and fee field validation (Transactor base class method) - - Check `sfDelegate` field validity (requires `featurePermissionDelegationV1_1` amendment) - - Validate the sponsor fields - - Calls **preflight0()** internally for early sanity checks: - - Verify transaction ID is not zero - - Verify NetworkID matches (for networks > 1024) - - Check for invalid pseudo-transaction flags - - Verify `Account` field is present and not zero - - Validate `Fee` field is XRP, non-negative, and within acceptable range - - Check signing key validity via `preflightCheckSigningKey()` - - Verify `AccountTxnID` and `TicketSequence` are not both present (incompatible) - - Check `tfInnerBatchTxn` flag validity - -4. **preflightUniversal()**: Cross-cutting amount validation (Transactor base class method) - - Runs after `preflight1()` and before the derived class's `preflight()` - - When the `fixCleanup3_2_0` amendment is enabled, recursively checks every amount field in - the transaction (including nested objects and arrays) and returns `temBAD_AMOUNT` if any is malformed - -5. **Derived::preflight()**: Transaction-specific validation (override in derived class) - - Each transaction type implements its own preflight checks - - Example: Payment verifies amount fields, path structure, etc. - - Returns `NotTEC` error code or `tesSUCCESS` - -6. **preflight2()**: Signature validation (Transactor base class method) - - Check for simulation mode via `preflightCheckSimulateKeys()` - - Verify signature appears valid (cryptographic check) - - Validate multi-signature if present - - Check signature authorization requirements - -7. **preflightSigValidated()**: Post-signature validation (Transactor base class method, rarely overridden) - - Optional checks after signature validation - - Returns `tesSUCCESS` by default - -**Output**: PreflightResult containing: -- Transaction result code (NotTEC) -- TxConsequences (fee, potential spend, sequences consumed) -- Original context information - -Transactions that fail preflight validation are never added to the ledger. Preflight returns error codes like `tem` (malformed) that indicate fundamental problems with the transaction format. Since preflight does not access ledger state, these failures are detected before the transaction could claim a fee or consume a sequence number. If preflight fails, preclaim is not executed.[^preflight-check] - -[^preflight-check]: Preflight result check before preclaim: [`applySteps.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/applySteps.cpp#L406-L407) - -**Transaction Consequences**: - -During preflight, each transaction computes its `TxConsequences` - metadata describing the transaction's impact on the account and subsequent transactions. Transactions are classified into two categories: **normal** transactions (payments, offers, etc.) that perform standard operations, and **blocker** transactions that modify account properties affecting whether subsequent transactions can claim a fee (such as setting authorization requirements). The consequences track several properties: -- `fee_`: Transaction fee in XRP -- `potentialSpend_`: Maximum XRP that could be spent (excluding fee) -- `seqProx_`: Sequence or ticket being used -- `sequencesConsumed_`: Number of sequences consumed (usually 1) - -These properties are read by TxQ (transaction queue) to determine if transactions can be queued, estimate account balance, and determine transaction ordering constraints. - -## 3.2. Preclaim - -**Purpose**: Ledger-based validation - determines if transaction will claim a fee - -**Context**: PreclaimContext -- `app`: Application instance -- `view`: Read-only ledger view -- `tx`: Transaction being validated -- `preflightResult`: Result from preflight -- `flags`: Apply flags - -**Validation checks**: - -Preclaim validation is divided into two phases: - -**Phase 1: Pre-signature validation** (must return NotTEC - no tec codes allowed) -1. `checkSeqProxy`: Verify sequence number or ticket exists -2. `checkPriorTxAndLastLedger`: Check PriorTxnID and LastLedgerSequence fields -3. `checkSponsor`: Verify the sponsor account exists -4. `invokeCheckPermission`: Verify delegate permissions -5. `checkSign`: Verify signature matches account authorization - -All checks before and including signature verification must return NotTEC codes. Allowing tec results before signature verification would risk fee theft, as the fee would be charged before confirming the signature is valid. - -**Phase 2: Post-signature validation** (can return TER including tec codes) -1. `checkFee`: Verify the fee payer has sufficient balance for the fee -2. **Transaction-specific checks** (from derived class): - - Implemented in derived class `preclaim()` method - - Example: Payment checks if destination exists, validates paths, credentials, etc. - -**Output**: PreclaimResult containing: -- Transaction result code -- `likelyToClaimFee` flag (true if tesSUCCESS, or a tec code when not a retry) -- Original context information - -Transactions that fail preclaim may or may not be added to the ledger depending on the error code. The `likelyToClaimFee` flag is set to true if the preclaim result is `tesSUCCESS`, or a `tec` error code (values >= 100) **when the transaction is not being applied as a retry** (i.e. the `TapRetry` flag is not set).[^likely-to-claim-fee] Transactions with `tec` errors are added to the ledger, consume the fee, and increment the account's sequence number, even though the transaction's intended operation fails. Other error codes (`tem`, `tef`, `ter`, `tel`) result in the transaction not being added to the ledger.[^doapply-check] This distinction ensures the network is protected from spam (by charging fees for transactions that pass basic validation) while not penalizing users for transactions that fail due to malformation or other non-chargeable issues. - -[^likely-to-claim-fee]: likelyToClaimFee flag calculation: [`applySteps.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/tx/applySteps.h#L216). The `tec`-and-not-retry rule lives in [`isTecClaimHardFail`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/tx/applySteps.h#L28). -[^doapply-check]: doApply checks likelyToClaimFee flag: [`applySteps.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/applySteps.cpp#L440-L441) - -## 3.3. DoApply - -**Purpose**: Execute the transaction and modify ledger state - -**Context**: ApplyContext -- `app`: Application instance -- `tx`: Transaction being executed -- `preclaimResult`: Result from preclaim -- `view()`: Writable ledger view (OpenView) - -**Execution flow**: - -1. **doApply wrapper** (in applySteps.cpp): - - Verifies ledger sequence matches between preclaim and apply views - - Returns `{tefEXCEPTION, false}` if sequence mismatch - - Checks `likelyToClaimFee` flag - if false, returns preclaim result without applying - - Creates ApplyContext and invokes the transactor - - Catches exceptions and returns `{tefEXCEPTION, false}` on any exception - -2. **Transactor::operator()** (entry point for transaction execution): - - Checks if preclaim result is `tesSUCCESS` - - If yes, calls `apply()` method - - Handles various result codes (tecOVERSIZE, tecKILLED, etc.) - - Determines if transaction should be applied to ledger - -3. **Transactor::apply()** (base class execution): - - Calls `preCompute()` to perform per-transaction setup (e.g. validating the account) - - Calls `consumeSeqProxy()` to consume sequence or delete ticket - - Calls `payFee()` to deduct the transaction fee from the fee payer - - Updates AccountTxnID if present - - Calls derived class `doApply()` for transaction-specific logic - -4. **Derived class::doApply()** (transaction-specific): - - Implements the actual transaction logic - - Modifies ledger state through the view - - Returns TER code indicating success/failure - -**Output**: ApplyResult containing: -- Final TER code -- `applied` flag (whether transaction was applied to ledger) -- Transaction metadata (if applied) - -# 4. Transaction Result Codes - -Transaction result codes (TER) are categorized by prefix and meaning: - -| Prefix | Range | Meaning | Fee Claimed | Included in Ledger | -|---------|--------------|------------------------------------------------------------------------------------|-------------|--------------------| -| **tel** | -399 to -300 | Local error - should not be relayed | No | No | -| **tem** | -299 to -200 | Malformed transaction - permanent failure | No | No | -| **tef** | -199 to -100 | Failed to apply - not retried, but could succeed under different ledger state[^tef] | No | No | -| **ter** | -99 to -1 | Temporary failure that will be retried by the server that returned the result code | No | No | -| **tes** | 0 | Success | Yes | Yes | -| **tec** | 100+ | Claimed fee - failed but fee charged | Yes | Yes | - -[^tef]: tef characterization from source comments: [`TER.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/TER.h#L143-L154) - -# 5. Ledger Views and Sandboxes - -Ledger views provide controlled access to the ledger state during transaction processing. The view system implements a hierarchy where each layer can wrap another, allowing for staged state changes and conditional application. -Changes made to a view can be applied to its parent or discarded. - -Each layer: -- Reads through to parent layers -- Writes accumulate at current layer -- apply() pushes changes to parent - -**RawView** - -Subclasses can modify any ledger entries. - -**ReadView** - -Provides read-only access to ledger state: -- Query ledger entries via read() -- Check existence via exists() -- Access fees and amendment rules - -**ApplyView** - -Extends ReadView with write operations: -- peek(): Get mutable reference to ledger entry -- insert(): Create new ledger entry -- update(): Mark entry as modified -- erase(): Delete ledger entry - -Changes are tracked but not committed until explicitly applied. - -**Sandbox** - -A writable view that batches state changes: -- Layers on top of another ApplyView or ReadView -- Accumulates all state modifications in memory -- Changes applied atomically via apply(RawView&) (the parent view implements RawView) or discarded by destructing the sandbox - -Usage pattern: -```c++ -// Create sandbox on top of base view -Sandbox sb(&baseView); - -// Make changes -auto sle = sb.peek(keylet::account(alice)); -sle->setFieldU32(sfSequence, 100); -sb.update(sle); - -// Apply all changes atomically -sb.apply(ctx.rawView()); - -// OR: discard by letting sb go out of scope -``` - -**PaymentSandbox** - -During a payment or offer crossing, intermediate steps transfer funds between accounts. Without special handling, credits from one step could make subsequent steps see -inflated balances, allowing more liquidity than actually exists. - -PaymentSandbox maintains two tracking systems: - -1. **Normal sandbox (`items_`)**: Tracks all actual ledger entry modifications: - - AccountRoot balance changes (XRP) - - RippleState balance changes (tokens/IOUs) - - MPToken balance changes (MPTs) - - AccountRoot owner count changes - - Any other ledger entry modifications - -2. **Deferred credits table (`tab_`)**: Tracks metadata for query purposes during transaction execution: - - Credits, debits, self-debits, and original balances (for XRP, tokens, and MPTs) - - Maximum owner counts seen per account - -**Hooks for Balance Management:** - -Accounts in a payment are not allowed to use assets acquired during that payment. Balance hooks are virtual methods declared on ReadView and ApplyView that PaymentSandbox overrides to enforce this rule. When the flow engine queries an account's balance (e.g., via `accountHolds` or `xrpLiquid`), the balance hook subtracts newly acquired credits, so subsequent steps see only the pre-payment balance. Credit hooks record each transfer into `tab_` so the balance hooks have the data they need. There are separate hooks for IOUs (XRP and tokens) and MPTs: - -**IOU Hooks (XRP and Tokens):** -- `balanceHookIOU(account, issuer, amount)`: Returns the usable balance, adjusted so that newly acquired assets are not counted[^balanceHook] -- `creditHookIOU(from, to, amount, preCreditBalance)`: Records IOU credits in `tab_` for later querying - -**MPT Hooks:** -- `balanceHookMPT(account, issue, amount)`: Returns the usable MPT balance, adjusted so that newly acquired assets are not counted -- `balanceHookSelfIssueMPT(issue, amount)`: Returns issuer's self-debit balance for MPT -- `creditHookMPT(from, to, amount, preCreditBalanceHolder, preCreditBalanceIssuer)`: Records MPT credits in `tab_` for later querying -- `issuerSelfDebitHookMPT(issue, amount, preCreditBalance)`: Records issuer self-debit operations in `tab_` - -**Note**: Actual balance changes are always written through `view.update()` which modifies `items_`. The credit hooks are called alongside the actual change to track metadata in `tab_` for query purposes during transaction execution. - -**Hooks for Reserve Management:** - -Accounts cannot use freed reserves acquired during the transaction's execution. PaymentSandbox enforces this through: - -- `ownerCountHook(account, count)`: Returns the **maximum** owner counts the account has reached during the transaction's execution (tracked in `tab_`), not the current values. When calculating available balance (via `xrpLiquid`), this ensures freed reserves cannot be used mid-transaction. With the `Sponsor` amendment, the owner, sponsored, and sponsoring counters are tracked together as a group. - -- `adjustOwnerCountHook(account, cur, next)`: Records owner count changes in `tab_` to maintain the maximum value across all nested payment sandboxes. - -**Example**: Account starts with OwnerCount = 3: -1. Transaction deletes a trust line -> OwnerCount becomes 2 (written to `items_`, tracked in `tab_`) -2. Reserve calculation checks available balance -3. `ownerCountHook` returns 3 (max from `tab_`) -4. Account cannot use the freed reserve until transaction completes - -**Applying Changes:** - -When `apply()` is called, changes are committed as follows: - -- `apply(RawView& to)`: Commits all `items_` to ledger (all actual ledger entry modifications). The `tab_` metadata is not committed - it's only used during transaction execution for queries. - -- `apply(PaymentSandbox& to)`: Merges both `items_` (ledger changes) and `tab_` (metadata) to parent PaymentSandbox. This allows nested sandboxes to propagate both actual changes and deferred credit metadata up the chain. - -Sandboxes can be layered to create hierarchies of changes. For example: - -``` -RawView (actual ledger) - ↑ -Sandbox sb1 (transaction-level changes) - ↑ -PaymentSandbox psb (payment-level changes) - ↑ -PaymentSandbox nested (strand-level changes) -``` - -## 5.1. Atomic Application - -When `apply()` is called, all accumulated changes are pushed to the parent view by iterating over modified entries and applying each one. The parent can be another Sandbox (staged commit) or a RawView (final commit). - -The atomicity guarantee is RAII-based: either `apply()` is called and all buffered changes propagate to the parent, or the sandbox is destroyed without calling `apply()` and all changes are discarded. - -### 5.1.1. Conditional Atomicity - -Conditional atomicity allows transactions to prepare multiple potential outcomes and commit only one based on the result. By creating two parallel sandboxes on the same parent view, the transaction can work on both a success path and a failure path simultaneously, then selectively apply only the appropriate one[^conditional-atomicity]. - -```c++ -// Create two parallel sandboxes on the same parent view -Sandbox sb(&ctx_.view()); // success path -Sandbox sbCancel(&ctx_.view()); // failure path (e.g., cleanup only) - -auto const result = applyGuts(sb, sbCancel); - -// Apply only the appropriate sandbox -if (result.second) - sb.apply(ctx_.rawView()); -else - sbCancel.apply(ctx_.rawView()); -``` - -[^conditional-atomicity]: Conditional atomicity pattern in OfferCreate: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L969-L990) - -[^balanceHook]: Balance hook description from source comments: [`ReadView.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/ledger/ReadView.h#L149-L153) - -# 6. Fees and Reserves - -Every transaction destroys a small amount of XRP as its fee. The minimum fee derives from the network's base fee and grows with load and with the number of signatures. The fee is checked in preclaim (`checkFee`) and deducted during apply (`payFee`), and it is charged even when the transaction fails with a `tec` code (see [section 4](#4-transaction-result-codes)).[^fees] - -Reserves are XRP an account must hold but cannot spend: a base reserve for the account itself plus one owner reserve increment for each object it owns. Reserves are not consumed. They gate creation: a transaction that would create an object fails unless the owner's balance covers the increased requirement. The per-object documents describe who bears each object's reserve.[^reserves] - -# 7. Sponsorship (Sponsor Amendment, XLS-68) - -The `Sponsor` amendment (XLS-68) lets a sponsor account pay another account's fees and cover its reserves. A transaction opts in with the common `Sponsor` and `SponsorFlags` fields, choosing fee sponsorship, reserve sponsorship, or both. The sponsor approves by co-signing the transaction (`SponsorSignature`) or in advance through a standing `Sponsorship` ledger entry, managed with the `SponsorshipSet` and `SponsorshipTransfer` transactions. The pipeline hooks are described in [section 3](#3-transaction-processing-pipeline).[^sponsorship] - -With fee sponsorship, the sponsor becomes the fee payer. With reserve sponsorship, a created object records its sponsor (the `Sponsor` field on most entry types, `HighSponsor` or `LowSponsor` per trust line side) and counts against the sponsor's reserve instead of the owner's: the owner count used for reserve calculations becomes `OwnerCount - SponsoredOwnerCount + SponsoringOwnerCount`. Deletion releases the reserve against the recorded sponsor.[^sponsor-reserve] - -# 8. Batch Transactions (BatchV1_1 Amendment) - -The `BatchV1_1` amendment adds the `Batch` transaction, which wraps several inner transactions that apply together on a closed ledger. Inner transactions carry the `tfInnerBatchTxn` flag, skip individual signature checks because the outer batch's signers authorize them, and return `tef` codes where an open-ledger submission would return `tel` codes. Preflight rejects a transaction whose flag disagrees with its batch context with `temINVALID_INNER_BATCH`.[^batch] - -[^fees]: [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/Transactor.cpp#L448-L473), [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/Transactor.cpp#L621-L695) -[^reserves]: [`Fees.h`](https://github.com/XRPLF/rippled/blob/3.3.0/include/xrpl/protocol/Fees.h#L46-L56) -[^sponsorship]: [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/Transactor.cpp#L175-L225), [`transactions.macro`](https://github.com/XRPLF/rippled/blob/3.3.0/include/xrpl/protocol/detail/transactions.macro#L1168-L1195), [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.3.0/include/xrpl/protocol/detail/ledger_entries.macro#L627-L637) -[^sponsor-reserve]: [`LedgerFormats.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/protocol/LedgerFormats.cpp#L11-L21), [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L142-L200), [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L229-L266), [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L359-L378) -[^batch]: [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/Transactor.cpp#L282-L290), [`TER.h`](https://github.com/XRPLF/rippled/blob/3.3.0/include/xrpl/protocol/TER.h#L180-L181) +# Index + +- [1. Introduction](#1-introduction) +- [2. System Design](#2-system-design) + - [2.1. Transaction Processing Classes](#21-transaction-processing-classes) + - [2.2. Processing Flow](#22-processing-flow) +- [3. Transaction Processing Pipeline](#3-transaction-processing-pipeline) + - [3.1. Preflight](#31-preflight) + - [3.2. Preclaim](#32-preclaim) + - [3.3. DoApply](#33-doapply) +- [4. Transaction Result Codes](#4-transaction-result-codes) +- [5. Ledger Views and Sandboxes](#5-ledger-views-and-sandboxes) + - [5.1. Atomic Application](#51-atomic-application) + - [5.1.1. Conditional Atomicity](#511-conditional-atomicity) +- [6. Fees and Reserves](#6-fees-and-reserves) +- [7. Sponsorship (Sponsor Amendment, XLS-68)](#7-sponsorship-sponsor-amendment-xls-68) +- [8. Batch Transactions (BatchV1_1 Amendment)](#8-batch-transactions-batchv1_1-amendment) + +# 1. Introduction + + +> [!IMPORTANT] +> N.B.: Transaction processing in `xrpld` is a complex system. This document presents a simplified view focused on providing sufficient context for understanding payment-related documentation. It covers the essential concepts and mechanisms without exhaustively detailing every aspect of transaction processing. + +Transactions are the mechanism for modifying the XRP Ledger state. New transactions representing user intent enter the network exclusively through RPC submission - clients submit transactions via commands like `submit` or `submit_multisigned` to a `xrpld` server. Once a transaction passes initial validation, it is relayed to other nodes through peer-to-peer propagation via `TMTransaction` protocol messages. + +Every transaction, regardless of how it arrived at a node, goes through the same three-phase processing pipeline: preflight (static validation), preclaim (ledger-based validation), and doApply (execution). All transaction types inherit from the `Transactor` base class, which provides the common infrastructure for these validation and execution stages. Both RPC-submitted and peer-propagated transactions converge at `processTransaction`, which orchestrates the preflight, preclaim, and doApply stages. + +When a ledger closes, consensus determines which transactions are included and each server independently computes the same deterministic transaction order. Transactions are then applied in multiple passes to ensure all transactions that can successfully execute are included in the ledger, with early passes allowing retries for transactions that may succeed after other transactions are applied. + +# 2. System Design + +## 2.1. Transaction Processing Classes + +The diagram below shows the key classes involved in transaction processing. Methods shown are commonly used during transaction validation and execution, not an exhaustive list. + +```mermaid +classDiagram + class STObject { + <> + +getAccountID(SField) + +isFieldPresent(SField) + +getFieldAmount(SField) + +isFlag(uint32_t) + } + + class STTx { + +getTransactionID() + +getTxnType() + +getSeqProxy() + +getSigningPubKey() + +checkSign() + } + + class Transactor { + <> + #ApplyContext ctx_ + #AccountID accountID_ + #XRPAmount preFeeBalance_ + +operator()() ApplyResult + +apply() TER + +doApply()* TER + +preclaim()$ TER + +checkSeqProxy()$ NotTEC + +checkFee()$ TER + +checkSign()$ NotTEC + } + + class Payment { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class OfferCreate { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class OfferCancel { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class TrustSet { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class AMMCreate { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class AMMDeposit { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class AMMWithdraw { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class AMMVote { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class AMMBid { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class AMMDelete { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class CredentialCreate { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class CredentialAccept { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class CredentialDelete { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class PermissionedDomainSet { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class PermissionedDomainDelete { + +preflight()$ NotTEC + +preclaim()$ TER + +doApply() TER + } + + class PreflightContext { + +Application app + +STTx tx + +Rules rules + +ApplyFlags flags + } + + class PreclaimContext { + +Application app + +ReadView view + +STTx tx + +TER preflightResult + +ApplyFlags flags + } + + class ApplyContext { + +Application app + +OpenView view + +STTx tx + +TER preclaimResult + } + + class PreflightResult { + +STTx tx + +TxConsequences consequences + +NotTEC ter + } + + class PreclaimResult { + +ReadView view + +STTx tx + +TER ter + +bool likelyToClaimFee + } + + class ApplyResult { + +TER ter + +bool applied + +TxMeta metadata + } + + STTx --|> STObject : inherits + STTx --> Transactor : processed by + Transactor <|-- Payment + Transactor <|-- OfferCreate + Transactor <|-- OfferCancel + Transactor <|-- TrustSet + Transactor <|-- AMMCreate + Transactor <|-- AMMDeposit + Transactor <|-- AMMWithdraw + Transactor <|-- AMMVote + Transactor <|-- AMMBid + Transactor <|-- AMMDelete + Transactor <|-- CredentialCreate + Transactor <|-- CredentialAccept + Transactor <|-- CredentialDelete + Transactor <|-- PermissionedDomainSet + Transactor <|-- PermissionedDomainDelete + + PreflightContext --> PreflightResult : used to create + PreclaimContext --> PreclaimResult : used to create + ApplyContext --> ApplyResult : used to create +``` +*Figure: Simplified Transaction Class Diagram showing payment-related Transactors* + +## 2.2. Processing Flow + +Transaction processing follows a three-phase pipeline: preflight (static validation), preclaim (ledger-based validation), and doApply (execution). Each phase can fail and return an error to the client. The `Transactor` base class coordinates this flow by calling into derived transaction classes at specific validation and execution points. + +The table below shows the main functions called during each phase. The "Implemented By" column indicates whether the function is implemented in `applySteps.cpp` (the top-level orchestrator for each phase), the `Transactor` base class (providing common behavior for all transactions), or the `Derived` transaction-specific class (e.g., `Payment`, `AMMCreate`). "Transactor (overridable)" means the base class provides a default implementation that derived classes may optionally override. + +| Phase | Function | Implemented By | Description | +|---------------|-------------------------------|---------------------------|--------------------------------------------------------------------------| +| **Preflight** | `invokePreflight()` | Transactor | Orchestrates preflight phase: checks tx type feature, calls other checks | +| | `checkExtraFeatures()` | Transactor (overridable) | Check if optional fields require specific amendments | +| | `preflight1()` | Transactor | Basic validation (account, fee, flags) - calls `preflight0()` | +| | `preflightUniversal()` | Transactor | Cross-cutting amount validation (gated by `fixCleanup3_2_0`) | +| | `preflight()` | Derived | **Required override** - transaction-specific static validation | +| | `preflight2()` | Transactor | Signature validation | +| | `preflightSigValidated()` | Transactor (overridable) | Optional post-signature validation | +| **Preclaim** | `invokePreclaim()` | applySteps.cpp | Orchestrates preclaim phase | +| | `checkSeqProxy()` | Transactor | Validate sequence number or ticket | +| | `checkPriorTxAndLastLedger()` | Transactor | Check prior transaction and last ledger sequence | +| | `checkSponsor()` | Transactor | Validate the sponsor account and any signatureless `Sponsorship` authorization (`Sponsor` amendment) | +| | `invokeCheckPermission()` | Transactor | Verify account permissions (delegate transaction-level and granular permissions) | +| | `checkSign()` | Transactor | Verify signature authorization | +| | `checkFee()` | Transactor | Verify the fee payer has sufficient balance for the fee | +| | `preclaim()` | Derived | Transaction-specific ledger-based validation | +| **Apply** | `doApply()` | applySteps.cpp | Orchestrates apply phase | +| | `operator()()` | Transactor | Entry point, exception handling | +| | `apply()` | Transactor | Orchestrates doApply flow | +| | `preCompute()` | Transactor | Per-transaction setup (validates account) | +| | `consumeSeqProxy()` | Transactor | Consume sequence or delete ticket | +| | `payFee()` | Transactor | Deduct the transaction fee from the fee payer | +| | `doApply()` | Derived | **Required override** - transaction-specific execution | + + +# 3. Transaction Processing Pipeline + +Every transaction is processed through three distinct phases: + +``` +preflight -> preclaim -> doApply +``` + +## 3.1. Preflight + +**Purpose**: Static validation - checks that don't require ledger state + +**Context**: PreflightContext +- `app`: Application instance +- `tx`: Transaction being validated +- `rules`: Amendment rules in effect +- `flags`: Apply flags + +**Validation flow**: + +Preflight validation is orchestrated by `Transactor::invokePreflight()` which calls the following stages in order: + +1. **Transaction Type Feature Check**: Verify the transaction type itself is enabled + - Check if transaction type requires a specific amendment (via `Permission::getInstance().getTxFeature()`) + - Return `temDISABLED` if required amendment is not enabled + +2. **checkExtraFeatures()**: Check optional field amendments (Transactor base class method) + - Each transaction can override to check if optional fields require specific amendments + - Called before preflight1, allows early rejection based on amendment rules + - Example: Payment checks if `sfCredentialIDs` field requires `featureCredentials` amendment + - Example: OfferCreate checks if `sfDomainID` field requires `featurePermissionedDEX` amendment + - Returns `false` (causes `temDISABLED`) if required amendments are not enabled + - Returns `true` by default (base class implementation) + +3. **preflight1()**: Account and fee field validation (Transactor base class method) + - Check `sfDelegate` field validity (requires `featurePermissionDelegationV1_1` amendment) + - Validate the sponsor fields + - Calls **preflight0()** internally for early sanity checks: + - Verify transaction ID is not zero + - Verify NetworkID matches (for networks > 1024) + - Check for invalid pseudo-transaction flags + - Verify `Account` field is present and not zero + - Validate `Fee` field is XRP, non-negative, and within acceptable range + - Check signing key validity via `preflightCheckSigningKey()` + - Verify `AccountTxnID` and `TicketSequence` are not both present (incompatible) + - Check `tfInnerBatchTxn` flag validity + +4. **preflightUniversal()**: Cross-cutting amount validation (Transactor base class method) + - Runs after `preflight1()` and before the derived class's `preflight()` + - When the `fixCleanup3_2_0` amendment is enabled, recursively checks every amount field in + the transaction (including nested objects and arrays) and returns `temBAD_AMOUNT` if any is malformed + +5. **Derived::preflight()**: Transaction-specific validation (override in derived class) + - Each transaction type implements its own preflight checks + - Example: Payment verifies amount fields, path structure, etc. + - Returns `NotTEC` error code or `tesSUCCESS` + +6. **preflight2()**: Signature validation (Transactor base class method) + - Check for simulation mode via `preflightCheckSimulateKeys()` + - Verify signature appears valid (cryptographic check) + - Validate multi-signature if present + - Check signature authorization requirements + +7. **preflightSigValidated()**: Post-signature validation (Transactor base class method, rarely overridden) + - Optional checks after signature validation + - Returns `tesSUCCESS` by default + +**Output**: PreflightResult containing: +- Transaction result code (NotTEC) +- TxConsequences (fee, potential spend, sequences consumed) +- Original context information + +Transactions that fail preflight validation are never added to the ledger. Preflight returns error codes like `tem` (malformed) that indicate fundamental problems with the transaction format. Since preflight does not access ledger state, these failures are detected before the transaction could claim a fee or consume a sequence number. If preflight fails, preclaim is not executed.[^preflight-check] + +[^preflight-check]: Preflight result check before preclaim: [`applySteps.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/applySteps.cpp#L406-L407) + +**Transaction Consequences**: + +During preflight, each transaction computes its `TxConsequences` - metadata describing the transaction's impact on the account and subsequent transactions. Transactions are classified into two categories: **normal** transactions (payments, offers, etc.) that perform standard operations, and **blocker** transactions that modify account properties affecting whether subsequent transactions can claim a fee (such as setting authorization requirements). The consequences track several properties: +- `fee_`: Transaction fee in XRP +- `potentialSpend_`: Maximum XRP that could be spent (excluding fee) +- `seqProx_`: Sequence or ticket being used +- `sequencesConsumed_`: Number of sequences consumed (usually 1) + +These properties are read by TxQ (transaction queue) to determine if transactions can be queued, estimate account balance, and determine transaction ordering constraints. + +## 3.2. Preclaim + +**Purpose**: Ledger-based validation - determines if transaction will claim a fee + +**Context**: PreclaimContext +- `app`: Application instance +- `view`: Read-only ledger view +- `tx`: Transaction being validated +- `preflightResult`: Result from preflight +- `flags`: Apply flags + +**Validation checks**: + +Preclaim validation is divided into two phases: + +**Phase 1: Pre-signature validation** (must return NotTEC - no tec codes allowed) +1. `checkSeqProxy`: Verify sequence number or ticket exists +2. `checkPriorTxAndLastLedger`: Check PriorTxnID and LastLedgerSequence fields +3. `checkSponsor`: Verify the sponsor account exists +4. `invokeCheckPermission`: Verify delegate permissions +5. `checkSign`: Verify signature matches account authorization + +All checks before and including signature verification must return NotTEC codes. Allowing tec results before signature verification would risk fee theft, as the fee would be charged before confirming the signature is valid. + +**Phase 2: Post-signature validation** (can return TER including tec codes) +1. `checkFee`: Verify the fee payer has sufficient balance for the fee +2. **Transaction-specific checks** (from derived class): + - Implemented in derived class `preclaim()` method + - Example: Payment checks if destination exists, validates paths, credentials, etc. + +**Output**: PreclaimResult containing: +- Transaction result code +- `likelyToClaimFee` flag (true if tesSUCCESS, or a tec code when not a retry) +- Original context information + +Transactions that fail preclaim may or may not be added to the ledger depending on the error code. The `likelyToClaimFee` flag is set to true if the preclaim result is `tesSUCCESS`, or a `tec` error code (values >= 100) **when the transaction is not being applied as a retry** (i.e. the `TapRetry` flag is not set).[^likely-to-claim-fee] Transactions with `tec` errors are added to the ledger, consume the fee, and increment the account's sequence number, even though the transaction's intended operation fails. Other error codes (`tem`, `tef`, `ter`, `tel`) result in the transaction not being added to the ledger.[^doapply-check] This distinction ensures the network is protected from spam (by charging fees for transactions that pass basic validation) while not penalizing users for transactions that fail due to malformation or other non-chargeable issues. + +[^likely-to-claim-fee]: likelyToClaimFee flag calculation: [`applySteps.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/tx/applySteps.h#L216). The `tec`-and-not-retry rule lives in [`isTecClaimHardFail`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/tx/applySteps.h#L28). +[^doapply-check]: doApply checks likelyToClaimFee flag: [`applySteps.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/applySteps.cpp#L440-L441) + +## 3.3. DoApply + +**Purpose**: Execute the transaction and modify ledger state + +**Context**: ApplyContext +- `app`: Application instance +- `tx`: Transaction being executed +- `preclaimResult`: Result from preclaim +- `view()`: Writable ledger view (OpenView) + +**Execution flow**: + +1. **doApply wrapper** (in applySteps.cpp): + - Verifies ledger sequence matches between preclaim and apply views + - Returns `{tefEXCEPTION, false}` if sequence mismatch + - Checks `likelyToClaimFee` flag - if false, returns preclaim result without applying + - Creates ApplyContext and invokes the transactor + - Catches exceptions and returns `{tefEXCEPTION, false}` on any exception + +2. **Transactor::operator()** (entry point for transaction execution): + - Checks if preclaim result is `tesSUCCESS` + - If yes, calls `apply()` method + - Handles various result codes (tecOVERSIZE, tecKILLED, etc.) + - Determines if transaction should be applied to ledger + +3. **Transactor::apply()** (base class execution): + - Calls `preCompute()` to perform per-transaction setup (e.g. validating the account) + - Calls `consumeSeqProxy()` to consume sequence or delete ticket + - Calls `payFee()` to deduct the transaction fee from the fee payer + - Updates AccountTxnID if present + - Calls derived class `doApply()` for transaction-specific logic + +4. **Derived class::doApply()** (transaction-specific): + - Implements the actual transaction logic + - Modifies ledger state through the view + - Returns TER code indicating success/failure + +**Output**: ApplyResult containing: +- Final TER code +- `applied` flag (whether transaction was applied to ledger) +- Transaction metadata (if applied) + +# 4. Transaction Result Codes + +Transaction result codes (TER) are categorized by prefix and meaning: + +| Prefix | Range | Meaning | Fee Claimed | Included in Ledger | +|---------|--------------|------------------------------------------------------------------------------------|-------------|--------------------| +| **tel** | -399 to -300 | Local error - should not be relayed | No | No | +| **tem** | -299 to -200 | Malformed transaction - permanent failure | No | No | +| **tef** | -199 to -100 | Failed to apply - not retried, but could succeed under different ledger state[^tef] | No | No | +| **ter** | -99 to -1 | Temporary failure that will be retried by the server that returned the result code | No | No | +| **tes** | 0 | Success | Yes | Yes | +| **tec** | 100+ | Claimed fee - failed but fee charged | Yes | Yes | + +[^tef]: tef characterization from source comments: [`TER.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/protocol/TER.h#L143-L154) + +# 5. Ledger Views and Sandboxes + +Ledger views provide controlled access to the ledger state during transaction processing. The view system implements a hierarchy where each layer can wrap another, allowing for staged state changes and conditional application. +Changes made to a view can be applied to its parent or discarded. + +Each layer: +- Reads through to parent layers +- Writes accumulate at current layer +- apply() pushes changes to parent + +**RawView** + +Subclasses can modify any ledger entries. + +**ReadView** + +Provides read-only access to ledger state: +- Query ledger entries via read() +- Check existence via exists() +- Access fees and amendment rules + +**ApplyView** + +Extends ReadView with write operations: +- peek(): Get mutable reference to ledger entry +- insert(): Create new ledger entry +- update(): Mark entry as modified +- erase(): Delete ledger entry + +Changes are tracked but not committed until explicitly applied. + +**Sandbox** + +A writable view that batches state changes: +- Layers on top of another ApplyView or ReadView +- Accumulates all state modifications in memory +- Changes applied atomically via apply(RawView&) (the parent view implements RawView) or discarded by destructing the sandbox + +Usage pattern: +```c++ +// Create sandbox on top of base view +Sandbox sb(&baseView); + +// Make changes +auto sle = sb.peek(keylet::account(alice)); +sle->setFieldU32(sfSequence, 100); +sb.update(sle); + +// Apply all changes atomically +sb.apply(ctx.rawView()); + +// OR: discard by letting sb go out of scope +``` + +**PaymentSandbox** + +During a payment or offer crossing, intermediate steps transfer funds between accounts. Without special handling, credits from one step could make subsequent steps see +inflated balances, allowing more liquidity than actually exists. + +PaymentSandbox maintains two tracking systems: + +1. **Normal sandbox (`items_`)**: Tracks all actual ledger entry modifications: + - AccountRoot balance changes (XRP) + - RippleState balance changes (tokens/IOUs) + - MPToken balance changes (MPTs) + - AccountRoot owner count changes + - Any other ledger entry modifications + +2. **Deferred credits table (`tab_`)**: Tracks metadata for query purposes during transaction execution: + - Credits, debits, self-debits, and original balances (for XRP, tokens, and MPTs) + - Maximum owner counts seen per account + +**Hooks for Balance Management:** + +Accounts in a payment are not allowed to use assets acquired during that payment. Balance hooks are virtual methods declared on ReadView and ApplyView that PaymentSandbox overrides to enforce this rule. When the flow engine queries an account's balance (e.g., via `accountHolds` or `xrpLiquid`), the balance hook subtracts newly acquired credits, so subsequent steps see only the pre-payment balance. Credit hooks record each transfer into `tab_` so the balance hooks have the data they need. There are separate hooks for IOUs (XRP and tokens) and MPTs: + +**IOU Hooks (XRP and Tokens):** +- `balanceHookIOU(account, issuer, amount)`: Returns the usable balance, adjusted so that newly acquired assets are not counted[^balanceHook] +- `creditHookIOU(from, to, amount, preCreditBalance)`: Records IOU credits in `tab_` for later querying + +**MPT Hooks:** +- `balanceHookMPT(account, issue, amount)`: Returns the usable MPT balance, adjusted so that newly acquired assets are not counted +- `balanceHookSelfIssueMPT(issue, amount)`: Returns issuer's self-debit balance for MPT +- `creditHookMPT(from, to, amount, preCreditBalanceHolder, preCreditBalanceIssuer)`: Records MPT credits in `tab_` for later querying +- `issuerSelfDebitHookMPT(issue, amount, preCreditBalance)`: Records issuer self-debit operations in `tab_` + +**Note**: Actual balance changes are always written through `view.update()` which modifies `items_`. The credit hooks are called alongside the actual change to track metadata in `tab_` for query purposes during transaction execution. + +**Hooks for Reserve Management:** + +Accounts cannot use freed reserves acquired during the transaction's execution. PaymentSandbox enforces this through: + +- `ownerCountHook(account, count)`: Returns the **maximum** owner counts the account has reached during the transaction's execution (tracked in `tab_`), not the current values. When calculating available balance (via `xrpLiquid`), this ensures freed reserves cannot be used mid-transaction. With the `Sponsor` amendment, the owner, sponsored, and sponsoring counters are tracked together as a group. + +- `adjustOwnerCountHook(account, cur, next)`: Records owner count changes in `tab_` to maintain the maximum value across all nested payment sandboxes. + +**Example**: Account starts with OwnerCount = 3: +1. Transaction deletes a trust line -> OwnerCount becomes 2 (written to `items_`, tracked in `tab_`) +2. Reserve calculation checks available balance +3. `ownerCountHook` returns 3 (max from `tab_`) +4. Account cannot use the freed reserve until transaction completes + +**Applying Changes:** + +When `apply()` is called, changes are committed as follows: + +- `apply(RawView& to)`: Commits all `items_` to ledger (all actual ledger entry modifications). The `tab_` metadata is not committed - it's only used during transaction execution for queries. + +- `apply(PaymentSandbox& to)`: Merges both `items_` (ledger changes) and `tab_` (metadata) to parent PaymentSandbox. This allows nested sandboxes to propagate both actual changes and deferred credit metadata up the chain. + +Sandboxes can be layered to create hierarchies of changes. For example: + +``` +RawView (actual ledger) + ↑ +Sandbox sb1 (transaction-level changes) + ↑ +PaymentSandbox psb (payment-level changes) + ↑ +PaymentSandbox nested (strand-level changes) +``` + +## 5.1. Atomic Application + +When `apply()` is called, all accumulated changes are pushed to the parent view by iterating over modified entries and applying each one. The parent can be another Sandbox (staged commit) or a RawView (final commit). + +The atomicity guarantee is RAII-based: either `apply()` is called and all buffered changes propagate to the parent, or the sandbox is destroyed without calling `apply()` and all changes are discarded. + +### 5.1.1. Conditional Atomicity + +Conditional atomicity allows transactions to prepare multiple potential outcomes and commit only one based on the result. By creating two parallel sandboxes on the same parent view, the transaction can work on both a success path and a failure path simultaneously, then selectively apply only the appropriate one[^conditional-atomicity]. + +```c++ +// Create two parallel sandboxes on the same parent view +Sandbox sb(&ctx_.view()); // success path +Sandbox sbCancel(&ctx_.view()); // failure path (e.g., cleanup only) + +auto const result = applyGuts(sb, sbCancel); + +// Apply only the appropriate sandbox +if (result.second) + sb.apply(ctx_.rawView()); +else + sbCancel.apply(ctx_.rawView()); +``` + +[^conditional-atomicity]: Conditional atomicity pattern in OfferCreate: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L969-L990) + +[^balanceHook]: Balance hook description from source comments: [`ReadView.h`](https://github.com/XRPLF/rippled/blob/3.2.0/include/xrpl/ledger/ReadView.h#L149-L153) + +# 6. Fees and Reserves + +Every transaction destroys a small amount of XRP as its fee. The minimum fee derives from the network's base fee and grows with load and with the number of signatures. The fee is checked in preclaim (`checkFee`) and deducted during apply (`payFee`), and it is charged even when the transaction fails with a `tec` code (see [section 4](#4-transaction-result-codes)).[^fees] + +Reserves are XRP an account must hold but cannot spend: a base reserve for the account itself plus one owner reserve increment for each object it owns. Reserves are not consumed. They gate creation: a transaction that would create an object fails unless the owner's balance covers the increased requirement. The per-object documents describe who bears each object's reserve.[^reserves] + +# 7. Sponsorship (Sponsor Amendment, XLS-68) + +The `Sponsor` amendment (XLS-68) lets a sponsor account pay another account's fees and cover its reserves. A transaction opts in with the common `Sponsor` and `SponsorFlags` fields, choosing fee sponsorship, reserve sponsorship, or both. The sponsor approves by co-signing the transaction (`SponsorSignature`) or in advance through a standing `Sponsorship` ledger entry, managed with the `SponsorshipSet` and `SponsorshipTransfer` transactions. The pipeline hooks are described in [section 3](#3-transaction-processing-pipeline).[^sponsorship] + +With fee sponsorship, the sponsor becomes the fee payer. With reserve sponsorship, a created object records its sponsor (the `Sponsor` field on most entry types, `HighSponsor` or `LowSponsor` per trust line side) and counts against the sponsor's reserve instead of the owner's: the owner count used for reserve calculations becomes `OwnerCount - SponsoredOwnerCount + SponsoringOwnerCount`. Deletion releases the reserve against the recorded sponsor.[^sponsor-reserve] + +# 8. Batch Transactions (BatchV1_1 Amendment) + +The `BatchV1_1` amendment adds the `Batch` transaction, which wraps several inner transactions that apply together on a closed ledger. Inner transactions carry the `tfInnerBatchTxn` flag, skip individual signature checks because the outer batch's signers authorize them, and return `tef` codes where an open-ledger submission would return `tel` codes. Preflight rejects a transaction whose flag disagrees with its batch context with `temINVALID_INNER_BATCH`.[^batch] + +[^fees]: [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/Transactor.cpp#L448-L473), [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/Transactor.cpp#L621-L695) +[^reserves]: [`Fees.h`](https://github.com/XRPLF/rippled/blob/3.3.0/include/xrpl/protocol/Fees.h#L46-L56) +[^sponsorship]: [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/Transactor.cpp#L175-L225), [`transactions.macro`](https://github.com/XRPLF/rippled/blob/3.3.0/include/xrpl/protocol/detail/transactions.macro#L1168-L1195), [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.3.0/include/xrpl/protocol/detail/ledger_entries.macro#L627-L637) +[^sponsor-reserve]: [`LedgerFormats.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/protocol/LedgerFormats.cpp#L11-L21), [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L142-L200), [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L229-L266), [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L359-L378) +[^batch]: [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/Transactor.cpp#L282-L290), [`TER.h`](https://github.com/XRPLF/rippled/blob/3.3.0/include/xrpl/protocol/TER.h#L180-L181) diff --git a/docs/trust_lines/README.md b/docs/trust_lines/README.md index 9c76ebd..bbb9288 100644 --- a/docs/trust_lines/README.md +++ b/docs/trust_lines/README.md @@ -1,378 +1,378 @@ -# Index - -- [1. Introduction](#1-introduction) - - [1.1. Default State](#11-default-state) -- [2. Ledger Entries](#2-ledger-entries) - - [2.1. RippleState Ledger Entry](#21-ripplestate-ledger-entry) - - [2.1.1. Object Identifier](#211-object-identifier) - - [2.1.2. Fields](#212-fields) - - [2.1.2.1. Flags](#2121-flags) - - [2.1.3. Pseudo-accounts](#213-pseudo-accounts) - - [2.1.4. Ownership](#214-ownership) - - [2.1.5. Reserves](#215-reserves) -- [3. Transactions](#3-transactions) - - [3.1. RippleState Transactions](#31-ripplestate-transactions) - - [3.1.1. TrustSet Transaction](#311-trustset-transaction) - - [3.1.1.1. Failure Conditions](#3111-failure-conditions) - - [3.1.1.2. State Changes](#3112-state-changes) - - [3.1.2. Clawback Transaction](#312-clawback-transaction) - - [3.1.2.1. Failure Conditions](#3121-failure-conditions) - - [3.1.2.2. State Changes](#3122-state-changes) - -# 1. Introduction - -Trust lines are a mechanism that enables the XRP Ledger to support user-issued [IOUs](../glossary.md#iou). They represent bilateral relationships between accounts that establish trust limits and govern the flow of value for specific IOUs. - -Think of a trust line as a credit agreement: it defines how much of a particular IOU one account holds from an issuer, along with the terms of that relationship. - -For example, suppose Alice is an issuer and Bob wants to hold USD issued by her. Bob would create a trust line, specifying USD as the currency code and Alice's address as the issuer. -Alice can now send USD to Bob. -The IOU is identified by currency code `USD` and `Alice's address`. The balance between Alice and Bob is stored on the trust line. -If Alice sent 10 USD to Bob, her balance would be -10, while Bob's balance would be 10. Please note that `xrpld` implementation may not store the balance as such, but the user will see it that way. -Alice can keep issuing the same IOU to other parties, and her total balance would be the sum of her balance on each of the trust lines. - -If Alice's account contains `RequireAuth` flag, then her trust lines have to be authorized. This means that, after Bob creates a trust line, Alice has to authorize it before Bob can receive any IOUs on that trust line. Alice authorizes the trust line by sending a TrustSet transaction with the `tfSetfAuth` flag, specifying Bob's address in the `LimitAmount.issuer` field. Alice may choose to freeze or deep freeze the trust line. She can also clawback IOUs from a trust line. - -Trust lines have a concept of QualityIn and QualityOut. This is covered in [Cross Currency Payments section](../payments/README.md#42-cross-currency-payment-execution) and [trust line quality in DirectStepI](../flow/steps.md#221-quality-implementation). For the sake of manipulating `RippleState` ledger entry, it is important to know that a quality of `1,000,000,000` is the default quality (QUALITY_ONE). This represents a 1:1 transfer rate, meaning the full amount is transferred without adjustment during cross-currency payments through this trust line. - -Trust line limits define the maximum amount of an IOU an account is willing to hold. The `LimitAmount` field in the `TrustSet` transaction specifies this maximum. A limit of 0 means the account will not accept any incoming IOUs on that trust line. Trust line limits are soft limits - they can be exceeded during offer crossing, as creating an offer is considered explicit consent to receive IOUs. -See [DirectIOfferCrossingStep](../flow/steps.md#23-directioffercrossingstep-offer-crossing-specific-implementation) for implementation details. - -Payments on the XRP Ledger often need to flow through intermediate accounts to reach the destination. For example, if Alice wants to pay Bob in USD and both hold trust lines to the same issuer, the payment flows through the issuer: Alice's balance on her trust line with Issuer decreases, and Bob's balance on his trust line with Issuer increases. When an account other than the issuer sits between two trust lines for the same currency, the payment can also flow through that account, entering on one trust line and exiting on another. This is called **rippling**. - -The NoRipple flag (`lsfLowNoRipple` / `lsfHighNoRipple`) is a per-account, per-trust-line flag that controls whether a trust line can be used for rippling. A payment is blocked from rippling through an account only when that account has NoRipple set on **both** the trust line the payment enters on and the trust line it exits on. If the account has NoRipple cleared on at least one of the two trust lines, the payment can flow through. - -On a newly created trust line, each side's NoRipple flag is initialized independently. The account that submits the `TrustSet` controls its **own** side: NoRipple is set there only if that transaction includes `tfSetNoRipple`. The **counterparty's** side is set automatically when the counterparty's account does not have `lsfDefaultRipple` (the account-level flag set via AccountSet's `asfDefaultRipple`). Because issuers set `DefaultRipple`, a holder opening a trust line to an issuer leaves the issuer's side clear, so the both-sides condition is never met and payments can always ripple through the issuer. A regular holder, by contrast, is not protected automatically: to stop payments from rippling through their own account (for example across USD.IssuerA and USD.IssuerB), the holder must set `tfSetNoRipple` on each line so that both sides carry NoRipple. - -NoRipple is checked during both [path finding](../path_finding/README.md) and [payment execution](../flow/steps.md#215-check-implementation). Path finding uses NoRipple as a heuristic filter to avoid exploring paths that would be rejected. The flow engine enforces it as a hard constraint, failing the strand with `terNO_RIPPLE` when violated. See [trust line creation](#3112-state-changes) for how NoRipple flags are initialized. - -## 1.1. Default State - -A trust line is in **default state** when both accounts have all their parameters set to default values. The default state is important because: -- Trust lines in default state are automatically deleted to reduce ledger bloat -- Default state determines whether an account must pay a reserve for its side of the trust line -- Attempting to create a trust line in default state fails with `tecNO_LINE_REDUNDANT` - -**Default values for an account's side of a trust line:** - -- **QualityIn**: 0 or absent (equivalent to QUALITY_ONE = 1,000,000,000) -- **QualityOut**: 0 or absent (equivalent to QUALITY_ONE = 1,000,000,000) -- **NoRipple flag**: set if the account does **not** have `lsfDefaultRipple`; cleared if it does -- **Freeze flag**: not set -- **Limit**: 0 -- **Balance**: 0 or negative from the account's perspective (meaning the account owes IOUs rather than holds them) - -Both the low and high accounts must have their parameters in the default state for the trust line to be considered in the default state and eligible for deletion. - -**Issuer vs Holder perspective:** - -- **Issuer** (negative balance): Has limit = 0, balance <= 0. The issuer's side is typically in default state when no IOUs have been redeemed yet. -- **Holder** (positive balance): Has limit > 0, balance >= 0. The holder's side requires a non-zero limit to receive IOUs, so it's not in default state while the trust line is usable. - -When a TrustSet transaction is processed and the trust line would transition to default state, the `RippleState` object is deleted, and both accounts' owner directories are updated to remove the trust line entries. - -# 2. Ledger Entries - -```mermaid -graph LR - A[Account A
Low Account] - B[Account B
High Account] - R[RippleState
Currency: USD
Balance: -10] - - A -->|lowLimit
lowQualityIn/Out
LowNode| R - B -->|highLimit
highQualityIn/Out
HighNode| R - - R -.->|Balance: -10
A owes B
A is issuer| A - R -.->|Balance: +10
B holds IOUs
B is holder| B -``` - -## 2.1. RippleState Ledger Entry - -A single `RippleState` ledger entry represents the trust line relationship between two accounts for a specific **currency code**. `RippleState` stores two account IDs in canonical order: the first is always the account ID sorted lower (the **low account**), and the second is the account ID sorted higher (the **high account**). Please see [xrpl.org](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/ripplestate#high-vs.-low-account). - -**Storage vs User Perspective:** - -- **What's stored**: One `RippleState` entry with a single `sfBalance` field from the low account's perspective -- **What users see**: Each account sees their own balance on the trust line, which is the inverse of their peer's balance -- **Example**: If `sfBalance = -10` (stored), low account sees `-10` and high account sees `+10` - -**Issuer** is not defined by `RippleState`. The issuer is conceptually the entity that created (issued) the IOU, but nothing in `RippleState` stores that information explicitly. The issuer is determined by the balance direction: the account with a negative balance (owing IOUs) is issuing, while the account with a positive balance is holding (has -redeemed IOUs). When the balance is `0`, either party can potentially issue IOUs, limited by the other party's trust line limit. The flow engine determines the issuer contextually based on the debt direction during payment execution. See [DirectStepI debt direction](../flow/steps.md#211-revimp-implementation) for details. - -The `sfBalance` field in `RippleState` is stored from the low account's perspective: - -- Positive balance = Low account holds currency (owes nothing, has credit) -- Negative balance = High account holds currency (low account owes money) - -### 2.1.1. Object Identifier - -The key of the `RippleState` object is the result -of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values -concatenated in order: - -- The `RippleState` space key `0x0072` (lowercase `r`) -- The `AccountID` of the *low* account. -- The `AccountID` of the *high* account. -- The 160-bit `Currency` code. - -*Low* account is the account with a lower `ID` than the *high* account, ensuring that a trust line between any two -accounts is always represented by the same `RippleState`. - -### 2.1.2. Fields - -Please -see [RippleState Fields](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/ripplestate#ripplestate-fields) - -#### 2.1.2.1. Flags - -Please -see [RippleState Flags](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/ripplestate#ripplestate-flags) - -### 2.1.3. Pseudo-accounts - -Trust lines can be created or modified with certain pseudo-accounts as the destination: - -- **AMM accounts** (has `sfAMMID`): Can create new trust lines for the AMM's LP token, or modify existing trust lines -- **Vault accounts** (has `sfVaultID`) and **LoanBroker accounts** (has `sfLoanBrokerID`): Can only modify existing trust lines; attempting to create a new one fails with `tecNO_PERMISSION` -- **Other pseudo-accounts**: Cannot create or modify trust lines (fails with `tecPSEUDO_ACCOUNT`) - -The TrustSet transaction never creates, deletes, or modifies the pseudo-account itself - it only creates or modifies -the `RippleState` entry that represents the trust line relationship. - -### 2.1.4. Ownership - -A `RippleState` ledger entry is jointly owned by both participating accounts. When a trust line is created, it is added to the owner directory of both the low account and the high account. - -The `RootIndex` of each account's owner `DirectoryNode` is the result of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values concatenated in order: - -- The `OwnerDirectory` space key `0x004F` (uppercase `O`) -- The `AccountID` of the account - -The `sfLowNode` and `sfHighNode` fields in the `RippleState` entry store the directory page indices where the trust line appears in each account's owner directory. These serve as deletion hints for efficient removal when the trust line is deleted. - -### 2.1.5. Reserves - -Every non-default trust line increments the account's `OwnerCount`, which raises its reserve requirement. The `TrustSet` transaction enforces that incremental reserve only when the account already owns two or more objects; while it owns fewer than two, a new trust line is allowed even if the account's balance would not cover the extra reserve (this lets a gateway fund new users cheaply). - -An account's side of a trust line requires a reserve when any of the following are in a non-default state: - -- **QualityIn** is set (non-zero) -- **QualityOut** is set (non-zero) -- **NoRipple flag** differs from the account's default ripple setting -- **Freeze flag** is set -- **Limit** is non-zero -- **Balance** is positive (account holds IOUs) - -When a trust line side transitions from default to non-default state, the `lsfLowReserve` or `lsfHighReserve` flag is -set and the account's `OwnerCount` is incremented. When all parameters return to default state, the reserve flag is -cleared and `OwnerCount` is decremented. - -Under the `Sponsor` amendment, the reserve for the source account's side of a trust line can be covered by a reserve sponsor. Each side records its own sponsor in the `LowSponsor` or `HighSponsor` field of the `RippleState` entry. Only the transaction's own account's side can be sponsored, never the counterparty's. The reserve waiver for accounts owning fewer than two objects does not apply to a sponsored transaction. When a sponsored side returns to default state, the reserve release is accounted against the recorded sponsor and the field is removed. Trust lines created implicitly during payment execution and offer crossing are never sponsored. The `SponsorshipTransfer` transaction can start, reassign, or end sponsorship of a side that currently holds a reserve. The sponsorship mechanism is described in the [transactions documentation](../transactions/README.md).[^tl-sponsor] - -[^tl-sponsor]: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.3.0/include/xrpl/protocol/detail/ledger_entries.macro#L280-L294), [`TrustSet.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/token/TrustSet.cpp#L319-L331), [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1480-L1497), [`SponsorshipTransfer.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp#L238-L295) - -# 3. Transactions - -## 3.1. RippleState Transactions - -### 3.1.1. TrustSet Transaction - -The `TrustSet` transaction creates, modifies or deletes a `RippleState` ledger entry and manages the corresponding `DirectoryNode` entries. - -When a trust line is created or deleted, it affects the owner directories of both participating accounts. Each account maintains an owner directory that tracks all ledger objects it owns, including its trust lines. The `sfLowNode` and `sfHighNode` fields in the `RippleState` entry store the page numbers where the trust line appears in each account's owner directory - -| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | -|-------------------|:-----------------:|:-----------:|:---------:|:-------------:|:-------------:|:------------------------------------------------------------------------------------------------------------------------------------| -| `TransactionType` | :heavy_checkmark: | `No` | `String` | `UINT16` | | The transaction type. Must be `TrustSet` | -| `LimitAmount` | :heavy_checkmark: | `Yes` | `Object` | `Amount` | | Object defining the currency and the peer account when manipulating a trust line | -| `QualityIn` | | `Yes` | `Number` | `UINT32` | `None` | Quality ratio applied when receiving IOUs on this trust line. When absent or 0, defaults to QUALITY_ONE (1,000,000,000 = 1:1 ratio) | -| `QualityOut` | | `Yes` | `Number` | `UINT32` | `None` | Quality ratio applied when sending IOUs on this trust line. When absent or 0, defaults to QUALITY_ONE (1,000,000,000 = 1:1 ratio) | -| `Flags` | | `Yes` | `String` | `UINT32` | `0` | | - -The `LimitAmount` field specifies the maximum amount of the IOU the account is willing to hold. It is a composite field containing the currency code, the peer account, and the limit value. Setting the value to 0 means the account will not accept any incoming IOUs on that trust line. - -Counterintuitively, `LimitAmount.issuer` is not always the issuer account, even in the business logic sense. For -example, if the issuer wants to authorize a trust line, they will send their account id in `account` and **holder's** -account id as the `issuer`. - -For flags, please refer -to [TrustSet Flags](https://xrpl.org/docs/references/protocol/transactions/types/trustset#trustset-flags). - -**Terminology in this document** - -- *Source account* is the account specified as `account` in the transaction. -- *Destination account* is the account specified as `issuer` field in `LimitAmount`. - -#### 3.1.1.1. Failure Conditions - -**Static validation:** - -- `temINVALID_FLAG`: one of the specified flags is not one of [flags](#2121-flags). -- `temINVALID_FLAG`: flags contain `tfSetDeepFreeze` or `tfClearDeepFreeze` and [DeepFreeze amendment](https://xrpl.org/resources/known-amendments#deepfreeze) is not enabled. -- `temBAD_AMOUNT`: `LimitAmount` is XRP and mantissa is bigger than `100000000000000000ull`. This is a defensive `isLegalNet` check; in practice an XRP `LimitAmount` fails with `temBAD_LIMIT` (below). -- `temBAD_LIMIT`: `LimitAmount` is XRP. -- `temBAD_CURRENCY`: `currency` field in `LimitAmount` is `XRP`. -- `temBAD_LIMIT`: `value` field in `LimitAmount` is less than `0`. -- `temDST_NEEDED`: `issuer` field in `LimitAmount` is not specified or it represents a [noAccount](transactions/README.md#noAccount). - -**Validation against the ledger view:** - -- `terNO_ACCOUNT`: source account does not exist. -- `tefNO_AUTH_REQUIRED`: source account does not have a `lsfRequireAuth` flag set, but the transaction contains `tfSetfAuth` flag. -- `temDST_IS_SRC`: the source account and the destination account (`LimitAmount.issuer`) are the same. -- `tecNO_DST`: the [AMM](https://xrpl.org/resources/known-amendments#amm) or [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault) amendment is enabled and the destination (issuer) account does not exist. -- `tecNO_PERMISSION`: the destination account has the `lsfDisallowIncomingTrustline` flag set: - - If the trust line already exists, do not fail (the [fixDisallowIncomingV1](https://xrpl.org/resources/known-amendments#fixdisallowincomingv1) amendment that introduced this exemption was retired, so it applies unconditionally). -- If the destination account is a pseudo-account: - - `sfAMMID`: destination is an AMM account (has `sfAMMID` field), but the trust line does not already exist between source and AMM. - - `tecAMM_EMPTY`: AMM has zero LP IOUs - cannot create trust lines to empty AMMs. - - `tecNO_PERMISSION`: currency in the trust line request does not match the AMM's LP token currency. - - `tecINTERNAL`: AMM ledger entry cannot be found. - - `tecNO_PERMISSION`: destination is a Vault account (has `sfVaultID` field) or LoanBroker account (has `sfLoanBrokerID` field) and the trust line does not already exist. - - `tecPSEUDO_ACCOUNT`: destination is any other type of pseudo-account. -- If [DeepFreeze](https://xrpl.org/resources/known-amendments#deepfreeze) amendment is enabled, validate freeze flag combinations: - - `tecNO_PERMISSION`: source account has `lsfNoFreeze` flag set and the transaction contains `tfSetFreeze` or `tfSetDeepFreeze` - flags. - - `tecNO_PERMISSION`: transaction contains both freeze flags (`tfSetFreeze` or `tfSetDeepFreeze`) and unfreeze flags ( - `tfClearFreeze` or `tfClearDeepFreeze`) - user should not be able to send - conflicting `set` and `clear` flags in the same instructions. - - `tecNO_PERMISSION`: transaction results in a trust line having a deep freeze set but a normal freeze cleared - user should not be able to deep freeze a trust line that is not in frozen state. - -**Validation during doApply:** - -- `tefINTERNAL`: source account does not exist. -- `tecNO_DST`: destination account does not exist. -- `tecNO_PERMISSION`: the user is trying to set `tfSetNoRipple` and the source account's balance on the trust line is negative. -- `tecINSUF_RESERVE_LINE`: user does not have enough balance to cover the reserve and wants to modify an existing trust line, regardless - of whether they or the counterparty have created the original trust line. For a sponsored reserve, the sponsor has insufficient XRP or the pre-funded sponsorship has no remaining owner-count allowance. -- `tecNO_LINE_INSUF_RESERVE`: user does not have enough balance to cover the reserve and wants to create a new trust line. For a sponsored reserve, the sponsor has insufficient XRP or the pre-funded sponsorship has no remaining owner-count allowance. -- `tecNO_LINE_REDUNDANT`: trust line does not already exist, amount is `0`, and `QualityIn` and `QualityOut` are either not set, or set to their default value (`1,000,000,000`), and if `tfSetfAuth` flag is not set. - -#### 3.1.1.2. State Changes - -- `RippleState` object is **deleted** if an existing trust line exists when sending `TrustSet` transaction and: - - If the trust line is in its [default state](#11-default-state) after updating it. - - If the currency code of the IOU is `XRP`. This is a defensive check; a `TrustSet` with an `XRP` currency is already rejected at preflight (`temBAD_CURRENCY`/`temBAD_LIMIT`), so this deletion branch is not reached in normal flow. - - When deleted, the trust line is removed from both accounts' owner directories: - - Removed from low account's owner directory via `dirRemove` using the `sfLowNode` page number. - - Removed from high account's owner directory via `dirRemove` using the `sfHighNode` page number. - - If removing the trust line empties a non-root directory page, that page is deleted and the directory chain is repaired. - - If either removal fails, the transaction returns `tefBAD_LEDGER`. - - -- `RippleState` object is **modified**: - - If `QualityIn` field was specified in the transaction: - - If `QualityIn` is non-zero, set the appropriate quality field (`sfLowQualityIn` for low source account, `sfHighQualityIn` for high source account) to the `QualityIn` value. Unlike `QualityOut`, a `QualityIn` equal to `1,000,000,000` (QUALITY_ONE) is stored as-is, not folded to the default. - - If `QualityIn` is `0` (or absent), clear the appropriate quality field (`sfLowQualityIn` for low source account, `sfHighQualityIn` for high source account). - - If `QualityOut` field was specified in the transaction: - - If `QualityOut` != `1,000,000,000`, set the appropriate quality field (`sfLowQualityOut` for low source - account, `sfHighQualityOut` for high source account) to the `QualityOut` value - - If `QualityOut` = `1,000,000,000`, clear the appropriate quality field (`sfLowQualityOut` for low source - account, `sfHighQualityOut` for high source account) - - If the transaction contains `tfSetNoRipple` flag and not `tfClearNoRipple` flag: - - If the source account's balance on the trust line is non-negative, set the appropriate NoRipple flag (`lsfLowNoRipple` for low account, `lsfHighNoRipple` for high account) - - If the source account's balance is negative, the transaction fails with `tecNO_PERMISSION` - - If the transaction contains `tfClearNoRipple` flag and not `tfSetNoRipple` flag, clear the appropriate NoRipple flag (`lsfLowNoRipple` for low account, `lsfHighNoRipple` for high account) - - If the transaction contains `tfSetfAuth` flag, set the appropriate authorization flag for the source account's side only (`lsfLowAuth` if the source is the low account, `lsfHighAuth` if it is the high account). - - Note: if, after applying the above modifications, the trust line is in its [default state](#11-default-state), it is deleted rather than kept as modified (see deletion conditions above)[^modify-then-delete]. - - If account's parameters in a trust line change to non-default values such that it requires reserve but did not - before: - - Set appropriate `lsfLowReserve` or `lsfHighReserve` flag - - If the transaction's reserve is sponsored, record the sponsor in the side's `LowSponsor` or `HighSponsor` field - - If account no longer requires reserve because its values in a trust line are now default values: - - Clear appropriate `lsfLowReserve` or `lsfHighReserve` flag - - The reserve release is accounted against the sponsor recorded on that side, if any, and the sponsor field is removed - - Only the NoRipple, freeze, authorization, and reserve flag bits are individually set or cleared (as described above); all other stored flag bits are preserved, and `sfFlags` is rewritten only if it changed. - - -- `RippleState` object is **created**: - - If an existing trust line for the same IOU does not exist between the two accounts. - - When created, the trust line is added to both accounts' owner directories: - - Added to low account's owner directory via `dirInsert`. The page number is stored in `sfLowNode`. - - Added to high account's owner directory via `dirInsert`. The page number is stored in `sfHighNode`. - - If either directory is full, the transaction fails with `tecDIR_FULL`. - - NoRipple flags are initialized on both sides of the trust line[^trustcreate-noripple]: - - The source account's NoRipple flag (`lsfLowNoRipple` or `lsfHighNoRipple`) is set if the TrustSet transaction contains `tfSetNoRipple` and not `tfClearNoRipple`[^trustcreate-noripple-src]. - - The destination account's NoRipple flag is set if the destination account does **not** have `lsfDefaultRipple` on their account[^trustcreate-noripple-dst]. `lsfDefaultRipple` is an account-level flag set via AccountSet (`asfDefaultRipple`). When an issuer sets `lsfDefaultRipple`, new trust lines are created without NoRipple on the issuer's side, allowing rippling by default. - -[^modify-then-delete]: Default state check and deletion after modification: [`TrustSet.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/token/TrustSet.cpp#L617-L622) -[^trustcreate-noripple]: NoRipple initialization in trustCreate: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L264-L281) -[^trustcreate-noripple-src]: Source account NoRipple from transaction flags: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L264-L267) -[^trustcreate-noripple-dst]: Destination account NoRipple from lsfDefaultRipple: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L277-L281) - - -- `DirectoryNode` object is **created or modified**: - - When a trust line is created, it is added to both participating accounts' owner directories. - - If an owner directory page is full (32 entries), a new page is created and linked to the directory chain. - - Before the `fixDirectoryLimit` amendment, if creating a new page would exceed 262,144 pages, the transaction fails with `tecDIR_FULL`. With `fixDirectoryLimit` enabled, that cap is removed and a new page can only fail to be created on 64-bit page-number overflow. - - -- `AccountRoot` object is **modified**: - - If account now requires reserve but did not before: - - Increment `sfOwnerCount` by 1, without overflowing ```std::uint32_t```. - - If account no longer requires reserve: - - Decrement `sfOwnerCount` by 1, without going below `0`. - -### 3.1.2. Clawback Transaction - -**Design note** - in `xrpld`, the same transactor implementation of `Clawback` is used to clawback -both [IOUs](../glossary.md#iou) and [MPTs](../glossary.md#mpt). `xrpld`'s `Clawback` implementation of -`Transactor` uses an adaptation of a visitor pattern. Any transaction containing an MPT in `Amount` field will visit -`MPTIssue` implementation, while transactions referring to IOUs in `Amount` field will visit `Issue` implementation. -This allows for the code to be sufficiently separated that even when describing `xrpld` implementation of Clawback we -can discuss two implementations separately. - -Transaction fields are described in [Clawback Fields](https://xrpl.org/docs/references/protocol/transactions/types/clawback#clawback-fields). - -**Terminology in this document** - -- *Issuer* is the account specified as `account` in the transaction. -- *Holder* is the account specified, counterintuitively, as `issuer` field in `Amount`. This is a commonly used - pattern to use `issuer` to denote a peer's account. - -#### 3.1.2.1. Failure Conditions - -Static validation - -- `temINVALID_FLAG`: any flags, other than universal transaction flags, are specified. -- `temMALFORMED`: - - For IOU clawback: user provided `Holder` field in the transaction (IOUs use `Amount.issuer` to specify the holder). - - For MPT clawback: user did NOT provide `Holder` field in the transaction (MPTs require the `Holder` field). -- `temBAD_AMOUNT`: - - issuer and holder are the same account (for IOU clawback; an MPT clawback with the same issuer and holder returns `temMALFORMED` instead). - - `Amount` provided is XRP. - - `Amount` is not bigger than `0`. - -Validation against the ledger view - -- `terNO_ACCOUNT`: issuer's or holder's account does not exist. -- `tecAMM_ACCOUNT`: holder's account is an AMM account (has `sfAMMID`) and the [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault) amendment is not enabled. -- `tecPSEUDO_ACCOUNT`: If `SingleAssetVault` is enabled and holder's account is any pseudo-account. -- `tecNO_PERMISSION`: issuer does not have `lsfAllowTrustLineClawback` or it does have `lsfNoFreeze`. Only - trust lines of issuers that can be frozen and that allow trust line clawback can be clawed back. -- `tecNO_LINE`: there is no existing trust line between issuer and holder for the specified currency. -- `tecNO_PERMISSION`: the account specified as `account` (issuer) is not actually the issuer based on the trust line - balance. The true issuer is the account with the negative balance (owing IOUs), not the positive balance (holding - IOUs). This error occurs when the transaction sender has the accounts reversed. -- `tecINSUFFICIENT_FUNDS`: the holder's available balance (computed via `accountHolds`) is `0` or negative. This is the spendable balance, which can differ from the raw trust-line balance. - - -#### 3.1.2.2. State Changes - -- `RippleState` object is **modified**: - - Amount specified in `Amount` field is transferred from holder to issuer on the trust line for specified currency - code. This is done by subtracting the amount from holder's account and adding it to issuer's account. No limits or - fees are enforced. - - If the `amount` is bigger than the available balance, only the available balance is moved. - - If the clawback returns the holder's side of the trust line to its default state (the holder's balance falls from positive to zero, and its limit, qualities, NoRipple, and freeze are already at default), the holder's `lsfLowReserve` or `lsfHighReserve` flag is cleared and the holder's `OwnerCount` is decremented. Only the holder (sender) side is adjusted; the issuer's side is not. - - -- `RippleState` object is **deleted**: - - If, after clawing back, the trust line is in its default state, `RippleState` is deleted. - - When deleted, the trust line is removed from both accounts' owner directories: - - Removed from low account's owner directory via `dirRemove` using the `sfLowNode` page number. - - Removed from high account's owner directory via `dirRemove` using the `sfHighNode` page number. - - If removing the trust line empties a non-root directory page, that page is deleted and the directory chain is repaired. - - -- `AccountRoot` (`sfOwnerCount`): deleting the `RippleState` does not by itself change any account's `sfOwnerCount`. The only owner-count change a clawback makes is the single holder-side decrement noted above, applied when the holder's side returns to default (whether or not the line is then deleted). The issuer's `sfOwnerCount` is not adjusted by a clawback. +# Index + +- [1. Introduction](#1-introduction) + - [1.1. Default State](#11-default-state) +- [2. Ledger Entries](#2-ledger-entries) + - [2.1. RippleState Ledger Entry](#21-ripplestate-ledger-entry) + - [2.1.1. Object Identifier](#211-object-identifier) + - [2.1.2. Fields](#212-fields) + - [2.1.2.1. Flags](#2121-flags) + - [2.1.3. Pseudo-accounts](#213-pseudo-accounts) + - [2.1.4. Ownership](#214-ownership) + - [2.1.5. Reserves](#215-reserves) +- [3. Transactions](#3-transactions) + - [3.1. RippleState Transactions](#31-ripplestate-transactions) + - [3.1.1. TrustSet Transaction](#311-trustset-transaction) + - [3.1.1.1. Failure Conditions](#3111-failure-conditions) + - [3.1.1.2. State Changes](#3112-state-changes) + - [3.1.2. Clawback Transaction](#312-clawback-transaction) + - [3.1.2.1. Failure Conditions](#3121-failure-conditions) + - [3.1.2.2. State Changes](#3122-state-changes) + +# 1. Introduction + +Trust lines are a mechanism that enables the XRP Ledger to support user-issued [IOUs](../glossary.md#iou). They represent bilateral relationships between accounts that establish trust limits and govern the flow of value for specific IOUs. + +Think of a trust line as a credit agreement: it defines how much of a particular IOU one account holds from an issuer, along with the terms of that relationship. + +For example, suppose Alice is an issuer and Bob wants to hold USD issued by her. Bob would create a trust line, specifying USD as the currency code and Alice's address as the issuer. +Alice can now send USD to Bob. +The IOU is identified by currency code `USD` and `Alice's address`. The balance between Alice and Bob is stored on the trust line. +If Alice sent 10 USD to Bob, her balance would be -10, while Bob's balance would be 10. Please note that `xrpld` implementation may not store the balance as such, but the user will see it that way. +Alice can keep issuing the same IOU to other parties, and her total balance would be the sum of her balance on each of the trust lines. + +If Alice's account contains `RequireAuth` flag, then her trust lines have to be authorized. This means that, after Bob creates a trust line, Alice has to authorize it before Bob can receive any IOUs on that trust line. Alice authorizes the trust line by sending a TrustSet transaction with the `tfSetfAuth` flag, specifying Bob's address in the `LimitAmount.issuer` field. Alice may choose to freeze or deep freeze the trust line. She can also clawback IOUs from a trust line. + +Trust lines have a concept of QualityIn and QualityOut. This is covered in [Cross Currency Payments section](../payments/README.md#42-cross-currency-payment-execution) and [trust line quality in DirectStepI](../flow/steps.md#221-quality-implementation). For the sake of manipulating `RippleState` ledger entry, it is important to know that a quality of `1,000,000,000` is the default quality (QUALITY_ONE). This represents a 1:1 transfer rate, meaning the full amount is transferred without adjustment during cross-currency payments through this trust line. + +Trust line limits define the maximum amount of an IOU an account is willing to hold. The `LimitAmount` field in the `TrustSet` transaction specifies this maximum. A limit of 0 means the account will not accept any incoming IOUs on that trust line. Trust line limits are soft limits - they can be exceeded during offer crossing, as creating an offer is considered explicit consent to receive IOUs. +See [DirectIOfferCrossingStep](../flow/steps.md#23-directioffercrossingstep-offer-crossing-specific-implementation) for implementation details. + +Payments on the XRP Ledger often need to flow through intermediate accounts to reach the destination. For example, if Alice wants to pay Bob in USD and both hold trust lines to the same issuer, the payment flows through the issuer: Alice's balance on her trust line with Issuer decreases, and Bob's balance on his trust line with Issuer increases. When an account other than the issuer sits between two trust lines for the same currency, the payment can also flow through that account, entering on one trust line and exiting on another. This is called **rippling**. + +The NoRipple flag (`lsfLowNoRipple` / `lsfHighNoRipple`) is a per-account, per-trust-line flag that controls whether a trust line can be used for rippling. A payment is blocked from rippling through an account only when that account has NoRipple set on **both** the trust line the payment enters on and the trust line it exits on. If the account has NoRipple cleared on at least one of the two trust lines, the payment can flow through. + +On a newly created trust line, each side's NoRipple flag is initialized independently. The account that submits the `TrustSet` controls its **own** side: NoRipple is set there only if that transaction includes `tfSetNoRipple`. The **counterparty's** side is set automatically when the counterparty's account does not have `lsfDefaultRipple` (the account-level flag set via AccountSet's `asfDefaultRipple`). Because issuers set `DefaultRipple`, a holder opening a trust line to an issuer leaves the issuer's side clear, so the both-sides condition is never met and payments can always ripple through the issuer. A regular holder, by contrast, is not protected automatically: to stop payments from rippling through their own account (for example across USD.IssuerA and USD.IssuerB), the holder must set `tfSetNoRipple` on each line so that both sides carry NoRipple. + +NoRipple is checked during both [path finding](../path_finding/README.md) and [payment execution](../flow/steps.md#215-check-implementation). Path finding uses NoRipple as a heuristic filter to avoid exploring paths that would be rejected. The flow engine enforces it as a hard constraint, failing the strand with `terNO_RIPPLE` when violated. See [trust line creation](#3112-state-changes) for how NoRipple flags are initialized. + +## 1.1. Default State + +A trust line is in **default state** when both accounts have all their parameters set to default values. The default state is important because: +- Trust lines in default state are automatically deleted to reduce ledger bloat +- Default state determines whether an account must pay a reserve for its side of the trust line +- Attempting to create a trust line in default state fails with `tecNO_LINE_REDUNDANT` + +**Default values for an account's side of a trust line:** + +- **QualityIn**: 0 or absent (equivalent to QUALITY_ONE = 1,000,000,000) +- **QualityOut**: 0 or absent (equivalent to QUALITY_ONE = 1,000,000,000) +- **NoRipple flag**: set if the account does **not** have `lsfDefaultRipple`; cleared if it does +- **Freeze flag**: not set +- **Limit**: 0 +- **Balance**: 0 or negative from the account's perspective (meaning the account owes IOUs rather than holds them) + +Both the low and high accounts must have their parameters in the default state for the trust line to be considered in the default state and eligible for deletion. + +**Issuer vs Holder perspective:** + +- **Issuer** (negative balance): Has limit = 0, balance <= 0. The issuer's side is typically in default state when no IOUs have been redeemed yet. +- **Holder** (positive balance): Has limit > 0, balance >= 0. The holder's side requires a non-zero limit to receive IOUs, so it's not in default state while the trust line is usable. + +When a TrustSet transaction is processed and the trust line would transition to default state, the `RippleState` object is deleted, and both accounts' owner directories are updated to remove the trust line entries. + +# 2. Ledger Entries + +```mermaid +graph LR + A[Account A
Low Account] + B[Account B
High Account] + R[RippleState
Currency: USD
Balance: -10] + + A -->|lowLimit
lowQualityIn/Out
LowNode| R + B -->|highLimit
highQualityIn/Out
HighNode| R + + R -.->|Balance: -10
A owes B
A is issuer| A + R -.->|Balance: +10
B holds IOUs
B is holder| B +``` + +## 2.1. RippleState Ledger Entry + +A single `RippleState` ledger entry represents the trust line relationship between two accounts for a specific **currency code**. `RippleState` stores two account IDs in canonical order: the first is always the account ID sorted lower (the **low account**), and the second is the account ID sorted higher (the **high account**). Please see [xrpl.org](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/ripplestate#high-vs.-low-account). + +**Storage vs User Perspective:** + +- **What's stored**: One `RippleState` entry with a single `sfBalance` field from the low account's perspective +- **What users see**: Each account sees their own balance on the trust line, which is the inverse of their peer's balance +- **Example**: If `sfBalance = -10` (stored), low account sees `-10` and high account sees `+10` + +**Issuer** is not defined by `RippleState`. The issuer is conceptually the entity that created (issued) the IOU, but nothing in `RippleState` stores that information explicitly. The issuer is determined by the balance direction: the account with a negative balance (owing IOUs) is issuing, while the account with a positive balance is holding (has +redeemed IOUs). When the balance is `0`, either party can potentially issue IOUs, limited by the other party's trust line limit. The flow engine determines the issuer contextually based on the debt direction during payment execution. See [DirectStepI debt direction](../flow/steps.md#211-revimp-implementation) for details. + +The `sfBalance` field in `RippleState` is stored from the low account's perspective: + +- Positive balance = Low account holds currency (owes nothing, has credit) +- Negative balance = High account holds currency (low account owes money) + +### 2.1.1. Object Identifier + +The key of the `RippleState` object is the result +of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values +concatenated in order: + +- The `RippleState` space key `0x0072` (lowercase `r`) +- The `AccountID` of the *low* account. +- The `AccountID` of the *high* account. +- The 160-bit `Currency` code. + +*Low* account is the account with a lower `ID` than the *high* account, ensuring that a trust line between any two +accounts is always represented by the same `RippleState`. + +### 2.1.2. Fields + +Please +see [RippleState Fields](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/ripplestate#ripplestate-fields) + +#### 2.1.2.1. Flags + +Please +see [RippleState Flags](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/ripplestate#ripplestate-flags) + +### 2.1.3. Pseudo-accounts + +Trust lines can be created or modified with certain pseudo-accounts as the destination: + +- **AMM accounts** (has `sfAMMID`): Can create new trust lines for the AMM's LP token, or modify existing trust lines +- **Vault accounts** (has `sfVaultID`) and **LoanBroker accounts** (has `sfLoanBrokerID`): Can only modify existing trust lines; attempting to create a new one fails with `tecNO_PERMISSION` +- **Other pseudo-accounts**: Cannot create or modify trust lines (fails with `tecPSEUDO_ACCOUNT`) + +The TrustSet transaction never creates, deletes, or modifies the pseudo-account itself - it only creates or modifies +the `RippleState` entry that represents the trust line relationship. + +### 2.1.4. Ownership + +A `RippleState` ledger entry is jointly owned by both participating accounts. When a trust line is created, it is added to the owner directory of both the low account and the high account. + +The `RootIndex` of each account's owner `DirectoryNode` is the result of [SHA512-Half](https://xrpl.org/docs/references/protocol/data-types/basic-data-types#hashes) of the following values concatenated in order: + +- The `OwnerDirectory` space key `0x004F` (uppercase `O`) +- The `AccountID` of the account + +The `sfLowNode` and `sfHighNode` fields in the `RippleState` entry store the directory page indices where the trust line appears in each account's owner directory. These serve as deletion hints for efficient removal when the trust line is deleted. + +### 2.1.5. Reserves + +Every non-default trust line increments the account's `OwnerCount`, which raises its reserve requirement. The `TrustSet` transaction enforces that incremental reserve only when the account already owns two or more objects; while it owns fewer than two, a new trust line is allowed even if the account's balance would not cover the extra reserve (this lets a gateway fund new users cheaply). + +An account's side of a trust line requires a reserve when any of the following are in a non-default state: + +- **QualityIn** is set (non-zero) +- **QualityOut** is set (non-zero) +- **NoRipple flag** differs from the account's default ripple setting +- **Freeze flag** is set +- **Limit** is non-zero +- **Balance** is positive (account holds IOUs) + +When a trust line side transitions from default to non-default state, the `lsfLowReserve` or `lsfHighReserve` flag is +set and the account's `OwnerCount` is incremented. When all parameters return to default state, the reserve flag is +cleared and `OwnerCount` is decremented. + +Under the `Sponsor` amendment, the reserve for the source account's side of a trust line can be covered by a reserve sponsor. Each side records its own sponsor in the `LowSponsor` or `HighSponsor` field of the `RippleState` entry. Only the transaction's own account's side can be sponsored, never the counterparty's. The reserve waiver for accounts owning fewer than two objects does not apply to a sponsored transaction. When a sponsored side returns to default state, the reserve release is accounted against the recorded sponsor and the field is removed. Trust lines created implicitly during payment execution and offer crossing are never sponsored. The `SponsorshipTransfer` transaction can start, reassign, or end sponsorship of a side that currently holds a reserve. The sponsorship mechanism is described in the [transactions documentation](../transactions/README.md).[^tl-sponsor] + +[^tl-sponsor]: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/3.3.0/include/xrpl/protocol/detail/ledger_entries.macro#L280-L294), [`TrustSet.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/token/TrustSet.cpp#L319-L331), [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1480-L1497), [`SponsorshipTransfer.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp#L238-L295) + +# 3. Transactions + +## 3.1. RippleState Transactions + +### 3.1.1. TrustSet Transaction + +The `TrustSet` transaction creates, modifies or deletes a `RippleState` ledger entry and manages the corresponding `DirectoryNode` entries. + +When a trust line is created or deleted, it affects the owner directories of both participating accounts. Each account maintains an owner directory that tracks all ledger objects it owns, including its trust lines. The `sfLowNode` and `sfHighNode` fields in the `RippleState` entry store the page numbers where the trust line appears in each account's owner directory + +| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | +|-------------------|:-----------------:|:-----------:|:---------:|:-------------:|:-------------:|:------------------------------------------------------------------------------------------------------------------------------------| +| `TransactionType` | :heavy_checkmark: | `No` | `String` | `UINT16` | | The transaction type. Must be `TrustSet` | +| `LimitAmount` | :heavy_checkmark: | `Yes` | `Object` | `Amount` | | Object defining the currency and the peer account when manipulating a trust line | +| `QualityIn` | | `Yes` | `Number` | `UINT32` | `None` | Quality ratio applied when receiving IOUs on this trust line. When absent or 0, defaults to QUALITY_ONE (1,000,000,000 = 1:1 ratio) | +| `QualityOut` | | `Yes` | `Number` | `UINT32` | `None` | Quality ratio applied when sending IOUs on this trust line. When absent or 0, defaults to QUALITY_ONE (1,000,000,000 = 1:1 ratio) | +| `Flags` | | `Yes` | `String` | `UINT32` | `0` | | + +The `LimitAmount` field specifies the maximum amount of the IOU the account is willing to hold. It is a composite field containing the currency code, the peer account, and the limit value. Setting the value to 0 means the account will not accept any incoming IOUs on that trust line. + +Counterintuitively, `LimitAmount.issuer` is not always the issuer account, even in the business logic sense. For +example, if the issuer wants to authorize a trust line, they will send their account id in `account` and **holder's** +account id as the `issuer`. + +For flags, please refer +to [TrustSet Flags](https://xrpl.org/docs/references/protocol/transactions/types/trustset#trustset-flags). + +**Terminology in this document** + +- *Source account* is the account specified as `account` in the transaction. +- *Destination account* is the account specified as `issuer` field in `LimitAmount`. + +#### 3.1.1.1. Failure Conditions + +**Static validation:** + +- `temINVALID_FLAG`: one of the specified flags is not one of [flags](#2121-flags). +- `temINVALID_FLAG`: flags contain `tfSetDeepFreeze` or `tfClearDeepFreeze` and [DeepFreeze amendment](https://xrpl.org/resources/known-amendments#deepfreeze) is not enabled. +- `temBAD_AMOUNT`: `LimitAmount` is XRP and mantissa is bigger than `100000000000000000ull`. This is a defensive `isLegalNet` check; in practice an XRP `LimitAmount` fails with `temBAD_LIMIT` (below). +- `temBAD_LIMIT`: `LimitAmount` is XRP. +- `temBAD_CURRENCY`: `currency` field in `LimitAmount` is `XRP`. +- `temBAD_LIMIT`: `value` field in `LimitAmount` is less than `0`. +- `temDST_NEEDED`: `issuer` field in `LimitAmount` is not specified or it represents a [noAccount](transactions/README.md#noAccount). + +**Validation against the ledger view:** + +- `terNO_ACCOUNT`: source account does not exist. +- `tefNO_AUTH_REQUIRED`: source account does not have a `lsfRequireAuth` flag set, but the transaction contains `tfSetfAuth` flag. +- `temDST_IS_SRC`: the source account and the destination account (`LimitAmount.issuer`) are the same. +- `tecNO_DST`: the [AMM](https://xrpl.org/resources/known-amendments#amm) or [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault) amendment is enabled and the destination (issuer) account does not exist. +- `tecNO_PERMISSION`: the destination account has the `lsfDisallowIncomingTrustline` flag set: + - If the trust line already exists, do not fail (the [fixDisallowIncomingV1](https://xrpl.org/resources/known-amendments#fixdisallowincomingv1) amendment that introduced this exemption was retired, so it applies unconditionally). +- If the destination account is a pseudo-account: + - `sfAMMID`: destination is an AMM account (has `sfAMMID` field), but the trust line does not already exist between source and AMM. + - `tecAMM_EMPTY`: AMM has zero LP IOUs - cannot create trust lines to empty AMMs. + - `tecNO_PERMISSION`: currency in the trust line request does not match the AMM's LP token currency. + - `tecINTERNAL`: AMM ledger entry cannot be found. + - `tecNO_PERMISSION`: destination is a Vault account (has `sfVaultID` field) or LoanBroker account (has `sfLoanBrokerID` field) and the trust line does not already exist. + - `tecPSEUDO_ACCOUNT`: destination is any other type of pseudo-account. +- If [DeepFreeze](https://xrpl.org/resources/known-amendments#deepfreeze) amendment is enabled, validate freeze flag combinations: + - `tecNO_PERMISSION`: source account has `lsfNoFreeze` flag set and the transaction contains `tfSetFreeze` or `tfSetDeepFreeze` + flags. + - `tecNO_PERMISSION`: transaction contains both freeze flags (`tfSetFreeze` or `tfSetDeepFreeze`) and unfreeze flags ( + `tfClearFreeze` or `tfClearDeepFreeze`) - user should not be able to send + conflicting `set` and `clear` flags in the same instructions. + - `tecNO_PERMISSION`: transaction results in a trust line having a deep freeze set but a normal freeze cleared - user should not be able to deep freeze a trust line that is not in frozen state. + +**Validation during doApply:** + +- `tefINTERNAL`: source account does not exist. +- `tecNO_DST`: destination account does not exist. +- `tecNO_PERMISSION`: the user is trying to set `tfSetNoRipple` and the source account's balance on the trust line is negative. +- `tecINSUF_RESERVE_LINE`: user does not have enough balance to cover the reserve and wants to modify an existing trust line, regardless + of whether they or the counterparty have created the original trust line. For a sponsored reserve, the sponsor has insufficient XRP or the pre-funded sponsorship has no remaining owner-count allowance. +- `tecNO_LINE_INSUF_RESERVE`: user does not have enough balance to cover the reserve and wants to create a new trust line. For a sponsored reserve, the sponsor has insufficient XRP or the pre-funded sponsorship has no remaining owner-count allowance. +- `tecNO_LINE_REDUNDANT`: trust line does not already exist, amount is `0`, and `QualityIn` and `QualityOut` are either not set, or set to their default value (`1,000,000,000`), and if `tfSetfAuth` flag is not set. + +#### 3.1.1.2. State Changes + +- `RippleState` object is **deleted** if an existing trust line exists when sending `TrustSet` transaction and: + - If the trust line is in its [default state](#11-default-state) after updating it. + - If the currency code of the IOU is `XRP`. This is a defensive check; a `TrustSet` with an `XRP` currency is already rejected at preflight (`temBAD_CURRENCY`/`temBAD_LIMIT`), so this deletion branch is not reached in normal flow. + - When deleted, the trust line is removed from both accounts' owner directories: + - Removed from low account's owner directory via `dirRemove` using the `sfLowNode` page number. + - Removed from high account's owner directory via `dirRemove` using the `sfHighNode` page number. + - If removing the trust line empties a non-root directory page, that page is deleted and the directory chain is repaired. + - If either removal fails, the transaction returns `tefBAD_LEDGER`. + + +- `RippleState` object is **modified**: + - If `QualityIn` field was specified in the transaction: + - If `QualityIn` is non-zero, set the appropriate quality field (`sfLowQualityIn` for low source account, `sfHighQualityIn` for high source account) to the `QualityIn` value. Unlike `QualityOut`, a `QualityIn` equal to `1,000,000,000` (QUALITY_ONE) is stored as-is, not folded to the default. + - If `QualityIn` is `0` (or absent), clear the appropriate quality field (`sfLowQualityIn` for low source account, `sfHighQualityIn` for high source account). + - If `QualityOut` field was specified in the transaction: + - If `QualityOut` != `1,000,000,000`, set the appropriate quality field (`sfLowQualityOut` for low source + account, `sfHighQualityOut` for high source account) to the `QualityOut` value + - If `QualityOut` = `1,000,000,000`, clear the appropriate quality field (`sfLowQualityOut` for low source + account, `sfHighQualityOut` for high source account) + - If the transaction contains `tfSetNoRipple` flag and not `tfClearNoRipple` flag: + - If the source account's balance on the trust line is non-negative, set the appropriate NoRipple flag (`lsfLowNoRipple` for low account, `lsfHighNoRipple` for high account) + - If the source account's balance is negative, the transaction fails with `tecNO_PERMISSION` + - If the transaction contains `tfClearNoRipple` flag and not `tfSetNoRipple` flag, clear the appropriate NoRipple flag (`lsfLowNoRipple` for low account, `lsfHighNoRipple` for high account) + - If the transaction contains `tfSetfAuth` flag, set the appropriate authorization flag for the source account's side only (`lsfLowAuth` if the source is the low account, `lsfHighAuth` if it is the high account). + - Note: if, after applying the above modifications, the trust line is in its [default state](#11-default-state), it is deleted rather than kept as modified (see deletion conditions above)[^modify-then-delete]. + - If account's parameters in a trust line change to non-default values such that it requires reserve but did not + before: + - Set appropriate `lsfLowReserve` or `lsfHighReserve` flag + - If the transaction's reserve is sponsored, record the sponsor in the side's `LowSponsor` or `HighSponsor` field + - If account no longer requires reserve because its values in a trust line are now default values: + - Clear appropriate `lsfLowReserve` or `lsfHighReserve` flag + - The reserve release is accounted against the sponsor recorded on that side, if any, and the sponsor field is removed + - Only the NoRipple, freeze, authorization, and reserve flag bits are individually set or cleared (as described above); all other stored flag bits are preserved, and `sfFlags` is rewritten only if it changed. + + +- `RippleState` object is **created**: + - If an existing trust line for the same IOU does not exist between the two accounts. + - When created, the trust line is added to both accounts' owner directories: + - Added to low account's owner directory via `dirInsert`. The page number is stored in `sfLowNode`. + - Added to high account's owner directory via `dirInsert`. The page number is stored in `sfHighNode`. + - If either directory is full, the transaction fails with `tecDIR_FULL`. + - NoRipple flags are initialized on both sides of the trust line[^trustcreate-noripple]: + - The source account's NoRipple flag (`lsfLowNoRipple` or `lsfHighNoRipple`) is set if the TrustSet transaction contains `tfSetNoRipple` and not `tfClearNoRipple`[^trustcreate-noripple-src]. + - The destination account's NoRipple flag is set if the destination account does **not** have `lsfDefaultRipple` on their account[^trustcreate-noripple-dst]. `lsfDefaultRipple` is an account-level flag set via AccountSet (`asfDefaultRipple`). When an issuer sets `lsfDefaultRipple`, new trust lines are created without NoRipple on the issuer's side, allowing rippling by default. + +[^modify-then-delete]: Default state check and deletion after modification: [`TrustSet.cpp`](https://github.com/XRPLF/rippled/blob/3.3.0/src/libxrpl/tx/transactors/token/TrustSet.cpp#L617-L622) +[^trustcreate-noripple]: NoRipple initialization in trustCreate: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L264-L281) +[^trustcreate-noripple-src]: Source account NoRipple from transaction flags: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L264-L267) +[^trustcreate-noripple-dst]: Destination account NoRipple from lsfDefaultRipple: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/3.2.0/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L277-L281) + + +- `DirectoryNode` object is **created or modified**: + - When a trust line is created, it is added to both participating accounts' owner directories. + - If an owner directory page is full (32 entries), a new page is created and linked to the directory chain. + - Before the `fixDirectoryLimit` amendment, if creating a new page would exceed 262,144 pages, the transaction fails with `tecDIR_FULL`. With `fixDirectoryLimit` enabled, that cap is removed and a new page can only fail to be created on 64-bit page-number overflow. + + +- `AccountRoot` object is **modified**: + - If account now requires reserve but did not before: + - Increment `sfOwnerCount` by 1, without overflowing ```std::uint32_t```. + - If account no longer requires reserve: + - Decrement `sfOwnerCount` by 1, without going below `0`. + +### 3.1.2. Clawback Transaction + +**Design note** - in `xrpld`, the same transactor implementation of `Clawback` is used to clawback +both [IOUs](../glossary.md#iou) and [MPTs](../glossary.md#mpt). `xrpld`'s `Clawback` implementation of +`Transactor` uses an adaptation of a visitor pattern. Any transaction containing an MPT in `Amount` field will visit +`MPTIssue` implementation, while transactions referring to IOUs in `Amount` field will visit `Issue` implementation. +This allows for the code to be sufficiently separated that even when describing `xrpld` implementation of Clawback we +can discuss two implementations separately. + +Transaction fields are described in [Clawback Fields](https://xrpl.org/docs/references/protocol/transactions/types/clawback#clawback-fields). + +**Terminology in this document** + +- *Issuer* is the account specified as `account` in the transaction. +- *Holder* is the account specified, counterintuitively, as `issuer` field in `Amount`. This is a commonly used + pattern to use `issuer` to denote a peer's account. + +#### 3.1.2.1. Failure Conditions + +Static validation + +- `temINVALID_FLAG`: any flags, other than universal transaction flags, are specified. +- `temMALFORMED`: + - For IOU clawback: user provided `Holder` field in the transaction (IOUs use `Amount.issuer` to specify the holder). + - For MPT clawback: user did NOT provide `Holder` field in the transaction (MPTs require the `Holder` field). +- `temBAD_AMOUNT`: + - issuer and holder are the same account (for IOU clawback; an MPT clawback with the same issuer and holder returns `temMALFORMED` instead). + - `Amount` provided is XRP. + - `Amount` is not bigger than `0`. + +Validation against the ledger view + +- `terNO_ACCOUNT`: issuer's or holder's account does not exist. +- `tecAMM_ACCOUNT`: holder's account is an AMM account (has `sfAMMID`) and the [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault) amendment is not enabled. +- `tecPSEUDO_ACCOUNT`: If `SingleAssetVault` is enabled and holder's account is any pseudo-account. +- `tecNO_PERMISSION`: issuer does not have `lsfAllowTrustLineClawback` or it does have `lsfNoFreeze`. Only + trust lines of issuers that can be frozen and that allow trust line clawback can be clawed back. +- `tecNO_LINE`: there is no existing trust line between issuer and holder for the specified currency. +- `tecNO_PERMISSION`: the account specified as `account` (issuer) is not actually the issuer based on the trust line + balance. The true issuer is the account with the negative balance (owing IOUs), not the positive balance (holding + IOUs). This error occurs when the transaction sender has the accounts reversed. +- `tecINSUFFICIENT_FUNDS`: the holder's available balance (computed via `accountHolds`) is `0` or negative. This is the spendable balance, which can differ from the raw trust-line balance. + + +#### 3.1.2.2. State Changes + +- `RippleState` object is **modified**: + - Amount specified in `Amount` field is transferred from holder to issuer on the trust line for specified currency + code. This is done by subtracting the amount from holder's account and adding it to issuer's account. No limits or + fees are enforced. + - If the `amount` is bigger than the available balance, only the available balance is moved. + - If the clawback returns the holder's side of the trust line to its default state (the holder's balance falls from positive to zero, and its limit, qualities, NoRipple, and freeze are already at default), the holder's `lsfLowReserve` or `lsfHighReserve` flag is cleared and the holder's `OwnerCount` is decremented. Only the holder (sender) side is adjusted; the issuer's side is not. + + +- `RippleState` object is **deleted**: + - If, after clawing back, the trust line is in its default state, `RippleState` is deleted. + - When deleted, the trust line is removed from both accounts' owner directories: + - Removed from low account's owner directory via `dirRemove` using the `sfLowNode` page number. + - Removed from high account's owner directory via `dirRemove` using the `sfHighNode` page number. + - If removing the trust line empties a non-root directory page, that page is deleted and the directory chain is repaired. + + +- `AccountRoot` (`sfOwnerCount`): deleting the `RippleState` does not by itself change any account's `sfOwnerCount`. The only owner-count change a clawback makes is the single holder-side decrement noted above, applied when the holder's side returns to default (whether or not the line is then deleted). The issuer's `sfOwnerCount` is not adjusted by a clawback. From 0ab6a9a198cd38714b0f20d7ca0227eed3705b67 Mon Sep 17 00:00:00 2001 From: Dejan Cabrilo Date: Fri, 14 Aug 2026 11:23:37 +0200 Subject: [PATCH 2/2] Enforce LF line endings, ignore normalization commit in blame --- .git-blame-ignore-revs | 2 ++ .gitattributes | 1 + 2 files changed, 3 insertions(+) create mode 100644 .git-blame-ignore-revs create mode 100644 .gitattributes diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..d1decad --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Line-ending normalization (CRLF to LF), whitespace-only +15baae0f90036284238e0c3a543e3d7434bd5e38 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1fe2478 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.md text eol=lf